From 17c7d9a99d0ecce792fed73c72efd64ee0ae1e30 Mon Sep 17 00:00:00 2001 From: Anoop Date: Wed, 26 Oct 2022 22:16:34 +0530 Subject: [PATCH] Rebranded Commit --- .editorconfig | 15 + .eslintignore | 8 + .eslintrc | 155 + .flake8 | 37 + .git-blame-ignore-revs | 30 + .github/CONTRIBUTING.md | 36 + .github/ISSUE_TEMPLATE/bug_report.md | 47 + .github/ISSUE_TEMPLATE/config.yml | 5 + .github/ISSUE_TEMPLATE/feature_request.md | 28 + .../question-about-using-influxframework.md | 19 + .github/PULL_REQUEST_TEMPLATE.md | 33 + .github/dependabot.yml | 6 + .github/frappe-framework-logo.svg | 5 + .github/helper/consumer_db/mariadb.json | 18 + .github/helper/consumer_db/postgres.json | 17 + .github/helper/documentation.py | 50 + .github/helper/flake8.conf | 75 + .github/helper/install.sh | 64 + .github/helper/install_dependencies.sh | 14 + .github/helper/producer_db/mariadb.json | 16 + .github/helper/producer_db/postgres.json | 16 + .github/helper/roulette.py | 126 + .github/helper/translation.py | 53 + .github/labeler.yml | 4 + .github/stale.yml | 34 + .github/try-on-f-cloud-button.svg | 32 + .github/workflows/create-release.yml | 34 + .github/workflows/labeller.yml | 12 + .github/workflows/linters.yml | 86 + .github/workflows/on_release.yml | 65 + .github/workflows/patch-mariadb-tests.yml | 147 + .github/workflows/publish-assets-develop.yml | 45 + .github/workflows/server-mariadb-tests.yml | 123 + .github/workflows/server-postgres-tests.yml | 126 + .github/workflows/ui-tests.yml | 162 + .gitignore | 196 + .mergify.yml | 119 + .pre-commit-config.yaml | 67 + .releaserc | 24 + .snyk | 101 + .stylelintrc | 9 + CODEOWNERS | 7 + CODE_OF_CONDUCT.md | 46 + LICENSE | 21 + README.md | 74 + SECURITY.md | 7 + attributions.md | 31 + bandit.yml | 1 + codecov.yml | 35 + commitlint.config.js | 25 + cypress.config.js | 24 + cypress/fixtures/child_table_doctype.js | 30 + cypress/fixtures/child_table_doctype_1.js | 59 + .../fixtures/custom_submittable_doctype.js | 53 + .../fixtures/data_field_validation_doctype.js | 65 + cypress/fixtures/datetime_doctype.js | 48 + cypress/fixtures/doctype_to_link.js | 45 + cypress/fixtures/doctype_with_child_table.js | 52 + cypress/fixtures/doctype_with_phone.js | 46 + cypress/fixtures/doctype_with_tab_break.js | 54 + cypress/fixtures/example.json | 5 + cypress/fixtures/sample_image.jpg | Bin 0 -> 249403 bytes cypress/integration/api.js | 44 + cypress/integration/awesome_bar.js | 57 + cypress/integration/control_attach.js | 95 + cypress/integration/control_autocomplete.js | 64 + cypress/integration/control_barcode.js | 57 + cypress/integration/control_color.js | 80 + cypress/integration/control_data.js | 145 + cypress/integration/control_date.js | 89 + cypress/integration/control_date_range.js | 48 + cypress/integration/control_duration.js | 46 + cypress/integration/control_dynamic_link.js | 159 + cypress/integration/control_float.js | 88 + cypress/integration/control_icon.js | 55 + cypress/integration/control_link.js | 339 + .../integration/control_markdown_editor.js | 22 + cypress/integration/control_phone.js | 92 + cypress/integration/control_rating.js | 54 + cypress/integration/control_select.js | 41 + cypress/integration/custom_buttons.js | 57 + cypress/integration/customize_form.js | 23 + cypress/integration/dashboard_chart.js | 22 + cypress/integration/dashboard_links.js | 91 + .../integration/data_field_form_validation.js | 45 + cypress/integration/datetime.js | 126 + .../datetime_field_form_validation.js | 19 + cypress/integration/depends_on.js | 152 + cypress/integration/discussions.js | 101 + cypress/integration/file_uploader.js | 86 + cypress/integration/first_day_of_the_week.js | 51 + cypress/integration/folder_navigation.js | 92 + cypress/integration/form.js | 166 + cypress/integration/form_tab_break.js | 30 + cypress/integration/form_tour.js | 94 + cypress/integration/grid.js | 114 + cypress/integration/grid_configuration.js | 23 + cypress/integration/grid_keyboard_shortcut.js | 47 + cypress/integration/grid_pagination.js | 80 + cypress/integration/grid_search.js | 133 + cypress/integration/kanban.js | 99 + cypress/integration/list_paging.js | 42 + cypress/integration/list_view.js | 70 + cypress/integration/list_view_settings.js | 38 + cypress/integration/login.js | 66 + cypress/integration/multi_select_dialog.js | 102 + cypress/integration/navigation.js | 29 + cypress/integration/number_card.js | 22 + cypress/integration/query_report.js | 91 + cypress/integration/recorder.js | 72 + cypress/integration/relative_time_filters.js | 47 + cypress/integration/report_view.js | 47 + cypress/integration/routing.js | 40 + cypress/integration/sidebar.js | 86 + cypress/integration/table_multiselect.js | 59 + cypress/integration/theme_switcher_dialog.js | 29 + cypress/integration/timeline.js | 91 + cypress/integration/url_data_field.js | 42 + cypress/integration/view_routing.js | 231 + cypress/integration/web_form.js | 271 + cypress/integration/workspace.js | 214 + cypress/integration/workspace_blocks.js | 151 + cypress/plugins/index.js | 17 + cypress/support/commands.js | 483 ++ cypress/support/e2e.js | 29 + cypress/tsconfig.json | 12 + esbuild/build-cleanup.js | 31 + esbuild/esbuild.js | 506 ++ esbuild/ignore-assets.js | 11 + esbuild/index.js | 1 + esbuild/influxframework-html.js | 44 + esbuild/sass_options.js | 24 + esbuild/utils.js | 146 + generate_bootstrap_theme.js | 28 + hooks.md | 36 + influxframework/__init__.py | 2399 +++++++ influxframework/api.py | 262 + influxframework/app.py | 391 ++ influxframework/auth.py | 554 ++ influxframework/automation/__init__.py | 0 .../automation/doctype/__init__.py | 0 .../doctype/assignment_rule/__init__.py | 0 .../assignment_rule/assignment_rule.js | 77 + .../assignment_rule/assignment_rule.json | 179 + .../assignment_rule/assignment_rule.py | 373 ++ .../assignment_rule/test_assignment_rule.py | 337 + .../doctype/assignment_rule_day/__init__.py | 0 .../assignment_rule_day.json | 28 + .../assignment_rule_day.py | 9 + .../doctype/assignment_rule_user/__init__.py | 0 .../assignment_rule_user.json | 34 + .../assignment_rule_user.py | 9 + .../doctype/auto_repeat/__init__.py | 0 .../doctype/auto_repeat/auto_repeat.js | 122 + .../doctype/auto_repeat/auto_repeat.json | 262 + .../doctype/auto_repeat/auto_repeat.py | 551 ++ .../doctype/auto_repeat/auto_repeat_list.js | 11 + .../auto_repeat/auto_repeat_schedule.html | 19 + .../doctype/auto_repeat/test_auto_repeat.py | 286 + .../doctype/auto_repeat_day/__init__.py | 0 .../auto_repeat_day/auto_repeat_day.json | 33 + .../auto_repeat_day/auto_repeat_day.py | 9 + .../automation/doctype/milestone/__init__.py | 0 .../automation/doctype/milestone/milestone.js | 7 + .../doctype/milestone/milestone.json | 230 + .../automation/doctype/milestone/milestone.py | 13 + .../doctype/milestone/test_milestone.py | 8 + .../doctype/milestone_tracker/__init__.py | 0 .../milestone_tracker/milestone_tracker.js | 31 + .../milestone_tracker/milestone_tracker.json | 162 + .../milestone_tracker/milestone_tracker.py | 51 + .../test_milestone_tracker.py | 46 + .../automation/workspace/tools/tools.json | 249 + influxframework/boot.py | 438 ++ influxframework/build.py | 423 ++ influxframework/cache_manager.py | 245 + influxframework/change_log/__init__.py | 0 influxframework/change_log/current/readme.md | 3 + influxframework/change_log/v10/v10_0_0.md | 10 + influxframework/change_log/v11/v11_1_0.md | 19 + influxframework/change_log/v12/v12_0_0.md | 29 + influxframework/change_log/v13/v13_0_0.md | 54 + influxframework/change_log/v13/v13_1_0.md | 22 + influxframework/change_log/v13/v13_2_0.md | 32 + influxframework/change_log/v13/v13_3_0.md | 49 + influxframework/change_log/v14/v14_0_0.md | 29 + influxframework/change_log/v5/v5_0_18.md | 6 + influxframework/change_log/v5/v5_0_20.md | 1 + influxframework/change_log/v5/v5_0_32.md | 5 + influxframework/change_log/v5/v5_1_0.md | 3 + influxframework/change_log/v5/v5_1_1.md | 1 + influxframework/change_log/v5/v5_3_0.md | 40 + influxframework/change_log/v5/v5_4_0.md | 1 + influxframework/change_log/v6/v6_0_0.md | 3 + influxframework/change_log/v6/v6_0_8.md | 1 + influxframework/change_log/v6/v6_12_0.md | 1 + influxframework/change_log/v6/v6_13_0.md | 4 + influxframework/change_log/v6/v6_14_1.md | 1 + influxframework/change_log/v6/v6_15_0.md | 4 + influxframework/change_log/v6/v6_16_1.md | 1 + influxframework/change_log/v6/v6_16_4.md | 1 + influxframework/change_log/v6/v6_17_0.md | 4 + influxframework/change_log/v6/v6_1_0.md | 7 + influxframework/change_log/v6/v6_20_0.md | 5 + influxframework/change_log/v6/v6_21_0.md | 1 + influxframework/change_log/v6/v6_22_0.md | 2 + influxframework/change_log/v6/v6_23_0.md | 2 + influxframework/change_log/v6/v6_25_0.md | 4 + influxframework/change_log/v6/v6_26_0.md | 2 + influxframework/change_log/v6/v6_26_6.md | 1 + influxframework/change_log/v6/v6_27_1.md | 6 + influxframework/change_log/v6/v6_27_11.md | 2 + influxframework/change_log/v6/v6_2_0.md | 3 + influxframework/change_log/v6/v6_3_0.md | 2 + influxframework/change_log/v6/v6_4_0.md | 1 + influxframework/change_log/v6/v6_4_8.md | 20 + influxframework/change_log/v6/v6_5_0.md | 2 + influxframework/change_log/v6/v6_6_0.md | 1 + influxframework/change_log/v6/v6_7_0.md | 3 + influxframework/change_log/v6/v6_8_0.md | 2 + influxframework/change_log/v7/v7_0_0.md | 36 + influxframework/change_log/v7/v7_0_18.md | 2 + influxframework/change_log/v7/v7_1_0.md | 24 + influxframework/change_log/v7/v7_2_0.md | 18 + influxframework/change_log/v8/v8_0_0.md | 41 + influxframework/change_log/v8/v8_7_0.md | 2 + influxframework/change_log/v8/v8_8_0.md | 2 + influxframework/client.py | 485 ++ influxframework/commands/__init__.py | 127 + influxframework/commands/redis_utils.py | 73 + influxframework/commands/scheduler.py | 229 + influxframework/commands/site.py | 1369 ++++ influxframework/commands/translate.py | 111 + influxframework/commands/utils.py | 1180 ++++ influxframework/config/__init__.py | 77 + influxframework/contacts/__init__.py | 0 .../contacts/address_and_contact.py | 205 + influxframework/contacts/doctype/__init__.py | 0 .../contacts/doctype/address/__init__.py | 0 .../contacts/doctype/address/address.js | 75 + .../contacts/doctype/address/address.json | 216 + .../contacts/doctype/address/address.py | 291 + .../contacts/doctype/address/test_address.py | 30 + .../doctype/address_template/__init__.py | 0 .../address_template/address_template.js | 16 + .../address_template/address_template.json | 152 + .../address_template/address_template.py | 55 + .../address_template/test_address_template.py | 39 + .../contacts/doctype/contact/__init__.py | 0 .../contacts/doctype/contact/contact.js | 151 + .../contacts/doctype/contact/contact.json | 385 ++ .../contacts/doctype/contact/contact.py | 329 + .../contacts/doctype/contact/contact_list.js | 3 + .../contacts/doctype/contact/test_contact.py | 51 + .../doctype/contact/test_records.json | 39 + .../doctype/contact_email/__init__.py | 0 .../doctype/contact_email/contact_email.json | 39 + .../doctype/contact_email/contact_email.py | 9 + .../doctype/contact_phone/__init__.py | 0 .../doctype/contact_phone/contact_phone.json | 50 + .../doctype/contact_phone/contact_phone.py | 9 + .../contacts/doctype/gender/__init__.py | 0 .../contacts/doctype/gender/gender.js | 6 + .../contacts/doctype/gender/gender.json | 48 + .../contacts/doctype/gender/gender.py | 8 + .../contacts/doctype/gender/test_gender.py | 7 + .../contacts/doctype/salutation/__init__.py | 0 .../contacts/doctype/salutation/salutation.js | 6 + .../doctype/salutation/salutation.json | 61 + .../contacts/doctype/salutation/salutation.py | 8 + .../doctype/salutation/test_records.json | 8 + .../doctype/salutation/test_salutation.py | 7 + influxframework/contacts/report/__init__.py | 0 .../report/addresses_and_contacts/__init__.py | 0 .../addresses_and_contacts.js | 33 + .../addresses_and_contacts.json | 32 + .../addresses_and_contacts.py | 131 + .../test_addresses_and_contacts.py | 117 + influxframework/core/README.md | 1 + influxframework/core/__init__.py | 2 + influxframework/core/api/__init__.py | 0 influxframework/core/api/file.py | 121 + influxframework/core/doctype/__init__.py | 2 + .../core/doctype/access_log/__init__.py | 0 .../core/doctype/access_log/access_log.js | 17 + .../core/doctype/access_log/access_log.json | 156 + .../core/doctype/access_log/access_log.py | 71 + .../doctype/access_log/test_access_log.py | 175 + .../core/doctype/activity_log/__init__.py | 0 .../core/doctype/activity_log/activity_log.js | 6 + .../doctype/activity_log/activity_log.json | 186 + .../core/doctype/activity_log/activity_log.py | 51 + .../doctype/activity_log/activity_log_list.js | 12 + .../core/doctype/activity_log/feed.py | 102 + .../doctype/activity_log/test_activity_log.py | 95 + .../core/doctype/block_module/__init__.py | 0 .../doctype/block_module/block_module.json | 71 + .../core/doctype/block_module/block_module.py | 8 + .../core/doctype/comment/__init__.py | 0 .../core/doctype/comment/comment.js | 7 + .../core/doctype/comment/comment.json | 153 + .../core/doctype/comment/comment.py | 179 + .../core/doctype/comment/test_comment.py | 104 + .../core/doctype/communication/README.md | 1 + .../core/doctype/communication/__init__.py | 2 + .../doctype/communication/communication.js | 356 + .../doctype/communication/communication.json | 462 ++ .../doctype/communication/communication.py | 596 ++ .../communication/communication_list.js | 32 + .../core/doctype/communication/email.py | 279 + .../core/doctype/communication/mixins.py | 309 + .../communication/test_communication.py | 395 ++ .../doctype/communication/test_records.json | 10 + .../doctype/communication_link/__init__.py | 0 .../communication_link.json | 47 + .../communication_link/communication_link.py | 13 + .../core/doctype/custom_docperm/__init__.py | 0 .../doctype/custom_docperm/custom_docperm.js | 6 + .../custom_docperm/custom_docperm.json | 249 + .../doctype/custom_docperm/custom_docperm.py | 10 + .../custom_docperm/test_custom_docperm.py | 9 + .../core/doctype/custom_role/__init__.py | 0 .../core/doctype/custom_role/custom_role.js | 6 + .../core/doctype/custom_role/custom_role.json | 240 + .../core/doctype/custom_role/custom_role.py | 21 + .../doctype/custom_role/test_custom_role.py | 9 + .../core/doctype/data_export/__init__.py | 0 .../core/doctype/data_export/data_export.js | 170 + .../core/doctype/data_export/data_export.json | 250 + .../core/doctype/data_export/data_export.py | 8 + .../core/doctype/data_export/exporter.py | 443 ++ .../doctype/data_export/test_data_exporter.py | 113 + .../core/doctype/data_import/__init__.py | 0 .../core/doctype/data_import/data_import.css | 3 + .../core/doctype/data_import/data_import.js | 544 ++ .../core/doctype/data_import/data_import.json | 197 + .../core/doctype/data_import/data_import.py | 274 + .../doctype/data_import/data_import_list.js | 44 + .../core/doctype/data_import/exporter.py | 255 + .../fixtures/sample_import_file.csv | 5 + .../sample_import_file_for_update.csv | 2 + .../sample_import_file_without_mandatory.csv | 5 + .../core/doctype/data_import/importer.py | 1272 ++++ .../doctype/data_import/test_data_import.py | 8 + .../core/doctype/data_import/test_exporter.py | 100 + .../core/doctype/data_import/test_importer.py | 251 + .../core/doctype/data_import_log/__init__.py | 0 .../data_import_log/data_import_log.js | 7 + .../data_import_log/data_import_log.json | 84 + .../data_import_log/data_import_log.py | 9 + .../data_import_log/test_data_import_log.py | 9 + .../core/doctype/defaultvalue/README.md | 1 + .../core/doctype/defaultvalue/__init__.py | 2 + .../doctype/defaultvalue/defaultvalue.json | 90 + .../core/doctype/defaultvalue/defaultvalue.py | 25 + .../core/doctype/deleted_document/__init__.py | 0 .../deleted_document/deleted_document.js | 22 + .../deleted_document/deleted_document.json | 81 + .../deleted_document/deleted_document.py | 64 + .../deleted_document/deleted_document_list.js | 50 + .../deleted_document/test_deleted_document.py | 9 + .../core/doctype/docfield/README.md | 3 + .../core/doctype/docfield/__init__.py | 2 + .../core/doctype/docfield/docfield.json | 560 ++ .../core/doctype/docfield/docfield.py | 31 + .../core/doctype/docperm/__init__.py | 2 + .../core/doctype/docperm/docperm.json | 229 + .../core/doctype/docperm/docperm.py | 8 + .../core/doctype/docshare/__init__.py | 0 .../core/doctype/docshare/docshare.js | 6 + .../core/doctype/docshare/docshare.json | 110 + .../core/doctype/docshare/docshare.py | 82 + .../core/doctype/docshare/test_docshare.py | 127 + .../core/doctype/docshare/test_records.json | 1 + .../core/doctype/doctype/README.md | 15 + .../core/doctype/doctype/__init__.py | 2 + .../doctype/boilerplate/controller._py | 8 + .../doctype/doctype/boilerplate/controller.js | 8 + .../doctype/boilerplate/controller_list.html | 34 + .../doctype/boilerplate/controller_list.js | 5 + .../boilerplate/templates/controller.html | 7 + .../boilerplate/templates/controller_row.html | 4 + .../doctype/boilerplate/test_controller._py | 9 + .../core/doctype/doctype/doctype.js | 195 + .../core/doctype/doctype/doctype.json | 749 +++ .../core/doctype/doctype/doctype.py | 1741 +++++ .../core/doctype/doctype/patches/set_route.py | 8 + .../core/doctype/doctype/test_doctype.py | 763 +++ .../core/doctype/doctype_action/__init__.py | 0 .../doctype_action/doctype_action.json | 74 + .../doctype/doctype_action/doctype_action.py | 9 + .../core/doctype/doctype_link/__init__.py | 0 .../doctype/doctype_link/doctype_link.json | 87 + .../core/doctype/doctype_link/doctype_link.py | 9 + .../core/doctype/doctype_state/__init__.py | 0 .../doctype/doctype_state/doctype_state.json | 50 + .../doctype/doctype_state/doctype_state.py | 9 + .../doctype/document_naming_rule/__init__.py | 0 .../document_naming_rule.js | 70 + .../document_naming_rule.json | 107 + .../document_naming_rule.py | 46 + .../test_document_naming_rule.py | 70 + .../__init__.py | 0 .../document_naming_rule_condition.js | 7 + .../document_naming_rule_condition.json | 49 + .../document_naming_rule_condition.py | 9 + .../test_document_naming_rule_condition.py | 8 + .../document_naming_settings/__init__.py | 0 .../document_naming_settings.js | 70 + .../document_naming_settings.json | 133 + .../document_naming_settings.py | 222 + .../test_document_naming_settings.py | 65 + .../doctype/document_share_key/__init__.py | 0 .../document_share_key/document_share_key.js | 7 + .../document_share_key.json | 73 + .../document_share_key/document_share_key.py | 20 + .../test_document_share_key.py | 9 + .../core/doctype/domain/__init__.py | 0 influxframework/core/doctype/domain/domain.js | 6 + .../core/doctype/domain/domain.json | 54 + influxframework/core/doctype/domain/domain.py | 130 + .../core/doctype/domain/test_domain.py | 7 + .../core/doctype/domain_settings/__init__.py | 0 .../domain_settings/domain_settings.js | 69 + .../domain_settings/domain_settings.json | 153 + .../domain_settings/domain_settings.py | 90 + .../core/doctype/dynamic_link/__init__.py | 0 .../doctype/dynamic_link/dynamic_link.json | 47 + .../core/doctype/dynamic_link/dynamic_link.py | 28 + .../core/doctype/error_log/__init__.py | 0 .../core/doctype/error_log/error_log.js | 17 + .../core/doctype/error_log/error_log.json | 88 + .../core/doctype/error_log/error_log.py | 26 + .../core/doctype/error_log/error_log_list.js | 25 + .../core/doctype/error_log/test_error_log.py | 14 + .../core/doctype/error_snapshot/__init__.py | 0 .../doctype/error_snapshot/error_object.html | 12 + .../error_snapshot/error_snapshot.html | 77 + .../doctype/error_snapshot/error_snapshot.js | 20 + .../error_snapshot/error_snapshot.json | 398 ++ .../doctype/error_snapshot/error_snapshot.py | 40 + .../error_snapshot/error_snapshot_list.js | 19 + .../error_snapshot/test_error_snapshot.py | 9 + influxframework/core/doctype/file/__init__.py | 2 + .../core/doctype/file/exceptions.py | 12 + influxframework/core/doctype/file/file.js | 45 + influxframework/core/doctype/file/file.json | 215 + influxframework/core/doctype/file/file.py | 721 ++ .../core/doctype/file/file_list.js | 0 .../core/doctype/file/test_file.py | 711 ++ influxframework/core/doctype/file/utils.py | 340 + .../core/doctype/has_domain/__init__.py | 0 .../core/doctype/has_domain/has_domain.json | 72 + .../core/doctype/has_domain/has_domain.py | 8 + .../core/doctype/has_role/__init__.py | 0 .../core/doctype/has_role/has_role.json | 64 + .../core/doctype/has_role/has_role.py | 11 + .../doctype/installed_application/__init__.py | 0 .../installed_application.json | 49 + .../installed_application.py | 9 + .../installed_applications/__init__.py | 0 .../installed_applications.js | 7 + .../installed_applications.json | 42 + .../installed_applications.py | 20 + .../test_installed_applications.py | 8 + .../core/doctype/language/__init__.py | 0 .../core/doctype/language/language.js | 6 + .../core/doctype/language/language.json | 85 + .../core/doctype/language/language.py | 65 + .../core/doctype/language/test_language.py | 9 + .../core/doctype/log_setting_user/__init__.py | 0 .../log_setting_user/log_setting_user.js | 7 + .../log_setting_user/log_setting_user.json | 34 + .../log_setting_user/log_setting_user.py | 9 + .../log_setting_user/test_log_setting_user.py | 8 + .../core/doctype/log_settings/__init__.py | 0 .../core/doctype/log_settings/log_settings.js | 14 + .../doctype/log_settings/log_settings.json | 43 + .../core/doctype/log_settings/log_settings.py | 188 + .../doctype/log_settings/test_log_settings.py | 105 + .../core/doctype/logs_to_clear/__init__.py | 0 .../doctype/logs_to_clear/logs_to_clear.json | 43 + .../doctype/logs_to_clear/logs_to_clear.py | 9 + .../core/doctype/module_def/README.md | 1 + .../core/doctype/module_def/__init__.py | 2 + .../core/doctype/module_def/module_def.js | 13 + .../core/doctype/module_def/module_def.json | 166 + .../core/doctype/module_def/module_def.py | 69 + .../doctype/module_def/test_module_def.py | 9 + .../core/doctype/module_profile/__init__.py | 0 .../doctype/module_profile/module_profile.js | 23 + .../module_profile/module_profile.json | 66 + .../doctype/module_profile/module_profile.py | 11 + .../module_profile/test_module_profile.py | 29 + .../core/doctype/navbar_item/__init__.py | 0 .../core/doctype/navbar_item/navbar_item.js | 7 + .../core/doctype/navbar_item/navbar_item.json | 89 + .../core/doctype/navbar_item/navbar_item.py | 9 + .../doctype/navbar_item/test_navbar_item.py | 8 + .../core/doctype/navbar_settings/__init__.py | 0 .../navbar_settings/navbar_settings.js | 7 + .../navbar_settings/navbar_settings.json | 91 + .../navbar_settings/navbar_settings.py | 43 + .../navbar_settings/test_navbar_settings.py | 8 + .../core/doctype/package/__init__.py | 0 .../GNU Affero General Public License.md | 614 ++ .../licenses/GNU General Public License.md | 617 ++ .../doctype/package/licenses/MIT License.md | 17 + .../core/doctype/package/package.js | 20 + .../core/doctype/package/package.json | 76 + .../core/doctype/package/package.py | 19 + .../core/doctype/package/test_package.py | 110 + .../core/doctype/package_import/__init__.py | 0 .../doctype/package_import/package_import.js | 7 + .../package_import/package_import.json | 65 + .../doctype/package_import/package_import.py | 65 + .../package_import/test_package_import.py | 9 + .../core/doctype/package_release/__init__.py | 0 .../package_release/package_release.js | 7 + .../package_release/package_release.json | 95 + .../package_release/package_release.py | 111 + .../package_release/test_package_release.py | 9 + influxframework/core/doctype/page/README.md | 1 + influxframework/core/doctype/page/__init__.py | 2 + influxframework/core/doctype/page/page.js | 16 + influxframework/core/doctype/page/page.json | 415 ++ influxframework/core/doctype/page/page.py | 172 + .../doctype/page/patches/drop_unused_pages.py | 6 + .../core/doctype/page/test_page.py | 18 + .../core/doctype/page/test_records.json | 1 + .../core/doctype/patch_log/README.md | 1 + .../core/doctype/patch_log/__init__.py | 2 + .../core/doctype/patch_log/patch_log.js | 8 + .../core/doctype/patch_log/patch_log.json | 44 + .../core/doctype/patch_log/patch_log.py | 10 + .../core/doctype/patch_log/test_patch_log.py | 9 + .../core/doctype/prepared_report/__init__.py | 0 .../prepared_report/prepared_report.js | 45 + .../prepared_report/prepared_report.json | 140 + .../prepared_report/prepared_report.py | 170 + .../prepared_report/prepared_report_list.js | 12 + .../prepared_report/test_prepared_report.py | 28 + influxframework/core/doctype/report/README.md | 1 + .../core/doctype/report/__init__.py | 2 + .../doctype/report/boilerplate/controller.js | 9 + .../doctype/report/boilerplate/controller.py | 9 + influxframework/core/doctype/report/report.js | 58 + .../core/doctype/report/report.json | 247 + influxframework/core/doctype/report/report.py | 382 ++ .../core/doctype/report/test_records.json | 10 + .../core/doctype/report/test_report.py | 397 ++ .../doctype/report/user_activity_report.json | 17 + .../user_activity_report_without_sort.json | 17 + .../core/doctype/report_column/__init__.py | 0 .../doctype/report_column/report_column.json | 61 + .../doctype/report_column/report_column.py | 9 + .../core/doctype/report_filter/__init__.py | 0 .../doctype/report_filter/report_filter.json | 71 + .../doctype/report_filter/report_filter.py | 9 + influxframework/core/doctype/role/README.md | 1 + influxframework/core/doctype/role/__init__.py | 2 + .../v13_set_default_desk_properties.py | 13 + influxframework/core/doctype/role/role.js | 24 + influxframework/core/doctype/role/role.json | 176 + influxframework/core/doctype/role/role.py | 108 + .../core/doctype/role/test_records.json | 22 + .../core/doctype/role/test_role.py | 54 + .../__init__.py | 0 .../role_permission_for_page_and_report.js | 127 + .../role_permission_for_page_and_report.json | 327 + .../role_permission_for_page_and_report.py | 89 + .../core/doctype/role_profile/__init__.py | 0 .../core/doctype/role_profile/role_profile.js | 20 + .../doctype/role_profile/role_profile.json | 80 + .../core/doctype/role_profile/role_profile.py | 20 + .../doctype/role_profile/test_role_profile.py | 46 + .../core/doctype/rq_job/__init__.py | 0 influxframework/core/doctype/rq_job/rq_job.js | 30 + .../core/doctype/rq_job/rq_job.json | 162 + influxframework/core/doctype/rq_job/rq_job.py | 193 + .../core/doctype/rq_job/rq_job_list.js | 32 + .../core/doctype/rq_job/test_rq_job.py | 91 + .../core/doctype/rq_worker/__init__.py | 0 .../core/doctype/rq_worker/rq_worker.js | 9 + .../core/doctype/rq_worker/rq_worker.json | 138 + .../core/doctype/rq_worker/rq_worker.py | 69 + .../core/doctype/rq_worker/test_rq_worker.py | 17 + .../doctype/scheduled_job_log/__init__.py | 0 .../scheduled_job_log/scheduled_job_log.js | 7 + .../scheduled_job_log/scheduled_job_log.json | 65 + .../scheduled_job_log/scheduled_job_log.py | 14 + .../scheduled_job_log_list.js | 7 + .../test_scheduled_job_log.py | 8 + .../doctype/scheduled_job_type/__init__.py | 0 .../scheduled_job_type/scheduled_job_type.js | 7 + .../scheduled_job_type.json | 128 + .../scheduled_job_type/scheduled_job_type.py | 214 + .../test_scheduled_job_type.py | 73 + .../core/doctype/server_script/__init__.py | 0 .../doctype/server_script/server_script.js | 80 + .../doctype/server_script/server_script.json | 138 + .../doctype/server_script/server_script.py | 204 + .../server_script/server_script_utils.py | 78 + .../server_script/test_server_script.py | 213 + .../core/doctype/session_default/__init__.py | 0 .../session_default/session_default.json | 29 + .../session_default/session_default.py | 9 + .../session_default_settings/__init__.py | 0 .../session_default_settings.js | 15 + .../session_default_settings.json | 39 + .../session_default_settings.py | 48 + .../test_session_default_settings.py | 31 + .../core/doctype/sms_parameter/README.md | 1 + .../core/doctype/sms_parameter/__init__.py | 0 .../doctype/sms_parameter/sms_parameter.json | 128 + .../doctype/sms_parameter/sms_parameter.py | 8 + .../core/doctype/sms_settings/README.md | 1 + .../core/doctype/sms_settings/__init__.py | 0 .../core/doctype/sms_settings/sms_settings.js | 0 .../doctype/sms_settings/sms_settings.json | 80 + .../core/doctype/sms_settings/sms_settings.py | 143 + .../doctype/sms_settings/test_sms_settings.py | 7 + .../core/doctype/success_action/__init__.py | 0 .../doctype/success_action/success_action.js | 60 + .../success_action/success_action.json | 259 + .../doctype/success_action/success_action.py | 8 + .../core/doctype/system_settings/__init__.py | 0 .../system_settings/system_settings.js | 49 + .../system_settings/system_settings.json | 560 ++ .../system_settings/system_settings.py | 103 + .../system_settings/test_system_settings.py | 7 + .../core/doctype/transaction_log/__init__.py | 0 .../core/doctype/transaction_log/readme.md | 16 + .../transaction_log/test_transaction_log.py | 46 + .../transaction_log/transaction_log.js | 4 + .../transaction_log/transaction_log.json | 476 ++ .../transaction_log/transaction_log.py | 66 + .../core/doctype/translation/__init__.py | 0 .../doctype/translation/test_translation.py | 117 + .../core/doctype/translation/translation.js | 8 + .../core/doctype/translation/translation.json | 111 + .../core/doctype/translation/translation.py | 92 + influxframework/core/doctype/user/__init__.py | 0 .../core/doctype/user/test_records.json | 95 + .../core/doctype/user/test_user.py | 463 ++ influxframework/core/doctype/user/user.js | 353 + influxframework/core/doctype/user/user.json | 765 +++ influxframework/core/doctype/user/user.py | 1181 ++++ .../core/doctype/user/user_list.js | 19 + .../doctype/user_document_type/__init__.py | 0 .../user_document_type.json | 109 + .../user_document_type/user_document_type.py | 9 + .../core/doctype/user_email/__init__.py | 0 .../core/doctype/user_email/user_email.json | 73 + .../core/doctype/user_email/user_email.py | 8 + .../core/doctype/user_group/__init__.py | 0 .../doctype/user_group/test_user_group.py | 8 + .../core/doctype/user_group/user_group.js | 7 + .../core/doctype/user_group/user_group.json | 48 + .../core/doctype/user_group/user_group.py | 15 + .../doctype/user_group_member/__init__.py | 0 .../test_user_group_member.py | 8 + .../user_group_member/user_group_member.js | 7 + .../user_group_member/user_group_member.json | 32 + .../user_group_member/user_group_member.py | 9 + .../core/doctype/user_permission/__init__.py | 0 .../user_permission/test_user_permission.py | 317 + .../user_permission/user_permission.js | 58 + .../user_permission/user_permission.json | 117 + .../user_permission/user_permission.py | 327 + .../user_permission/user_permission_help.html | 8 + .../user_permission/user_permission_list.js | 293 + .../user_select_document_type/__init__.py | 0 .../user_select_document_type.json | 33 + .../user_select_document_type.py | 9 + .../doctype/user_social_login/__init__.py | 0 .../user_social_login/user_social_login.json | 189 + .../user_social_login/user_social_login.py | 8 + .../core/doctype/user_type/__init__.py | 0 .../core/doctype/user_type/test_user_type.py | 64 + .../core/doctype/user_type/user_type.js | 71 + .../core/doctype/user_type/user_type.json | 145 + .../core/doctype/user_type/user_type.py | 326 + .../doctype/user_type/user_type_dashboard.py | 5 + .../core/doctype/user_type/user_type_list.js | 10 + .../core/doctype/user_type_module/__init__.py | 0 .../user_type_module/user_type_module.json | 33 + .../user_type_module/user_type_module.py | 9 + .../core/doctype/version/__init__.py | 0 .../core/doctype/version/test_records.json | 1 + .../core/doctype/version/test_version.py | 58 + .../core/doctype/version/version.js | 12 + .../core/doctype/version/version.json | 247 + .../core/doctype/version/version.py | 131 + .../core/doctype/version/version_view.html | 95 + .../core/doctype/view_log/__init__.py | 0 .../core/doctype/view_log/test_view_log.py | 34 + .../core/doctype/view_log/view_log.js | 6 + .../core/doctype/view_log/view_log.json | 163 + .../core/doctype/view_log/view_log.py | 8 + .../core/form_tour/doctype/doctype.json | 56 + influxframework/core/notifications.py | 46 + influxframework/core/page/__init__.py | 2 + .../core/page/background_jobs/__init__.py | 0 .../page/background_jobs/background_jobs.css | 47 + .../page/background_jobs/background_jobs.html | 58 + .../page/background_jobs/background_jobs.js | 136 + .../page/background_jobs/background_jobs.json | 22 + .../page/background_jobs/background_jobs.py | 78 + .../background_jobs/background_workers.html | 51 + .../core/page/dashboard_view/__init__.py | 0 .../page/dashboard_view/dashboard_view.js | 196 + .../page/dashboard_view/dashboard_view.json | 19 + .../core/page/permission_manager/README.md | 1 + .../core/page/permission_manager/__init__.py | 2 + .../permission_manager/permission_manager.css | 51 + .../permission_manager/permission_manager.js | 510 ++ .../permission_manager.json | 20 + .../permission_manager/permission_manager.py | 169 + .../permission_manager_help.html | 41 + .../core/page/recorder/__init__.py | 0 .../core/page/recorder/recorder.js | 28 + .../core/page/recorder/recorder.json | 23 + influxframework/core/report/__init__.py | 2 + .../__init__.py | 0 .../database_storage_usage_by_tables.js | 7 + .../database_storage_usage_by_tables.json | 28 + .../database_storage_usage_by_tables.py | 40 + .../test_database_storage_usage_by_tables.py | 15 + .../report/document_share_report/__init__.py | 0 .../document_share_report.json | 24 + .../permitted_documents_for_user/__init__.py | 0 .../permitted_documents_for_user.js | 34 + .../permitted_documents_for_user.json | 23 + .../permitted_documents_for_user.py | 63 + .../report/transaction_log_report/__init__.py | 0 .../transaction_log_report.js | 11 + .../transaction_log_report.json | 26 + .../transaction_log_report.py | 98 + influxframework/core/utils.py | 95 + influxframework/core/web_form/__init__.py | 0 .../core/web_form/edit_profile/__init__.py | 0 .../web_form/edit_profile/edit_profile.js | 3 + .../web_form/edit_profile/edit_profile.json | 157 + .../web_form/edit_profile/edit_profile.py | 3 + .../core/workspace/build/build.json | 308 + .../core/workspace/settings/settings.json | 380 ++ .../core/workspace/users/users.json | 187 + influxframework/coverage.py | 66 + influxframework/custom/__init__.py | 0 influxframework/custom/doctype/__init__.py | 0 .../custom/doctype/client_script/README.md | 11 + .../custom/doctype/client_script/__init__.py | 2 + .../doctype/client_script/client_script.js | 158 + .../doctype/client_script/client_script.json | 114 + .../doctype/client_script/client_script.py | 12 + .../client_script/test_client_script.py | 9 + .../client_script/ui_test_client_script.js | 98 + .../custom/doctype/custom_field/README.md | 1 + .../custom/doctype/custom_field/__init__.py | 2 + .../doctype/custom_field/custom_field.js | 102 + .../doctype/custom_field/custom_field.json | 478 ++ .../doctype/custom_field/custom_field.py | 231 + .../doctype/custom_field/test_custom_field.py | 39 + .../doctype/custom_field/test_records.json | 1 + .../custom/doctype/customize_form/README.md | 1 + .../custom/doctype/customize_form/__init__.py | 2 + .../doctype/customize_form/customize_form.js | 360 + .../customize_form/customize_form.json | 393 ++ .../doctype/customize_form/customize_form.py | 675 ++ .../customize_form/test_customize_form.py | 405 ++ .../doctype/customize_form_field/__init__.py | 2 + .../customize_form_field.json | 480 ++ .../customize_form_field.py | 8 + .../custom/doctype/doctype_layout/__init__.py | 0 .../doctype/doctype_layout/doctype_layout.js | 105 + .../doctype_layout/doctype_layout.json | 75 + .../doctype/doctype_layout/doctype_layout.py | 77 + .../convert_web_forms_to_doctype_layout.py | 18 + .../doctype_layout/test_doctype_layout.py | 8 + .../doctype/doctype_layout_field/__init__.py | 0 .../doctype_layout_field.json | 38 + .../doctype_layout_field.py | 9 + .../custom/doctype/property_setter/README.md | 1 + .../doctype/property_setter/__init__.py | 2 + .../property_setter/property_setter.js | 10 + .../property_setter/property_setter.json | 154 + .../property_setter/property_setter.py | 73 + .../property_setter/test_property_setter.py | 9 + .../doctype/property_setter/test_records.json | 10 + .../custom/fixtures/temp_doctype.json | 168 + .../custom/fixtures/temp_singles.json | 168 + .../form_tour/custom_field/custom_field.json | 79 + .../customization/customization.json | 44 + .../custom_doctype/custom_doctype.json | 21 + .../custom_field/custom_field.json | 21 + .../naming_series/naming_series.json | 20 + .../print_format/print_format.json | 21 + .../report_builder/report_builder.json | 22 + .../role_permissions/role_permissions.json | 20 + .../onboarding_step/workflows/workflows.json | 20 + .../customization/customization.json | 171 + influxframework/data/google_fonts.json | 56 + influxframework/database/__init__.py | 65 + influxframework/database/database.py | 1344 ++++ influxframework/database/db_manager.py | 85 + influxframework/database/mariadb/__init__.py | 0 influxframework/database/mariadb/database.py | 426 ++ .../database/mariadb/framework_mariadb.sql | 337 + influxframework/database/mariadb/schema.py | 126 + influxframework/database/mariadb/setup_db.py | 178 + influxframework/database/postgres/__init__.py | 0 influxframework/database/postgres/database.py | 443 ++ .../database/postgres/framework_postgres.sql | 346 + influxframework/database/postgres/schema.py | 172 + influxframework/database/postgres/setup_db.py | 121 + influxframework/database/query.py | 565 ++ influxframework/database/schema.py | 380 ++ influxframework/database/sequence.py | 84 + influxframework/database/utils.py | 54 + influxframework/defaults.py | 244 + influxframework/deferred_insert.py | 59 + influxframework/desk/__init__.py | 2 + influxframework/desk/calendar.py | 57 + influxframework/desk/desk_page.py | 58 + influxframework/desk/desktop.py | 591 ++ influxframework/desk/doctype/__init__.py | 0 .../desk/doctype/bulk_update/__init__.py | 0 .../desk/doctype/bulk_update/bulk_update.js | 69 + .../desk/doctype/bulk_update/bulk_update.json | 204 + .../desk/doctype/bulk_update/bulk_update.py | 71 + .../desk/doctype/calendar_view/__init__.py | 0 .../doctype/calendar_view/calendar_view.js | 36 + .../doctype/calendar_view/calendar_view.json | 81 + .../doctype/calendar_view/calendar_view.py | 8 + .../desk/doctype/console_log/__init__.py | 0 .../desk/doctype/console_log/console_log.js | 7 + .../desk/doctype/console_log/console_log.json | 52 + .../desk/doctype/console_log/console_log.py | 9 + .../doctype/console_log/test_console_log.py | 8 + .../desk/doctype/dashboard/__init__.py | 0 .../desk/doctype/dashboard/dashboard.js | 30 + .../desk/doctype/dashboard/dashboard.json | 115 + .../desk/doctype/dashboard/dashboard.py | 132 + .../desk/doctype/dashboard/dashboard_list.js | 16 + .../desk/doctype/dashboard/test_dashboard.py | 7 + .../desk/doctype/dashboard_chart/__init__.py | 0 .../dashboard_chart/dashboard_chart.js | 553 ++ .../dashboard_chart/dashboard_chart.json | 336 + .../dashboard_chart/dashboard_chart.py | 408 ++ .../dashboard_chart/test_dashboard_chart.py | 286 + .../doctype/dashboard_chart_field/__init__.py | 0 .../dashboard_chart_field.json | 37 + .../dashboard_chart_field.py | 9 + .../doctype/dashboard_chart_link/__init__.py | 0 .../dashboard_chart_link.json | 41 + .../dashboard_chart_link.py | 9 + .../dashboard_chart_source/__init__.py | 0 .../dashboard_chart_source.js | 4 + .../dashboard_chart_source.json | 69 + .../dashboard_chart_source.py | 27 + .../test_dashboard_chart_source.py | 7 + .../doctype/dashboard_settings/__init__.py | 0 .../dashboard_settings/dashboard_settings.js | 7 + .../dashboard_settings.json | 51 + .../dashboard_settings/dashboard_settings.py | 49 + .../desk/doctype/desktop_icon/__init__.py | 0 .../desk/doctype/desktop_icon/desktop_icon.js | 6 + .../doctype/desktop_icon/desktop_icon.json | 736 ++ .../desk/doctype/desktop_icon/desktop_icon.py | 551 ++ influxframework/desk/doctype/event/README.md | 1 + .../desk/doctype/event/__init__.py | 2 + influxframework/desk/doctype/event/event.js | 101 + influxframework/desk/doctype/event/event.json | 321 + influxframework/desk/doctype/event/event.py | 429 ++ .../desk/doctype/event/event_calendar.js | 16 + .../desk/doctype/event/event_list.js | 8 + .../desk/doctype/event/test_event.py | 138 + .../desk/doctype/event/test_records.json | 23 + .../doctype/event_participants/__init__.py | 0 .../event_participants.json | 108 + .../event_participants/event_participants.py | 7 + .../desk/doctype/form_tour/__init__.py | 0 .../desk/doctype/form_tour/form_tour.js | 127 + .../desk/doctype/form_tour/form_tour.json | 112 + .../desk/doctype/form_tour/form_tour.py | 27 + .../desk/doctype/form_tour/test_form_tour.py | 9 + .../desk/doctype/form_tour_step/__init__.py | 0 .../form_tour_step/form_tour_step.json | 128 + .../doctype/form_tour_step/form_tour_step.py | 9 + .../doctype/global_search_doctype/__init__.py | 0 .../global_search_doctype.json | 29 + .../global_search_doctype.py | 9 + .../global_search_settings/__init__.py | 0 .../global_search_settings.js | 32 + .../global_search_settings.json | 39 + .../global_search_settings.py | 91 + .../desk/doctype/kanban_board/__init__.py | 0 .../desk/doctype/kanban_board/kanban_board.js | 45 + .../doctype/kanban_board/kanban_board.json | 124 + .../desk/doctype/kanban_board/kanban_board.py | 267 + .../doctype/kanban_board/test_kanban_board.py | 9 + .../doctype/kanban_board_column/__init__.py | 0 .../kanban_board_column.json | 55 + .../kanban_board_column.py | 8 + .../desk/doctype/list_filter/__init__.py | 0 .../desk/doctype/list_filter/list_filter.json | 188 + .../desk/doctype/list_filter/list_filter.py | 8 + .../doctype/list_view_settings/__init__.py | 0 .../list_view_settings/list_view_settings.js | 7 + .../list_view_settings.json | 76 + .../list_view_settings/list_view_settings.py | 88 + .../test_list_view_settings.py | 8 + .../doctype/module_onboarding/__init__.py | 0 .../module_onboarding/module_onboarding.js | 27 + .../module_onboarding/module_onboarding.json | 117 + .../module_onboarding/module_onboarding.py | 53 + .../test_module_onboarding.py | 8 + influxframework/desk/doctype/note/README.md | 1 + influxframework/desk/doctype/note/__init__.py | 0 influxframework/desk/doctype/note/note.js | 54 + influxframework/desk/doctype/note/note.json | 106 + influxframework/desk/doctype/note/note.py | 53 + .../desk/doctype/note/note_list.js | 13 + .../desk/doctype/note/test_note.py | 77 + .../desk/doctype/note/test_records.json | 7 + .../desk/doctype/note_seen_by/__init__.py | 0 .../doctype/note_seen_by/note_seen_by.json | 64 + .../desk/doctype/note_seen_by/note_seen_by.py | 8 + .../desk/doctype/notification_log/__init__.py | 0 .../notification_log/notification_log.js | 45 + .../notification_log/notification_log.json | 120 + .../notification_log/notification_log.py | 186 + .../notification_log/notification_log_list.js | 7 + .../notification_log/test_notification_log.py | 53 + .../doctype/notification_settings/__init__.py | 0 .../notification_settings.js | 27 + .../notification_settings.json | 136 + .../notification_settings.py | 95 + .../test_notification_settings.py | 9 + .../__init__.py | 0 .../notification_subscribed_document.json | 30 + .../notification_subscribed_document.py | 9 + .../desk/doctype/number_card/__init__.py | 0 .../desk/doctype/number_card/number_card.js | 493 ++ .../desk/doctype/number_card/number_card.json | 249 + .../desk/doctype/number_card/number_card.py | 236 + .../doctype/number_card/test_number_card.py | 8 + .../desk/doctype/number_card_link/__init__.py | 0 .../number_card_link/number_card_link.json | 31 + .../number_card_link/number_card_link.py | 9 + .../doctype/onboarding_permission/__init__.py | 0 .../onboarding_permission.js | 7 + .../onboarding_permission.json | 32 + .../onboarding_permission.py | 9 + .../test_onboarding_permission.py | 8 + .../desk/doctype/onboarding_step/__init__.py | 0 .../onboarding_step/onboarding_step.js | 86 + .../onboarding_step/onboarding_step.json | 254 + .../onboarding_step/onboarding_step.py | 30 + .../onboarding_step/test_onboarding_step.py | 8 + .../doctype/onboarding_step_map/__init__.py | 0 .../onboarding_step_map.json | 32 + .../onboarding_step_map.py | 9 + .../desk/doctype/route_history/__init__.py | 0 .../doctype/route_history/route_history.js | 6 + .../doctype/route_history/route_history.json | 52 + .../doctype/route_history/route_history.py | 42 + .../route_history/route_history_list.js | 7 + .../desk/doctype/system_console/__init__.py | 0 .../doctype/system_console/system_console.js | 106 + .../system_console/system_console.json | 109 + .../doctype/system_console/system_console.py | 56 + .../system_console/test_system_console.py | 18 + influxframework/desk/doctype/tag/__init__.py | 0 influxframework/desk/doctype/tag/tag.js | 7 + influxframework/desk/doctype/tag/tag.json | 50 + influxframework/desk/doctype/tag/tag.py | 195 + influxframework/desk/doctype/tag/test_tag.py | 34 + .../desk/doctype/tag_link/__init__.py | 0 .../desk/doctype/tag_link/tag_link.js | 7 + .../desk/doctype/tag_link/tag_link.json | 83 + .../desk/doctype/tag_link/tag_link.py | 9 + .../desk/doctype/tag_link/test_tag_link.py | 8 + influxframework/desk/doctype/todo/README.md | 1 + influxframework/desk/doctype/todo/__init__.py | 2 + .../desk/doctype/todo/test_todo.py | 153 + influxframework/desk/doctype/todo/todo.js | 55 + influxframework/desk/doctype/todo/todo.json | 199 + influxframework/desk/doctype/todo/todo.py | 148 + .../desk/doctype/todo/todo_calendar.js | 29 + .../desk/doctype/todo/todo_list.js | 44 + .../desk/doctype/workspace/__init__.py | 0 .../desk/doctype/workspace/test_workspace.py | 100 + .../desk/doctype/workspace/workspace.js | 30 + .../desk/doctype/workspace/workspace.json | 212 + .../desk/doctype/workspace/workspace.py | 354 + .../desk/doctype/workspace_chart/__init__.py | 0 .../workspace_chart/workspace_chart.json | 39 + .../workspace_chart/workspace_chart.py | 9 + .../desk/doctype/workspace_link/__init__.py | 0 .../workspace_link/workspace_link.json | 125 + .../doctype/workspace_link/workspace_link.py | 9 + .../doctype/workspace_quick_list/__init__.py | 0 .../workspace_quick_list.json | 60 + .../workspace_quick_list.py | 9 + .../doctype/workspace_shortcut/__init__.py | 0 .../workspace_shortcut.json | 112 + .../workspace_shortcut/workspace_shortcut.py | 9 + influxframework/desk/form/__init__.py | 2 + influxframework/desk/form/assign_to.py | 245 + influxframework/desk/form/document_follow.py | 342 + influxframework/desk/form/linked_with.py | 648 ++ influxframework/desk/form/load.py | 508 ++ influxframework/desk/form/meta.py | 285 + influxframework/desk/form/save.py | 68 + influxframework/desk/form/test_form.py | 20 + influxframework/desk/form/utils.py | 104 + influxframework/desk/gantt.py | 17 + influxframework/desk/leaderboard.py | 54 + influxframework/desk/like.py | 104 + influxframework/desk/link_preview.py | 54 + influxframework/desk/listview.py | 64 + influxframework/desk/moduleview.py | 615 ++ influxframework/desk/notifications.py | 362 + influxframework/desk/page/__init__.py | 0 influxframework/desk/page/activity/README.md | 1 + .../desk/page/activity/__init__.py | 0 .../desk/page/activity/activity.css | 74 + .../desk/page/activity/activity.js | 244 + .../desk/page/activity/activity.json | 20 + .../desk/page/activity/activity.py | 65 + .../desk/page/activity/activity_row.html | 42 + influxframework/desk/page/backups/__init__.py | 0 influxframework/desk/page/backups/backups.css | 14 + .../desk/page/backups/backups.html | 27 + influxframework/desk/page/backups/backups.js | 45 + .../desk/page/backups/backups.json | 21 + influxframework/desk/page/backups/backups.py | 120 + .../desk/page/leaderboard/__init__.py | 0 .../desk/page/leaderboard/leaderboard.css | 85 + .../desk/page/leaderboard/leaderboard.js | 411 ++ .../desk/page/leaderboard/leaderboard.json | 19 + .../desk/page/leaderboard/leaderboard.py | 13 + .../desk/page/setup_wizard/__init__.py | 0 .../page/setup_wizard/install_fixtures.py | 66 + .../desk/page/setup_wizard/setup_wizard.js | 638 ++ .../desk/page/setup_wizard/setup_wizard.json | 23 + .../desk/page/setup_wizard/setup_wizard.py | 446 ++ .../desk/page/translation_tool/__init__.py | 0 .../translation_tool/translation_tool.css | 37 + .../translation_tool/translation_tool.html | 20 + .../page/translation_tool/translation_tool.js | 473 ++ .../translation_tool/translation_tool.json | 26 + .../desk/page/user_profile/__init__.py | 0 .../desk/page/user_profile/user_profile.css | 30 + .../desk/page/user_profile/user_profile.html | 44 + .../desk/page/user_profile/user_profile.js | 6 + .../desk/page/user_profile/user_profile.json | 23 + .../desk/page/user_profile/user_profile.py | 113 + .../user_profile/user_profile_controller.js | 495 ++ .../user_profile/user_profile_sidebar.html | 60 + influxframework/desk/query_report.py | 755 +++ influxframework/desk/report/__init__.py | 0 influxframework/desk/report/todo/__init__.py | 0 influxframework/desk/report/todo/todo.js | 7 + influxframework/desk/report/todo/todo.json | 23 + influxframework/desk/report/todo/todo.py | 69 + influxframework/desk/report_dump.py | 107 + influxframework/desk/reportview.py | 733 ++ influxframework/desk/search.py | 369 + influxframework/desk/treeview.py | 84 + influxframework/desk/utils.py | 29 + influxframework/email/__init__.py | 118 + influxframework/email/doctype/__init__.py | 0 .../doctype/auto_email_report/__init__.py | 0 .../auto_email_report/auto_email_report.js | 156 + .../auto_email_report/auto_email_report.json | 238 + .../auto_email_report/auto_email_report.py | 296 + .../test_auto_email_report.py | 64 + .../email/doctype/document_follow/__init__.py | 0 .../document_follow/document_follow.js | 4 + .../document_follow/document_follow.json | 78 + .../document_follow/document_follow.py | 8 + .../document_follow/test_document_follow.py | 245 + .../email/doctype/email_account/__init__.py | 0 .../doctype/email_account/email_account.js | 256 + .../doctype/email_account/email_account.json | 642 ++ .../doctype/email_account/email_account.py | 935 +++ .../email_account/email_account_list.js | 24 + .../email_account/test_email_account.py | 627 ++ .../email_account/test_mails/incoming-1.raw | 91 + .../email_account/test_mails/incoming-2.raw | 511 ++ .../email_account/test_mails/incoming-3.raw | 183 + .../email_account/test_mails/incoming-4.raw | 138 + .../test_mails/incoming-self-sent.raw | 91 + .../incoming-subject-placeholder.raw | 183 + .../email_account/test_mails/reply-1.raw | 47 + .../email_account/test_mails/reply-2.raw | 45 + .../email_account/test_mails/reply-3.raw | 45 + .../email_account/test_mails/reply-4.raw | 75 + .../doctype/email_account/test_records.json | 29 + .../email/doctype/email_domain/__init__.py | 0 .../doctype/email_domain/email_domain.js | 22 + .../doctype/email_domain/email_domain.json | 157 + .../doctype/email_domain/email_domain.py | 101 + .../doctype/email_domain/test_email_domain.py | 39 + .../doctype/email_domain/test_records.json | 32 + .../doctype/email_flag_queue/__init__.py | 0 .../email_flag_queue/email_flag_queue.js | 6 + .../email_flag_queue/email_flag_queue.json | 67 + .../email_flag_queue/email_flag_queue.py | 8 + .../email_flag_queue/test_email_flag_queue.py | 9 + .../email/doctype/email_group/__init__.py | 0 .../email/doctype/email_group/email_group.js | 74 + .../doctype/email_group/email_group.json | 79 + .../email/doctype/email_group/email_group.py | 115 + .../doctype/email_group/test_email_group.py | 9 + .../doctype/email_group/test_records.json | 6 + .../doctype/email_group_member/__init__.py | 0 .../email_group_member/email_group_member.js | 6 + .../email_group_member.json | 70 + .../email_group_member/email_group_member.py | 19 + .../test_email_group_member.py | 9 + .../email/doctype/email_queue/__init__.py | 0 .../email/doctype/email_queue/email_queue.js | 38 + .../doctype/email_queue/email_queue.json | 176 + .../email/doctype/email_queue/email_queue.py | 734 ++ .../doctype/email_queue/email_queue_list.js | 41 + .../doctype/email_queue/test_email_queue.py | 41 + .../doctype/email_queue_recipient/__init__.py | 0 .../email_queue_recipient.json | 46 + .../email_queue_recipient.py | 20 + .../email/doctype/email_rule/__init__.py | 0 .../email/doctype/email_rule/email_rule.js | 6 + .../email/doctype/email_rule/email_rule.json | 128 + .../email/doctype/email_rule/email_rule.py | 8 + .../doctype/email_rule/test_email_rule.py | 7 + .../email/doctype/email_template/__init__.py | 0 .../doctype/email_template/email_template.js | 6 + .../email_template/email_template.json | 89 + .../doctype/email_template/email_template.py | 41 + .../email_template/test_email_template.py | 7 + .../doctype/email_unsubscribe/__init__.py | 0 .../email_unsubscribe/email_unsubscribe.js | 6 + .../email_unsubscribe/email_unsubscribe.json | 175 + .../email_unsubscribe/email_unsubscribe.py | 46 + .../test_email_unsubscribe.py | 9 + .../email/doctype/imap_folder/__init__.py | 0 .../doctype/imap_folder/imap_folder.json | 53 + .../email/doctype/imap_folder/imap_folder.py | 9 + .../email/doctype/newsletter/__init__.py | 0 .../email/doctype/newsletter/exceptions.py | 16 + .../email/doctype/newsletter/newsletter.js | 227 + .../email/doctype/newsletter/newsletter.json | 264 + .../email/doctype/newsletter/newsletter.py | 350 + .../doctype/newsletter/newsletter_list.js | 12 + .../newsletter/templates/newsletter.html | 65 + .../newsletter/templates/newsletter_row.html | 15 + .../doctype/newsletter/test_newsletter.py | 248 + .../doctype/newsletter_attachment/__init__.py | 0 .../newsletter_attachment.json | 31 + .../newsletter_attachment.py | 9 + .../newsletter_email_group/__init__.py | 0 .../newsletter_email_group.json | 42 + .../newsletter_email_group.py | 8 + .../email/doctype/notification/__init__.py | 0 .../doctype/notification/notification.js | 202 + .../doctype/notification/notification.json | 306 + .../doctype/notification/notification.py | 485 ++ .../doctype/notification/test_notification.py | 385 ++ .../doctype/notification/test_records.json | 76 + .../notification_recipient/__init__.py | 0 .../notification_recipient.json | 61 + .../notification_recipient.py | 8 + .../email/doctype/unhandled_email/__init__.py | 0 .../unhandled_email/test_unhandled_email.py | 9 + .../unhandled_email/unhandled_email.json | 212 + .../unhandled_email/unhandled_email.py | 15 + influxframework/email/email_body.py | 596 ++ influxframework/email/inbox.py | 133 + influxframework/email/oauth.py | 168 + influxframework/email/page/__init__.py | 0 influxframework/email/queue.py | 193 + influxframework/email/receive.py | 1014 +++ influxframework/email/smtp.py | 153 + influxframework/email/test_email_body.py | 203 + influxframework/email/test_smtp.py | 88 + influxframework/email/utils.py | 18 + influxframework/event_streaming/__init__.py | 0 .../event_streaming/doctype/__init__.py | 0 .../document_type_field_mapping/__init__.py | 0 .../document_type_field_mapping.json | 73 + .../document_type_field_mapping.py | 9 + .../doctype/document_type_mapping/__init__.py | 0 .../document_type_mapping.js | 37 + .../document_type_mapping.json | 71 + .../document_type_mapping.py | 181 + .../test_document_type_mapping.py | 8 + .../doctype/event_consumer/__init__.py | 0 .../doctype/event_consumer/event_consumer.js | 17 + .../event_consumer/event_consumer.json | 97 + .../doctype/event_consumer/event_consumer.py | 216 + .../event_consumer/test_event_consumer.py | 8 + .../event_consumer_document_type/__init__.py | 0 .../event_consumer_document_type.json | 61 + .../event_consumer_document_type.py | 9 + .../doctype/event_producer/__init__.py | 0 .../doctype/event_producer/event_producer.js | 25 + .../event_producer/event_producer.json | 96 + .../doctype/event_producer/event_producer.py | 569 ++ .../event_producer/test_event_producer.py | 400 ++ .../event_producer_document_type/__init__.py | 0 .../event_producer_document_type.json | 86 + .../event_producer_document_type.py | 9 + .../event_producer_last_update/__init__.py | 0 .../event_producer_last_update.js | 7 + .../event_producer_last_update.json | 53 + .../event_producer_last_update.py | 9 + .../test_event_producer_last_update.py | 8 + .../doctype/event_sync_log/__init__.py | 0 .../doctype/event_sync_log/event_sync_log.js | 24 + .../event_sync_log/event_sync_log.json | 137 + .../doctype/event_sync_log/event_sync_log.py | 9 + .../event_sync_log/event_sync_log_list.js | 9 + .../event_sync_log/test_event_sync_log.py | 8 + .../doctype/event_update_log/__init__.py | 0 .../event_update_log/event_update_log.js | 7 + .../event_update_log/event_update_log.json | 77 + .../event_update_log/event_update_log.py | 296 + .../event_update_log/test_event_update_log.py | 8 + .../event_update_log_consumer/__init__.py | 0 .../event_update_log_consumer.json | 32 + .../event_update_log_consumer.py | 9 + influxframework/exceptions.py | 281 + influxframework/geo/__init__.py | 0 influxframework/geo/country_info.json | 2993 +++++++++ influxframework/geo/country_info.py | 74 + influxframework/geo/doctype/__init__.py | 0 influxframework/geo/doctype/country/README.md | 1 + .../geo/doctype/country/__init__.py | 0 .../geo/doctype/country/country.js | 6 + .../geo/doctype/country/country.json | 90 + .../geo/doctype/country/country.py | 8 + .../geo/doctype/country/test_country.py | 6 + .../geo/doctype/country/test_records.json | 6 + .../geo/doctype/currency/README.md | 1 + .../geo/doctype/currency/__init__.py | 0 .../geo/doctype/currency/currency.js | 11 + .../geo/doctype/currency/currency.json | 122 + .../geo/doctype/currency/currency.py | 11 + .../geo/doctype/currency/test_currency.py | 13 + .../geo/doctype/currency/test_records.json | 1 + influxframework/geo/languages.json | 326 + influxframework/geo/report/__init__.py | 0 influxframework/geo/utils.py | 100 + influxframework/handler.py | 325 + influxframework/hooks.py | 376 ++ influxframework/influxframeworkclient.py | 425 ++ influxframework/installer.py | 828 +++ influxframework/integrations/__init__.py | 0 .../integrations/doctype/__init__.py | 0 .../doctype/connected_app/__init__.py | 0 .../doctype/connected_app/connected_app.js | 38 + .../doctype/connected_app/connected_app.json | 169 + .../doctype/connected_app/connected_app.py | 144 + .../connected_app/test_connected_app.py | 148 + .../doctype/connected_app/test_records.json | 13 + .../doctype/dropbox_settings/__init__.py | 0 .../dropbox_settings/dropbox_settings.js | 54 + .../dropbox_settings/dropbox_settings.json | 129 + .../dropbox_settings/dropbox_settings.py | 412 ++ .../dropbox_settings/test_dropbox_settings.py | 8 + .../doctype/google_calendar/__init__.py | 0 .../google_calendar/google_calendar.js | 67 + .../google_calendar/google_calendar.json | 146 + .../google_calendar/google_calendar.py | 745 +++ .../doctype/google_contacts/__init__.py | 0 .../google_contacts/google_contacts.js | 63 + .../google_contacts/google_contacts.json | 132 + .../google_contacts/google_contacts.py | 289 + .../doctype/google_drive/__init__.py | 0 .../doctype/google_drive/google_drive.js | 70 + .../doctype/google_drive/google_drive.json | 133 + .../doctype/google_drive/google_drive.py | 216 + .../doctype/google_drive/test_google_drive.py | 8 + .../doctype/google_settings/__init__.py | 0 .../google_settings/google_settings.js | 14 + .../google_settings/google_settings.json | 100 + .../google_settings/google_settings.py | 24 + .../google_settings/test_google_settings.py | 43 + .../doctype/integration_request/__init__.py | 0 .../integration_request.js | 6 + .../integration_request.json | 154 + .../integration_request.py | 37 + .../test_integration_request.py | 10 + .../doctype/ldap_group_mapping/__init__.py | 0 .../ldap_group_mapping.json | 41 + .../ldap_group_mapping/ldap_group_mapping.py | 9 + .../doctype/ldap_settings/__init__.py | 0 .../doctype/ldap_settings/ldap_settings.js | 6 + .../doctype/ldap_settings/ldap_settings.json | 320 + .../doctype/ldap_settings/ldap_settings.py | 375 ++ .../test_data_ldif_activedirectory.json | 338 + .../test_data_ldif_openldap.json | 400 ++ .../ldap_settings/test_ldap_settings.py | 651 ++ .../oauth_authorization_code/__init__.py | 0 .../oauth_authorization_code.js | 6 + .../oauth_authorization_code.json | 112 + .../oauth_authorization_code.py | 9 + .../test_oauth_authorization_code.py | 10 + .../doctype/oauth_bearer_token/__init__.py | 0 .../oauth_bearer_token/oauth_bearer_token.js | 6 + .../oauth_bearer_token.json | 96 + .../oauth_bearer_token/oauth_bearer_token.py | 13 + .../test_oauth_bearer_token.py | 10 + .../doctype/oauth_client/__init__.py | 0 .../doctype/oauth_client/oauth_client.js | 6 + .../doctype/oauth_client/oauth_client.json | 517 ++ .../doctype/oauth_client/oauth_client.py | 27 + .../doctype/oauth_client/test_oauth_client.py | 10 + .../doctype/oauth_client/test_records.json | 15 + .../oauth_provider_settings/__init__.py | 0 .../oauth_provider_settings.js | 6 + .../oauth_provider_settings.json | 90 + .../oauth_provider_settings.py | 23 + .../doctype/oauth_scope/__init__.py | 0 .../doctype/oauth_scope/oauth_scope.json | 30 + .../doctype/oauth_scope/oauth_scope.py | 9 + .../doctype/query_parameters/__init__.py | 0 .../query_parameters/query_parameters.json | 37 + .../query_parameters/query_parameters.py | 9 + .../doctype/s3_backup_settings/__init__.py | 0 .../s3_backup_settings/s3_backup_settings.js | 26 + .../s3_backup_settings.json | 153 + .../s3_backup_settings/s3_backup_settings.py | 177 + .../test_s3_backup_settings.py | 7 + .../doctype/slack_webhook_url/__init__.py | 0 .../slack_webhook_url/slack_webhook_url.js | 4 + .../slack_webhook_url/slack_webhook_url.json | 61 + .../slack_webhook_url/slack_webhook_url.py | 55 + .../test_slack_webhook_url.py | 7 + .../doctype/social_login_key/__init__.py | 0 .../social_login_key/social_login_key.js | 84 + .../social_login_key/social_login_key.json | 187 + .../social_login_key/social_login_key.py | 190 + .../social_login_key/test_social_login_key.py | 139 + .../doctype/social_login_keys/__init__.py | 0 .../social_login_keys/social_login_keys.py | 6 + .../doctype/token_cache/__init__.py | 0 .../doctype/token_cache/test_records.json | 18 + .../doctype/token_cache/test_token_cache.py | 36 + .../doctype/token_cache/token_cache.js | 7 + .../doctype/token_cache/token_cache.json | 110 + .../doctype/token_cache/token_cache.py | 65 + .../integrations/doctype/webhook/__init__.py | 79 + .../doctype/webhook/test_webhook.py | 208 + .../integrations/doctype/webhook/webhook.js | 125 + .../integrations/doctype/webhook/webhook.json | 231 + .../integrations/doctype/webhook/webhook.py | 194 + .../doctype/webhook_data/__init__.py | 0 .../doctype/webhook_data/webhook_data.json | 130 + .../doctype/webhook_data/webhook_data.py | 9 + .../doctype/webhook_header/__init__.py | 0 .../webhook_header/webhook_header.json | 101 + .../doctype/webhook_header/webhook_header.py | 9 + .../doctype/webhook_request_log/__init__.py | 0 .../test_webhook_request_log.py | 9 + .../webhook_request_log.js | 7 + .../webhook_request_log.json | 83 + .../webhook_request_log.py | 9 + influxframework/integrations/google_oauth.py | 201 + .../influxframework_providers/__init__.py | 13 + .../influxframeworkcloud.py | 36 + influxframework/integrations/oauth2.py | 242 + influxframework/integrations/oauth2_logins.py | 63 + .../integrations/offsite_backup_utils.py | 121 + influxframework/integrations/utils.py | 101 + .../workspace/integrations/integrations.json | 213 + influxframework/middlewares.py | 28 + influxframework/migrate.py | 179 + influxframework/model/__init__.py | 188 + influxframework/model/base_document.py | 1242 ++++ influxframework/model/create_new.py | 186 + influxframework/model/db_query.py | 1139 ++++ influxframework/model/delete_doc.py | 454 ++ influxframework/model/docfield.py | 63 + influxframework/model/docstatus.py | 25 + influxframework/model/document.py | 1597 +++++ influxframework/model/dynamic_links.py | 61 + influxframework/model/mapper.py | 264 + influxframework/model/meta.py | 811 +++ influxframework/model/naming.py | 559 ++ influxframework/model/rename_doc.py | 721 ++ influxframework/model/sync.py | 130 + influxframework/model/utils/__init__.py | 133 + influxframework/model/utils/link_count.py | 53 + influxframework/model/utils/rename_doc.py | 65 + influxframework/model/utils/rename_field.py | 176 + influxframework/model/utils/user_settings.py | 102 + influxframework/model/virtual_doctype.py | 52 + influxframework/model/workflow.py | 352 + influxframework/modules.txt | 13 + influxframework/modules/__init__.py | 1 + influxframework/modules/export_file.py | 158 + influxframework/modules/import_file.py | 290 + influxframework/modules/patch_handler.py | 230 + influxframework/modules/utils.py | 337 + influxframework/monitor.py | 124 + influxframework/oauth.py | 621 ++ influxframework/parallel_test_runner.py | 308 + influxframework/patches.txt | 216 + influxframework/patches/__init__.py | 0 influxframework/patches/v10_0/__init__.py | 0 ..._chat_by_default_within_system_settings.py | 13 + .../patches/v10_0/enhance_security.py | 32 + .../increase_single_table_column_length.py | 9 + .../v10_0/migrate_passwords_passlib.py | 23 + .../v10_0/modify_naming_series_table.py | 10 + .../modify_smallest_currency_fraction.py | 8 + .../v10_0/refactor_social_login_keys.py | 159 + .../v10_0/reload_countries_and_currencies.py | 8 + ...remove_custom_field_for_disabled_domain.py | 14 + .../patches/v10_0/set_default_locking_time.py | 9 + .../v10_0/set_no_copy_to_workflow_state.py | 13 + influxframework/patches/v11_0/__init__.py | 0 .../apply_customization_to_custom_doctype.py | 52 + .../v11_0/change_email_signature_fieldtype.py | 16 + .../v11_0/copy_fetch_data_from_options.py | 38 + .../patches/v11_0/create_contact_for_user.py | 28 + .../v11_0/delete_all_prepared_reports.py | 9 + .../delete_duplicate_user_permissions.py | 20 + .../drop_column_apply_user_permissions.py | 14 + .../v11_0/fix_order_by_in_reports_json.py | 35 + ...all_prepared_report_attachments_private.py | 31 + ...igrate_report_settings_for_new_listview.py | 34 + .../v11_0/multiple_references_in_events.py | 24 + .../v11_0/reload_and_rename_view_log.py | 28 + ...pe_user_permissions_for_page_and_report.py | 8 + .../patches/v11_0/remove_skip_for_doctype.py | 92 + .../rename_email_alert_to_notification.py | 14 + .../v11_0/rename_google_maps_doctype.py | 9 + ...rename_standard_reply_to_email_template.py | 8 + ...rkflow_action_to_workflow_action_master.py | 10 + .../v11_0/replicate_old_user_permissions.py | 100 + .../set_allow_self_approval_in_workflow.py | 6 + .../v11_0/set_default_letter_head_source.py | 8 + .../patches/v11_0/set_dropbox_file_backup.py | 9 + ...and_modified_value_for_user_permissions.py | 9 + .../v11_0/update_list_user_settings.py | 35 + influxframework/patches/v12_0/__init__.py | 0 ...change_existing_dashboard_chart_filters.py | 31 + .../create_notification_settings_for_user.py | 13 + .../patches/v12_0/delete_duplicate_indexes.py | 53 + .../delete_feedback_request_if_exists.py | 5 + .../patches/v12_0/fix_email_id_formatting.py | 61 + .../patches/v12_0/fix_public_private_files.py | 36 + .../move_email_and_phone_to_child_table.py | 110 + ..._form_attachments_to_attachments_folder.py | 12 + .../move_timeline_links_to_dynamic_links.py | 66 + .../remove_deprecated_fields_from_doctype.py | 12 + .../remove_example_email_thread_notify.py | 10 + .../patches/v12_0/remove_feedback_rating.py | 10 + .../patches/v12_0/rename_events_repeat_on.py | 37 + .../rename_uploaded_files_with_proper_name.py | 33 + .../v12_0/replace_null_values_in_tables.py | 30 + .../patches/v12_0/reset_home_settings.py | 12 + .../v12_0/set_correct_assign_value_in_docs.py | 28 + .../patches/v12_0/set_correct_url_in_files.py | 39 + .../v12_0/set_default_incoming_email_port.py | 43 + .../v12_0/set_default_password_reset_limit.py | 9 + .../v12_0/set_primary_key_in_series.py | 26 + .../setup_comments_from_communications.py | 34 + .../patches/v12_0/setup_email_linking.py | 5 + influxframework/patches/v12_0/setup_tags.py | 47 + ..._auto_repeat_status_and_not_submittable.py | 34 + .../patches/v12_0/update_global_search.py | 8 + .../patches/v12_0/update_print_format_type.py | 18 + influxframework/patches/v13_0/__init__.py | 0 .../v13_0/add_standard_navbar_items.py | 9 + .../add_switch_theme_to_navbar_settings.py | 24 + .../add_toggle_width_in_navbar_settings.py | 24 + ...eate_custom_dashboards_cards_and_charts.py | 48 + ...delete_event_producer_and_consumer_keys.py | 11 + .../v13_0/delete_package_publish_tool.py | 10 + .../patches/v13_0/email_unsubscribe.py | 14 + .../patches/v13_0/enable_custom_script.py | 14 + .../patches/v13_0/encrypt_2fa_secrets.py | 45 + .../generate_theme_files_in_public_folder.py | 19 + .../patches/v13_0/increase_password_length.py | 5 + influxframework/patches/v13_0/jinja_hook.py | 17 + .../patches/v13_0/make_user_type.py | 12 + .../v13_0/migrate_translation_column_data.py | 8 + .../patches/v13_0/queryreport_columns.py | 18 + influxframework/patches/v13_0/remove_chat.py | 19 + .../patches/v13_0/remove_custom_link.py | 18 + .../v13_0/remove_duplicate_navbar_items.py | 14 + .../remove_invalid_options_for_data_fields.py | 15 + .../remove_tailwind_from_page_builder.py | 11 + .../patches/v13_0/remove_twilio_settings.py | 20 + .../patches/v13_0/remove_web_view.py | 7 + .../v13_0/rename_custom_client_script.py | 13 + .../v13_0/rename_desk_page_to_workspace.py | 22 + ...name_is_custom_field_in_dashboard_chart.py | 12 + ...list_view_setting_to_list_view_settings.py | 26 + .../v13_0/rename_notification_fields.py | 16 + .../patches/v13_0/rename_onboarding.py | 9 + ...place_field_target_with_open_in_new_tab.py | 10 + .../patches/v13_0/replace_old_data_import.py | 20 + .../patches/v13_0/reset_corrupt_defaults.py | 33 + ...set_existing_dashboard_charts_as_public.py | 21 + .../v13_0/set_first_day_of_the_week.py | 8 + .../set_path_for_homepage_in_web_page_view.py | 6 + .../patches/v13_0/set_read_times.py | 23 + .../v13_0/set_route_for_blog_category.py | 9 + .../patches/v13_0/set_social_icons.py | 10 + .../patches/v13_0/set_unique_for_page_view.py | 7 + .../patches/v13_0/site_wise_logging.py | 11 + .../update_date_filters_in_user_settings.py | 56 + .../patches/v13_0/update_duration_options.py | 32 + .../update_icons_in_customized_desk_pages.py | 18 + .../v13_0/update_newsletter_content_type.py | 14 + .../update_notification_channel_if_empty.py | 17 + .../patches/v13_0/web_template_set_module.py | 17 + .../v13_0/website_theme_custom_scss.py | 28 + influxframework/patches/v14_0/__init__.py | 0 ...manage_subscriptions_in_navbar_settings.py | 25 + .../v14_0/clear_long_pending_stale_logs.py | 41 + .../patches/v14_0/copy_mail_data.py | 25 + .../v14_0/delete_data_migration_tool.py | 12 + .../patches/v14_0/delete_payment_gateways.py | 16 + .../patches/v14_0/different_encryption_key.py | 16 + .../patches/v14_0/drop_data_import_legacy.py | 23 + .../patches/v14_0/drop_unused_indexes.py | 56 + .../event_streaming_deprecation_warning.py | 9 + .../patches/v14_0/log_settings_migration.py | 29 + .../patches/v14_0/remove_db_aggregation.py | 35 + .../patches/v14_0/remove_is_first_startup.py | 8 + .../v14_0/remove_post_and_post_comment.py | 6 + .../patches/v14_0/reset_creation_datetime.py | 40 + .../patches/v14_0/save_ratings_in_fraction.py | 37 + .../v14_0/set_document_expiry_default.py | 9 + .../v14_0/set_suspend_email_queue_default.py | 13 + .../v14_0/setup_likes_from_feedback.py | 30 + .../patches/v14_0/transform_todo_schema.py | 12 + .../update_auto_account_deletion_duration.py | 6 + ...date_color_names_in_kanban_board_column.py | 22 + .../patches/v14_0/update_github_endpoints.py | 10 + .../v14_0/update_integration_request.py | 21 + .../v14_0/update_is_system_generated_flag.py | 20 + .../v14_0/update_multistep_webforms.py | 12 + .../patches/v14_0/update_webforms.py | 14 + .../patches/v14_0/update_workspace2.py | 93 + influxframework/permissions.py | 734 ++ influxframework/printing/__init__.py | 0 influxframework/printing/doctype/__init__.py | 0 .../printing/doctype/letter_head/__init__.py | 0 .../doctype/letter_head/letter_head.js | 8 + .../doctype/letter_head/letter_head.json | 198 + .../doctype/letter_head/letter_head.py | 98 + .../doctype/letter_head/test_letter_head.py | 14 + .../network_printer_settings/__init__.py | 0 .../network_printer_settings.js | 29 + .../network_printer_settings.json | 76 + .../network_printer_settings.py | 40 + .../test_network_printer_settings.py | 9 + .../printing/doctype/print_format/__init__.py | 0 .../doctype/print_format/print_format.js | 85 + .../doctype/print_format/print_format.json | 284 + .../doctype/print_format/print_format.py | 132 + .../doctype/print_format/test_print_format.py | 32 + .../doctype/print_format/test_records.json | 9 + .../print_format_field_template/__init__.py | 0 .../print_format_field_template.js | 7 + .../print_format_field_template.json | 101 + .../print_format_field_template.py | 43 + .../test_print_format_field_template.py | 9 + .../doctype/print_heading/__init__.py | 0 .../doctype/print_heading/print_heading.js | 6 + .../doctype/print_heading/print_heading.json | 145 + .../doctype/print_heading/print_heading.py | 9 + .../print_heading/test_print_heading.py | 8 + .../doctype/print_settings/__init__.py | 0 .../doctype/print_settings/print_settings.js | 23 + .../print_settings/print_settings.json | 197 + .../doctype/print_settings/print_settings.py | 26 + .../print_settings/test_print_settings.py | 7 + .../printing/doctype/print_style/__init__.py | 0 .../doctype/print_style/print_style.js | 10 + .../doctype/print_style/print_style.json | 214 + .../doctype/print_style/print_style.py | 25 + .../doctype/print_style/test_print_style.py | 8 + .../form_tour/letter_head/letter_head.json | 53 + .../form_tour/print_format/print_format.json | 94 + influxframework/printing/page/__init__.py | 0 .../printing/page/print/__init__.py | 0 influxframework/printing/page/print/print.js | 863 +++ .../printing/page/print/print.json | 18 + influxframework/printing/page/print/print.py | 22 + .../page/print/print_skeleton_loading.html | 164 + .../page/print_format_builder/__init__.py | 0 .../print_format_builder.css | 160 + .../print_format_builder.js | 851 +++ .../print_format_builder.json | 23 + .../print_format_builder.py | 19 + .../print_format_builder_column_selector.html | 36 + .../print_format_builder_field.html | 46 + .../print_format_builder_layout.html | 30 + .../print_format_builder_section.html | 23 + .../print_format_builder_sidebar.html | 21 + .../print_format_builder_start.html | 18 + .../print_format_builder_beta/__init__.py | 0 .../print_format_builder_beta.css | 3 + .../print_format_builder_beta.js | 110 + .../print_format_builder_beta.json | 22 + .../print_format_builder_beta.py | 18 + .../printing/print_style/__init__.py | 0 .../printing/print_style/classic/__init__.py | 0 .../printing/print_style/classic/classic.json | 15 + .../printing/print_style/modern/__init__.py | 0 .../printing/print_style/modern/modern.json | 15 + .../print_style/monochrome/__init__.py | 0 .../print_style/monochrome/monochrome.json | 15 + .../printing/print_style/redesign/__init__.py | 0 .../print_style/redesign/redesign.json | 14 + influxframework/public/css/bootstrap.css | 5819 ++++++++++++++++ .../css/fonts/fontawesome/FontAwesome.otf | Bin 0 -> 134808 bytes .../public/css/fonts/fontawesome/LICENSE | 4 + .../fonts/fontawesome/font-awesome.min.css | 4 + .../fonts/fontawesome/fontawesome-webfont.eot | Bin 0 -> 165742 bytes .../fonts/fontawesome/fontawesome-webfont.svg | 2671 ++++++++ .../fonts/fontawesome/fontawesome-webfont.ttf | Bin 0 -> 165548 bytes .../fontawesome/fontawesome-webfont.woff | Bin 0 -> 98024 bytes .../fontawesome/fontawesome-webfont.woff2 | Bin 0 -> 77160 bytes .../public/css/fonts/inter/LICENSE.txt | 94 + .../public/css/fonts/inter/inter.css | 152 + .../public/css/fonts/inter/inter_black.woff | Bin 0 -> 138628 bytes .../public/css/fonts/inter/inter_black.woff2 | Bin 0 -> 102832 bytes .../css/fonts/inter/inter_blackitalic.woff | Bin 0 -> 145612 bytes .../css/fonts/inter/inter_blackitalic.woff2 | Bin 0 -> 108564 bytes .../public/css/fonts/inter/inter_bold.woff | Bin 0 -> 143100 bytes .../public/css/fonts/inter/inter_bold.woff2 | Bin 0 -> 106052 bytes .../css/fonts/inter/inter_bolditalic.woff | Bin 0 -> 149808 bytes .../css/fonts/inter/inter_bolditalic.woff2 | Bin 0 -> 111644 bytes .../css/fonts/inter/inter_extrabold.woff | Bin 0 -> 142760 bytes .../css/fonts/inter/inter_extrabold.woff2 | Bin 0 -> 106048 bytes .../fonts/inter/inter_extrabolditalic.woff | Bin 0 -> 149372 bytes .../fonts/inter/inter_extrabolditalic.woff2 | Bin 0 -> 111896 bytes .../css/fonts/inter/inter_extralight.woff | Bin 0 -> 140736 bytes .../css/fonts/inter/inter_extralight.woff2 | Bin 0 -> 104128 bytes .../fonts/inter/inter_extralightitalic.woff | Bin 0 -> 148664 bytes .../fonts/inter/inter_extralightitalic.woff2 | Bin 0 -> 110820 bytes .../css/fonts/inter/inter_italic.var.woff2 | Bin 0 -> 240240 bytes .../public/css/fonts/inter/inter_italic.woff | Bin 0 -> 143188 bytes .../public/css/fonts/inter/inter_italic.woff2 | Bin 0 -> 106604 bytes .../public/css/fonts/inter/inter_light.woff | Bin 0 -> 140612 bytes .../public/css/fonts/inter/inter_light.woff2 | Bin 0 -> 103944 bytes .../css/fonts/inter/inter_lightitalic.woff | Bin 0 -> 148812 bytes .../css/fonts/inter/inter_lightitalic.woff2 | Bin 0 -> 111212 bytes .../public/css/fonts/inter/inter_medium.woff | Bin 0 -> 142340 bytes .../public/css/fonts/inter/inter_medium.woff2 | Bin 0 -> 105500 bytes .../css/fonts/inter/inter_mediumitalic.woff | Bin 0 -> 149704 bytes .../css/fonts/inter/inter_mediumitalic.woff2 | Bin 0 -> 111968 bytes .../public/css/fonts/inter/inter_regular.woff | Bin 0 -> 133856 bytes .../css/fonts/inter/inter_regular.woff2 | Bin 0 -> 98804 bytes .../css/fonts/inter/inter_roman.var.woff2 | Bin 0 -> 224744 bytes .../css/fonts/inter/inter_semibold.woff | Bin 0 -> 142760 bytes .../css/fonts/inter/inter_semibold.woff2 | Bin 0 -> 105992 bytes .../css/fonts/inter/inter_semibolditalic.woff | Bin 0 -> 149776 bytes .../fonts/inter/inter_semibolditalic.woff2 | Bin 0 -> 111676 bytes .../public/css/fonts/inter/inter_thin.woff | Bin 0 -> 135872 bytes .../public/css/fonts/inter/inter_thin.woff2 | Bin 0 -> 99556 bytes .../css/fonts/inter/inter_thinitalic.woff | Bin 0 -> 144128 bytes .../css/fonts/inter/inter_thinitalic.woff2 | Bin 0 -> 106320 bytes influxframework/public/css/hljs-night-owl.css | 183 + .../public/css/octicons/LICENSE.txt | 9 + influxframework/public/css/octicons/README.md | 1 + .../public/css/octicons/octicons-local.ttf | Bin 0 -> 50856 bytes .../public/css/octicons/octicons.css | 221 + .../public/css/octicons/octicons.eot | Bin 0 -> 29160 bytes .../public/css/octicons/octicons.less | 220 + .../public/css/octicons/octicons.scss | 220 + .../public/css/octicons/octicons.svg | 183 + .../public/css/octicons/octicons.ttf | Bin 0 -> 28992 bytes .../public/css/octicons/octicons.woff | Bin 0 -> 16060 bytes .../css/octicons/sprockets-octicons.scss | 217 + influxframework/public/css/tree.css | 95 + influxframework/public/css/tree_grid.css | 15 + .../public/html/print_template.html | 44 + .../public/icons/social/facebook.svg | 3 + influxframework/public/icons/social/fair.svg | 14 + .../public/icons/social/frappe.svg | 12 + .../public/icons/social/github.svg | 3 + .../public/icons/social/google.svg | 6 + .../public/icons/social/google_drive.svg | 1 + .../public/icons/social/office_365.svg | 53 + .../public/icons/social/salesforce.svg | 3 + .../public/icons/timeless/icon-check.svg | 3 + .../public/icons/timeless/icons.svg | 954 +++ .../public/icons/timeless/message.svg | 3 + .../public/icons/timeless/search.svg | 3 + influxframework/public/images/background.png | Bin 0 -> 12460 bytes .../public/images/color-circle.png | Bin 0 -> 3783 bytes .../public/images/default-avatar.png | Bin 0 -> 45267 bytes .../public/images/default-skin.svg | 1 + .../public/images/fallback-thumbnail.jpg | Bin 0 -> 2598 bytes .../public/images/frappe-favicon.svg | 19 + .../public/images/frappe-framework-logo.png | Bin 0 -> 1978 bytes .../public/images/frappe-framework-logo.svg | 5 + influxframework/public/images/frappe-logo.png | Bin 0 -> 1978 bytes .../images/help/print-style-classic.png | Bin 0 -> 97352 bytes .../public/images/help/print-style-modern.png | Bin 0 -> 101181 bytes .../images/help/print-style-monochrome.png | Bin 0 -> 100658 bytes .../images/help/print-style-standard.png | Bin 0 -> 100605 bytes .../public/images/leaflet/layers-2x.png | Bin 0 -> 1259 bytes .../public/images/leaflet/layers.png | Bin 0 -> 696 bytes .../images/leaflet/leafletmarker-icon.png | Bin 0 -> 1466 bytes .../images/leaflet/leafletmarker-shadow.png | Bin 0 -> 618 bytes .../public/images/leaflet/lego.png | Bin 0 -> 226340 bytes .../public/images/leaflet/marker-icon-2x.png | Bin 0 -> 2586 bytes .../public/images/leaflet/marker-icon.png | Bin 0 -> 1466 bytes .../public/images/leaflet/marker-shadow.png | Bin 0 -> 618 bytes .../public/images/leaflet/spritesheet-2x.png | Bin 0 -> 3581 bytes .../public/images/leaflet/spritesheet.png | Bin 0 -> 1906 bytes .../public/images/leaflet/spritesheet.svg | 156 + .../public/images/signature-placeholder.png | Bin 0 -> 3124 bytes influxframework/public/images/smiley.png | Bin 0 -> 390 bytes .../public/images/ui-states/404.png | Bin 0 -> 96089 bytes .../images/ui-states/empty-app-state.svg | 6 + .../public/images/ui-states/empty.png | Bin 0 -> 50732 bytes .../images/ui-states/event-empty-state.svg | 9 + .../images/ui-states/grid-empty-state.svg | 9 + .../images/ui-states/list-empty-state.svg | 10 + .../ui-states/notification-empty-state.svg | 9 + .../images/ui-states/search-empty-state.svg | 9 + .../public/images/ui-states/success-color.png | Bin 0 -> 64316 bytes .../public/images/ui/ajax-loader.gif | Bin 0 -> 1737 bytes influxframework/public/images/ui/bot.png | Bin 0 -> 14886 bytes .../public/images/ui/bubble-tea-happy.svg | 11 + .../public/images/ui/bubble-tea-smile.svg | 8 + .../public/images/ui/bubble-tea-sorry.svg | 12 + influxframework/public/images/up.png | Bin 0 -> 235 bytes .../public/js/bootstrap-4-web.bundle.js | 68 + influxframework/public/js/controls.bundle.js | 4 + .../public/js/data_import_tools.bundle.js | 1 + influxframework/public/js/desk.bundle.js | 111 + influxframework/public/js/dialog.bundle.js | 7 + influxframework/public/js/form.bundle.js | 16 + .../public/js/influxframework-web.bundle.js | 27 + .../public/js/influxframework/assets.js | 186 + .../build_events/BuildError.vue | 111 + .../build_events/BuildSuccess.vue | 61 + .../build_events/build_events.bundle.js | 73 + .../public/js/influxframework/change_log.html | 18 + .../public/js/influxframework/class.js | 92 + .../color_picker/color_picker.js | 206 + .../js/influxframework/color_picker/utils.js | 115 + .../data_import/data_exporter.js | 347 + .../data_import/import_preview.js | 352 + .../js/influxframework/data_import/index.js | 2 + .../public/js/influxframework/db.js | 132 + .../public/js/influxframework/defaults.js | 108 + .../public/js/influxframework/desk.js | 690 ++ .../js/influxframework/doctype/index.js | 117 + .../public/js/influxframework/dom.js | 382 ++ .../js/influxframework/event_emitter.js | 36 + .../file_uploader/FileBrowser.vue | 196 + .../file_uploader/FilePreview.vue | 220 + .../file_uploader/FileUploader.vue | 617 ++ .../file_uploader/ImageCropper.vue | 133 + .../file_uploader/ProgressRing.vue | 76 + .../file_uploader/TreeNode.vue | 61 + .../influxframework/file_uploader/WebLink.vue | 33 + .../js/influxframework/file_uploader/index.js | 127 + .../public/js/influxframework/form/column.js | 47 + .../influxframework/form/controls/attach.js | 134 + .../form/controls/attach_image.js | 24 + .../form/controls/autocomplete.js | 256 + .../influxframework/form/controls/barcode.js | 74 + .../form/controls/base_control.js | 292 + .../form/controls/base_input.js | 195 + .../influxframework/form/controls/button.js | 67 + .../js/influxframework/form/controls/check.js | 43 + .../js/influxframework/form/controls/code.js | 211 + .../js/influxframework/form/controls/color.js | 117 + .../influxframework/form/controls/comment.js | 108 + .../influxframework/form/controls/control.js | 53 + .../influxframework/form/controls/currency.js | 20 + .../js/influxframework/form/controls/data.js | 299 + .../js/influxframework/form/controls/date.js | 184 + .../form/controls/date_range.js | 66 + .../form/controls/datepicker_i18n.js | 138 + .../influxframework/form/controls/datetime.js | 93 + .../influxframework/form/controls/duration.js | 155 + .../form/controls/dynamic_link.js | 32 + .../js/influxframework/form/controls/float.js | 39 + .../form/controls/geolocation.js | 222 + .../influxframework/form/controls/heading.js | 5 + .../js/influxframework/form/controls/html.js | 33 + .../form/controls/html_editor.js | 15 + .../js/influxframework/form/controls/icon.js | 101 + .../js/influxframework/form/controls/image.js | 23 + .../js/influxframework/form/controls/int.js | 44 + .../js/influxframework/form/controls/json.js | 6 + .../js/influxframework/form/controls/link.js | 641 ++ .../form/controls/markdown_editor.js | 103 + .../form/controls/multicheck.js | 177 + .../form/controls/multiselect.js | 114 + .../form/controls/multiselect_list.js | 264 + .../form/controls/multiselect_pills.js | 162 + .../influxframework/form/controls/password.js | 52 + .../js/influxframework/form/controls/phone.js | 220 + .../controls/quill-mention/blots/mention.js | 40 + .../controls/quill-mention/constants/keys.js | 10 + .../controls/quill-mention/quill.mention.js | 394 ++ .../influxframework/form/controls/rating.js | 100 + .../influxframework/form/controls/select.js | 161 + .../form/controls/signature.js | 141 + .../js/influxframework/form/controls/table.js | 141 + .../form/controls/table_multiselect.js | 186 + .../js/influxframework/form/controls/text.js | 23 + .../form/controls/text_editor.js | 318 + .../js/influxframework/form/controls/time.js | 109 + .../js/influxframework/form/dashboard.js | 628 ++ .../form/footer/base_timeline.js | 121 + .../js/influxframework/form/footer/footer.js | 72 + .../form/footer/form_timeline.js | 633 ++ .../version_timeline_content_builder.js | 239 + .../public/js/influxframework/form/form.js | 2050 ++++++ .../js/influxframework/form/form_tour.js | 308 + .../js/influxframework/form/form_viewers.js | 71 + .../js/influxframework/form/formatters.js | 411 ++ .../public/js/influxframework/form/grid.js | 1166 ++++ .../influxframework/form/grid_pagination.js | 184 + .../js/influxframework/form/grid_row.js | 1436 ++++ .../js/influxframework/form/grid_row_form.js | 148 + .../public/js/influxframework/form/layout.js | 720 ++ .../js/influxframework/form/link_selector.js | 249 + .../js/influxframework/form/linked_with.js | 80 + .../form/multi_select_dialog.js | 650 ++ .../js/influxframework/form/print_utils.js | 208 + .../js/influxframework/form/quick_entry.js | 299 + .../public/js/influxframework/form/save.js | 344 + .../js/influxframework/form/script_helpers.js | 66 + .../js/influxframework/form/script_manager.js | 268 + .../public/js/influxframework/form/section.js | 152 + .../influxframework/form/sidebar/assign_to.js | 322 + .../form/sidebar/attachments.js | 222 + .../form/sidebar/document_follow.js | 153 + .../form/sidebar/form_sidebar.js | 284 + .../form/sidebar/form_sidebar_users.js | 77 + .../js/influxframework/form/sidebar/review.js | 198 + .../js/influxframework/form/sidebar/share.js | 197 + .../form/sidebar/user_image.js | 87 + .../js/influxframework/form/success_action.js | 106 + .../public/js/influxframework/form/tab.js | 101 + .../form/templates/address_list.html | 22 + .../form/templates/contact_list.html | 54 + .../form/templates/form_dashboard.html | 20 + .../form/templates/form_footer.html | 9 + .../form/templates/form_links.html | 33 + .../form/templates/form_sidebar.html | 160 + .../form/templates/print_layout.html | 56 + .../form/templates/report_links.html | 23 + .../form/templates/set_sharing.html | 71 + .../form/templates/timeline_message_box.html | 106 + .../form/templates/users_in_sidebar.html | 13 + .../public/js/influxframework/form/toolbar.js | 729 ++ .../js/influxframework/form/undo_manager.js | 74 + .../js/influxframework/form/workflow.js | 152 + .../public/js/influxframework/format.js | 21 + .../icon_picker/icon_picker.js | 88 + .../js/influxframework/list/base_list.js | 876 +++ .../influxframework/list/bulk_operations.js | 385 ++ .../js/influxframework/list/list_factory.js | 97 + .../js/influxframework/list/list_filter.js | 200 + .../js/influxframework/list/list_settings.js | 404 ++ .../js/influxframework/list/list_sidebar.html | 76 + .../js/influxframework/list/list_sidebar.js | 242 + .../list/list_sidebar_group_by.js | 268 + .../list/list_sidebar_stat.html | 16 + .../js/influxframework/list/list_view.js | 1878 ++++++ .../list_view_permission_restrictions.html | 16 + .../influxframework/list/list_view_select.js | 337 + .../public/js/influxframework/logtypes.js | 35 + .../public/js/influxframework/meta_tag.js | 19 + .../js/influxframework/microtemplate.js | 215 + .../js/influxframework/model/create_new.js | 372 ++ .../js/influxframework/model/indicator.js | 113 + .../public/js/influxframework/model/meta.js | 334 + .../public/js/influxframework/model/model.js | 808 +++ .../public/js/influxframework/model/perm.js | 300 + .../public/js/influxframework/model/sync.js | 180 + .../js/influxframework/model/user_settings.js | 57 + .../js/influxframework/model/workflow.js | 94 + .../js/influxframework/module_editor.js | 59 + .../phone_picker/phone_picker.js | 105 + .../public/js/influxframework/polyfill.js | 85 + .../public/js/influxframework/provide.js | 50 + .../public/js/influxframework/query_string.js | 79 + .../recorder/RecorderDetail.vue | 282 + .../influxframework/recorder/RecorderRoot.vue | 17 + .../recorder/RequestDetail.vue | 303 + .../js/influxframework/recorder/recorder.js | 48 + .../public/js/influxframework/request.js | 650 ++ .../public/js/influxframework/roles_editor.js | 139 + .../public/js/influxframework/router.js | 549 ++ .../js/influxframework/router_history.js | 40 + .../js/influxframework/scanner/index.js | 100 + .../js/influxframework/socketio_client.js | 265 + .../public/js/influxframework/translate.js | 39 + .../ui/alt_keyboard_shortcuts.js | 178 + .../public/js/influxframework/ui/app_icon.js | 62 + .../public/js/influxframework/ui/capture.js | 337 + .../public/js/influxframework/ui/chart.js | 39 + .../public/js/influxframework/ui/colors.js | 144 + .../public/js/influxframework/ui/datatable.js | 3 + .../public/js/influxframework/ui/dialog.js | 286 + .../public/js/influxframework/ui/driver.js | 3 + .../js/influxframework/ui/field_group.js | 179 + .../ui/filters/edit_filter.html | 26 + .../ui/filters/field_select.js | 190 + .../js/influxframework/ui/filters/filter.js | 578 ++ .../influxframework/ui/filters/filter_list.js | 359 + .../public/js/influxframework/ui/find.js | 18 + .../influxframework/ui/group_by/group_by.html | 53 + .../influxframework/ui/group_by/group_by.js | 411 ++ .../public/js/influxframework/ui/iconbar.js | 73 + .../public/js/influxframework/ui/keyboard.js | 338 + .../public/js/influxframework/ui/like.js | 170 + .../js/influxframework/ui/link_preview.js | 230 + .../public/js/influxframework/ui/listing.html | 34 + .../public/js/influxframework/ui/messages.js | 470 ++ .../ui/notifications/notifications.js | 440 ++ .../public/js/influxframework/ui/page.html | 83 + .../public/js/influxframework/ui/page.js | 911 +++ .../public/js/influxframework/ui/sidebar.js | 70 + .../public/js/influxframework/ui/slides.js | 456 ++ .../js/influxframework/ui/sort_selector.html | 25 + .../js/influxframework/ui/sort_selector.js | 193 + .../js/influxframework/ui/tag_editor.js | 130 + .../public/js/influxframework/ui/tags.js | 115 + .../js/influxframework/ui/theme_switcher.js | 163 + .../js/influxframework/ui/toolbar/about.js | 68 + .../influxframework/ui/toolbar/awesome_bar.js | 355 + .../influxframework/ui/toolbar/fuzzy_match.js | 182 + .../js/influxframework/ui/toolbar/navbar.html | 120 + .../js/influxframework/ui/toolbar/search.html | 8 + .../js/influxframework/ui/toolbar/search.js | 404 ++ .../ui/toolbar/search_utils.js | 629 ++ .../ui/toolbar/subscription.js | 80 + .../influxframework/ui/toolbar/tag_utils.js | 108 + .../js/influxframework/ui/toolbar/toolbar.js | 292 + .../public/js/influxframework/ui/tree.js | 321 + .../ui/workspace_loading_skeleton.html | 24 + .../workspace_sidebar_loading_skeleton.html | 22 + .../public/js/influxframework/upload.js | 7 + .../utils/address_and_contact.js | 45 + .../public/js/influxframework/utils/common.js | 415 ++ .../influxframework/utils/dashboard_utils.js | 283 + .../js/influxframework/utils/datatable.js | 22 + .../js/influxframework/utils/datatype.js | 81 + .../js/influxframework/utils/datetime.js | 303 + .../js/influxframework/utils/diffview.js | 100 + .../js/influxframework/utils/display_image.js | 0 .../utils/energy_point_utils.js | 65 + .../js/influxframework/utils/file_manager.js | 58 + .../public/js/influxframework/utils/help.js | 55 + .../js/influxframework/utils/help_links.js | 85 + .../js/influxframework/utils/number_format.js | 259 + .../influxframework/utils/number_systems.js | 34 + .../js/influxframework/utils/pretty_date.js | 104 + .../js/influxframework/utils/preview_email.js | 35 + .../public/js/influxframework/utils/tools.js | 79 + .../public/js/influxframework/utils/urllib.js | 77 + .../public/js/influxframework/utils/user.js | 139 + .../public/js/influxframework/utils/utils.js | 1605 +++++ .../js/influxframework/utils/web_template.js | 72 + .../js/influxframework/views/breadcrumbs.js | 208 + .../views/calendar/calendar.js | 474 ++ .../js/influxframework/views/communication.js | 855 +++ .../js/influxframework/views/container.js | 104 + .../views/dashboard/dashboard_view.js | 514 ++ .../js/influxframework/views/factory.js | 51 + .../influxframework/views/file/file_view.js | 473 ++ .../js/influxframework/views/formview.js | 119 + .../influxframework/views/gantt/gantt_view.js | 232 + .../influxframework/views/image/image_view.js | 365 + .../views/image/image_view_item_row.html | 35 + .../views/image/photoswipe_dom.html | 73 + .../influxframework/views/inbox/inbox_view.js | 217 + .../js/influxframework/views/interaction.js | 367 + .../views/kanban/kanban_board.bundle.js | 941 +++ .../views/kanban/kanban_board.html | 13 + .../views/kanban/kanban_card.html | 24 + .../views/kanban/kanban_column.html | 28 + .../views/kanban/kanban_settings.js | 247 + .../views/kanban/kanban_view.js | 380 ++ .../js/influxframework/views/map/map_view.js | 91 + .../js/influxframework/views/modules_home.js | 23 + .../js/influxframework/views/pageview.js | 155 + .../views/reports/print_grid.html | 65 + .../views/reports/print_tree.html | 106 + .../views/reports/query_report.js | 1957 ++++++ .../views/reports/report_factory.js | 15 + .../views/reports/report_utils.js | 161 + .../views/reports/report_view.js | 1606 +++++ .../views/translation_manager.js | 107 + .../js/influxframework/views/treeview.js | 487 ++ .../views/workspace/blocks/block.js | 339 + .../views/workspace/blocks/card.js | 62 + .../views/workspace/blocks/chart.js | 63 + .../views/workspace/blocks/header.js | 142 + .../views/workspace/blocks/header_size.js | 124 + .../views/workspace/blocks/index.js | 29 + .../views/workspace/blocks/onboarding.js | 141 + .../views/workspace/blocks/paragraph.js | 224 + .../views/workspace/blocks/quick_list.js | 63 + .../views/workspace/blocks/shortcut.js | 92 + .../views/workspace/blocks/spacer.js | 57 + .../views/workspace/workspace.js | 1367 ++++ .../js/influxframework/web_form/web_form.js | 452 ++ .../influxframework/web_form/web_form_list.js | 403 ++ .../web_form/webform_script.js | 97 + .../js/influxframework/widgets/base_widget.js | 202 + .../influxframework/widgets/chart_widget.js | 795 +++ .../influxframework/widgets/links_widget.js | 124 + .../js/influxframework/widgets/new_widget.js | 64 + .../widgets/number_card_widget.js | 342 + .../widgets/onboarding_widget.js | 599 ++ .../widgets/quick_list_widget.js | 249 + .../widgets/shortcut_widget.js | 77 + .../influxframework/widgets/widget_dialog.js | 662 ++ .../influxframework/widgets/widget_group.js | 237 + .../js/integrations/google_drive_picker.js | 95 + influxframework/public/js/jquery-bootstrap.js | 31 + .../public/js/lib/fullcalendar/LICENCE.txt | 20 + .../js/lib/fullcalendar/fullcalendar.min.css | 5 + .../js/lib/fullcalendar/fullcalendar.min.js | 12 + .../public/js/lib/fullcalendar/locale-all.js | 6 + .../public/js/lib/jSignature.min.js | 1462 ++++ .../public/js/lib/jquery/LICENCE.txt | 36 + .../public/js/lib/jquery/jquery.min.js | 4 + influxframework/public/js/lib/leaflet/LICENSE | 23 + .../public/js/lib/leaflet/leaflet.css | 632 ++ .../public/js/lib/leaflet/leaflet.js | 5 + .../L.Control.Locate.css | 12 + .../L.Control.Locate.js | 591 ++ .../js/lib/leaflet_control_locate/LICENSE | 21 + .../public/js/lib/leaflet_draw/MIT-LICENSE.md | 20 + .../js/lib/leaflet_draw/leaflet.draw.css | 10 + .../js/lib/leaflet_draw/leaflet.draw.js | 1702 +++++ .../public/js/lib/leaflet_easy_button/LICENSE | 7 + .../lib/leaflet_easy_button/easy-button.css | 56 + .../js/lib/leaflet_easy_button/easy-button.js | 370 + influxframework/public/js/lib/moment.js | 5 + .../public/js/lib/photoswipe/LICENCE | 21 + .../public/js/lib/photoswipe/default-skin.css | 483 ++ .../lib/photoswipe/photoswipe-ui-default.js | 861 +++ .../public/js/lib/photoswipe/photoswipe.css | 178 + .../public/js/lib/photoswipe/photoswipe.js | 3718 +++++++++++ influxframework/public/js/libs.bundle.js | 7 + influxframework/public/js/list.bundle.js | 42 + influxframework/public/js/logtypes.bundle.js | 1 + .../print_format_builder/ConfigureColumns.vue | 111 + .../public/js/print_format_builder/Field.vue | 360 + .../js/print_format_builder/HTMLEditor.vue | 68 + .../print_format_builder/LetterHeadEditor.vue | 335 + .../js/print_format_builder/Preview.vue | 132 + .../js/print_format_builder/PrintFormat.vue | 136 + .../PrintFormatBuilder.vue | 74 + .../PrintFormatControls.vue | 336 + .../PrintFormatSection.vue | 245 + .../print_format_builder.bundle.js | 61 + .../public/js/print_format_builder/store.js | 158 + .../public/js/print_format_builder/utils.js | 152 + influxframework/public/js/recorder.bundle.js | 1 + influxframework/public/js/report.bundle.js | 8 + .../js/user_profile_controller.bundle.js | 1 + .../public/js/video_player.bundle.js | 3 + influxframework/public/js/web_form.bundle.js | 6 + influxframework/public/scss/common/alert.scss | 12 + .../public/scss/common/awesomeplete.scss | 67 + .../public/scss/common/buttons.scss | 105 + .../public/scss/common/color_picker.scss | 132 + .../public/scss/common/controls.scss | 478 ++ .../public/scss/common/css_variables.scss | 271 + .../public/scss/common/datepicker.scss | 95 + influxframework/public/scss/common/flex.scss | 86 + influxframework/public/scss/common/form.scss | 29 + .../public/scss/common/global.scss | 154 + influxframework/public/scss/common/grid.scss | 519 ++ .../public/scss/common/icon_picker.scss | 95 + influxframework/public/scss/common/icons.scss | 71 + .../public/scss/common/indicator.scss | 182 + .../public/scss/common/mixins.scss | 86 + influxframework/public/scss/common/modal.scss | 259 + .../public/scss/common/phone_picker.scss | 144 + influxframework/public/scss/common/quill.scss | 247 + influxframework/public/scss/desk.bundle.scss | 6 + influxframework/public/scss/desk/avatar.scss | 208 + .../public/scss/desk/breadcrumb.scss | 27 + .../public/scss/desk/calendar.scss | 208 + .../public/scss/desk/css_variables.scss | 53 + influxframework/public/scss/desk/dark.scss | 189 + .../public/scss/desk/dashboard_view.scss | 64 + .../public/scss/desk/data_import.scss | 17 + influxframework/public/scss/desk/desktop.scss | 1426 ++++ influxframework/public/scss/desk/driver.scss | 79 + .../public/scss/desk/file_view.scss | 132 + influxframework/public/scss/desk/filters.scss | 108 + influxframework/public/scss/desk/form.scss | 395 ++ influxframework/public/scss/desk/global.scss | 629 ++ .../public/scss/desk/global_search.scss | 120 + .../public/scss/desk/image_view.scss | 246 + influxframework/public/scss/desk/index.scss | 51 + .../scss/desk/influxframework_datatable.scss | 173 + .../scss/desk/influxframework_gantt.scss | 82 + influxframework/public/scss/desk/kanban.scss | 367 + .../public/scss/desk/link_preview.scss | 63 + influxframework/public/scss/desk/list.scss | 467 ++ influxframework/public/scss/desk/mobile.scss | 382 ++ influxframework/public/scss/desk/module.scss | 145 + influxframework/public/scss/desk/navbar.scss | 101 + .../public/scss/desk/notification.scss | 319 + influxframework/public/scss/desk/page.scss | 191 + influxframework/public/scss/desk/plyr.scss | 11 + .../public/scss/desk/print_preview.scss | 155 + influxframework/public/scss/desk/report.scss | 282 + .../public/scss/desk/role_editor.scss | 49 + .../public/scss/desk/scrollbar.scss | 30 + influxframework/public/scss/desk/sidebar.scss | 495 ++ influxframework/public/scss/desk/slides.scss | 137 + influxframework/public/scss/desk/tags.scss | 16 + .../public/scss/desk/theme_switcher.scss | 98 + .../public/scss/desk/timeline.scss | 133 + influxframework/public/scss/desk/toast.scss | 149 + influxframework/public/scss/desk/tree.scss | 134 + .../public/scss/desk/typography.scss | 28 + .../public/scss/desk/user_profile.scss | 101 + .../public/scss/desk/variables.scss | 150 + influxframework/public/scss/desk/version.scss | 33 + .../public/scss/element/checkbox.scss | 55 + .../public/scss/element/radio.scss | 37 + influxframework/public/scss/email.bundle.scss | 343 + influxframework/public/scss/login.bundle.scss | 221 + influxframework/public/scss/print.bundle.scss | 37 + .../public/scss/print_format.bundle.scss | 5 + .../public/scss/report.bundle.scss | 2 + .../public/scss/web_form.bundle.scss | 3 + .../public/scss/website.bundle.scss | 1 + influxframework/public/scss/website/base.scss | 96 + influxframework/public/scss/website/blog.scss | 131 + .../public/scss/website/css_variables.scss | 33 + influxframework/public/scss/website/doc.scss | 204 + .../public/scss/website/error-state.scss | 19 + .../public/scss/website/footer.scss | 101 + .../public/scss/website/index.scss | 345 + .../public/scss/website/markdown.scss | 132 + .../scss/website/multilevel_dropdown.scss | 17 + .../public/scss/website/my_account.scss | 101 + .../public/scss/website/navbar.scss | 127 + .../public/scss/website/page_builder.scss | 733 ++ .../public/scss/website/portal.scss | 7 + .../public/scss/website/search.scss | 40 + .../public/scss/website/sidebar.scss | 52 + .../public/scss/website/variables.scss | 136 + .../public/scss/website/web_form.scss | 495 ++ .../public/scss/website/website_avatar.scss | 8 + .../public/scss/website/website_image.scss | 73 + influxframework/public/sounds/alert.mp3 | Bin 0 -> 10448 bytes influxframework/public/sounds/cancel.mp3 | Bin 0 -> 8776 bytes influxframework/public/sounds/chime.mp3 | Bin 0 -> 12955 bytes influxframework/public/sounds/click.mp3 | Bin 0 -> 5850 bytes influxframework/public/sounds/delete.mp3 | Bin 0 -> 9612 bytes influxframework/public/sounds/email.mp3 | Bin 0 -> 9743 bytes influxframework/public/sounds/error.mp3 | Bin 0 -> 38451 bytes influxframework/public/sounds/submit.mp3 | Bin 0 -> 15463 bytes influxframework/query_builder/__init__.py | 22 + influxframework/query_builder/builder.py | 93 + influxframework/query_builder/custom.py | 104 + influxframework/query_builder/functions.py | 127 + influxframework/query_builder/terms.py | 120 + influxframework/query_builder/utils.py | 124 + influxframework/rate_limiter.py | 154 + influxframework/realtime.py | 143 + influxframework/recorder.py | 189 + influxframework/search/__init__.py | 14 + influxframework/search/full_text_search.py | 165 + .../search/test_full_text_search.py | 132 + influxframework/search/website_search.py | 145 + influxframework/sessions.py | 491 ++ influxframework/share.py | 227 + influxframework/social/__init__.py | 0 influxframework/social/doctype/__init__.py | 0 .../doctype/energy_point_log/__init__.py | 0 .../energy_point_log/energy_point_log.js | 50 + .../energy_point_log/energy_point_log.json | 134 + .../energy_point_log/energy_point_log.py | 379 ++ .../energy_point_log/energy_point_log_list.js | 29 + .../energy_point_log/test_energy_point_log.py | 378 ++ .../doctype/energy_point_rule/__init__.py | 0 .../energy_point_rule/energy_point_rule.js | 58 + .../energy_point_rule/energy_point_rule.json | 140 + .../energy_point_rule/energy_point_rule.py | 136 + .../doctype/energy_point_settings/__init__.py | 0 .../energy_point_settings.js | 50 + .../energy_point_settings.json | 70 + .../energy_point_settings.py | 65 + .../test_energy_point_settings.py | 9 + .../social/doctype/review_level/__init__.py | 0 .../doctype/review_level/review_level.js | 7 + .../doctype/review_level/review_level.json | 143 + .../doctype/review_level/review_level.py | 9 + influxframework/templates/__init__.py | 0 influxframework/templates/base.html | 118 + .../templates/discussions/button.html | 6 + .../templates/discussions/comment_box.html | 35 + .../templates/discussions/discussions.js | 310 + .../discussions/discussions_section.html | 77 + .../templates/discussions/reply_card.html | 50 + .../templates/discussions/reply_section.html | 52 + .../templates/discussions/search.html | 2 + .../templates/discussions/sidebar.html | 24 + .../templates/discussions/topic_modal.html | 15 + influxframework/templates/doc.html | 116 + influxframework/templates/emails/.txt | 0 influxframework/templates/emails/__init__.py | 0 .../emails/account_deletion_notification.html | 4 + .../emails/administrator_logged_in.html | 3 + .../templates/emails/auto_email_report.html | 67 + .../templates/emails/auto_repeat_fail.html | 8 + .../templates/emails/auto_reply.html | 7 + .../emails/data_deletion_approval.html | 8 + .../emails/delete_data_confirmation.html | 12 + .../templates/emails/document_follow.html | 88 + .../templates/emails/download_data.html | 10 + .../templates/emails/email_footer.html | 33 + .../templates/emails/email_header.html | 12 + .../emails/energy_points_summary.html | 54 + .../emails/file_backup_notification.html | 6 + .../templates/emails/new_message.html | 5 + .../templates/emails/new_notification.html | 11 + .../templates/emails/new_user.html | 24 + .../templates/emails/newsletter.html | 13 + .../templates/emails/password_reset.html | 7 + .../templates/emails/print_link.html | 3 + .../templates/emails/standard.html | 67 + .../templates/emails/upcoming_events.html | 9 + .../templates/emails/verification_code.html | 1 + .../templates/emails/workflow_action.html | 8 + .../templates/form_grid/fields.html | 46 + .../templates/includes/__init__.py | 0 .../app_analytics/google_analytics.html | 25 + .../app_analytics/heap_analytics.html | 6 + .../app_analytics/mixpanel_analytics.html | 6 + .../templates/includes/avatar_macro.html | 18 + .../templates/includes/blog/blogger.html | 14 + .../templates/includes/blog/hero.html | 12 + .../templates/includes/breadcrumbs.html | 24 + .../templates/includes/comments/__init__.py | 0 .../templates/includes/comments/comment.html | 18 + .../templates/includes/comments/comments.html | 299 + .../templates/includes/comments/comments.py | 67 + influxframework/templates/includes/contact.js | 47 + .../templates/includes/footer/footer.html | 12 + .../includes/footer/footer_extension.html | 0 .../includes/footer/footer_grouped_links.html | 33 + .../includes/footer/footer_info.html | 23 + .../includes/footer/footer_links.html | 24 + .../footer/footer_logo_extension.html | 16 + .../includes/footer/footer_powered.html | 1 + .../templates/includes/form_macros.html | 13 + .../templates/includes/full_index.html | 11 + .../templates/includes/image_with_blur.html | 29 + .../includes/integrations/third_party_apps.js | 9 + .../templates/includes/likes/__init__.py | 0 .../templates/includes/likes/likes.html | 41 + .../templates/includes/likes/likes.py | 77 + .../templates/includes/list/__init__.py | 0 .../templates/includes/list/filters.html | 22 + .../templates/includes/list/filters.js | 47 + .../templates/includes/list/list.html | 26 + .../templates/includes/list/list.js | 45 + .../templates/includes/list/row_template.html | 15 + .../templates/includes/login/login.js | 353 + .../templates/includes/macros.html | 13 + .../templates/includes/meta_block.html | 8 + .../includes/navbar/dropdown_items.html | 18 + .../includes/navbar/dropdown_login.html | 17 + .../templates/includes/navbar/navbar.html | 32 + .../includes/navbar/navbar_items.html | 108 + .../includes/navbar/navbar_login.html | 27 + .../includes/navbar/navbar_search.html | 9 + .../includes/oauth_confirmation.html | 47 + .../templates/includes/print_table.html | 26 + .../templates/includes/search_box.html | 28 + .../templates/includes/search_result.html | 6 + .../templates/includes/search_template.html | 52 + .../templates/includes/slideshow.html | 50 + .../templates/includes/splash_screen.html | 4 + .../templates/includes/static_index.html | 12 + .../templates/includes/web_block.html | 32 + .../templates/includes/web_sidebar.html | 82 + .../includes/website_theme/footer.css | 19 + .../includes/website_theme/navbar.css | 103 + influxframework/templates/pages/__init__.py | 0 .../templates/pages/integrations/__init__.py | 0 .../pages/integrations/gcalendar-success.html | 21 + .../templates/print_format/macros.html | 13 + .../templates/print_format/macros/Attach.html | 7 + .../print_format/macros/AttachImage.html | 7 + .../templates/print_format/macros/Check.html | 9 + .../templates/print_format/macros/Code.html | 7 + .../templates/print_format/macros/Color.html | 8 + .../templates/print_format/macros/Data.html | 10 + .../print_format/macros/Divider.html | 2 + .../print_format/macros/FieldTemplate.html | 4 + .../templates/print_format/macros/HTML.html | 3 + .../print_format/macros/Markdown.html | 9 + .../templates/print_format/macros/Rating.html | 22 + .../print_format/macros/Signature.html | 7 + .../templates/print_format/macros/Spacer.html | 2 + .../templates/print_format/macros/Table.html | 30 + .../templates/print_format/print_footer.html | 24 + .../templates/print_format/print_format.css | 135 + .../templates/print_format/print_format.html | 40 + .../print_format/print_format_font.css | 9 + .../templates/print_format/print_header.html | 24 + .../print_formats/pdf_header_footer.html | 75 + .../templates/print_formats/standard.html | 39 + .../print_formats/standard_macros.html | 215 + influxframework/templates/signup.html | 22 + .../templates/styles/card_style.css | 87 + .../templates/styles/discussion_style.css | 270 + influxframework/templates/styles/standard.css | 183 + .../templates/test/_test_base.html | 9 + .../test/_test_base_breadcrumbs.html | 20 + influxframework/templates/web.html | 76 + influxframework/test_runner.py | 543 ++ influxframework/tests/__init__.py | 17 + .../tests/data/email_with_image.txt | 5923 +++++++++++++++++ .../tests/data/exif_sample_image.jpg | Bin 0 -> 161713 bytes .../data/sample_image_for_optimization.jpg | Bin 0 -> 249403 bytes influxframework/tests/data/sample_svg.svg | 12 + influxframework/tests/test_api.py | 298 + influxframework/tests/test_assign.py | 96 + influxframework/tests/test_auth.py | 167 + influxframework/tests/test_background_jobs.py | 49 + influxframework/tests/test_base_document.py | 17 + influxframework/tests/test_boilerplate.py | 182 + influxframework/tests/test_boot.py | 28 + influxframework/tests/test_bot.py | 8 + influxframework/tests/test_caching.py | 94 + influxframework/tests/test_child_table.py | 63 + influxframework/tests/test_client.py | 243 + influxframework/tests/test_commands.py | 749 +++ influxframework/tests/test_config.py | 16 + influxframework/tests/test_cors.py | 67 + influxframework/tests/test_db.py | 909 +++ influxframework/tests/test_db_query.py | 833 +++ influxframework/tests/test_db_update.py | 137 + influxframework/tests/test_defaults.py | 73 + influxframework/tests/test_deferred_insert.py | 12 + influxframework/tests/test_docstatus.py | 25 + influxframework/tests/test_document.py | 458 ++ influxframework/tests/test_document_locks.py | 18 + influxframework/tests/test_domainification.py | 165 + influxframework/tests/test_dynamic_links.py | 83 + influxframework/tests/test_email.py | 317 + .../tests/test_exporter_fixtures.py | 286 + influxframework/tests/test_fixture_import.py | 78 + influxframework/tests/test_fmt_datetime.py | 129 + influxframework/tests/test_fmt_money.py | 109 + influxframework/tests/test_form_load.py | 199 + influxframework/tests/test_formatter.py | 19 + influxframework/tests/test_geo_ip.py | 13 + influxframework/tests/test_global_search.py | 207 + influxframework/tests/test_goal.py | 48 + influxframework/tests/test_hooks.py | 69 + .../tests/test_influxframework_client.py | 223 + influxframework/tests/test_linked_with.py | 127 + influxframework/tests/test_listview.py | 67 + influxframework/tests/test_model_utils.py | 27 + influxframework/tests/test_monitor.py | 68 + influxframework/tests/test_naming.py | 374 ++ influxframework/tests/test_nestedset.py | 240 + influxframework/tests/test_oauth20.py | 373 ++ influxframework/tests/test_password.py | 133 + influxframework/tests/test_patches.py | 177 + influxframework/tests/test_pdf.py | 52 + influxframework/tests/test_perf.py | 117 + influxframework/tests/test_permissions.py | 703 ++ influxframework/tests/test_printview.py | 20 + influxframework/tests/test_query.py | 126 + influxframework/tests/test_query_builder.py | 383 ++ influxframework/tests/test_query_report.py | 71 + influxframework/tests/test_rate_limiter.py | 115 + influxframework/tests/test_recorder.py | 125 + influxframework/tests/test_redis.py | 73 + influxframework/tests/test_rename_doc.py | 273 + influxframework/tests/test_safe_exec.py | 61 + influxframework/tests/test_scheduler.py | 113 + influxframework/tests/test_search.py | 259 + influxframework/tests/test_seen.py | 51 + influxframework/tests/test_sequence.py | 49 + influxframework/tests/test_sitemap.py | 15 + influxframework/tests/test_test_utils.py | 34 + influxframework/tests/test_translate.py | 250 + influxframework/tests/test_twofactor.py | 247 + influxframework/tests/test_utils.py | 680 ++ influxframework/tests/test_virtual_doctype.py | 153 + influxframework/tests/test_webform.py | 83 + influxframework/tests/test_website.py | 365 + influxframework/tests/tests_geo_utils.py | 54 + .../tests/translation_test_file.txt | 35 + influxframework/tests/ui_test_helpers.py | 580 ++ influxframework/tests/utils.py | 171 + influxframework/translate.py | 1304 ++++ influxframework/translations/af.csv | 4702 +++++++++++++ influxframework/translations/am.csv | 4702 +++++++++++++ influxframework/translations/ar.csv | 4702 +++++++++++++ influxframework/translations/bg.csv | 4702 +++++++++++++ influxframework/translations/bn.csv | 4702 +++++++++++++ influxframework/translations/bo.csv | 0 influxframework/translations/bs.csv | 4702 +++++++++++++ influxframework/translations/ca.csv | 4702 +++++++++++++ influxframework/translations/cs.csv | 4702 +++++++++++++ influxframework/translations/cz.csv | 1473 ++++ influxframework/translations/da-DK.csv | 9 + influxframework/translations/da.csv | 4702 +++++++++++++ influxframework/translations/da_dk.csv | 8 + influxframework/translations/de.csv | 4815 ++++++++++++++ influxframework/translations/el.csv | 4702 +++++++++++++ influxframework/translations/en-GB.csv | 0 influxframework/translations/en-US.csv | 18 + influxframework/translations/en.csv | 0 influxframework/translations/en_gb.csv | 0 influxframework/translations/en_us.csv | 18 + influxframework/translations/es-AR.csv | 3 + influxframework/translations/es-BO.csv | 0 influxframework/translations/es-CL.csv | 37 + influxframework/translations/es-CO.csv | 0 influxframework/translations/es-DO.csv | 0 influxframework/translations/es-EC.csv | 1 + influxframework/translations/es-GT.csv | 1 + influxframework/translations/es-MX.csv | 30 + influxframework/translations/es-NI.csv | 5 + influxframework/translations/es-PE.csv | 490 ++ influxframework/translations/es.csv | 4717 +++++++++++++ influxframework/translations/es_ar.csv | 2 + influxframework/translations/es_bo.csv | 0 influxframework/translations/es_cl.csv | 8 + influxframework/translations/es_co.csv | 6 + influxframework/translations/es_do.csv | 0 influxframework/translations/es_ec.csv | 0 influxframework/translations/es_es.csv | 0 influxframework/translations/es_gt.csv | 0 influxframework/translations/es_mx.csv | 8 + influxframework/translations/es_ni.csv | 5 + influxframework/translations/es_pe.csv | 426 ++ influxframework/translations/et.csv | 4702 +++++++++++++ influxframework/translations/fa.csv | 4702 +++++++++++++ influxframework/translations/fi.csv | 4702 +++++++++++++ influxframework/translations/fil.csv | 0 influxframework/translations/fr-CA.csv | 26 + influxframework/translations/fr.csv | 4741 +++++++++++++ influxframework/translations/fr_ca.csv | 1 + influxframework/translations/gu.csv | 4702 +++++++++++++ influxframework/translations/he.csv | 4702 +++++++++++++ influxframework/translations/hi.csv | 4702 +++++++++++++ influxframework/translations/hr.csv | 4702 +++++++++++++ influxframework/translations/hu.csv | 4702 +++++++++++++ influxframework/translations/id.csv | 4702 +++++++++++++ influxframework/translations/is.csv | 4702 +++++++++++++ influxframework/translations/it.csv | 4702 +++++++++++++ influxframework/translations/ja.csv | 4702 +++++++++++++ influxframework/translations/km.csv | 4702 +++++++++++++ influxframework/translations/kn.csv | 4702 +++++++++++++ influxframework/translations/ko.csv | 4702 +++++++++++++ influxframework/translations/ku.csv | 4702 +++++++++++++ influxframework/translations/lo.csv | 4702 +++++++++++++ influxframework/translations/lt.csv | 4702 +++++++++++++ influxframework/translations/lv.csv | 4702 +++++++++++++ influxframework/translations/mk.csv | 4702 +++++++++++++ influxframework/translations/ml.csv | 4702 +++++++++++++ influxframework/translations/mr.csv | 4702 +++++++++++++ influxframework/translations/ms.csv | 4702 +++++++++++++ influxframework/translations/my.csv | 4702 +++++++++++++ influxframework/translations/nl.csv | 4702 +++++++++++++ influxframework/translations/no.csv | 4702 +++++++++++++ influxframework/translations/pl.csv | 4702 +++++++++++++ influxframework/translations/ps.csv | 4702 +++++++++++++ influxframework/translations/pt-BR.csv | 4702 +++++++++++++ influxframework/translations/pt.csv | 4702 +++++++++++++ influxframework/translations/pt_br.csv | 4702 +++++++++++++ influxframework/translations/quc.csv | 0 influxframework/translations/ro.csv | 4702 +++++++++++++ influxframework/translations/ru.csv | 4695 +++++++++++++ influxframework/translations/rw.csv | 4702 +++++++++++++ influxframework/translations/se.csv | 0 influxframework/translations/si.csv | 4702 +++++++++++++ influxframework/translations/sk.csv | 4702 +++++++++++++ influxframework/translations/sl.csv | 4702 +++++++++++++ influxframework/translations/sq.csv | 4702 +++++++++++++ influxframework/translations/sr-BA.csv | 484 ++ influxframework/translations/sr-SP.csv | 484 ++ influxframework/translations/sr.csv | 4702 +++++++++++++ influxframework/translations/sr_ba.csv | 0 influxframework/translations/sr_sp.csv | 454 ++ influxframework/translations/sv.csv | 4702 +++++++++++++ influxframework/translations/sw.csv | 4702 +++++++++++++ influxframework/translations/ta.csv | 4702 +++++++++++++ influxframework/translations/te.csv | 4702 +++++++++++++ influxframework/translations/th.csv | 4702 +++++++++++++ influxframework/translations/tr.csv | 4702 +++++++++++++ influxframework/translations/uk.csv | 4702 +++++++++++++ influxframework/translations/ur.csv | 4702 +++++++++++++ influxframework/translations/uz.csv | 4702 +++++++++++++ influxframework/translations/vi.csv | 4702 +++++++++++++ influxframework/translations/zh-TW.csv | 3062 +++++++++ influxframework/translations/zh.csv | 4702 +++++++++++++ influxframework/translations/zh_tw.csv | 4202 ++++++++++++ influxframework/twofactor.py | 509 ++ influxframework/utils/__init__.py | 1055 +++ influxframework/utils/background_jobs.py | 377 ++ influxframework/utils/backups.py | 785 +++ influxframework/utils/bench_helper.py | 109 + influxframework/utils/boilerplate.py | 528 ++ influxframework/utils/caching.py | 130 + influxframework/utils/change_log.py | 311 + influxframework/utils/commands.py | 65 + influxframework/utils/connections.py | 45 + influxframework/utils/csvutils.py | 223 + influxframework/utils/dashboard.py | 115 + influxframework/utils/data.py | 2148 ++++++ influxframework/utils/dateutils.py | 173 + influxframework/utils/diff.py | 59 + influxframework/utils/doctor.py | 144 + influxframework/utils/error.py | 258 + influxframework/utils/file_lock.py | 53 + influxframework/utils/file_manager.py | 463 ++ influxframework/utils/fixtures.py | 69 + influxframework/utils/formatters.py | 141 + influxframework/utils/global_search.py | 553 ++ influxframework/utils/goal.py | 145 + influxframework/utils/html_utils.py | 745 +++ influxframework/utils/identicon.py | 107 + influxframework/utils/image.py | 69 + influxframework/utils/install.py | 341 + influxframework/utils/jinja.py | 176 + influxframework/utils/jinja_globals.py | 148 + influxframework/utils/lazy_loader.py | 35 + influxframework/utils/logger.py | 101 + influxframework/utils/make_random.py | 66 + influxframework/utils/momentjs.py | 4795 +++++++++++++ influxframework/utils/nestedset.py | 404 ++ influxframework/utils/oauth.py | 346 + influxframework/utils/password.py | 234 + influxframework/utils/password_strength.py | 189 + influxframework/utils/pdf.py | 255 + influxframework/utils/print_format.py | 172 + influxframework/utils/redis_queue.py | 85 + influxframework/utils/redis_wrapper.py | 250 + influxframework/utils/response.py | 282 + influxframework/utils/safe_exec.py | 487 ++ influxframework/utils/scheduler.py | 194 + influxframework/utils/subscription.py | 35 + influxframework/utils/testutils.py | 20 + influxframework/utils/user.py | 423 ++ influxframework/utils/verified_command.py | 84 + influxframework/utils/weasyprint.py | 253 + influxframework/utils/xlsxutils.py | 115 + influxframework/website/__init__.py | 2 + influxframework/website/css/web_form.css | 104 + influxframework/website/dashboard_fixtures.py | 43 + influxframework/website/doctype/__init__.py | 0 .../doctype/about_us_settings/README.md | 1 + .../doctype/about_us_settings/__init__.py | 0 .../about_us_settings/about_us_settings.js | 8 + .../about_us_settings/about_us_settings.json | 104 + .../about_us_settings/about_us_settings.py | 19 + .../test_about_us_settings.py | 8 + .../doctype/about_us_team_member/README.md | 1 + .../doctype/about_us_team_member/__init__.py | 0 .../about_us_team_member.json | 109 + .../about_us_team_member.py | 11 + .../website/doctype/blog_category/README.md | 1 + .../website/doctype/blog_category/__init__.py | 0 .../doctype/blog_category/blog_category.js | 6 + .../doctype/blog_category/blog_category.json | 76 + .../doctype/blog_category/blog_category.py | 18 + .../templates/blog_category.html | 9 + .../templates/blog_category_row.html | 4 + .../blog_category/test_blog_category.py | 8 + .../doctype/blog_category/test_records.json | 17 + .../website/doctype/blog_post/README.md | 1 + .../website/doctype/blog_post/__init__.py | 0 .../website/doctype/blog_post/blog_post.js | 59 + .../website/doctype/blog_post/blog_post.json | 253 + .../website/doctype/blog_post/blog_post.py | 354 + .../doctype/blog_post/blog_post_list.js | 10 + .../blog_post/templates/blog_post.html | 91 + .../blog_post/templates/blog_post_list.html | 90 + .../blog_post/templates/blog_post_row.html | 43 + .../doctype/blog_post/test_blog_post.py | 204 + .../doctype/blog_post/test_records.json | 38 + .../doctype/blog_post/ui_test_blog_post.js | 36 + .../website/doctype/blog_settings/README.md | 1 + .../website/doctype/blog_settings/__init__.py | 0 .../doctype/blog_settings/blog_settings.js | 6 + .../doctype/blog_settings/blog_settings.json | 154 + .../doctype/blog_settings/blog_settings.py | 23 + .../blog_settings/test_blog_settings.py | 8 + .../website/doctype/blogger/README.md | 1 + .../website/doctype/blogger/__init__.py | 0 .../website/doctype/blogger/blogger.js | 6 + .../website/doctype/blogger/blogger.json | 100 + .../website/doctype/blogger/blogger.py | 36 + .../website/doctype/blogger/test_blogger.py | 6 + .../website/doctype/blogger/test_records.json | 17 + .../website/doctype/color/__init__.py | 0 .../website/doctype/color/color.js | 7 + .../website/doctype/color/color.json | 44 + .../website/doctype/color/color.py | 9 + .../website/doctype/color/test_color.py | 8 + .../website/doctype/company_history/README.md | 1 + .../doctype/company_history/__init__.py | 0 .../company_history/company_history.json | 83 + .../company_history/company_history.py | 11 + .../doctype/contact_us_settings/README.md | 1 + .../doctype/contact_us_settings/__init__.py | 0 .../contact_us_settings.js | 8 + .../contact_us_settings.json | 143 + .../contact_us_settings.py | 14 + .../doctype/discussion_reply/__init__.py | 0 .../discussion_reply/discussion_reply.js | 7 + .../discussion_reply/discussion_reply.json | 53 + .../discussion_reply/discussion_reply.py | 62 + .../discussion_reply/test_discussion_reply.py | 9 + .../doctype/discussion_topic/__init__.py | 0 .../discussion_topic/discussion_topic.js | 7 + .../discussion_topic/discussion_topic.json | 63 + .../discussion_topic/discussion_topic.py | 48 + .../discussion_topic/test_discussion_topic.py | 9 + .../website/doctype/help_article/__init__.py | 0 .../doctype/help_article/help_article.js | 23 + .../doctype/help_article/help_article.json | 121 + .../doctype/help_article/help_article.py | 119 + .../help_article/templates/help_article.html | 64 + .../templates/help_article_row.html | 15 + .../doctype/help_article/test_help_article.py | 10 + .../website/doctype/help_category/__init__.py | 0 .../doctype/help_category/help_category.js | 6 + .../doctype/help_category/help_category.json | 202 + .../doctype/help_category/help_category.py | 26 + .../help_category/test_help_category.py | 10 + .../__init__.py | 0 .../personal_data_deletion_request.js | 34 + .../personal_data_deletion_request.json | 70 + .../personal_data_deletion_request.py | 393 ++ .../test_personal_data_deletion_request.py | 71 + .../personal_data_deletion_step/__init__.py | 0 .../personal_data_deletion_step.json | 66 + .../personal_data_deletion_step.py | 9 + .../__init__.py | 0 .../personal_data_download_request.js | 10 + .../personal_data_download_request.json | 65 + .../personal_data_download_request.py | 69 + .../test_personal_data_download_request.py | 68 + .../doctype/portal_menu_item/__init__.py | 0 .../portal_menu_item/portal_menu_item.json | 216 + .../portal_menu_item/portal_menu_item.py | 9 + .../doctype/portal_settings/__init__.py | 0 .../portal_settings/portal_settings.js | 25 + .../portal_settings/portal_settings.json | 79 + .../portal_settings/portal_settings.py | 52 + .../portal_settings/test_portal_settings.py | 8 + .../doctype/social_link_settings/__init__.py | 0 .../social_link_settings.json | 43 + .../social_link_settings.py | 9 + .../website/doctype/top_bar_item/README.md | 1 + .../website/doctype/top_bar_item/__init__.py | 0 .../doctype/top_bar_item/top_bar_item.json | 79 + .../doctype/top_bar_item/top_bar_item.py | 9 + .../website/doctype/web_form/__init__.py | 0 .../doctype/web_form/templates/web_form.html | 199 + .../web_form/templates/web_form_skeleton.html | 38 + .../doctype/web_form/templates/web_list.html | 44 + .../doctype/web_form/test_records.json | 49 + .../website/doctype/web_form/test_web_form.py | 77 + .../website/doctype/web_form/web_form.js | 249 + .../website/doctype/web_form/web_form.json | 358 + .../website/doctype/web_form/web_form.py | 610 ++ .../website/doctype/web_form/web_form_list.js | 10 + .../doctype/web_form_field/__init__.py | 0 .../web_form_field/web_form_field.json | 159 + .../doctype/web_form_field/web_form_field.py | 9 + .../doctype/web_form_list_column/__init__.py | 0 .../web_form_list_column.json | 48 + .../web_form_list_column.py | 9 + .../website/doctype/web_page/README.md | 1 + .../website/doctype/web_page/__init__.py | 0 .../doctype/web_page/templates/web_page.html | 62 + .../web_page/templates/web_page_row.html | 4 + .../doctype/web_page/test_records.json | 35 + .../website/doctype/web_page/test_web_page.py | 65 + .../website/doctype/web_page/web_page.js | 136 + .../website/doctype/web_page/web_page.json | 364 + .../website/doctype/web_page/web_page.py | 252 + .../website/doctype/web_page/web_page_list.js | 10 + .../doctype/web_page_block/__init__.py | 0 .../web_page_block/web_page_block.json | 128 + .../doctype/web_page_block/web_page_block.py | 9 + .../website/doctype/web_page_view/__init__.py | 0 .../web_page_view/test_web_page_view.py | 8 + .../doctype/web_page_view/web_page_view.js | 7 + .../doctype/web_page_view/web_page_view.json | 85 + .../doctype/web_page_view/web_page_view.py | 55 + .../website/doctype/web_template/__init__.py | 0 .../doctype/web_template/test_web_template.py | 115 + .../doctype/web_template/web_template.js | 25 + .../doctype/web_template/web_template.json | 80 + .../doctype/web_template/web_template.py | 113 + .../doctype/web_template_field/__init__.py | 0 .../test_web_template_field.py | 8 + .../web_template_field/web_template_field.js | 7 + .../web_template_field.json | 67 + .../web_template_field/web_template_field.py | 9 + .../doctype/website_meta_tag/__init__.py | 0 .../website_meta_tag/website_meta_tag.json | 107 + .../website_meta_tag/website_meta_tag.py | 19 + .../doctype/website_route_meta/__init__.py | 0 .../test_website_route_meta.py | 38 + .../website_route_meta/website_route_meta.js | 15 + .../website_route_meta.json | 43 + .../website_route_meta/website_route_meta.py | 10 + .../website_route_redirect/__init__.py | 0 .../website_route_redirect.json | 35 + .../website_route_redirect.py | 9 + .../website/doctype/website_script/README.md | 1 + .../doctype/website_script/__init__.py | 0 .../doctype/website_script/website_script.js | 6 + .../website_script/website_script.json | 88 + .../doctype/website_script/website_script.py | 17 + .../doctype/website_settings/README.md | 1 + .../doctype/website_settings/__init__.py | 0 .../website_settings/google_indexing.py | 60 + .../website_settings/test_website_settings.py | 35 + .../website_settings/website_settings.js | 151 + .../website_settings/website_settings.json | 454 ++ .../website_settings/website_settings.py | 244 + .../doctype/website_sidebar/__init__.py | 0 .../website_sidebar/test_website_sidebar.py | 10 + .../website_sidebar/website_sidebar.js | 6 + .../website_sidebar/website_sidebar.json | 119 + .../website_sidebar/website_sidebar.py | 30 + .../doctype/website_sidebar_item/__init__.py | 0 .../website_sidebar_item.json | 123 + .../website_sidebar_item.py | 9 + .../doctype/website_slideshow/README.md | 1 + .../doctype/website_slideshow/__init__.py | 0 .../test_website_slideshow.py | 10 + .../website_slideshow/website_slideshow.js | 59 + .../website_slideshow/website_slideshow.json | 192 + .../website_slideshow/website_slideshow.py | 39 + .../doctype/website_slideshow_item/README.md | 1 + .../website_slideshow_item/__init__.py | 0 .../website_slideshow_item.json | 56 + .../website_slideshow_item.py | 11 + .../website/doctype/website_theme/__init__.py | 0 .../doctype/website_theme/custom_theme.css | 17 + .../website_theme/test_website_theme.py | 40 + .../doctype/website_theme/website_theme.js | 102 + .../doctype/website_theme/website_theme.json | 210 + .../doctype/website_theme/website_theme.py | 195 + .../website_theme/website_theme_template.scss | 54 + .../website_theme_ignore_app/__init__.py | 0 .../website_theme_ignore_app.json | 32 + .../website_theme_ignore_app.py | 9 + influxframework/website/js/bootstrap-4.js | 65 + .../website/js/syntax_highlight.js | 18 + influxframework/website/js/website.js | 703 ++ .../module_onboarding/website/website.json | 38 + .../add_blog_category/add_blog_category.json | 19 + .../create_blogger/create_blogger.json | 19 + .../enable_website_tracking.json | 21 + .../introduction_to_website.json | 19 + .../web_page_tour/web_page_tour.json | 19 + .../website/page_renderers/base_renderer.py | 27 + .../page_renderers/base_template_page.py | 77 + .../website/page_renderers/document_page.py | 94 + .../website/page_renderers/error_page.py | 17 + .../website/page_renderers/list_page.py | 12 + .../website/page_renderers/not_found_page.py | 35 + .../page_renderers/not_permitted_page.py | 22 + .../website/page_renderers/print_page.py | 24 + .../website/page_renderers/redirect_page.py | 22 + .../website/page_renderers/static_page.py | 44 + .../website/page_renderers/template_page.py | 309 + .../website/page_renderers/web_form.py | 13 + influxframework/website/path_resolver.py | 175 + influxframework/website/report/__init__.py | 0 .../report/website_analytics/__init__.py | 0 .../website_analytics/website_analytics.js | 32 + .../website_analytics/website_analytics.json | 27 + .../website_analytics/website_analytics.py | 192 + influxframework/website/router.py | 317 + influxframework/website/serve.py | 34 + influxframework/website/utils.py | 581 ++ influxframework/website/web_form/__init__.py | 0 .../website/web_form/request_data/__init__.py | 0 .../web_form/request_data/request_data.js | 3 + .../web_form/request_data/request_data.json | 52 + .../web_form/request_data/request_data.py | 2 + .../request_to_delete_data/__init__.py | 0 .../request_to_delete_data.js | 18 + .../request_to_delete_data.json | 53 + .../request_to_delete_data.py | 3 + .../website/web_template/__init__.py | 0 .../web_template/cover_image/__init__.py | 0 .../web_template/cover_image/cover_image.html | 5 + .../web_template/cover_image/cover_image.json | 34 + .../web_template/discussions/__init__.py | 0 .../web_template/discussions/discussions.html | 6 + .../web_template/discussions/discussions.json | 43 + .../web_template/full_width_image/__init__.py | 0 .../full_width_image/full_width_image.html | 5 + .../full_width_image/full_width_image.json | 29 + .../website/web_template/hero/__init__.py | 0 .../website/web_template/hero/hero.html | 24 + .../website/web_template/hero/hero.json | 61 + .../hero_with_right_image/__init__.py | 0 .../hero_with_right_image.html | 33 + .../hero_with_right_image.json | 63 + .../website/web_template/markdown/__init__.py | 0 .../web_template/markdown/markdown.html | 5 + .../web_template/markdown/markdown.json | 30 + .../web_template/primary_navbar/__init__.py | 0 .../primary_navbar/primary_navbar.html | 29 + .../primary_navbar/primary_navbar.json | 17 + .../section_with_cards/__init__.py | 0 .../section_with_cards.html | 50 + .../section_with_cards.json | 305 + .../__init__.py | 0 .../section_with_collapsible_content.html | 42 + .../section_with_collapsible_content.json | 54 + .../web_template/section_with_cta/__init__.py | 0 .../section_with_cta/section_with_cta.html | 26 + .../section_with_cta/section_with_cta.json | 50 + .../section_with_embed/__init__.py | 0 .../section_with_embed.html | 10 + .../section_with_embed.json | 32 + .../section_with_features/__init__.py | 0 .../section_with_features.html | 32 + .../section_with_features.json | 65 + .../section_with_image/__init__.py | 0 .../section_with_image.html | 14 + .../section_with_image.json | 47 + .../section_with_image_grid/__init__.py | 0 .../section_with_image_grid.html | 18 + .../section_with_image_grid.json | 51 + .../section_with_small_cta/__init__.py | 0 .../section_with_small_cta.html | 20 + .../section_with_small_cta.json | 38 + .../section_with_tabs/__init__.py | 0 .../section_with_tabs/section_with_tabs.html | 52 + .../section_with_tabs/section_with_tabs.json | 98 + .../section_with_testimonials/__init__.py | 0 .../section_with_testimonials.html | 31 + .../section_with_testimonials.json | 73 + .../section_with_videos/__init__.py | 0 .../section_with_videos.html | 24 + .../section_with_videos.json | 61 + .../web_template/slideshow/__init__.py | 0 .../web_template/slideshow/slideshow.html | 55 + .../web_template/slideshow/slideshow.json | 21 + .../split_section_with_image/__init__.py | 0 .../split_section_with_image.html | 36 + .../split_section_with_image.json | 67 + .../web_template/standard_footer/__init__.py | 0 .../standard_footer/standard_footer.html | 1 + .../standard_footer/standard_footer.json | 13 + .../web_template/standard_navbar/__init__.py | 0 .../standard_navbar/standard_navbar.html | 1 + .../standard_navbar/standard_navbar.json | 13 + .../web_template/testimonial/__init__.py | 0 .../web_template/testimonial/testimonial.html | 13 + .../web_template/testimonial/testimonial.json | 32 + .../website/website_components/metatags.py | 71 + influxframework/website/website_generator.py | 180 + .../website/website_theme/__init__.py | 0 .../website_theme/standard/__init__.py | 0 .../website_theme/standard/standard.json | 20 + .../website/workspace/website/website.json | 282 + influxframework/workflow/__init__.py | 0 influxframework/workflow/doctype/__init__.py | 0 .../workflow/doctype/workflow/README.md | 1 + .../workflow/doctype/workflow/__init__.py | 2 + .../doctype/workflow/test_records.json | 1 + .../doctype/workflow/test_workflow.py | 249 + .../workflow/doctype/workflow/workflow.js | 157 + .../workflow/doctype/workflow/workflow.json | 123 + .../workflow/doctype/workflow/workflow.py | 143 + .../doctype/workflow/workflow_list.js | 12 + .../doctype/workflow_action/README.md | 1 + .../doctype/workflow_action/__init__.py | 2 + .../workflow_action/test_workflow_action.py | 9 + .../workflow_action/workflow_action.js | 6 + .../workflow_action/workflow_action.json | 99 + .../workflow_action/workflow_action.py | 494 ++ .../workflow_action/workflow_action_list.js | 17 + .../workflow_action_master/__init__.py | 0 .../workflow_action_master.js | 6 + .../workflow_action_master.json | 89 + .../workflow_action_master.py | 8 + .../__init__.py | 0 .../workflow_action_permitted_role.json | 33 + .../workflow_action_permitted_role.py | 9 + .../doctype/workflow_document_state/README.md | 1 + .../workflow_document_state/__init__.py | 2 + .../workflow_document_state.json | 102 + .../workflow_document_state.py | 9 + .../workflow/doctype/workflow_state/README.md | 1 + .../doctype/workflow_state/__init__.py | 2 + .../doctype/workflow_state/test_records.json | 1 + .../workflow_state/test_workflow_state.py | 5 + .../doctype/workflow_state/workflow_state.js | 6 + .../workflow_state/workflow_state.json | 153 + .../doctype/workflow_state/workflow_state.py | 9 + .../doctype/workflow_transition/README.md | 1 + .../doctype/workflow_transition/__init__.py | 2 + .../workflow_transition.json | 100 + .../workflow_transition.py | 9 + influxframework/www/404.html | 24 + influxframework/www/404.py | 6 + influxframework/www/__init__.py | 0 influxframework/www/_test/__init__.py | 0 influxframework/www/_test/_sidebar.json | 6 + .../www/_test/_test_custom_base.html | 4 + .../www/_test/_test_folder/__init__.py | 0 .../www/_test/_test_folder/_test_page.css | 3 + .../www/_test/_test_folder/_test_page.html | 6 + .../www/_test/_test_folder/_test_page.js | 1 + .../www/_test/_test_folder/_test_page.py | 3 + .../www/_test/_test_folder/_test_toc.md | 19 + .../www/_test/_test_folder/index.md | 9 + .../_test/_test_folder/new.csv/__init__.py | 0 .../www/_test/_test_folder/new.csv/index.html | 12 + influxframework/www/_test/_test_home_page.py | 2 + influxframework/www/_test/_test_metatags.html | 5 + influxframework/www/_test/_test_metatags.py | 6 + .../www/_test/_test_no_context.html | 1 + influxframework/www/_test/_test_no_context.py | 8 + .../www/_test/_test_safe_render_off.html | 7 + .../www/_test/_test_safe_render_on.html | 6 + influxframework/www/_test/_test_webform.py | 6 + influxframework/www/_test/assets/__init__.py | 0 .../www/_test/assets/css_asset.css | 1 + influxframework/www/_test/assets/file.zip | Bin 0 -> 156164 bytes influxframework/www/_test/assets/image | Bin 0 -> 161713 bytes influxframework/www/_test/assets/image.jpg | Bin 0 -> 161713 bytes .../www/_test/assets/js_asset.min.js | 2 + influxframework/www/_test/index.html | 1 + .../www/_test/problematic_page.html | 1 + .../www/_test/static-file-test.png | Bin 0 -> 440 bytes influxframework/www/about.html | 62 + influxframework/www/about.py | 12 + influxframework/www/app.html | 63 + influxframework/www/app.py | 96 + influxframework/www/complete_signup.html | 24 + influxframework/www/complete_signup.py | 2 + .../www/confirm_workflow_action.html | 16 + influxframework/www/contact.html | 87 + influxframework/www/contact.py | 70 + influxframework/www/error.html | 60 + influxframework/www/error.py | 16 + influxframework/www/list.html | 33 + influxframework/www/list.py | 248 + influxframework/www/login.html | 191 + influxframework/www/login.py | 144 + influxframework/www/me.html | 82 + influxframework/www/me.py | 16 + influxframework/www/message.html | 49 + influxframework/www/message.py | 34 + influxframework/www/modified_doc_alert.html | 19 + influxframework/www/printpreview.html | 10 + influxframework/www/printview.html | 48 + influxframework/www/printview.py | 644 ++ influxframework/www/profile.py | 9 + influxframework/www/qrcode.html | 27 + influxframework/www/qrcode.py | 40 + influxframework/www/robots.py | 13 + influxframework/www/robots.txt | 1 + influxframework/www/rss.py | 47 + influxframework/www/rss.xml | 18 + influxframework/www/search.html | 25 + influxframework/www/search.py | 63 + influxframework/www/sitemap.py | 72 + influxframework/www/sitemap.xml | 9 + influxframework/www/third_party_apps.html | 94 + influxframework/www/third_party_apps.py | 63 + influxframework/www/unsubscribe.html | 111 + influxframework/www/unsubscribe.py | 47 + influxframework/www/update-password.html | 235 + influxframework/www/update_password.py | 10 + influxframework/www/website_script.js | 33 + influxframework/www/website_script.py | 28 + node_utils.js | 50 + package.json | 84 + pyproject.toml | 108 + setup.py | 6 + sider.yml | 3 + socketio.js | 317 + yarn.lock | 3575 ++++++++++ 3015 files changed, 632866 insertions(+) create mode 100644 .editorconfig create mode 100644 .eslintignore create mode 100644 .eslintrc create mode 100644 .flake8 create mode 100644 .git-blame-ignore-revs create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/question-about-using-influxframework.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/dependabot.yml create mode 100644 .github/frappe-framework-logo.svg create mode 100644 .github/helper/consumer_db/mariadb.json create mode 100644 .github/helper/consumer_db/postgres.json create mode 100644 .github/helper/documentation.py create mode 100644 .github/helper/flake8.conf create mode 100644 .github/helper/install.sh create mode 100644 .github/helper/install_dependencies.sh create mode 100644 .github/helper/producer_db/mariadb.json create mode 100644 .github/helper/producer_db/postgres.json create mode 100644 .github/helper/roulette.py create mode 100644 .github/helper/translation.py create mode 100644 .github/labeler.yml create mode 100644 .github/stale.yml create mode 100644 .github/try-on-f-cloud-button.svg create mode 100644 .github/workflows/create-release.yml create mode 100644 .github/workflows/labeller.yml create mode 100644 .github/workflows/linters.yml create mode 100644 .github/workflows/on_release.yml create mode 100644 .github/workflows/patch-mariadb-tests.yml create mode 100644 .github/workflows/publish-assets-develop.yml create mode 100644 .github/workflows/server-mariadb-tests.yml create mode 100644 .github/workflows/server-postgres-tests.yml create mode 100644 .github/workflows/ui-tests.yml create mode 100644 .gitignore create mode 100644 .mergify.yml create mode 100644 .pre-commit-config.yaml create mode 100644 .releaserc create mode 100644 .snyk create mode 100644 .stylelintrc create mode 100644 CODEOWNERS create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 attributions.md create mode 100644 bandit.yml create mode 100644 codecov.yml create mode 100644 commitlint.config.js create mode 100644 cypress.config.js create mode 100644 cypress/fixtures/child_table_doctype.js create mode 100644 cypress/fixtures/child_table_doctype_1.js create mode 100644 cypress/fixtures/custom_submittable_doctype.js create mode 100644 cypress/fixtures/data_field_validation_doctype.js create mode 100644 cypress/fixtures/datetime_doctype.js create mode 100644 cypress/fixtures/doctype_to_link.js create mode 100644 cypress/fixtures/doctype_with_child_table.js create mode 100644 cypress/fixtures/doctype_with_phone.js create mode 100644 cypress/fixtures/doctype_with_tab_break.js create mode 100644 cypress/fixtures/example.json create mode 100644 cypress/fixtures/sample_image.jpg create mode 100644 cypress/integration/api.js create mode 100644 cypress/integration/awesome_bar.js create mode 100644 cypress/integration/control_attach.js create mode 100644 cypress/integration/control_autocomplete.js create mode 100644 cypress/integration/control_barcode.js create mode 100644 cypress/integration/control_color.js create mode 100644 cypress/integration/control_data.js create mode 100644 cypress/integration/control_date.js create mode 100644 cypress/integration/control_date_range.js create mode 100644 cypress/integration/control_duration.js create mode 100644 cypress/integration/control_dynamic_link.js create mode 100644 cypress/integration/control_float.js create mode 100644 cypress/integration/control_icon.js create mode 100644 cypress/integration/control_link.js create mode 100644 cypress/integration/control_markdown_editor.js create mode 100644 cypress/integration/control_phone.js create mode 100644 cypress/integration/control_rating.js create mode 100644 cypress/integration/control_select.js create mode 100644 cypress/integration/custom_buttons.js create mode 100644 cypress/integration/customize_form.js create mode 100644 cypress/integration/dashboard_chart.js create mode 100644 cypress/integration/dashboard_links.js create mode 100644 cypress/integration/data_field_form_validation.js create mode 100644 cypress/integration/datetime.js create mode 100644 cypress/integration/datetime_field_form_validation.js create mode 100644 cypress/integration/depends_on.js create mode 100644 cypress/integration/discussions.js create mode 100644 cypress/integration/file_uploader.js create mode 100644 cypress/integration/first_day_of_the_week.js create mode 100644 cypress/integration/folder_navigation.js create mode 100644 cypress/integration/form.js create mode 100644 cypress/integration/form_tab_break.js create mode 100644 cypress/integration/form_tour.js create mode 100644 cypress/integration/grid.js create mode 100644 cypress/integration/grid_configuration.js create mode 100644 cypress/integration/grid_keyboard_shortcut.js create mode 100644 cypress/integration/grid_pagination.js create mode 100644 cypress/integration/grid_search.js create mode 100644 cypress/integration/kanban.js create mode 100644 cypress/integration/list_paging.js create mode 100644 cypress/integration/list_view.js create mode 100644 cypress/integration/list_view_settings.js create mode 100644 cypress/integration/login.js create mode 100644 cypress/integration/multi_select_dialog.js create mode 100644 cypress/integration/navigation.js create mode 100644 cypress/integration/number_card.js create mode 100644 cypress/integration/query_report.js create mode 100644 cypress/integration/recorder.js create mode 100644 cypress/integration/relative_time_filters.js create mode 100644 cypress/integration/report_view.js create mode 100644 cypress/integration/routing.js create mode 100644 cypress/integration/sidebar.js create mode 100644 cypress/integration/table_multiselect.js create mode 100644 cypress/integration/theme_switcher_dialog.js create mode 100644 cypress/integration/timeline.js create mode 100644 cypress/integration/url_data_field.js create mode 100644 cypress/integration/view_routing.js create mode 100644 cypress/integration/web_form.js create mode 100644 cypress/integration/workspace.js create mode 100644 cypress/integration/workspace_blocks.js create mode 100644 cypress/plugins/index.js create mode 100644 cypress/support/commands.js create mode 100644 cypress/support/e2e.js create mode 100644 cypress/tsconfig.json create mode 100644 esbuild/build-cleanup.js create mode 100644 esbuild/esbuild.js create mode 100644 esbuild/ignore-assets.js create mode 100644 esbuild/index.js create mode 100644 esbuild/influxframework-html.js create mode 100644 esbuild/sass_options.js create mode 100644 esbuild/utils.js create mode 100644 generate_bootstrap_theme.js create mode 100644 hooks.md create mode 100644 influxframework/__init__.py create mode 100644 influxframework/api.py create mode 100644 influxframework/app.py create mode 100644 influxframework/auth.py create mode 100644 influxframework/automation/__init__.py create mode 100644 influxframework/automation/doctype/__init__.py create mode 100644 influxframework/automation/doctype/assignment_rule/__init__.py create mode 100644 influxframework/automation/doctype/assignment_rule/assignment_rule.js create mode 100644 influxframework/automation/doctype/assignment_rule/assignment_rule.json create mode 100644 influxframework/automation/doctype/assignment_rule/assignment_rule.py create mode 100644 influxframework/automation/doctype/assignment_rule/test_assignment_rule.py create mode 100644 influxframework/automation/doctype/assignment_rule_day/__init__.py create mode 100644 influxframework/automation/doctype/assignment_rule_day/assignment_rule_day.json create mode 100644 influxframework/automation/doctype/assignment_rule_day/assignment_rule_day.py create mode 100644 influxframework/automation/doctype/assignment_rule_user/__init__.py create mode 100644 influxframework/automation/doctype/assignment_rule_user/assignment_rule_user.json create mode 100644 influxframework/automation/doctype/assignment_rule_user/assignment_rule_user.py create mode 100644 influxframework/automation/doctype/auto_repeat/__init__.py create mode 100644 influxframework/automation/doctype/auto_repeat/auto_repeat.js create mode 100644 influxframework/automation/doctype/auto_repeat/auto_repeat.json create mode 100644 influxframework/automation/doctype/auto_repeat/auto_repeat.py create mode 100644 influxframework/automation/doctype/auto_repeat/auto_repeat_list.js create mode 100644 influxframework/automation/doctype/auto_repeat/auto_repeat_schedule.html create mode 100644 influxframework/automation/doctype/auto_repeat/test_auto_repeat.py create mode 100644 influxframework/automation/doctype/auto_repeat_day/__init__.py create mode 100644 influxframework/automation/doctype/auto_repeat_day/auto_repeat_day.json create mode 100644 influxframework/automation/doctype/auto_repeat_day/auto_repeat_day.py create mode 100644 influxframework/automation/doctype/milestone/__init__.py create mode 100644 influxframework/automation/doctype/milestone/milestone.js create mode 100644 influxframework/automation/doctype/milestone/milestone.json create mode 100644 influxframework/automation/doctype/milestone/milestone.py create mode 100644 influxframework/automation/doctype/milestone/test_milestone.py create mode 100644 influxframework/automation/doctype/milestone_tracker/__init__.py create mode 100644 influxframework/automation/doctype/milestone_tracker/milestone_tracker.js create mode 100644 influxframework/automation/doctype/milestone_tracker/milestone_tracker.json create mode 100644 influxframework/automation/doctype/milestone_tracker/milestone_tracker.py create mode 100644 influxframework/automation/doctype/milestone_tracker/test_milestone_tracker.py create mode 100644 influxframework/automation/workspace/tools/tools.json create mode 100644 influxframework/boot.py create mode 100644 influxframework/build.py create mode 100644 influxframework/cache_manager.py create mode 100644 influxframework/change_log/__init__.py create mode 100644 influxframework/change_log/current/readme.md create mode 100644 influxframework/change_log/v10/v10_0_0.md create mode 100644 influxframework/change_log/v11/v11_1_0.md create mode 100644 influxframework/change_log/v12/v12_0_0.md create mode 100644 influxframework/change_log/v13/v13_0_0.md create mode 100644 influxframework/change_log/v13/v13_1_0.md create mode 100644 influxframework/change_log/v13/v13_2_0.md create mode 100644 influxframework/change_log/v13/v13_3_0.md create mode 100644 influxframework/change_log/v14/v14_0_0.md create mode 100644 influxframework/change_log/v5/v5_0_18.md create mode 100644 influxframework/change_log/v5/v5_0_20.md create mode 100644 influxframework/change_log/v5/v5_0_32.md create mode 100644 influxframework/change_log/v5/v5_1_0.md create mode 100644 influxframework/change_log/v5/v5_1_1.md create mode 100644 influxframework/change_log/v5/v5_3_0.md create mode 100644 influxframework/change_log/v5/v5_4_0.md create mode 100644 influxframework/change_log/v6/v6_0_0.md create mode 100644 influxframework/change_log/v6/v6_0_8.md create mode 100644 influxframework/change_log/v6/v6_12_0.md create mode 100644 influxframework/change_log/v6/v6_13_0.md create mode 100644 influxframework/change_log/v6/v6_14_1.md create mode 100644 influxframework/change_log/v6/v6_15_0.md create mode 100644 influxframework/change_log/v6/v6_16_1.md create mode 100644 influxframework/change_log/v6/v6_16_4.md create mode 100644 influxframework/change_log/v6/v6_17_0.md create mode 100644 influxframework/change_log/v6/v6_1_0.md create mode 100644 influxframework/change_log/v6/v6_20_0.md create mode 100644 influxframework/change_log/v6/v6_21_0.md create mode 100644 influxframework/change_log/v6/v6_22_0.md create mode 100644 influxframework/change_log/v6/v6_23_0.md create mode 100644 influxframework/change_log/v6/v6_25_0.md create mode 100644 influxframework/change_log/v6/v6_26_0.md create mode 100644 influxframework/change_log/v6/v6_26_6.md create mode 100644 influxframework/change_log/v6/v6_27_1.md create mode 100644 influxframework/change_log/v6/v6_27_11.md create mode 100644 influxframework/change_log/v6/v6_2_0.md create mode 100644 influxframework/change_log/v6/v6_3_0.md create mode 100644 influxframework/change_log/v6/v6_4_0.md create mode 100644 influxframework/change_log/v6/v6_4_8.md create mode 100644 influxframework/change_log/v6/v6_5_0.md create mode 100644 influxframework/change_log/v6/v6_6_0.md create mode 100644 influxframework/change_log/v6/v6_7_0.md create mode 100644 influxframework/change_log/v6/v6_8_0.md create mode 100644 influxframework/change_log/v7/v7_0_0.md create mode 100644 influxframework/change_log/v7/v7_0_18.md create mode 100644 influxframework/change_log/v7/v7_1_0.md create mode 100644 influxframework/change_log/v7/v7_2_0.md create mode 100644 influxframework/change_log/v8/v8_0_0.md create mode 100644 influxframework/change_log/v8/v8_7_0.md create mode 100644 influxframework/change_log/v8/v8_8_0.md create mode 100644 influxframework/client.py create mode 100644 influxframework/commands/__init__.py create mode 100644 influxframework/commands/redis_utils.py create mode 100644 influxframework/commands/scheduler.py create mode 100644 influxframework/commands/site.py create mode 100644 influxframework/commands/translate.py create mode 100644 influxframework/commands/utils.py create mode 100644 influxframework/config/__init__.py create mode 100644 influxframework/contacts/__init__.py create mode 100644 influxframework/contacts/address_and_contact.py create mode 100644 influxframework/contacts/doctype/__init__.py create mode 100644 influxframework/contacts/doctype/address/__init__.py create mode 100644 influxframework/contacts/doctype/address/address.js create mode 100644 influxframework/contacts/doctype/address/address.json create mode 100644 influxframework/contacts/doctype/address/address.py create mode 100644 influxframework/contacts/doctype/address/test_address.py create mode 100644 influxframework/contacts/doctype/address_template/__init__.py create mode 100644 influxframework/contacts/doctype/address_template/address_template.js create mode 100644 influxframework/contacts/doctype/address_template/address_template.json create mode 100644 influxframework/contacts/doctype/address_template/address_template.py create mode 100644 influxframework/contacts/doctype/address_template/test_address_template.py create mode 100644 influxframework/contacts/doctype/contact/__init__.py create mode 100644 influxframework/contacts/doctype/contact/contact.js create mode 100644 influxframework/contacts/doctype/contact/contact.json create mode 100644 influxframework/contacts/doctype/contact/contact.py create mode 100644 influxframework/contacts/doctype/contact/contact_list.js create mode 100644 influxframework/contacts/doctype/contact/test_contact.py create mode 100644 influxframework/contacts/doctype/contact/test_records.json create mode 100644 influxframework/contacts/doctype/contact_email/__init__.py create mode 100644 influxframework/contacts/doctype/contact_email/contact_email.json create mode 100644 influxframework/contacts/doctype/contact_email/contact_email.py create mode 100644 influxframework/contacts/doctype/contact_phone/__init__.py create mode 100644 influxframework/contacts/doctype/contact_phone/contact_phone.json create mode 100644 influxframework/contacts/doctype/contact_phone/contact_phone.py create mode 100644 influxframework/contacts/doctype/gender/__init__.py create mode 100644 influxframework/contacts/doctype/gender/gender.js create mode 100644 influxframework/contacts/doctype/gender/gender.json create mode 100644 influxframework/contacts/doctype/gender/gender.py create mode 100644 influxframework/contacts/doctype/gender/test_gender.py create mode 100644 influxframework/contacts/doctype/salutation/__init__.py create mode 100644 influxframework/contacts/doctype/salutation/salutation.js create mode 100644 influxframework/contacts/doctype/salutation/salutation.json create mode 100644 influxframework/contacts/doctype/salutation/salutation.py create mode 100644 influxframework/contacts/doctype/salutation/test_records.json create mode 100644 influxframework/contacts/doctype/salutation/test_salutation.py create mode 100644 influxframework/contacts/report/__init__.py create mode 100644 influxframework/contacts/report/addresses_and_contacts/__init__.py create mode 100644 influxframework/contacts/report/addresses_and_contacts/addresses_and_contacts.js create mode 100644 influxframework/contacts/report/addresses_and_contacts/addresses_and_contacts.json create mode 100644 influxframework/contacts/report/addresses_and_contacts/addresses_and_contacts.py create mode 100644 influxframework/contacts/report/addresses_and_contacts/test_addresses_and_contacts.py create mode 100644 influxframework/core/README.md create mode 100644 influxframework/core/__init__.py create mode 100644 influxframework/core/api/__init__.py create mode 100644 influxframework/core/api/file.py create mode 100644 influxframework/core/doctype/__init__.py create mode 100644 influxframework/core/doctype/access_log/__init__.py create mode 100644 influxframework/core/doctype/access_log/access_log.js create mode 100644 influxframework/core/doctype/access_log/access_log.json create mode 100644 influxframework/core/doctype/access_log/access_log.py create mode 100644 influxframework/core/doctype/access_log/test_access_log.py create mode 100644 influxframework/core/doctype/activity_log/__init__.py create mode 100644 influxframework/core/doctype/activity_log/activity_log.js create mode 100644 influxframework/core/doctype/activity_log/activity_log.json create mode 100644 influxframework/core/doctype/activity_log/activity_log.py create mode 100644 influxframework/core/doctype/activity_log/activity_log_list.js create mode 100644 influxframework/core/doctype/activity_log/feed.py create mode 100644 influxframework/core/doctype/activity_log/test_activity_log.py create mode 100644 influxframework/core/doctype/block_module/__init__.py create mode 100644 influxframework/core/doctype/block_module/block_module.json create mode 100644 influxframework/core/doctype/block_module/block_module.py create mode 100644 influxframework/core/doctype/comment/__init__.py create mode 100644 influxframework/core/doctype/comment/comment.js create mode 100644 influxframework/core/doctype/comment/comment.json create mode 100644 influxframework/core/doctype/comment/comment.py create mode 100644 influxframework/core/doctype/comment/test_comment.py create mode 100644 influxframework/core/doctype/communication/README.md create mode 100644 influxframework/core/doctype/communication/__init__.py create mode 100644 influxframework/core/doctype/communication/communication.js create mode 100644 influxframework/core/doctype/communication/communication.json create mode 100644 influxframework/core/doctype/communication/communication.py create mode 100644 influxframework/core/doctype/communication/communication_list.js create mode 100644 influxframework/core/doctype/communication/email.py create mode 100644 influxframework/core/doctype/communication/mixins.py create mode 100644 influxframework/core/doctype/communication/test_communication.py create mode 100644 influxframework/core/doctype/communication/test_records.json create mode 100644 influxframework/core/doctype/communication_link/__init__.py create mode 100644 influxframework/core/doctype/communication_link/communication_link.json create mode 100644 influxframework/core/doctype/communication_link/communication_link.py create mode 100644 influxframework/core/doctype/custom_docperm/__init__.py create mode 100644 influxframework/core/doctype/custom_docperm/custom_docperm.js create mode 100644 influxframework/core/doctype/custom_docperm/custom_docperm.json create mode 100644 influxframework/core/doctype/custom_docperm/custom_docperm.py create mode 100644 influxframework/core/doctype/custom_docperm/test_custom_docperm.py create mode 100644 influxframework/core/doctype/custom_role/__init__.py create mode 100644 influxframework/core/doctype/custom_role/custom_role.js create mode 100644 influxframework/core/doctype/custom_role/custom_role.json create mode 100644 influxframework/core/doctype/custom_role/custom_role.py create mode 100644 influxframework/core/doctype/custom_role/test_custom_role.py create mode 100644 influxframework/core/doctype/data_export/__init__.py create mode 100644 influxframework/core/doctype/data_export/data_export.js create mode 100644 influxframework/core/doctype/data_export/data_export.json create mode 100644 influxframework/core/doctype/data_export/data_export.py create mode 100644 influxframework/core/doctype/data_export/exporter.py create mode 100644 influxframework/core/doctype/data_export/test_data_exporter.py create mode 100644 influxframework/core/doctype/data_import/__init__.py create mode 100644 influxframework/core/doctype/data_import/data_import.css create mode 100644 influxframework/core/doctype/data_import/data_import.js create mode 100644 influxframework/core/doctype/data_import/data_import.json create mode 100644 influxframework/core/doctype/data_import/data_import.py create mode 100644 influxframework/core/doctype/data_import/data_import_list.js create mode 100644 influxframework/core/doctype/data_import/exporter.py create mode 100644 influxframework/core/doctype/data_import/fixtures/sample_import_file.csv create mode 100644 influxframework/core/doctype/data_import/fixtures/sample_import_file_for_update.csv create mode 100644 influxframework/core/doctype/data_import/fixtures/sample_import_file_without_mandatory.csv create mode 100644 influxframework/core/doctype/data_import/importer.py create mode 100644 influxframework/core/doctype/data_import/test_data_import.py create mode 100644 influxframework/core/doctype/data_import/test_exporter.py create mode 100644 influxframework/core/doctype/data_import/test_importer.py create mode 100644 influxframework/core/doctype/data_import_log/__init__.py create mode 100644 influxframework/core/doctype/data_import_log/data_import_log.js create mode 100644 influxframework/core/doctype/data_import_log/data_import_log.json create mode 100644 influxframework/core/doctype/data_import_log/data_import_log.py create mode 100644 influxframework/core/doctype/data_import_log/test_data_import_log.py create mode 100644 influxframework/core/doctype/defaultvalue/README.md create mode 100644 influxframework/core/doctype/defaultvalue/__init__.py create mode 100644 influxframework/core/doctype/defaultvalue/defaultvalue.json create mode 100644 influxframework/core/doctype/defaultvalue/defaultvalue.py create mode 100644 influxframework/core/doctype/deleted_document/__init__.py create mode 100644 influxframework/core/doctype/deleted_document/deleted_document.js create mode 100644 influxframework/core/doctype/deleted_document/deleted_document.json create mode 100644 influxframework/core/doctype/deleted_document/deleted_document.py create mode 100644 influxframework/core/doctype/deleted_document/deleted_document_list.js create mode 100644 influxframework/core/doctype/deleted_document/test_deleted_document.py create mode 100644 influxframework/core/doctype/docfield/README.md create mode 100644 influxframework/core/doctype/docfield/__init__.py create mode 100644 influxframework/core/doctype/docfield/docfield.json create mode 100644 influxframework/core/doctype/docfield/docfield.py create mode 100644 influxframework/core/doctype/docperm/__init__.py create mode 100644 influxframework/core/doctype/docperm/docperm.json create mode 100644 influxframework/core/doctype/docperm/docperm.py create mode 100644 influxframework/core/doctype/docshare/__init__.py create mode 100644 influxframework/core/doctype/docshare/docshare.js create mode 100644 influxframework/core/doctype/docshare/docshare.json create mode 100644 influxframework/core/doctype/docshare/docshare.py create mode 100644 influxframework/core/doctype/docshare/test_docshare.py create mode 100644 influxframework/core/doctype/docshare/test_records.json create mode 100644 influxframework/core/doctype/doctype/README.md create mode 100644 influxframework/core/doctype/doctype/__init__.py create mode 100644 influxframework/core/doctype/doctype/boilerplate/controller._py create mode 100644 influxframework/core/doctype/doctype/boilerplate/controller.js create mode 100644 influxframework/core/doctype/doctype/boilerplate/controller_list.html create mode 100644 influxframework/core/doctype/doctype/boilerplate/controller_list.js create mode 100644 influxframework/core/doctype/doctype/boilerplate/templates/controller.html create mode 100644 influxframework/core/doctype/doctype/boilerplate/templates/controller_row.html create mode 100644 influxframework/core/doctype/doctype/boilerplate/test_controller._py create mode 100644 influxframework/core/doctype/doctype/doctype.js create mode 100644 influxframework/core/doctype/doctype/doctype.json create mode 100644 influxframework/core/doctype/doctype/doctype.py create mode 100644 influxframework/core/doctype/doctype/patches/set_route.py create mode 100644 influxframework/core/doctype/doctype/test_doctype.py create mode 100644 influxframework/core/doctype/doctype_action/__init__.py create mode 100644 influxframework/core/doctype/doctype_action/doctype_action.json create mode 100644 influxframework/core/doctype/doctype_action/doctype_action.py create mode 100644 influxframework/core/doctype/doctype_link/__init__.py create mode 100644 influxframework/core/doctype/doctype_link/doctype_link.json create mode 100644 influxframework/core/doctype/doctype_link/doctype_link.py create mode 100644 influxframework/core/doctype/doctype_state/__init__.py create mode 100644 influxframework/core/doctype/doctype_state/doctype_state.json create mode 100644 influxframework/core/doctype/doctype_state/doctype_state.py create mode 100644 influxframework/core/doctype/document_naming_rule/__init__.py create mode 100644 influxframework/core/doctype/document_naming_rule/document_naming_rule.js create mode 100644 influxframework/core/doctype/document_naming_rule/document_naming_rule.json create mode 100644 influxframework/core/doctype/document_naming_rule/document_naming_rule.py create mode 100644 influxframework/core/doctype/document_naming_rule/test_document_naming_rule.py create mode 100644 influxframework/core/doctype/document_naming_rule_condition/__init__.py create mode 100644 influxframework/core/doctype/document_naming_rule_condition/document_naming_rule_condition.js create mode 100644 influxframework/core/doctype/document_naming_rule_condition/document_naming_rule_condition.json create mode 100644 influxframework/core/doctype/document_naming_rule_condition/document_naming_rule_condition.py create mode 100644 influxframework/core/doctype/document_naming_rule_condition/test_document_naming_rule_condition.py create mode 100644 influxframework/core/doctype/document_naming_settings/__init__.py create mode 100644 influxframework/core/doctype/document_naming_settings/document_naming_settings.js create mode 100644 influxframework/core/doctype/document_naming_settings/document_naming_settings.json create mode 100644 influxframework/core/doctype/document_naming_settings/document_naming_settings.py create mode 100644 influxframework/core/doctype/document_naming_settings/test_document_naming_settings.py create mode 100644 influxframework/core/doctype/document_share_key/__init__.py create mode 100644 influxframework/core/doctype/document_share_key/document_share_key.js create mode 100644 influxframework/core/doctype/document_share_key/document_share_key.json create mode 100644 influxframework/core/doctype/document_share_key/document_share_key.py create mode 100644 influxframework/core/doctype/document_share_key/test_document_share_key.py create mode 100644 influxframework/core/doctype/domain/__init__.py create mode 100644 influxframework/core/doctype/domain/domain.js create mode 100644 influxframework/core/doctype/domain/domain.json create mode 100644 influxframework/core/doctype/domain/domain.py create mode 100644 influxframework/core/doctype/domain/test_domain.py create mode 100644 influxframework/core/doctype/domain_settings/__init__.py create mode 100644 influxframework/core/doctype/domain_settings/domain_settings.js create mode 100644 influxframework/core/doctype/domain_settings/domain_settings.json create mode 100644 influxframework/core/doctype/domain_settings/domain_settings.py create mode 100644 influxframework/core/doctype/dynamic_link/__init__.py create mode 100644 influxframework/core/doctype/dynamic_link/dynamic_link.json create mode 100644 influxframework/core/doctype/dynamic_link/dynamic_link.py create mode 100644 influxframework/core/doctype/error_log/__init__.py create mode 100644 influxframework/core/doctype/error_log/error_log.js create mode 100644 influxframework/core/doctype/error_log/error_log.json create mode 100644 influxframework/core/doctype/error_log/error_log.py create mode 100644 influxframework/core/doctype/error_log/error_log_list.js create mode 100644 influxframework/core/doctype/error_log/test_error_log.py create mode 100644 influxframework/core/doctype/error_snapshot/__init__.py create mode 100644 influxframework/core/doctype/error_snapshot/error_object.html create mode 100644 influxframework/core/doctype/error_snapshot/error_snapshot.html create mode 100644 influxframework/core/doctype/error_snapshot/error_snapshot.js create mode 100644 influxframework/core/doctype/error_snapshot/error_snapshot.json create mode 100644 influxframework/core/doctype/error_snapshot/error_snapshot.py create mode 100644 influxframework/core/doctype/error_snapshot/error_snapshot_list.js create mode 100644 influxframework/core/doctype/error_snapshot/test_error_snapshot.py create mode 100644 influxframework/core/doctype/file/__init__.py create mode 100644 influxframework/core/doctype/file/exceptions.py create mode 100644 influxframework/core/doctype/file/file.js create mode 100644 influxframework/core/doctype/file/file.json create mode 100644 influxframework/core/doctype/file/file.py create mode 100644 influxframework/core/doctype/file/file_list.js create mode 100644 influxframework/core/doctype/file/test_file.py create mode 100644 influxframework/core/doctype/file/utils.py create mode 100644 influxframework/core/doctype/has_domain/__init__.py create mode 100644 influxframework/core/doctype/has_domain/has_domain.json create mode 100644 influxframework/core/doctype/has_domain/has_domain.py create mode 100644 influxframework/core/doctype/has_role/__init__.py create mode 100644 influxframework/core/doctype/has_role/has_role.json create mode 100644 influxframework/core/doctype/has_role/has_role.py create mode 100644 influxframework/core/doctype/installed_application/__init__.py create mode 100644 influxframework/core/doctype/installed_application/installed_application.json create mode 100644 influxframework/core/doctype/installed_application/installed_application.py create mode 100644 influxframework/core/doctype/installed_applications/__init__.py create mode 100644 influxframework/core/doctype/installed_applications/installed_applications.js create mode 100644 influxframework/core/doctype/installed_applications/installed_applications.json create mode 100644 influxframework/core/doctype/installed_applications/installed_applications.py create mode 100644 influxframework/core/doctype/installed_applications/test_installed_applications.py create mode 100644 influxframework/core/doctype/language/__init__.py create mode 100644 influxframework/core/doctype/language/language.js create mode 100644 influxframework/core/doctype/language/language.json create mode 100644 influxframework/core/doctype/language/language.py create mode 100644 influxframework/core/doctype/language/test_language.py create mode 100644 influxframework/core/doctype/log_setting_user/__init__.py create mode 100644 influxframework/core/doctype/log_setting_user/log_setting_user.js create mode 100644 influxframework/core/doctype/log_setting_user/log_setting_user.json create mode 100644 influxframework/core/doctype/log_setting_user/log_setting_user.py create mode 100644 influxframework/core/doctype/log_setting_user/test_log_setting_user.py create mode 100644 influxframework/core/doctype/log_settings/__init__.py create mode 100644 influxframework/core/doctype/log_settings/log_settings.js create mode 100644 influxframework/core/doctype/log_settings/log_settings.json create mode 100644 influxframework/core/doctype/log_settings/log_settings.py create mode 100644 influxframework/core/doctype/log_settings/test_log_settings.py create mode 100644 influxframework/core/doctype/logs_to_clear/__init__.py create mode 100644 influxframework/core/doctype/logs_to_clear/logs_to_clear.json create mode 100644 influxframework/core/doctype/logs_to_clear/logs_to_clear.py create mode 100644 influxframework/core/doctype/module_def/README.md create mode 100644 influxframework/core/doctype/module_def/__init__.py create mode 100644 influxframework/core/doctype/module_def/module_def.js create mode 100644 influxframework/core/doctype/module_def/module_def.json create mode 100644 influxframework/core/doctype/module_def/module_def.py create mode 100644 influxframework/core/doctype/module_def/test_module_def.py create mode 100644 influxframework/core/doctype/module_profile/__init__.py create mode 100644 influxframework/core/doctype/module_profile/module_profile.js create mode 100644 influxframework/core/doctype/module_profile/module_profile.json create mode 100644 influxframework/core/doctype/module_profile/module_profile.py create mode 100644 influxframework/core/doctype/module_profile/test_module_profile.py create mode 100644 influxframework/core/doctype/navbar_item/__init__.py create mode 100644 influxframework/core/doctype/navbar_item/navbar_item.js create mode 100644 influxframework/core/doctype/navbar_item/navbar_item.json create mode 100644 influxframework/core/doctype/navbar_item/navbar_item.py create mode 100644 influxframework/core/doctype/navbar_item/test_navbar_item.py create mode 100644 influxframework/core/doctype/navbar_settings/__init__.py create mode 100644 influxframework/core/doctype/navbar_settings/navbar_settings.js create mode 100644 influxframework/core/doctype/navbar_settings/navbar_settings.json create mode 100644 influxframework/core/doctype/navbar_settings/navbar_settings.py create mode 100644 influxframework/core/doctype/navbar_settings/test_navbar_settings.py create mode 100644 influxframework/core/doctype/package/__init__.py create mode 100644 influxframework/core/doctype/package/licenses/GNU Affero General Public License.md create mode 100644 influxframework/core/doctype/package/licenses/GNU General Public License.md create mode 100644 influxframework/core/doctype/package/licenses/MIT License.md create mode 100644 influxframework/core/doctype/package/package.js create mode 100644 influxframework/core/doctype/package/package.json create mode 100644 influxframework/core/doctype/package/package.py create mode 100644 influxframework/core/doctype/package/test_package.py create mode 100644 influxframework/core/doctype/package_import/__init__.py create mode 100644 influxframework/core/doctype/package_import/package_import.js create mode 100644 influxframework/core/doctype/package_import/package_import.json create mode 100644 influxframework/core/doctype/package_import/package_import.py create mode 100644 influxframework/core/doctype/package_import/test_package_import.py create mode 100644 influxframework/core/doctype/package_release/__init__.py create mode 100644 influxframework/core/doctype/package_release/package_release.js create mode 100644 influxframework/core/doctype/package_release/package_release.json create mode 100644 influxframework/core/doctype/package_release/package_release.py create mode 100644 influxframework/core/doctype/package_release/test_package_release.py create mode 100644 influxframework/core/doctype/page/README.md create mode 100644 influxframework/core/doctype/page/__init__.py create mode 100644 influxframework/core/doctype/page/page.js create mode 100644 influxframework/core/doctype/page/page.json create mode 100644 influxframework/core/doctype/page/page.py create mode 100644 influxframework/core/doctype/page/patches/drop_unused_pages.py create mode 100644 influxframework/core/doctype/page/test_page.py create mode 100644 influxframework/core/doctype/page/test_records.json create mode 100644 influxframework/core/doctype/patch_log/README.md create mode 100644 influxframework/core/doctype/patch_log/__init__.py create mode 100644 influxframework/core/doctype/patch_log/patch_log.js create mode 100644 influxframework/core/doctype/patch_log/patch_log.json create mode 100644 influxframework/core/doctype/patch_log/patch_log.py create mode 100644 influxframework/core/doctype/patch_log/test_patch_log.py create mode 100644 influxframework/core/doctype/prepared_report/__init__.py create mode 100644 influxframework/core/doctype/prepared_report/prepared_report.js create mode 100644 influxframework/core/doctype/prepared_report/prepared_report.json create mode 100644 influxframework/core/doctype/prepared_report/prepared_report.py create mode 100644 influxframework/core/doctype/prepared_report/prepared_report_list.js create mode 100644 influxframework/core/doctype/prepared_report/test_prepared_report.py create mode 100644 influxframework/core/doctype/report/README.md create mode 100644 influxframework/core/doctype/report/__init__.py create mode 100644 influxframework/core/doctype/report/boilerplate/controller.js create mode 100644 influxframework/core/doctype/report/boilerplate/controller.py create mode 100644 influxframework/core/doctype/report/report.js create mode 100644 influxframework/core/doctype/report/report.json create mode 100644 influxframework/core/doctype/report/report.py create mode 100644 influxframework/core/doctype/report/test_records.json create mode 100644 influxframework/core/doctype/report/test_report.py create mode 100644 influxframework/core/doctype/report/user_activity_report.json create mode 100644 influxframework/core/doctype/report/user_activity_report_without_sort.json create mode 100644 influxframework/core/doctype/report_column/__init__.py create mode 100644 influxframework/core/doctype/report_column/report_column.json create mode 100644 influxframework/core/doctype/report_column/report_column.py create mode 100644 influxframework/core/doctype/report_filter/__init__.py create mode 100644 influxframework/core/doctype/report_filter/report_filter.json create mode 100644 influxframework/core/doctype/report_filter/report_filter.py create mode 100644 influxframework/core/doctype/role/README.md create mode 100644 influxframework/core/doctype/role/__init__.py create mode 100644 influxframework/core/doctype/role/patches/v13_set_default_desk_properties.py create mode 100644 influxframework/core/doctype/role/role.js create mode 100644 influxframework/core/doctype/role/role.json create mode 100644 influxframework/core/doctype/role/role.py create mode 100644 influxframework/core/doctype/role/test_records.json create mode 100644 influxframework/core/doctype/role/test_role.py create mode 100644 influxframework/core/doctype/role_permission_for_page_and_report/__init__.py create mode 100644 influxframework/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.js create mode 100644 influxframework/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.json create mode 100644 influxframework/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py create mode 100644 influxframework/core/doctype/role_profile/__init__.py create mode 100644 influxframework/core/doctype/role_profile/role_profile.js create mode 100644 influxframework/core/doctype/role_profile/role_profile.json create mode 100644 influxframework/core/doctype/role_profile/role_profile.py create mode 100644 influxframework/core/doctype/role_profile/test_role_profile.py create mode 100644 influxframework/core/doctype/rq_job/__init__.py create mode 100644 influxframework/core/doctype/rq_job/rq_job.js create mode 100644 influxframework/core/doctype/rq_job/rq_job.json create mode 100644 influxframework/core/doctype/rq_job/rq_job.py create mode 100644 influxframework/core/doctype/rq_job/rq_job_list.js create mode 100644 influxframework/core/doctype/rq_job/test_rq_job.py create mode 100644 influxframework/core/doctype/rq_worker/__init__.py create mode 100644 influxframework/core/doctype/rq_worker/rq_worker.js create mode 100644 influxframework/core/doctype/rq_worker/rq_worker.json create mode 100644 influxframework/core/doctype/rq_worker/rq_worker.py create mode 100644 influxframework/core/doctype/rq_worker/test_rq_worker.py create mode 100644 influxframework/core/doctype/scheduled_job_log/__init__.py create mode 100644 influxframework/core/doctype/scheduled_job_log/scheduled_job_log.js create mode 100644 influxframework/core/doctype/scheduled_job_log/scheduled_job_log.json create mode 100644 influxframework/core/doctype/scheduled_job_log/scheduled_job_log.py create mode 100644 influxframework/core/doctype/scheduled_job_log/scheduled_job_log_list.js create mode 100644 influxframework/core/doctype/scheduled_job_log/test_scheduled_job_log.py create mode 100644 influxframework/core/doctype/scheduled_job_type/__init__.py create mode 100644 influxframework/core/doctype/scheduled_job_type/scheduled_job_type.js create mode 100644 influxframework/core/doctype/scheduled_job_type/scheduled_job_type.json create mode 100644 influxframework/core/doctype/scheduled_job_type/scheduled_job_type.py create mode 100644 influxframework/core/doctype/scheduled_job_type/test_scheduled_job_type.py create mode 100644 influxframework/core/doctype/server_script/__init__.py create mode 100644 influxframework/core/doctype/server_script/server_script.js create mode 100644 influxframework/core/doctype/server_script/server_script.json create mode 100644 influxframework/core/doctype/server_script/server_script.py create mode 100644 influxframework/core/doctype/server_script/server_script_utils.py create mode 100644 influxframework/core/doctype/server_script/test_server_script.py create mode 100644 influxframework/core/doctype/session_default/__init__.py create mode 100644 influxframework/core/doctype/session_default/session_default.json create mode 100644 influxframework/core/doctype/session_default/session_default.py create mode 100644 influxframework/core/doctype/session_default_settings/__init__.py create mode 100644 influxframework/core/doctype/session_default_settings/session_default_settings.js create mode 100644 influxframework/core/doctype/session_default_settings/session_default_settings.json create mode 100644 influxframework/core/doctype/session_default_settings/session_default_settings.py create mode 100644 influxframework/core/doctype/session_default_settings/test_session_default_settings.py create mode 100644 influxframework/core/doctype/sms_parameter/README.md create mode 100644 influxframework/core/doctype/sms_parameter/__init__.py create mode 100644 influxframework/core/doctype/sms_parameter/sms_parameter.json create mode 100644 influxframework/core/doctype/sms_parameter/sms_parameter.py create mode 100644 influxframework/core/doctype/sms_settings/README.md create mode 100644 influxframework/core/doctype/sms_settings/__init__.py create mode 100644 influxframework/core/doctype/sms_settings/sms_settings.js create mode 100644 influxframework/core/doctype/sms_settings/sms_settings.json create mode 100644 influxframework/core/doctype/sms_settings/sms_settings.py create mode 100644 influxframework/core/doctype/sms_settings/test_sms_settings.py create mode 100644 influxframework/core/doctype/success_action/__init__.py create mode 100644 influxframework/core/doctype/success_action/success_action.js create mode 100644 influxframework/core/doctype/success_action/success_action.json create mode 100644 influxframework/core/doctype/success_action/success_action.py create mode 100644 influxframework/core/doctype/system_settings/__init__.py create mode 100644 influxframework/core/doctype/system_settings/system_settings.js create mode 100644 influxframework/core/doctype/system_settings/system_settings.json create mode 100644 influxframework/core/doctype/system_settings/system_settings.py create mode 100644 influxframework/core/doctype/system_settings/test_system_settings.py create mode 100644 influxframework/core/doctype/transaction_log/__init__.py create mode 100644 influxframework/core/doctype/transaction_log/readme.md create mode 100644 influxframework/core/doctype/transaction_log/test_transaction_log.py create mode 100644 influxframework/core/doctype/transaction_log/transaction_log.js create mode 100644 influxframework/core/doctype/transaction_log/transaction_log.json create mode 100644 influxframework/core/doctype/transaction_log/transaction_log.py create mode 100644 influxframework/core/doctype/translation/__init__.py create mode 100644 influxframework/core/doctype/translation/test_translation.py create mode 100644 influxframework/core/doctype/translation/translation.js create mode 100644 influxframework/core/doctype/translation/translation.json create mode 100644 influxframework/core/doctype/translation/translation.py create mode 100644 influxframework/core/doctype/user/__init__.py create mode 100644 influxframework/core/doctype/user/test_records.json create mode 100644 influxframework/core/doctype/user/test_user.py create mode 100644 influxframework/core/doctype/user/user.js create mode 100644 influxframework/core/doctype/user/user.json create mode 100644 influxframework/core/doctype/user/user.py create mode 100644 influxframework/core/doctype/user/user_list.js create mode 100644 influxframework/core/doctype/user_document_type/__init__.py create mode 100644 influxframework/core/doctype/user_document_type/user_document_type.json create mode 100644 influxframework/core/doctype/user_document_type/user_document_type.py create mode 100644 influxframework/core/doctype/user_email/__init__.py create mode 100644 influxframework/core/doctype/user_email/user_email.json create mode 100644 influxframework/core/doctype/user_email/user_email.py create mode 100644 influxframework/core/doctype/user_group/__init__.py create mode 100644 influxframework/core/doctype/user_group/test_user_group.py create mode 100644 influxframework/core/doctype/user_group/user_group.js create mode 100644 influxframework/core/doctype/user_group/user_group.json create mode 100644 influxframework/core/doctype/user_group/user_group.py create mode 100644 influxframework/core/doctype/user_group_member/__init__.py create mode 100644 influxframework/core/doctype/user_group_member/test_user_group_member.py create mode 100644 influxframework/core/doctype/user_group_member/user_group_member.js create mode 100644 influxframework/core/doctype/user_group_member/user_group_member.json create mode 100644 influxframework/core/doctype/user_group_member/user_group_member.py create mode 100644 influxframework/core/doctype/user_permission/__init__.py create mode 100644 influxframework/core/doctype/user_permission/test_user_permission.py create mode 100644 influxframework/core/doctype/user_permission/user_permission.js create mode 100644 influxframework/core/doctype/user_permission/user_permission.json create mode 100644 influxframework/core/doctype/user_permission/user_permission.py create mode 100644 influxframework/core/doctype/user_permission/user_permission_help.html create mode 100644 influxframework/core/doctype/user_permission/user_permission_list.js create mode 100644 influxframework/core/doctype/user_select_document_type/__init__.py create mode 100644 influxframework/core/doctype/user_select_document_type/user_select_document_type.json create mode 100644 influxframework/core/doctype/user_select_document_type/user_select_document_type.py create mode 100644 influxframework/core/doctype/user_social_login/__init__.py create mode 100644 influxframework/core/doctype/user_social_login/user_social_login.json create mode 100644 influxframework/core/doctype/user_social_login/user_social_login.py create mode 100644 influxframework/core/doctype/user_type/__init__.py create mode 100644 influxframework/core/doctype/user_type/test_user_type.py create mode 100644 influxframework/core/doctype/user_type/user_type.js create mode 100644 influxframework/core/doctype/user_type/user_type.json create mode 100644 influxframework/core/doctype/user_type/user_type.py create mode 100644 influxframework/core/doctype/user_type/user_type_dashboard.py create mode 100644 influxframework/core/doctype/user_type/user_type_list.js create mode 100644 influxframework/core/doctype/user_type_module/__init__.py create mode 100644 influxframework/core/doctype/user_type_module/user_type_module.json create mode 100644 influxframework/core/doctype/user_type_module/user_type_module.py create mode 100644 influxframework/core/doctype/version/__init__.py create mode 100644 influxframework/core/doctype/version/test_records.json create mode 100644 influxframework/core/doctype/version/test_version.py create mode 100644 influxframework/core/doctype/version/version.js create mode 100644 influxframework/core/doctype/version/version.json create mode 100644 influxframework/core/doctype/version/version.py create mode 100644 influxframework/core/doctype/version/version_view.html create mode 100644 influxframework/core/doctype/view_log/__init__.py create mode 100644 influxframework/core/doctype/view_log/test_view_log.py create mode 100644 influxframework/core/doctype/view_log/view_log.js create mode 100644 influxframework/core/doctype/view_log/view_log.json create mode 100644 influxframework/core/doctype/view_log/view_log.py create mode 100644 influxframework/core/form_tour/doctype/doctype.json create mode 100644 influxframework/core/notifications.py create mode 100644 influxframework/core/page/__init__.py create mode 100644 influxframework/core/page/background_jobs/__init__.py create mode 100644 influxframework/core/page/background_jobs/background_jobs.css create mode 100644 influxframework/core/page/background_jobs/background_jobs.html create mode 100644 influxframework/core/page/background_jobs/background_jobs.js create mode 100644 influxframework/core/page/background_jobs/background_jobs.json create mode 100644 influxframework/core/page/background_jobs/background_jobs.py create mode 100644 influxframework/core/page/background_jobs/background_workers.html create mode 100644 influxframework/core/page/dashboard_view/__init__.py create mode 100644 influxframework/core/page/dashboard_view/dashboard_view.js create mode 100644 influxframework/core/page/dashboard_view/dashboard_view.json create mode 100644 influxframework/core/page/permission_manager/README.md create mode 100644 influxframework/core/page/permission_manager/__init__.py create mode 100644 influxframework/core/page/permission_manager/permission_manager.css create mode 100644 influxframework/core/page/permission_manager/permission_manager.js create mode 100644 influxframework/core/page/permission_manager/permission_manager.json create mode 100644 influxframework/core/page/permission_manager/permission_manager.py create mode 100644 influxframework/core/page/permission_manager/permission_manager_help.html create mode 100644 influxframework/core/page/recorder/__init__.py create mode 100644 influxframework/core/page/recorder/recorder.js create mode 100644 influxframework/core/page/recorder/recorder.json create mode 100644 influxframework/core/report/__init__.py create mode 100644 influxframework/core/report/database_storage_usage_by_tables/__init__.py create mode 100644 influxframework/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.js create mode 100644 influxframework/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.json create mode 100644 influxframework/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.py create mode 100644 influxframework/core/report/database_storage_usage_by_tables/test_database_storage_usage_by_tables.py create mode 100644 influxframework/core/report/document_share_report/__init__.py create mode 100644 influxframework/core/report/document_share_report/document_share_report.json create mode 100644 influxframework/core/report/permitted_documents_for_user/__init__.py create mode 100644 influxframework/core/report/permitted_documents_for_user/permitted_documents_for_user.js create mode 100644 influxframework/core/report/permitted_documents_for_user/permitted_documents_for_user.json create mode 100644 influxframework/core/report/permitted_documents_for_user/permitted_documents_for_user.py create mode 100644 influxframework/core/report/transaction_log_report/__init__.py create mode 100644 influxframework/core/report/transaction_log_report/transaction_log_report.js create mode 100644 influxframework/core/report/transaction_log_report/transaction_log_report.json create mode 100644 influxframework/core/report/transaction_log_report/transaction_log_report.py create mode 100644 influxframework/core/utils.py create mode 100644 influxframework/core/web_form/__init__.py create mode 100644 influxframework/core/web_form/edit_profile/__init__.py create mode 100644 influxframework/core/web_form/edit_profile/edit_profile.js create mode 100644 influxframework/core/web_form/edit_profile/edit_profile.json create mode 100644 influxframework/core/web_form/edit_profile/edit_profile.py create mode 100644 influxframework/core/workspace/build/build.json create mode 100644 influxframework/core/workspace/settings/settings.json create mode 100644 influxframework/core/workspace/users/users.json create mode 100644 influxframework/coverage.py create mode 100644 influxframework/custom/__init__.py create mode 100644 influxframework/custom/doctype/__init__.py create mode 100644 influxframework/custom/doctype/client_script/README.md create mode 100644 influxframework/custom/doctype/client_script/__init__.py create mode 100644 influxframework/custom/doctype/client_script/client_script.js create mode 100644 influxframework/custom/doctype/client_script/client_script.json create mode 100644 influxframework/custom/doctype/client_script/client_script.py create mode 100644 influxframework/custom/doctype/client_script/test_client_script.py create mode 100644 influxframework/custom/doctype/client_script/ui_test_client_script.js create mode 100644 influxframework/custom/doctype/custom_field/README.md create mode 100644 influxframework/custom/doctype/custom_field/__init__.py create mode 100644 influxframework/custom/doctype/custom_field/custom_field.js create mode 100644 influxframework/custom/doctype/custom_field/custom_field.json create mode 100644 influxframework/custom/doctype/custom_field/custom_field.py create mode 100644 influxframework/custom/doctype/custom_field/test_custom_field.py create mode 100644 influxframework/custom/doctype/custom_field/test_records.json create mode 100644 influxframework/custom/doctype/customize_form/README.md create mode 100644 influxframework/custom/doctype/customize_form/__init__.py create mode 100644 influxframework/custom/doctype/customize_form/customize_form.js create mode 100644 influxframework/custom/doctype/customize_form/customize_form.json create mode 100644 influxframework/custom/doctype/customize_form/customize_form.py create mode 100644 influxframework/custom/doctype/customize_form/test_customize_form.py create mode 100644 influxframework/custom/doctype/customize_form_field/__init__.py create mode 100644 influxframework/custom/doctype/customize_form_field/customize_form_field.json create mode 100644 influxframework/custom/doctype/customize_form_field/customize_form_field.py create mode 100644 influxframework/custom/doctype/doctype_layout/__init__.py create mode 100644 influxframework/custom/doctype/doctype_layout/doctype_layout.js create mode 100644 influxframework/custom/doctype/doctype_layout/doctype_layout.json create mode 100644 influxframework/custom/doctype/doctype_layout/doctype_layout.py create mode 100644 influxframework/custom/doctype/doctype_layout/patches/convert_web_forms_to_doctype_layout.py create mode 100644 influxframework/custom/doctype/doctype_layout/test_doctype_layout.py create mode 100644 influxframework/custom/doctype/doctype_layout_field/__init__.py create mode 100644 influxframework/custom/doctype/doctype_layout_field/doctype_layout_field.json create mode 100644 influxframework/custom/doctype/doctype_layout_field/doctype_layout_field.py create mode 100644 influxframework/custom/doctype/property_setter/README.md create mode 100644 influxframework/custom/doctype/property_setter/__init__.py create mode 100644 influxframework/custom/doctype/property_setter/property_setter.js create mode 100644 influxframework/custom/doctype/property_setter/property_setter.json create mode 100644 influxframework/custom/doctype/property_setter/property_setter.py create mode 100644 influxframework/custom/doctype/property_setter/test_property_setter.py create mode 100644 influxframework/custom/doctype/property_setter/test_records.json create mode 100644 influxframework/custom/fixtures/temp_doctype.json create mode 100644 influxframework/custom/fixtures/temp_singles.json create mode 100644 influxframework/custom/form_tour/custom_field/custom_field.json create mode 100644 influxframework/custom/module_onboarding/customization/customization.json create mode 100644 influxframework/custom/onboarding_step/custom_doctype/custom_doctype.json create mode 100644 influxframework/custom/onboarding_step/custom_field/custom_field.json create mode 100644 influxframework/custom/onboarding_step/naming_series/naming_series.json create mode 100644 influxframework/custom/onboarding_step/print_format/print_format.json create mode 100644 influxframework/custom/onboarding_step/report_builder/report_builder.json create mode 100644 influxframework/custom/onboarding_step/role_permissions/role_permissions.json create mode 100644 influxframework/custom/onboarding_step/workflows/workflows.json create mode 100644 influxframework/custom/workspace/customization/customization.json create mode 100644 influxframework/data/google_fonts.json create mode 100644 influxframework/database/__init__.py create mode 100644 influxframework/database/database.py create mode 100644 influxframework/database/db_manager.py create mode 100644 influxframework/database/mariadb/__init__.py create mode 100644 influxframework/database/mariadb/database.py create mode 100644 influxframework/database/mariadb/framework_mariadb.sql create mode 100644 influxframework/database/mariadb/schema.py create mode 100644 influxframework/database/mariadb/setup_db.py create mode 100644 influxframework/database/postgres/__init__.py create mode 100644 influxframework/database/postgres/database.py create mode 100644 influxframework/database/postgres/framework_postgres.sql create mode 100644 influxframework/database/postgres/schema.py create mode 100644 influxframework/database/postgres/setup_db.py create mode 100644 influxframework/database/query.py create mode 100644 influxframework/database/schema.py create mode 100644 influxframework/database/sequence.py create mode 100644 influxframework/database/utils.py create mode 100644 influxframework/defaults.py create mode 100644 influxframework/deferred_insert.py create mode 100644 influxframework/desk/__init__.py create mode 100644 influxframework/desk/calendar.py create mode 100644 influxframework/desk/desk_page.py create mode 100644 influxframework/desk/desktop.py create mode 100644 influxframework/desk/doctype/__init__.py create mode 100644 influxframework/desk/doctype/bulk_update/__init__.py create mode 100644 influxframework/desk/doctype/bulk_update/bulk_update.js create mode 100644 influxframework/desk/doctype/bulk_update/bulk_update.json create mode 100644 influxframework/desk/doctype/bulk_update/bulk_update.py create mode 100644 influxframework/desk/doctype/calendar_view/__init__.py create mode 100644 influxframework/desk/doctype/calendar_view/calendar_view.js create mode 100644 influxframework/desk/doctype/calendar_view/calendar_view.json create mode 100644 influxframework/desk/doctype/calendar_view/calendar_view.py create mode 100644 influxframework/desk/doctype/console_log/__init__.py create mode 100644 influxframework/desk/doctype/console_log/console_log.js create mode 100644 influxframework/desk/doctype/console_log/console_log.json create mode 100644 influxframework/desk/doctype/console_log/console_log.py create mode 100644 influxframework/desk/doctype/console_log/test_console_log.py create mode 100644 influxframework/desk/doctype/dashboard/__init__.py create mode 100644 influxframework/desk/doctype/dashboard/dashboard.js create mode 100644 influxframework/desk/doctype/dashboard/dashboard.json create mode 100644 influxframework/desk/doctype/dashboard/dashboard.py create mode 100644 influxframework/desk/doctype/dashboard/dashboard_list.js create mode 100644 influxframework/desk/doctype/dashboard/test_dashboard.py create mode 100644 influxframework/desk/doctype/dashboard_chart/__init__.py create mode 100644 influxframework/desk/doctype/dashboard_chart/dashboard_chart.js create mode 100644 influxframework/desk/doctype/dashboard_chart/dashboard_chart.json create mode 100644 influxframework/desk/doctype/dashboard_chart/dashboard_chart.py create mode 100644 influxframework/desk/doctype/dashboard_chart/test_dashboard_chart.py create mode 100644 influxframework/desk/doctype/dashboard_chart_field/__init__.py create mode 100644 influxframework/desk/doctype/dashboard_chart_field/dashboard_chart_field.json create mode 100644 influxframework/desk/doctype/dashboard_chart_field/dashboard_chart_field.py create mode 100644 influxframework/desk/doctype/dashboard_chart_link/__init__.py create mode 100644 influxframework/desk/doctype/dashboard_chart_link/dashboard_chart_link.json create mode 100644 influxframework/desk/doctype/dashboard_chart_link/dashboard_chart_link.py create mode 100644 influxframework/desk/doctype/dashboard_chart_source/__init__.py create mode 100644 influxframework/desk/doctype/dashboard_chart_source/dashboard_chart_source.js create mode 100644 influxframework/desk/doctype/dashboard_chart_source/dashboard_chart_source.json create mode 100644 influxframework/desk/doctype/dashboard_chart_source/dashboard_chart_source.py create mode 100644 influxframework/desk/doctype/dashboard_chart_source/test_dashboard_chart_source.py create mode 100644 influxframework/desk/doctype/dashboard_settings/__init__.py create mode 100644 influxframework/desk/doctype/dashboard_settings/dashboard_settings.js create mode 100644 influxframework/desk/doctype/dashboard_settings/dashboard_settings.json create mode 100644 influxframework/desk/doctype/dashboard_settings/dashboard_settings.py create mode 100644 influxframework/desk/doctype/desktop_icon/__init__.py create mode 100644 influxframework/desk/doctype/desktop_icon/desktop_icon.js create mode 100644 influxframework/desk/doctype/desktop_icon/desktop_icon.json create mode 100644 influxframework/desk/doctype/desktop_icon/desktop_icon.py create mode 100644 influxframework/desk/doctype/event/README.md create mode 100644 influxframework/desk/doctype/event/__init__.py create mode 100644 influxframework/desk/doctype/event/event.js create mode 100644 influxframework/desk/doctype/event/event.json create mode 100644 influxframework/desk/doctype/event/event.py create mode 100644 influxframework/desk/doctype/event/event_calendar.js create mode 100644 influxframework/desk/doctype/event/event_list.js create mode 100644 influxframework/desk/doctype/event/test_event.py create mode 100644 influxframework/desk/doctype/event/test_records.json create mode 100644 influxframework/desk/doctype/event_participants/__init__.py create mode 100644 influxframework/desk/doctype/event_participants/event_participants.json create mode 100644 influxframework/desk/doctype/event_participants/event_participants.py create mode 100644 influxframework/desk/doctype/form_tour/__init__.py create mode 100644 influxframework/desk/doctype/form_tour/form_tour.js create mode 100644 influxframework/desk/doctype/form_tour/form_tour.json create mode 100644 influxframework/desk/doctype/form_tour/form_tour.py create mode 100644 influxframework/desk/doctype/form_tour/test_form_tour.py create mode 100644 influxframework/desk/doctype/form_tour_step/__init__.py create mode 100644 influxframework/desk/doctype/form_tour_step/form_tour_step.json create mode 100644 influxframework/desk/doctype/form_tour_step/form_tour_step.py create mode 100644 influxframework/desk/doctype/global_search_doctype/__init__.py create mode 100644 influxframework/desk/doctype/global_search_doctype/global_search_doctype.json create mode 100644 influxframework/desk/doctype/global_search_doctype/global_search_doctype.py create mode 100644 influxframework/desk/doctype/global_search_settings/__init__.py create mode 100644 influxframework/desk/doctype/global_search_settings/global_search_settings.js create mode 100644 influxframework/desk/doctype/global_search_settings/global_search_settings.json create mode 100644 influxframework/desk/doctype/global_search_settings/global_search_settings.py create mode 100644 influxframework/desk/doctype/kanban_board/__init__.py create mode 100644 influxframework/desk/doctype/kanban_board/kanban_board.js create mode 100644 influxframework/desk/doctype/kanban_board/kanban_board.json create mode 100644 influxframework/desk/doctype/kanban_board/kanban_board.py create mode 100644 influxframework/desk/doctype/kanban_board/test_kanban_board.py create mode 100644 influxframework/desk/doctype/kanban_board_column/__init__.py create mode 100644 influxframework/desk/doctype/kanban_board_column/kanban_board_column.json create mode 100644 influxframework/desk/doctype/kanban_board_column/kanban_board_column.py create mode 100644 influxframework/desk/doctype/list_filter/__init__.py create mode 100644 influxframework/desk/doctype/list_filter/list_filter.json create mode 100644 influxframework/desk/doctype/list_filter/list_filter.py create mode 100644 influxframework/desk/doctype/list_view_settings/__init__.py create mode 100644 influxframework/desk/doctype/list_view_settings/list_view_settings.js create mode 100644 influxframework/desk/doctype/list_view_settings/list_view_settings.json create mode 100644 influxframework/desk/doctype/list_view_settings/list_view_settings.py create mode 100644 influxframework/desk/doctype/list_view_settings/test_list_view_settings.py create mode 100644 influxframework/desk/doctype/module_onboarding/__init__.py create mode 100644 influxframework/desk/doctype/module_onboarding/module_onboarding.js create mode 100644 influxframework/desk/doctype/module_onboarding/module_onboarding.json create mode 100644 influxframework/desk/doctype/module_onboarding/module_onboarding.py create mode 100644 influxframework/desk/doctype/module_onboarding/test_module_onboarding.py create mode 100644 influxframework/desk/doctype/note/README.md create mode 100644 influxframework/desk/doctype/note/__init__.py create mode 100644 influxframework/desk/doctype/note/note.js create mode 100644 influxframework/desk/doctype/note/note.json create mode 100644 influxframework/desk/doctype/note/note.py create mode 100644 influxframework/desk/doctype/note/note_list.js create mode 100644 influxframework/desk/doctype/note/test_note.py create mode 100644 influxframework/desk/doctype/note/test_records.json create mode 100644 influxframework/desk/doctype/note_seen_by/__init__.py create mode 100644 influxframework/desk/doctype/note_seen_by/note_seen_by.json create mode 100644 influxframework/desk/doctype/note_seen_by/note_seen_by.py create mode 100644 influxframework/desk/doctype/notification_log/__init__.py create mode 100644 influxframework/desk/doctype/notification_log/notification_log.js create mode 100644 influxframework/desk/doctype/notification_log/notification_log.json create mode 100644 influxframework/desk/doctype/notification_log/notification_log.py create mode 100644 influxframework/desk/doctype/notification_log/notification_log_list.js create mode 100644 influxframework/desk/doctype/notification_log/test_notification_log.py create mode 100644 influxframework/desk/doctype/notification_settings/__init__.py create mode 100644 influxframework/desk/doctype/notification_settings/notification_settings.js create mode 100644 influxframework/desk/doctype/notification_settings/notification_settings.json create mode 100644 influxframework/desk/doctype/notification_settings/notification_settings.py create mode 100644 influxframework/desk/doctype/notification_settings/test_notification_settings.py create mode 100644 influxframework/desk/doctype/notification_subscribed_document/__init__.py create mode 100644 influxframework/desk/doctype/notification_subscribed_document/notification_subscribed_document.json create mode 100644 influxframework/desk/doctype/notification_subscribed_document/notification_subscribed_document.py create mode 100644 influxframework/desk/doctype/number_card/__init__.py create mode 100644 influxframework/desk/doctype/number_card/number_card.js create mode 100644 influxframework/desk/doctype/number_card/number_card.json create mode 100644 influxframework/desk/doctype/number_card/number_card.py create mode 100644 influxframework/desk/doctype/number_card/test_number_card.py create mode 100644 influxframework/desk/doctype/number_card_link/__init__.py create mode 100644 influxframework/desk/doctype/number_card_link/number_card_link.json create mode 100644 influxframework/desk/doctype/number_card_link/number_card_link.py create mode 100644 influxframework/desk/doctype/onboarding_permission/__init__.py create mode 100644 influxframework/desk/doctype/onboarding_permission/onboarding_permission.js create mode 100644 influxframework/desk/doctype/onboarding_permission/onboarding_permission.json create mode 100644 influxframework/desk/doctype/onboarding_permission/onboarding_permission.py create mode 100644 influxframework/desk/doctype/onboarding_permission/test_onboarding_permission.py create mode 100644 influxframework/desk/doctype/onboarding_step/__init__.py create mode 100644 influxframework/desk/doctype/onboarding_step/onboarding_step.js create mode 100644 influxframework/desk/doctype/onboarding_step/onboarding_step.json create mode 100644 influxframework/desk/doctype/onboarding_step/onboarding_step.py create mode 100644 influxframework/desk/doctype/onboarding_step/test_onboarding_step.py create mode 100644 influxframework/desk/doctype/onboarding_step_map/__init__.py create mode 100644 influxframework/desk/doctype/onboarding_step_map/onboarding_step_map.json create mode 100644 influxframework/desk/doctype/onboarding_step_map/onboarding_step_map.py create mode 100644 influxframework/desk/doctype/route_history/__init__.py create mode 100644 influxframework/desk/doctype/route_history/route_history.js create mode 100644 influxframework/desk/doctype/route_history/route_history.json create mode 100644 influxframework/desk/doctype/route_history/route_history.py create mode 100644 influxframework/desk/doctype/route_history/route_history_list.js create mode 100644 influxframework/desk/doctype/system_console/__init__.py create mode 100644 influxframework/desk/doctype/system_console/system_console.js create mode 100644 influxframework/desk/doctype/system_console/system_console.json create mode 100644 influxframework/desk/doctype/system_console/system_console.py create mode 100644 influxframework/desk/doctype/system_console/test_system_console.py create mode 100644 influxframework/desk/doctype/tag/__init__.py create mode 100644 influxframework/desk/doctype/tag/tag.js create mode 100644 influxframework/desk/doctype/tag/tag.json create mode 100644 influxframework/desk/doctype/tag/tag.py create mode 100644 influxframework/desk/doctype/tag/test_tag.py create mode 100644 influxframework/desk/doctype/tag_link/__init__.py create mode 100644 influxframework/desk/doctype/tag_link/tag_link.js create mode 100644 influxframework/desk/doctype/tag_link/tag_link.json create mode 100644 influxframework/desk/doctype/tag_link/tag_link.py create mode 100644 influxframework/desk/doctype/tag_link/test_tag_link.py create mode 100644 influxframework/desk/doctype/todo/README.md create mode 100644 influxframework/desk/doctype/todo/__init__.py create mode 100644 influxframework/desk/doctype/todo/test_todo.py create mode 100644 influxframework/desk/doctype/todo/todo.js create mode 100644 influxframework/desk/doctype/todo/todo.json create mode 100644 influxframework/desk/doctype/todo/todo.py create mode 100644 influxframework/desk/doctype/todo/todo_calendar.js create mode 100644 influxframework/desk/doctype/todo/todo_list.js create mode 100644 influxframework/desk/doctype/workspace/__init__.py create mode 100644 influxframework/desk/doctype/workspace/test_workspace.py create mode 100644 influxframework/desk/doctype/workspace/workspace.js create mode 100644 influxframework/desk/doctype/workspace/workspace.json create mode 100644 influxframework/desk/doctype/workspace/workspace.py create mode 100644 influxframework/desk/doctype/workspace_chart/__init__.py create mode 100644 influxframework/desk/doctype/workspace_chart/workspace_chart.json create mode 100644 influxframework/desk/doctype/workspace_chart/workspace_chart.py create mode 100644 influxframework/desk/doctype/workspace_link/__init__.py create mode 100644 influxframework/desk/doctype/workspace_link/workspace_link.json create mode 100644 influxframework/desk/doctype/workspace_link/workspace_link.py create mode 100644 influxframework/desk/doctype/workspace_quick_list/__init__.py create mode 100644 influxframework/desk/doctype/workspace_quick_list/workspace_quick_list.json create mode 100644 influxframework/desk/doctype/workspace_quick_list/workspace_quick_list.py create mode 100644 influxframework/desk/doctype/workspace_shortcut/__init__.py create mode 100644 influxframework/desk/doctype/workspace_shortcut/workspace_shortcut.json create mode 100644 influxframework/desk/doctype/workspace_shortcut/workspace_shortcut.py create mode 100644 influxframework/desk/form/__init__.py create mode 100644 influxframework/desk/form/assign_to.py create mode 100644 influxframework/desk/form/document_follow.py create mode 100644 influxframework/desk/form/linked_with.py create mode 100644 influxframework/desk/form/load.py create mode 100644 influxframework/desk/form/meta.py create mode 100644 influxframework/desk/form/save.py create mode 100644 influxframework/desk/form/test_form.py create mode 100644 influxframework/desk/form/utils.py create mode 100644 influxframework/desk/gantt.py create mode 100644 influxframework/desk/leaderboard.py create mode 100644 influxframework/desk/like.py create mode 100644 influxframework/desk/link_preview.py create mode 100644 influxframework/desk/listview.py create mode 100644 influxframework/desk/moduleview.py create mode 100644 influxframework/desk/notifications.py create mode 100644 influxframework/desk/page/__init__.py create mode 100644 influxframework/desk/page/activity/README.md create mode 100644 influxframework/desk/page/activity/__init__.py create mode 100644 influxframework/desk/page/activity/activity.css create mode 100644 influxframework/desk/page/activity/activity.js create mode 100644 influxframework/desk/page/activity/activity.json create mode 100644 influxframework/desk/page/activity/activity.py create mode 100644 influxframework/desk/page/activity/activity_row.html create mode 100644 influxframework/desk/page/backups/__init__.py create mode 100644 influxframework/desk/page/backups/backups.css create mode 100644 influxframework/desk/page/backups/backups.html create mode 100644 influxframework/desk/page/backups/backups.js create mode 100644 influxframework/desk/page/backups/backups.json create mode 100644 influxframework/desk/page/backups/backups.py create mode 100644 influxframework/desk/page/leaderboard/__init__.py create mode 100644 influxframework/desk/page/leaderboard/leaderboard.css create mode 100644 influxframework/desk/page/leaderboard/leaderboard.js create mode 100644 influxframework/desk/page/leaderboard/leaderboard.json create mode 100644 influxframework/desk/page/leaderboard/leaderboard.py create mode 100644 influxframework/desk/page/setup_wizard/__init__.py create mode 100644 influxframework/desk/page/setup_wizard/install_fixtures.py create mode 100644 influxframework/desk/page/setup_wizard/setup_wizard.js create mode 100644 influxframework/desk/page/setup_wizard/setup_wizard.json create mode 100644 influxframework/desk/page/setup_wizard/setup_wizard.py create mode 100644 influxframework/desk/page/translation_tool/__init__.py create mode 100644 influxframework/desk/page/translation_tool/translation_tool.css create mode 100644 influxframework/desk/page/translation_tool/translation_tool.html create mode 100644 influxframework/desk/page/translation_tool/translation_tool.js create mode 100644 influxframework/desk/page/translation_tool/translation_tool.json create mode 100644 influxframework/desk/page/user_profile/__init__.py create mode 100644 influxframework/desk/page/user_profile/user_profile.css create mode 100644 influxframework/desk/page/user_profile/user_profile.html create mode 100644 influxframework/desk/page/user_profile/user_profile.js create mode 100644 influxframework/desk/page/user_profile/user_profile.json create mode 100644 influxframework/desk/page/user_profile/user_profile.py create mode 100644 influxframework/desk/page/user_profile/user_profile_controller.js create mode 100644 influxframework/desk/page/user_profile/user_profile_sidebar.html create mode 100644 influxframework/desk/query_report.py create mode 100644 influxframework/desk/report/__init__.py create mode 100644 influxframework/desk/report/todo/__init__.py create mode 100644 influxframework/desk/report/todo/todo.js create mode 100644 influxframework/desk/report/todo/todo.json create mode 100644 influxframework/desk/report/todo/todo.py create mode 100644 influxframework/desk/report_dump.py create mode 100644 influxframework/desk/reportview.py create mode 100644 influxframework/desk/search.py create mode 100644 influxframework/desk/treeview.py create mode 100644 influxframework/desk/utils.py create mode 100644 influxframework/email/__init__.py create mode 100644 influxframework/email/doctype/__init__.py create mode 100644 influxframework/email/doctype/auto_email_report/__init__.py create mode 100644 influxframework/email/doctype/auto_email_report/auto_email_report.js create mode 100644 influxframework/email/doctype/auto_email_report/auto_email_report.json create mode 100644 influxframework/email/doctype/auto_email_report/auto_email_report.py create mode 100644 influxframework/email/doctype/auto_email_report/test_auto_email_report.py create mode 100644 influxframework/email/doctype/document_follow/__init__.py create mode 100644 influxframework/email/doctype/document_follow/document_follow.js create mode 100644 influxframework/email/doctype/document_follow/document_follow.json create mode 100644 influxframework/email/doctype/document_follow/document_follow.py create mode 100644 influxframework/email/doctype/document_follow/test_document_follow.py create mode 100644 influxframework/email/doctype/email_account/__init__.py create mode 100644 influxframework/email/doctype/email_account/email_account.js create mode 100644 influxframework/email/doctype/email_account/email_account.json create mode 100644 influxframework/email/doctype/email_account/email_account.py create mode 100644 influxframework/email/doctype/email_account/email_account_list.js create mode 100644 influxframework/email/doctype/email_account/test_email_account.py create mode 100644 influxframework/email/doctype/email_account/test_mails/incoming-1.raw create mode 100644 influxframework/email/doctype/email_account/test_mails/incoming-2.raw create mode 100644 influxframework/email/doctype/email_account/test_mails/incoming-3.raw create mode 100644 influxframework/email/doctype/email_account/test_mails/incoming-4.raw create mode 100644 influxframework/email/doctype/email_account/test_mails/incoming-self-sent.raw create mode 100644 influxframework/email/doctype/email_account/test_mails/incoming-subject-placeholder.raw create mode 100644 influxframework/email/doctype/email_account/test_mails/reply-1.raw create mode 100644 influxframework/email/doctype/email_account/test_mails/reply-2.raw create mode 100644 influxframework/email/doctype/email_account/test_mails/reply-3.raw create mode 100644 influxframework/email/doctype/email_account/test_mails/reply-4.raw create mode 100644 influxframework/email/doctype/email_account/test_records.json create mode 100644 influxframework/email/doctype/email_domain/__init__.py create mode 100644 influxframework/email/doctype/email_domain/email_domain.js create mode 100644 influxframework/email/doctype/email_domain/email_domain.json create mode 100644 influxframework/email/doctype/email_domain/email_domain.py create mode 100644 influxframework/email/doctype/email_domain/test_email_domain.py create mode 100644 influxframework/email/doctype/email_domain/test_records.json create mode 100644 influxframework/email/doctype/email_flag_queue/__init__.py create mode 100644 influxframework/email/doctype/email_flag_queue/email_flag_queue.js create mode 100644 influxframework/email/doctype/email_flag_queue/email_flag_queue.json create mode 100644 influxframework/email/doctype/email_flag_queue/email_flag_queue.py create mode 100644 influxframework/email/doctype/email_flag_queue/test_email_flag_queue.py create mode 100644 influxframework/email/doctype/email_group/__init__.py create mode 100644 influxframework/email/doctype/email_group/email_group.js create mode 100644 influxframework/email/doctype/email_group/email_group.json create mode 100644 influxframework/email/doctype/email_group/email_group.py create mode 100644 influxframework/email/doctype/email_group/test_email_group.py create mode 100644 influxframework/email/doctype/email_group/test_records.json create mode 100644 influxframework/email/doctype/email_group_member/__init__.py create mode 100644 influxframework/email/doctype/email_group_member/email_group_member.js create mode 100644 influxframework/email/doctype/email_group_member/email_group_member.json create mode 100644 influxframework/email/doctype/email_group_member/email_group_member.py create mode 100644 influxframework/email/doctype/email_group_member/test_email_group_member.py create mode 100644 influxframework/email/doctype/email_queue/__init__.py create mode 100644 influxframework/email/doctype/email_queue/email_queue.js create mode 100644 influxframework/email/doctype/email_queue/email_queue.json create mode 100644 influxframework/email/doctype/email_queue/email_queue.py create mode 100644 influxframework/email/doctype/email_queue/email_queue_list.js create mode 100644 influxframework/email/doctype/email_queue/test_email_queue.py create mode 100644 influxframework/email/doctype/email_queue_recipient/__init__.py create mode 100644 influxframework/email/doctype/email_queue_recipient/email_queue_recipient.json create mode 100644 influxframework/email/doctype/email_queue_recipient/email_queue_recipient.py create mode 100644 influxframework/email/doctype/email_rule/__init__.py create mode 100644 influxframework/email/doctype/email_rule/email_rule.js create mode 100644 influxframework/email/doctype/email_rule/email_rule.json create mode 100644 influxframework/email/doctype/email_rule/email_rule.py create mode 100644 influxframework/email/doctype/email_rule/test_email_rule.py create mode 100644 influxframework/email/doctype/email_template/__init__.py create mode 100644 influxframework/email/doctype/email_template/email_template.js create mode 100644 influxframework/email/doctype/email_template/email_template.json create mode 100644 influxframework/email/doctype/email_template/email_template.py create mode 100644 influxframework/email/doctype/email_template/test_email_template.py create mode 100644 influxframework/email/doctype/email_unsubscribe/__init__.py create mode 100644 influxframework/email/doctype/email_unsubscribe/email_unsubscribe.js create mode 100644 influxframework/email/doctype/email_unsubscribe/email_unsubscribe.json create mode 100644 influxframework/email/doctype/email_unsubscribe/email_unsubscribe.py create mode 100644 influxframework/email/doctype/email_unsubscribe/test_email_unsubscribe.py create mode 100644 influxframework/email/doctype/imap_folder/__init__.py create mode 100644 influxframework/email/doctype/imap_folder/imap_folder.json create mode 100644 influxframework/email/doctype/imap_folder/imap_folder.py create mode 100644 influxframework/email/doctype/newsletter/__init__.py create mode 100644 influxframework/email/doctype/newsletter/exceptions.py create mode 100644 influxframework/email/doctype/newsletter/newsletter.js create mode 100644 influxframework/email/doctype/newsletter/newsletter.json create mode 100644 influxframework/email/doctype/newsletter/newsletter.py create mode 100644 influxframework/email/doctype/newsletter/newsletter_list.js create mode 100644 influxframework/email/doctype/newsletter/templates/newsletter.html create mode 100644 influxframework/email/doctype/newsletter/templates/newsletter_row.html create mode 100644 influxframework/email/doctype/newsletter/test_newsletter.py create mode 100644 influxframework/email/doctype/newsletter_attachment/__init__.py create mode 100644 influxframework/email/doctype/newsletter_attachment/newsletter_attachment.json create mode 100644 influxframework/email/doctype/newsletter_attachment/newsletter_attachment.py create mode 100644 influxframework/email/doctype/newsletter_email_group/__init__.py create mode 100644 influxframework/email/doctype/newsletter_email_group/newsletter_email_group.json create mode 100644 influxframework/email/doctype/newsletter_email_group/newsletter_email_group.py create mode 100644 influxframework/email/doctype/notification/__init__.py create mode 100644 influxframework/email/doctype/notification/notification.js create mode 100644 influxframework/email/doctype/notification/notification.json create mode 100644 influxframework/email/doctype/notification/notification.py create mode 100644 influxframework/email/doctype/notification/test_notification.py create mode 100644 influxframework/email/doctype/notification/test_records.json create mode 100644 influxframework/email/doctype/notification_recipient/__init__.py create mode 100644 influxframework/email/doctype/notification_recipient/notification_recipient.json create mode 100644 influxframework/email/doctype/notification_recipient/notification_recipient.py create mode 100644 influxframework/email/doctype/unhandled_email/__init__.py create mode 100644 influxframework/email/doctype/unhandled_email/test_unhandled_email.py create mode 100644 influxframework/email/doctype/unhandled_email/unhandled_email.json create mode 100644 influxframework/email/doctype/unhandled_email/unhandled_email.py create mode 100644 influxframework/email/email_body.py create mode 100644 influxframework/email/inbox.py create mode 100644 influxframework/email/oauth.py create mode 100644 influxframework/email/page/__init__.py create mode 100644 influxframework/email/queue.py create mode 100644 influxframework/email/receive.py create mode 100644 influxframework/email/smtp.py create mode 100644 influxframework/email/test_email_body.py create mode 100644 influxframework/email/test_smtp.py create mode 100644 influxframework/email/utils.py create mode 100644 influxframework/event_streaming/__init__.py create mode 100644 influxframework/event_streaming/doctype/__init__.py create mode 100644 influxframework/event_streaming/doctype/document_type_field_mapping/__init__.py create mode 100644 influxframework/event_streaming/doctype/document_type_field_mapping/document_type_field_mapping.json create mode 100644 influxframework/event_streaming/doctype/document_type_field_mapping/document_type_field_mapping.py create mode 100644 influxframework/event_streaming/doctype/document_type_mapping/__init__.py create mode 100644 influxframework/event_streaming/doctype/document_type_mapping/document_type_mapping.js create mode 100644 influxframework/event_streaming/doctype/document_type_mapping/document_type_mapping.json create mode 100644 influxframework/event_streaming/doctype/document_type_mapping/document_type_mapping.py create mode 100644 influxframework/event_streaming/doctype/document_type_mapping/test_document_type_mapping.py create mode 100644 influxframework/event_streaming/doctype/event_consumer/__init__.py create mode 100644 influxframework/event_streaming/doctype/event_consumer/event_consumer.js create mode 100644 influxframework/event_streaming/doctype/event_consumer/event_consumer.json create mode 100644 influxframework/event_streaming/doctype/event_consumer/event_consumer.py create mode 100644 influxframework/event_streaming/doctype/event_consumer/test_event_consumer.py create mode 100644 influxframework/event_streaming/doctype/event_consumer_document_type/__init__.py create mode 100644 influxframework/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.json create mode 100644 influxframework/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.py create mode 100644 influxframework/event_streaming/doctype/event_producer/__init__.py create mode 100644 influxframework/event_streaming/doctype/event_producer/event_producer.js create mode 100644 influxframework/event_streaming/doctype/event_producer/event_producer.json create mode 100644 influxframework/event_streaming/doctype/event_producer/event_producer.py create mode 100644 influxframework/event_streaming/doctype/event_producer/test_event_producer.py create mode 100644 influxframework/event_streaming/doctype/event_producer_document_type/__init__.py create mode 100644 influxframework/event_streaming/doctype/event_producer_document_type/event_producer_document_type.json create mode 100644 influxframework/event_streaming/doctype/event_producer_document_type/event_producer_document_type.py create mode 100644 influxframework/event_streaming/doctype/event_producer_last_update/__init__.py create mode 100644 influxframework/event_streaming/doctype/event_producer_last_update/event_producer_last_update.js create mode 100644 influxframework/event_streaming/doctype/event_producer_last_update/event_producer_last_update.json create mode 100644 influxframework/event_streaming/doctype/event_producer_last_update/event_producer_last_update.py create mode 100644 influxframework/event_streaming/doctype/event_producer_last_update/test_event_producer_last_update.py create mode 100644 influxframework/event_streaming/doctype/event_sync_log/__init__.py create mode 100644 influxframework/event_streaming/doctype/event_sync_log/event_sync_log.js create mode 100644 influxframework/event_streaming/doctype/event_sync_log/event_sync_log.json create mode 100644 influxframework/event_streaming/doctype/event_sync_log/event_sync_log.py create mode 100644 influxframework/event_streaming/doctype/event_sync_log/event_sync_log_list.js create mode 100644 influxframework/event_streaming/doctype/event_sync_log/test_event_sync_log.py create mode 100644 influxframework/event_streaming/doctype/event_update_log/__init__.py create mode 100644 influxframework/event_streaming/doctype/event_update_log/event_update_log.js create mode 100644 influxframework/event_streaming/doctype/event_update_log/event_update_log.json create mode 100644 influxframework/event_streaming/doctype/event_update_log/event_update_log.py create mode 100644 influxframework/event_streaming/doctype/event_update_log/test_event_update_log.py create mode 100644 influxframework/event_streaming/doctype/event_update_log_consumer/__init__.py create mode 100644 influxframework/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.json create mode 100644 influxframework/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.py create mode 100644 influxframework/exceptions.py create mode 100644 influxframework/geo/__init__.py create mode 100644 influxframework/geo/country_info.json create mode 100644 influxframework/geo/country_info.py create mode 100644 influxframework/geo/doctype/__init__.py create mode 100644 influxframework/geo/doctype/country/README.md create mode 100644 influxframework/geo/doctype/country/__init__.py create mode 100644 influxframework/geo/doctype/country/country.js create mode 100644 influxframework/geo/doctype/country/country.json create mode 100644 influxframework/geo/doctype/country/country.py create mode 100644 influxframework/geo/doctype/country/test_country.py create mode 100644 influxframework/geo/doctype/country/test_records.json create mode 100644 influxframework/geo/doctype/currency/README.md create mode 100644 influxframework/geo/doctype/currency/__init__.py create mode 100644 influxframework/geo/doctype/currency/currency.js create mode 100644 influxframework/geo/doctype/currency/currency.json create mode 100644 influxframework/geo/doctype/currency/currency.py create mode 100644 influxframework/geo/doctype/currency/test_currency.py create mode 100644 influxframework/geo/doctype/currency/test_records.json create mode 100644 influxframework/geo/languages.json create mode 100644 influxframework/geo/report/__init__.py create mode 100644 influxframework/geo/utils.py create mode 100644 influxframework/handler.py create mode 100644 influxframework/hooks.py create mode 100644 influxframework/influxframeworkclient.py create mode 100644 influxframework/installer.py create mode 100644 influxframework/integrations/__init__.py create mode 100644 influxframework/integrations/doctype/__init__.py create mode 100644 influxframework/integrations/doctype/connected_app/__init__.py create mode 100644 influxframework/integrations/doctype/connected_app/connected_app.js create mode 100644 influxframework/integrations/doctype/connected_app/connected_app.json create mode 100644 influxframework/integrations/doctype/connected_app/connected_app.py create mode 100644 influxframework/integrations/doctype/connected_app/test_connected_app.py create mode 100644 influxframework/integrations/doctype/connected_app/test_records.json create mode 100644 influxframework/integrations/doctype/dropbox_settings/__init__.py create mode 100644 influxframework/integrations/doctype/dropbox_settings/dropbox_settings.js create mode 100644 influxframework/integrations/doctype/dropbox_settings/dropbox_settings.json create mode 100644 influxframework/integrations/doctype/dropbox_settings/dropbox_settings.py create mode 100644 influxframework/integrations/doctype/dropbox_settings/test_dropbox_settings.py create mode 100644 influxframework/integrations/doctype/google_calendar/__init__.py create mode 100644 influxframework/integrations/doctype/google_calendar/google_calendar.js create mode 100644 influxframework/integrations/doctype/google_calendar/google_calendar.json create mode 100644 influxframework/integrations/doctype/google_calendar/google_calendar.py create mode 100644 influxframework/integrations/doctype/google_contacts/__init__.py create mode 100644 influxframework/integrations/doctype/google_contacts/google_contacts.js create mode 100644 influxframework/integrations/doctype/google_contacts/google_contacts.json create mode 100644 influxframework/integrations/doctype/google_contacts/google_contacts.py create mode 100644 influxframework/integrations/doctype/google_drive/__init__.py create mode 100644 influxframework/integrations/doctype/google_drive/google_drive.js create mode 100644 influxframework/integrations/doctype/google_drive/google_drive.json create mode 100644 influxframework/integrations/doctype/google_drive/google_drive.py create mode 100644 influxframework/integrations/doctype/google_drive/test_google_drive.py create mode 100644 influxframework/integrations/doctype/google_settings/__init__.py create mode 100644 influxframework/integrations/doctype/google_settings/google_settings.js create mode 100644 influxframework/integrations/doctype/google_settings/google_settings.json create mode 100644 influxframework/integrations/doctype/google_settings/google_settings.py create mode 100644 influxframework/integrations/doctype/google_settings/test_google_settings.py create mode 100644 influxframework/integrations/doctype/integration_request/__init__.py create mode 100644 influxframework/integrations/doctype/integration_request/integration_request.js create mode 100644 influxframework/integrations/doctype/integration_request/integration_request.json create mode 100644 influxframework/integrations/doctype/integration_request/integration_request.py create mode 100644 influxframework/integrations/doctype/integration_request/test_integration_request.py create mode 100644 influxframework/integrations/doctype/ldap_group_mapping/__init__.py create mode 100644 influxframework/integrations/doctype/ldap_group_mapping/ldap_group_mapping.json create mode 100644 influxframework/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py create mode 100644 influxframework/integrations/doctype/ldap_settings/__init__.py create mode 100644 influxframework/integrations/doctype/ldap_settings/ldap_settings.js create mode 100644 influxframework/integrations/doctype/ldap_settings/ldap_settings.json create mode 100644 influxframework/integrations/doctype/ldap_settings/ldap_settings.py create mode 100644 influxframework/integrations/doctype/ldap_settings/test_data_ldif_activedirectory.json create mode 100644 influxframework/integrations/doctype/ldap_settings/test_data_ldif_openldap.json create mode 100644 influxframework/integrations/doctype/ldap_settings/test_ldap_settings.py create mode 100644 influxframework/integrations/doctype/oauth_authorization_code/__init__.py create mode 100644 influxframework/integrations/doctype/oauth_authorization_code/oauth_authorization_code.js create mode 100644 influxframework/integrations/doctype/oauth_authorization_code/oauth_authorization_code.json create mode 100644 influxframework/integrations/doctype/oauth_authorization_code/oauth_authorization_code.py create mode 100644 influxframework/integrations/doctype/oauth_authorization_code/test_oauth_authorization_code.py create mode 100644 influxframework/integrations/doctype/oauth_bearer_token/__init__.py create mode 100644 influxframework/integrations/doctype/oauth_bearer_token/oauth_bearer_token.js create mode 100644 influxframework/integrations/doctype/oauth_bearer_token/oauth_bearer_token.json create mode 100644 influxframework/integrations/doctype/oauth_bearer_token/oauth_bearer_token.py create mode 100644 influxframework/integrations/doctype/oauth_bearer_token/test_oauth_bearer_token.py create mode 100644 influxframework/integrations/doctype/oauth_client/__init__.py create mode 100644 influxframework/integrations/doctype/oauth_client/oauth_client.js create mode 100644 influxframework/integrations/doctype/oauth_client/oauth_client.json create mode 100644 influxframework/integrations/doctype/oauth_client/oauth_client.py create mode 100644 influxframework/integrations/doctype/oauth_client/test_oauth_client.py create mode 100644 influxframework/integrations/doctype/oauth_client/test_records.json create mode 100644 influxframework/integrations/doctype/oauth_provider_settings/__init__.py create mode 100644 influxframework/integrations/doctype/oauth_provider_settings/oauth_provider_settings.js create mode 100644 influxframework/integrations/doctype/oauth_provider_settings/oauth_provider_settings.json create mode 100644 influxframework/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py create mode 100644 influxframework/integrations/doctype/oauth_scope/__init__.py create mode 100644 influxframework/integrations/doctype/oauth_scope/oauth_scope.json create mode 100644 influxframework/integrations/doctype/oauth_scope/oauth_scope.py create mode 100644 influxframework/integrations/doctype/query_parameters/__init__.py create mode 100644 influxframework/integrations/doctype/query_parameters/query_parameters.json create mode 100644 influxframework/integrations/doctype/query_parameters/query_parameters.py create mode 100644 influxframework/integrations/doctype/s3_backup_settings/__init__.py create mode 100644 influxframework/integrations/doctype/s3_backup_settings/s3_backup_settings.js create mode 100644 influxframework/integrations/doctype/s3_backup_settings/s3_backup_settings.json create mode 100644 influxframework/integrations/doctype/s3_backup_settings/s3_backup_settings.py create mode 100644 influxframework/integrations/doctype/s3_backup_settings/test_s3_backup_settings.py create mode 100644 influxframework/integrations/doctype/slack_webhook_url/__init__.py create mode 100644 influxframework/integrations/doctype/slack_webhook_url/slack_webhook_url.js create mode 100644 influxframework/integrations/doctype/slack_webhook_url/slack_webhook_url.json create mode 100644 influxframework/integrations/doctype/slack_webhook_url/slack_webhook_url.py create mode 100644 influxframework/integrations/doctype/slack_webhook_url/test_slack_webhook_url.py create mode 100644 influxframework/integrations/doctype/social_login_key/__init__.py create mode 100644 influxframework/integrations/doctype/social_login_key/social_login_key.js create mode 100644 influxframework/integrations/doctype/social_login_key/social_login_key.json create mode 100644 influxframework/integrations/doctype/social_login_key/social_login_key.py create mode 100644 influxframework/integrations/doctype/social_login_key/test_social_login_key.py create mode 100644 influxframework/integrations/doctype/social_login_keys/__init__.py create mode 100644 influxframework/integrations/doctype/social_login_keys/social_login_keys.py create mode 100644 influxframework/integrations/doctype/token_cache/__init__.py create mode 100644 influxframework/integrations/doctype/token_cache/test_records.json create mode 100644 influxframework/integrations/doctype/token_cache/test_token_cache.py create mode 100644 influxframework/integrations/doctype/token_cache/token_cache.js create mode 100644 influxframework/integrations/doctype/token_cache/token_cache.json create mode 100644 influxframework/integrations/doctype/token_cache/token_cache.py create mode 100644 influxframework/integrations/doctype/webhook/__init__.py create mode 100644 influxframework/integrations/doctype/webhook/test_webhook.py create mode 100644 influxframework/integrations/doctype/webhook/webhook.js create mode 100644 influxframework/integrations/doctype/webhook/webhook.json create mode 100644 influxframework/integrations/doctype/webhook/webhook.py create mode 100644 influxframework/integrations/doctype/webhook_data/__init__.py create mode 100644 influxframework/integrations/doctype/webhook_data/webhook_data.json create mode 100644 influxframework/integrations/doctype/webhook_data/webhook_data.py create mode 100644 influxframework/integrations/doctype/webhook_header/__init__.py create mode 100644 influxframework/integrations/doctype/webhook_header/webhook_header.json create mode 100644 influxframework/integrations/doctype/webhook_header/webhook_header.py create mode 100644 influxframework/integrations/doctype/webhook_request_log/__init__.py create mode 100644 influxframework/integrations/doctype/webhook_request_log/test_webhook_request_log.py create mode 100644 influxframework/integrations/doctype/webhook_request_log/webhook_request_log.js create mode 100644 influxframework/integrations/doctype/webhook_request_log/webhook_request_log.json create mode 100644 influxframework/integrations/doctype/webhook_request_log/webhook_request_log.py create mode 100644 influxframework/integrations/google_oauth.py create mode 100644 influxframework/integrations/influxframework_providers/__init__.py create mode 100644 influxframework/integrations/influxframework_providers/influxframeworkcloud.py create mode 100644 influxframework/integrations/oauth2.py create mode 100644 influxframework/integrations/oauth2_logins.py create mode 100644 influxframework/integrations/offsite_backup_utils.py create mode 100644 influxframework/integrations/utils.py create mode 100644 influxframework/integrations/workspace/integrations/integrations.json create mode 100644 influxframework/middlewares.py create mode 100644 influxframework/migrate.py create mode 100644 influxframework/model/__init__.py create mode 100644 influxframework/model/base_document.py create mode 100644 influxframework/model/create_new.py create mode 100644 influxframework/model/db_query.py create mode 100644 influxframework/model/delete_doc.py create mode 100644 influxframework/model/docfield.py create mode 100644 influxframework/model/docstatus.py create mode 100644 influxframework/model/document.py create mode 100644 influxframework/model/dynamic_links.py create mode 100644 influxframework/model/mapper.py create mode 100644 influxframework/model/meta.py create mode 100644 influxframework/model/naming.py create mode 100644 influxframework/model/rename_doc.py create mode 100644 influxframework/model/sync.py create mode 100644 influxframework/model/utils/__init__.py create mode 100644 influxframework/model/utils/link_count.py create mode 100644 influxframework/model/utils/rename_doc.py create mode 100644 influxframework/model/utils/rename_field.py create mode 100644 influxframework/model/utils/user_settings.py create mode 100644 influxframework/model/virtual_doctype.py create mode 100644 influxframework/model/workflow.py create mode 100644 influxframework/modules.txt create mode 100644 influxframework/modules/__init__.py create mode 100644 influxframework/modules/export_file.py create mode 100644 influxframework/modules/import_file.py create mode 100644 influxframework/modules/patch_handler.py create mode 100644 influxframework/modules/utils.py create mode 100644 influxframework/monitor.py create mode 100644 influxframework/oauth.py create mode 100644 influxframework/parallel_test_runner.py create mode 100644 influxframework/patches.txt create mode 100644 influxframework/patches/__init__.py create mode 100644 influxframework/patches/v10_0/__init__.py create mode 100644 influxframework/patches/v10_0/enable_chat_by_default_within_system_settings.py create mode 100644 influxframework/patches/v10_0/enhance_security.py create mode 100644 influxframework/patches/v10_0/increase_single_table_column_length.py create mode 100644 influxframework/patches/v10_0/migrate_passwords_passlib.py create mode 100644 influxframework/patches/v10_0/modify_naming_series_table.py create mode 100644 influxframework/patches/v10_0/modify_smallest_currency_fraction.py create mode 100644 influxframework/patches/v10_0/refactor_social_login_keys.py create mode 100644 influxframework/patches/v10_0/reload_countries_and_currencies.py create mode 100644 influxframework/patches/v10_0/remove_custom_field_for_disabled_domain.py create mode 100644 influxframework/patches/v10_0/set_default_locking_time.py create mode 100644 influxframework/patches/v10_0/set_no_copy_to_workflow_state.py create mode 100644 influxframework/patches/v11_0/__init__.py create mode 100644 influxframework/patches/v11_0/apply_customization_to_custom_doctype.py create mode 100644 influxframework/patches/v11_0/change_email_signature_fieldtype.py create mode 100644 influxframework/patches/v11_0/copy_fetch_data_from_options.py create mode 100644 influxframework/patches/v11_0/create_contact_for_user.py create mode 100644 influxframework/patches/v11_0/delete_all_prepared_reports.py create mode 100644 influxframework/patches/v11_0/delete_duplicate_user_permissions.py create mode 100644 influxframework/patches/v11_0/drop_column_apply_user_permissions.py create mode 100644 influxframework/patches/v11_0/fix_order_by_in_reports_json.py create mode 100644 influxframework/patches/v11_0/make_all_prepared_report_attachments_private.py create mode 100644 influxframework/patches/v11_0/migrate_report_settings_for_new_listview.py create mode 100644 influxframework/patches/v11_0/multiple_references_in_events.py create mode 100644 influxframework/patches/v11_0/reload_and_rename_view_log.py create mode 100644 influxframework/patches/v11_0/remove_doctype_user_permissions_for_page_and_report.py create mode 100644 influxframework/patches/v11_0/remove_skip_for_doctype.py create mode 100644 influxframework/patches/v11_0/rename_email_alert_to_notification.py create mode 100644 influxframework/patches/v11_0/rename_google_maps_doctype.py create mode 100644 influxframework/patches/v11_0/rename_standard_reply_to_email_template.py create mode 100644 influxframework/patches/v11_0/rename_workflow_action_to_workflow_action_master.py create mode 100644 influxframework/patches/v11_0/replicate_old_user_permissions.py create mode 100644 influxframework/patches/v11_0/set_allow_self_approval_in_workflow.py create mode 100644 influxframework/patches/v11_0/set_default_letter_head_source.py create mode 100644 influxframework/patches/v11_0/set_dropbox_file_backup.py create mode 100644 influxframework/patches/v11_0/set_missing_creation_and_modified_value_for_user_permissions.py create mode 100644 influxframework/patches/v11_0/update_list_user_settings.py create mode 100644 influxframework/patches/v12_0/__init__.py create mode 100644 influxframework/patches/v12_0/change_existing_dashboard_chart_filters.py create mode 100644 influxframework/patches/v12_0/create_notification_settings_for_user.py create mode 100644 influxframework/patches/v12_0/delete_duplicate_indexes.py create mode 100644 influxframework/patches/v12_0/delete_feedback_request_if_exists.py create mode 100644 influxframework/patches/v12_0/fix_email_id_formatting.py create mode 100644 influxframework/patches/v12_0/fix_public_private_files.py create mode 100644 influxframework/patches/v12_0/move_email_and_phone_to_child_table.py create mode 100644 influxframework/patches/v12_0/move_form_attachments_to_attachments_folder.py create mode 100644 influxframework/patches/v12_0/move_timeline_links_to_dynamic_links.py create mode 100644 influxframework/patches/v12_0/remove_deprecated_fields_from_doctype.py create mode 100644 influxframework/patches/v12_0/remove_example_email_thread_notify.py create mode 100644 influxframework/patches/v12_0/remove_feedback_rating.py create mode 100644 influxframework/patches/v12_0/rename_events_repeat_on.py create mode 100644 influxframework/patches/v12_0/rename_uploaded_files_with_proper_name.py create mode 100644 influxframework/patches/v12_0/replace_null_values_in_tables.py create mode 100644 influxframework/patches/v12_0/reset_home_settings.py create mode 100644 influxframework/patches/v12_0/set_correct_assign_value_in_docs.py create mode 100644 influxframework/patches/v12_0/set_correct_url_in_files.py create mode 100644 influxframework/patches/v12_0/set_default_incoming_email_port.py create mode 100644 influxframework/patches/v12_0/set_default_password_reset_limit.py create mode 100644 influxframework/patches/v12_0/set_primary_key_in_series.py create mode 100644 influxframework/patches/v12_0/setup_comments_from_communications.py create mode 100644 influxframework/patches/v12_0/setup_email_linking.py create mode 100644 influxframework/patches/v12_0/setup_tags.py create mode 100644 influxframework/patches/v12_0/update_auto_repeat_status_and_not_submittable.py create mode 100644 influxframework/patches/v12_0/update_global_search.py create mode 100644 influxframework/patches/v12_0/update_print_format_type.py create mode 100644 influxframework/patches/v13_0/__init__.py create mode 100644 influxframework/patches/v13_0/add_standard_navbar_items.py create mode 100644 influxframework/patches/v13_0/add_switch_theme_to_navbar_settings.py create mode 100644 influxframework/patches/v13_0/add_toggle_width_in_navbar_settings.py create mode 100644 influxframework/patches/v13_0/create_custom_dashboards_cards_and_charts.py create mode 100644 influxframework/patches/v13_0/delete_event_producer_and_consumer_keys.py create mode 100644 influxframework/patches/v13_0/delete_package_publish_tool.py create mode 100644 influxframework/patches/v13_0/email_unsubscribe.py create mode 100644 influxframework/patches/v13_0/enable_custom_script.py create mode 100644 influxframework/patches/v13_0/encrypt_2fa_secrets.py create mode 100644 influxframework/patches/v13_0/generate_theme_files_in_public_folder.py create mode 100644 influxframework/patches/v13_0/increase_password_length.py create mode 100644 influxframework/patches/v13_0/jinja_hook.py create mode 100644 influxframework/patches/v13_0/make_user_type.py create mode 100644 influxframework/patches/v13_0/migrate_translation_column_data.py create mode 100644 influxframework/patches/v13_0/queryreport_columns.py create mode 100644 influxframework/patches/v13_0/remove_chat.py create mode 100644 influxframework/patches/v13_0/remove_custom_link.py create mode 100644 influxframework/patches/v13_0/remove_duplicate_navbar_items.py create mode 100644 influxframework/patches/v13_0/remove_invalid_options_for_data_fields.py create mode 100644 influxframework/patches/v13_0/remove_tailwind_from_page_builder.py create mode 100644 influxframework/patches/v13_0/remove_twilio_settings.py create mode 100644 influxframework/patches/v13_0/remove_web_view.py create mode 100644 influxframework/patches/v13_0/rename_custom_client_script.py create mode 100644 influxframework/patches/v13_0/rename_desk_page_to_workspace.py create mode 100644 influxframework/patches/v13_0/rename_is_custom_field_in_dashboard_chart.py create mode 100644 influxframework/patches/v13_0/rename_list_view_setting_to_list_view_settings.py create mode 100644 influxframework/patches/v13_0/rename_notification_fields.py create mode 100644 influxframework/patches/v13_0/rename_onboarding.py create mode 100644 influxframework/patches/v13_0/replace_field_target_with_open_in_new_tab.py create mode 100644 influxframework/patches/v13_0/replace_old_data_import.py create mode 100644 influxframework/patches/v13_0/reset_corrupt_defaults.py create mode 100644 influxframework/patches/v13_0/set_existing_dashboard_charts_as_public.py create mode 100644 influxframework/patches/v13_0/set_first_day_of_the_week.py create mode 100644 influxframework/patches/v13_0/set_path_for_homepage_in_web_page_view.py create mode 100644 influxframework/patches/v13_0/set_read_times.py create mode 100644 influxframework/patches/v13_0/set_route_for_blog_category.py create mode 100644 influxframework/patches/v13_0/set_social_icons.py create mode 100644 influxframework/patches/v13_0/set_unique_for_page_view.py create mode 100644 influxframework/patches/v13_0/site_wise_logging.py create mode 100644 influxframework/patches/v13_0/update_date_filters_in_user_settings.py create mode 100644 influxframework/patches/v13_0/update_duration_options.py create mode 100644 influxframework/patches/v13_0/update_icons_in_customized_desk_pages.py create mode 100644 influxframework/patches/v13_0/update_newsletter_content_type.py create mode 100644 influxframework/patches/v13_0/update_notification_channel_if_empty.py create mode 100644 influxframework/patches/v13_0/web_template_set_module.py create mode 100644 influxframework/patches/v13_0/website_theme_custom_scss.py create mode 100644 influxframework/patches/v14_0/__init__.py create mode 100644 influxframework/patches/v14_0/add_manage_subscriptions_in_navbar_settings.py create mode 100644 influxframework/patches/v14_0/clear_long_pending_stale_logs.py create mode 100644 influxframework/patches/v14_0/copy_mail_data.py create mode 100644 influxframework/patches/v14_0/delete_data_migration_tool.py create mode 100644 influxframework/patches/v14_0/delete_payment_gateways.py create mode 100644 influxframework/patches/v14_0/different_encryption_key.py create mode 100644 influxframework/patches/v14_0/drop_data_import_legacy.py create mode 100644 influxframework/patches/v14_0/drop_unused_indexes.py create mode 100644 influxframework/patches/v14_0/event_streaming_deprecation_warning.py create mode 100644 influxframework/patches/v14_0/log_settings_migration.py create mode 100644 influxframework/patches/v14_0/remove_db_aggregation.py create mode 100644 influxframework/patches/v14_0/remove_is_first_startup.py create mode 100644 influxframework/patches/v14_0/remove_post_and_post_comment.py create mode 100644 influxframework/patches/v14_0/reset_creation_datetime.py create mode 100644 influxframework/patches/v14_0/save_ratings_in_fraction.py create mode 100644 influxframework/patches/v14_0/set_document_expiry_default.py create mode 100644 influxframework/patches/v14_0/set_suspend_email_queue_default.py create mode 100644 influxframework/patches/v14_0/setup_likes_from_feedback.py create mode 100644 influxframework/patches/v14_0/transform_todo_schema.py create mode 100644 influxframework/patches/v14_0/update_auto_account_deletion_duration.py create mode 100644 influxframework/patches/v14_0/update_color_names_in_kanban_board_column.py create mode 100644 influxframework/patches/v14_0/update_github_endpoints.py create mode 100644 influxframework/patches/v14_0/update_integration_request.py create mode 100644 influxframework/patches/v14_0/update_is_system_generated_flag.py create mode 100644 influxframework/patches/v14_0/update_multistep_webforms.py create mode 100644 influxframework/patches/v14_0/update_webforms.py create mode 100644 influxframework/patches/v14_0/update_workspace2.py create mode 100644 influxframework/permissions.py create mode 100644 influxframework/printing/__init__.py create mode 100644 influxframework/printing/doctype/__init__.py create mode 100644 influxframework/printing/doctype/letter_head/__init__.py create mode 100644 influxframework/printing/doctype/letter_head/letter_head.js create mode 100644 influxframework/printing/doctype/letter_head/letter_head.json create mode 100644 influxframework/printing/doctype/letter_head/letter_head.py create mode 100644 influxframework/printing/doctype/letter_head/test_letter_head.py create mode 100644 influxframework/printing/doctype/network_printer_settings/__init__.py create mode 100644 influxframework/printing/doctype/network_printer_settings/network_printer_settings.js create mode 100644 influxframework/printing/doctype/network_printer_settings/network_printer_settings.json create mode 100644 influxframework/printing/doctype/network_printer_settings/network_printer_settings.py create mode 100644 influxframework/printing/doctype/network_printer_settings/test_network_printer_settings.py create mode 100644 influxframework/printing/doctype/print_format/__init__.py create mode 100644 influxframework/printing/doctype/print_format/print_format.js create mode 100644 influxframework/printing/doctype/print_format/print_format.json create mode 100644 influxframework/printing/doctype/print_format/print_format.py create mode 100644 influxframework/printing/doctype/print_format/test_print_format.py create mode 100644 influxframework/printing/doctype/print_format/test_records.json create mode 100644 influxframework/printing/doctype/print_format_field_template/__init__.py create mode 100644 influxframework/printing/doctype/print_format_field_template/print_format_field_template.js create mode 100644 influxframework/printing/doctype/print_format_field_template/print_format_field_template.json create mode 100644 influxframework/printing/doctype/print_format_field_template/print_format_field_template.py create mode 100644 influxframework/printing/doctype/print_format_field_template/test_print_format_field_template.py create mode 100644 influxframework/printing/doctype/print_heading/__init__.py create mode 100644 influxframework/printing/doctype/print_heading/print_heading.js create mode 100644 influxframework/printing/doctype/print_heading/print_heading.json create mode 100644 influxframework/printing/doctype/print_heading/print_heading.py create mode 100644 influxframework/printing/doctype/print_heading/test_print_heading.py create mode 100644 influxframework/printing/doctype/print_settings/__init__.py create mode 100644 influxframework/printing/doctype/print_settings/print_settings.js create mode 100644 influxframework/printing/doctype/print_settings/print_settings.json create mode 100644 influxframework/printing/doctype/print_settings/print_settings.py create mode 100644 influxframework/printing/doctype/print_settings/test_print_settings.py create mode 100644 influxframework/printing/doctype/print_style/__init__.py create mode 100644 influxframework/printing/doctype/print_style/print_style.js create mode 100644 influxframework/printing/doctype/print_style/print_style.json create mode 100644 influxframework/printing/doctype/print_style/print_style.py create mode 100644 influxframework/printing/doctype/print_style/test_print_style.py create mode 100644 influxframework/printing/form_tour/letter_head/letter_head.json create mode 100644 influxframework/printing/form_tour/print_format/print_format.json create mode 100644 influxframework/printing/page/__init__.py create mode 100644 influxframework/printing/page/print/__init__.py create mode 100644 influxframework/printing/page/print/print.js create mode 100644 influxframework/printing/page/print/print.json create mode 100644 influxframework/printing/page/print/print.py create mode 100644 influxframework/printing/page/print/print_skeleton_loading.html create mode 100644 influxframework/printing/page/print_format_builder/__init__.py create mode 100644 influxframework/printing/page/print_format_builder/print_format_builder.css create mode 100644 influxframework/printing/page/print_format_builder/print_format_builder.js create mode 100644 influxframework/printing/page/print_format_builder/print_format_builder.json create mode 100644 influxframework/printing/page/print_format_builder/print_format_builder.py create mode 100644 influxframework/printing/page/print_format_builder/print_format_builder_column_selector.html create mode 100644 influxframework/printing/page/print_format_builder/print_format_builder_field.html create mode 100644 influxframework/printing/page/print_format_builder/print_format_builder_layout.html create mode 100644 influxframework/printing/page/print_format_builder/print_format_builder_section.html create mode 100644 influxframework/printing/page/print_format_builder/print_format_builder_sidebar.html create mode 100644 influxframework/printing/page/print_format_builder/print_format_builder_start.html create mode 100644 influxframework/printing/page/print_format_builder_beta/__init__.py create mode 100644 influxframework/printing/page/print_format_builder_beta/print_format_builder_beta.css create mode 100644 influxframework/printing/page/print_format_builder_beta/print_format_builder_beta.js create mode 100644 influxframework/printing/page/print_format_builder_beta/print_format_builder_beta.json create mode 100644 influxframework/printing/page/print_format_builder_beta/print_format_builder_beta.py create mode 100644 influxframework/printing/print_style/__init__.py create mode 100644 influxframework/printing/print_style/classic/__init__.py create mode 100644 influxframework/printing/print_style/classic/classic.json create mode 100644 influxframework/printing/print_style/modern/__init__.py create mode 100644 influxframework/printing/print_style/modern/modern.json create mode 100644 influxframework/printing/print_style/monochrome/__init__.py create mode 100644 influxframework/printing/print_style/monochrome/monochrome.json create mode 100644 influxframework/printing/print_style/redesign/__init__.py create mode 100644 influxframework/printing/print_style/redesign/redesign.json create mode 100644 influxframework/public/css/bootstrap.css create mode 100644 influxframework/public/css/fonts/fontawesome/FontAwesome.otf create mode 100644 influxframework/public/css/fonts/fontawesome/LICENSE create mode 100644 influxframework/public/css/fonts/fontawesome/font-awesome.min.css create mode 100644 influxframework/public/css/fonts/fontawesome/fontawesome-webfont.eot create mode 100644 influxframework/public/css/fonts/fontawesome/fontawesome-webfont.svg create mode 100644 influxframework/public/css/fonts/fontawesome/fontawesome-webfont.ttf create mode 100644 influxframework/public/css/fonts/fontawesome/fontawesome-webfont.woff create mode 100644 influxframework/public/css/fonts/fontawesome/fontawesome-webfont.woff2 create mode 100644 influxframework/public/css/fonts/inter/LICENSE.txt create mode 100644 influxframework/public/css/fonts/inter/inter.css create mode 100644 influxframework/public/css/fonts/inter/inter_black.woff create mode 100644 influxframework/public/css/fonts/inter/inter_black.woff2 create mode 100644 influxframework/public/css/fonts/inter/inter_blackitalic.woff create mode 100644 influxframework/public/css/fonts/inter/inter_blackitalic.woff2 create mode 100644 influxframework/public/css/fonts/inter/inter_bold.woff create mode 100644 influxframework/public/css/fonts/inter/inter_bold.woff2 create mode 100644 influxframework/public/css/fonts/inter/inter_bolditalic.woff create mode 100644 influxframework/public/css/fonts/inter/inter_bolditalic.woff2 create mode 100644 influxframework/public/css/fonts/inter/inter_extrabold.woff create mode 100644 influxframework/public/css/fonts/inter/inter_extrabold.woff2 create mode 100644 influxframework/public/css/fonts/inter/inter_extrabolditalic.woff create mode 100644 influxframework/public/css/fonts/inter/inter_extrabolditalic.woff2 create mode 100644 influxframework/public/css/fonts/inter/inter_extralight.woff create mode 100644 influxframework/public/css/fonts/inter/inter_extralight.woff2 create mode 100644 influxframework/public/css/fonts/inter/inter_extralightitalic.woff create mode 100644 influxframework/public/css/fonts/inter/inter_extralightitalic.woff2 create mode 100644 influxframework/public/css/fonts/inter/inter_italic.var.woff2 create mode 100644 influxframework/public/css/fonts/inter/inter_italic.woff create mode 100644 influxframework/public/css/fonts/inter/inter_italic.woff2 create mode 100644 influxframework/public/css/fonts/inter/inter_light.woff create mode 100644 influxframework/public/css/fonts/inter/inter_light.woff2 create mode 100644 influxframework/public/css/fonts/inter/inter_lightitalic.woff create mode 100644 influxframework/public/css/fonts/inter/inter_lightitalic.woff2 create mode 100644 influxframework/public/css/fonts/inter/inter_medium.woff create mode 100644 influxframework/public/css/fonts/inter/inter_medium.woff2 create mode 100644 influxframework/public/css/fonts/inter/inter_mediumitalic.woff create mode 100644 influxframework/public/css/fonts/inter/inter_mediumitalic.woff2 create mode 100644 influxframework/public/css/fonts/inter/inter_regular.woff create mode 100644 influxframework/public/css/fonts/inter/inter_regular.woff2 create mode 100644 influxframework/public/css/fonts/inter/inter_roman.var.woff2 create mode 100644 influxframework/public/css/fonts/inter/inter_semibold.woff create mode 100644 influxframework/public/css/fonts/inter/inter_semibold.woff2 create mode 100644 influxframework/public/css/fonts/inter/inter_semibolditalic.woff create mode 100644 influxframework/public/css/fonts/inter/inter_semibolditalic.woff2 create mode 100644 influxframework/public/css/fonts/inter/inter_thin.woff create mode 100644 influxframework/public/css/fonts/inter/inter_thin.woff2 create mode 100644 influxframework/public/css/fonts/inter/inter_thinitalic.woff create mode 100644 influxframework/public/css/fonts/inter/inter_thinitalic.woff2 create mode 100644 influxframework/public/css/hljs-night-owl.css create mode 100644 influxframework/public/css/octicons/LICENSE.txt create mode 100644 influxframework/public/css/octicons/README.md create mode 100644 influxframework/public/css/octicons/octicons-local.ttf create mode 100644 influxframework/public/css/octicons/octicons.css create mode 100644 influxframework/public/css/octicons/octicons.eot create mode 100644 influxframework/public/css/octicons/octicons.less create mode 100644 influxframework/public/css/octicons/octicons.scss create mode 100644 influxframework/public/css/octicons/octicons.svg create mode 100644 influxframework/public/css/octicons/octicons.ttf create mode 100644 influxframework/public/css/octicons/octicons.woff create mode 100644 influxframework/public/css/octicons/sprockets-octicons.scss create mode 100644 influxframework/public/css/tree.css create mode 100644 influxframework/public/css/tree_grid.css create mode 100644 influxframework/public/html/print_template.html create mode 100644 influxframework/public/icons/social/facebook.svg create mode 100644 influxframework/public/icons/social/fair.svg create mode 100644 influxframework/public/icons/social/frappe.svg create mode 100644 influxframework/public/icons/social/github.svg create mode 100644 influxframework/public/icons/social/google.svg create mode 100644 influxframework/public/icons/social/google_drive.svg create mode 100644 influxframework/public/icons/social/office_365.svg create mode 100644 influxframework/public/icons/social/salesforce.svg create mode 100644 influxframework/public/icons/timeless/icon-check.svg create mode 100644 influxframework/public/icons/timeless/icons.svg create mode 100644 influxframework/public/icons/timeless/message.svg create mode 100644 influxframework/public/icons/timeless/search.svg create mode 100644 influxframework/public/images/background.png create mode 100644 influxframework/public/images/color-circle.png create mode 100644 influxframework/public/images/default-avatar.png create mode 100644 influxframework/public/images/default-skin.svg create mode 100644 influxframework/public/images/fallback-thumbnail.jpg create mode 100644 influxframework/public/images/frappe-favicon.svg create mode 100644 influxframework/public/images/frappe-framework-logo.png create mode 100644 influxframework/public/images/frappe-framework-logo.svg create mode 100644 influxframework/public/images/frappe-logo.png create mode 100644 influxframework/public/images/help/print-style-classic.png create mode 100644 influxframework/public/images/help/print-style-modern.png create mode 100644 influxframework/public/images/help/print-style-monochrome.png create mode 100644 influxframework/public/images/help/print-style-standard.png create mode 100644 influxframework/public/images/leaflet/layers-2x.png create mode 100644 influxframework/public/images/leaflet/layers.png create mode 100644 influxframework/public/images/leaflet/leafletmarker-icon.png create mode 100644 influxframework/public/images/leaflet/leafletmarker-shadow.png create mode 100644 influxframework/public/images/leaflet/lego.png create mode 100644 influxframework/public/images/leaflet/marker-icon-2x.png create mode 100644 influxframework/public/images/leaflet/marker-icon.png create mode 100644 influxframework/public/images/leaflet/marker-shadow.png create mode 100644 influxframework/public/images/leaflet/spritesheet-2x.png create mode 100644 influxframework/public/images/leaflet/spritesheet.png create mode 100644 influxframework/public/images/leaflet/spritesheet.svg create mode 100644 influxframework/public/images/signature-placeholder.png create mode 100644 influxframework/public/images/smiley.png create mode 100644 influxframework/public/images/ui-states/404.png create mode 100644 influxframework/public/images/ui-states/empty-app-state.svg create mode 100644 influxframework/public/images/ui-states/empty.png create mode 100644 influxframework/public/images/ui-states/event-empty-state.svg create mode 100644 influxframework/public/images/ui-states/grid-empty-state.svg create mode 100644 influxframework/public/images/ui-states/list-empty-state.svg create mode 100644 influxframework/public/images/ui-states/notification-empty-state.svg create mode 100644 influxframework/public/images/ui-states/search-empty-state.svg create mode 100644 influxframework/public/images/ui-states/success-color.png create mode 100644 influxframework/public/images/ui/ajax-loader.gif create mode 100644 influxframework/public/images/ui/bot.png create mode 100644 influxframework/public/images/ui/bubble-tea-happy.svg create mode 100644 influxframework/public/images/ui/bubble-tea-smile.svg create mode 100644 influxframework/public/images/ui/bubble-tea-sorry.svg create mode 100644 influxframework/public/images/up.png create mode 100644 influxframework/public/js/bootstrap-4-web.bundle.js create mode 100644 influxframework/public/js/controls.bundle.js create mode 100644 influxframework/public/js/data_import_tools.bundle.js create mode 100644 influxframework/public/js/desk.bundle.js create mode 100644 influxframework/public/js/dialog.bundle.js create mode 100644 influxframework/public/js/form.bundle.js create mode 100644 influxframework/public/js/influxframework-web.bundle.js create mode 100644 influxframework/public/js/influxframework/assets.js create mode 100644 influxframework/public/js/influxframework/build_events/BuildError.vue create mode 100644 influxframework/public/js/influxframework/build_events/BuildSuccess.vue create mode 100644 influxframework/public/js/influxframework/build_events/build_events.bundle.js create mode 100644 influxframework/public/js/influxframework/change_log.html create mode 100644 influxframework/public/js/influxframework/class.js create mode 100644 influxframework/public/js/influxframework/color_picker/color_picker.js create mode 100644 influxframework/public/js/influxframework/color_picker/utils.js create mode 100644 influxframework/public/js/influxframework/data_import/data_exporter.js create mode 100644 influxframework/public/js/influxframework/data_import/import_preview.js create mode 100644 influxframework/public/js/influxframework/data_import/index.js create mode 100644 influxframework/public/js/influxframework/db.js create mode 100644 influxframework/public/js/influxframework/defaults.js create mode 100644 influxframework/public/js/influxframework/desk.js create mode 100644 influxframework/public/js/influxframework/doctype/index.js create mode 100644 influxframework/public/js/influxframework/dom.js create mode 100644 influxframework/public/js/influxframework/event_emitter.js create mode 100644 influxframework/public/js/influxframework/file_uploader/FileBrowser.vue create mode 100644 influxframework/public/js/influxframework/file_uploader/FilePreview.vue create mode 100644 influxframework/public/js/influxframework/file_uploader/FileUploader.vue create mode 100644 influxframework/public/js/influxframework/file_uploader/ImageCropper.vue create mode 100644 influxframework/public/js/influxframework/file_uploader/ProgressRing.vue create mode 100644 influxframework/public/js/influxframework/file_uploader/TreeNode.vue create mode 100644 influxframework/public/js/influxframework/file_uploader/WebLink.vue create mode 100644 influxframework/public/js/influxframework/file_uploader/index.js create mode 100644 influxframework/public/js/influxframework/form/column.js create mode 100644 influxframework/public/js/influxframework/form/controls/attach.js create mode 100644 influxframework/public/js/influxframework/form/controls/attach_image.js create mode 100644 influxframework/public/js/influxframework/form/controls/autocomplete.js create mode 100644 influxframework/public/js/influxframework/form/controls/barcode.js create mode 100644 influxframework/public/js/influxframework/form/controls/base_control.js create mode 100644 influxframework/public/js/influxframework/form/controls/base_input.js create mode 100644 influxframework/public/js/influxframework/form/controls/button.js create mode 100644 influxframework/public/js/influxframework/form/controls/check.js create mode 100644 influxframework/public/js/influxframework/form/controls/code.js create mode 100644 influxframework/public/js/influxframework/form/controls/color.js create mode 100644 influxframework/public/js/influxframework/form/controls/comment.js create mode 100644 influxframework/public/js/influxframework/form/controls/control.js create mode 100644 influxframework/public/js/influxframework/form/controls/currency.js create mode 100644 influxframework/public/js/influxframework/form/controls/data.js create mode 100644 influxframework/public/js/influxframework/form/controls/date.js create mode 100644 influxframework/public/js/influxframework/form/controls/date_range.js create mode 100644 influxframework/public/js/influxframework/form/controls/datepicker_i18n.js create mode 100644 influxframework/public/js/influxframework/form/controls/datetime.js create mode 100644 influxframework/public/js/influxframework/form/controls/duration.js create mode 100644 influxframework/public/js/influxframework/form/controls/dynamic_link.js create mode 100644 influxframework/public/js/influxframework/form/controls/float.js create mode 100644 influxframework/public/js/influxframework/form/controls/geolocation.js create mode 100644 influxframework/public/js/influxframework/form/controls/heading.js create mode 100644 influxframework/public/js/influxframework/form/controls/html.js create mode 100644 influxframework/public/js/influxframework/form/controls/html_editor.js create mode 100644 influxframework/public/js/influxframework/form/controls/icon.js create mode 100644 influxframework/public/js/influxframework/form/controls/image.js create mode 100644 influxframework/public/js/influxframework/form/controls/int.js create mode 100644 influxframework/public/js/influxframework/form/controls/json.js create mode 100644 influxframework/public/js/influxframework/form/controls/link.js create mode 100644 influxframework/public/js/influxframework/form/controls/markdown_editor.js create mode 100644 influxframework/public/js/influxframework/form/controls/multicheck.js create mode 100644 influxframework/public/js/influxframework/form/controls/multiselect.js create mode 100644 influxframework/public/js/influxframework/form/controls/multiselect_list.js create mode 100644 influxframework/public/js/influxframework/form/controls/multiselect_pills.js create mode 100644 influxframework/public/js/influxframework/form/controls/password.js create mode 100644 influxframework/public/js/influxframework/form/controls/phone.js create mode 100644 influxframework/public/js/influxframework/form/controls/quill-mention/blots/mention.js create mode 100644 influxframework/public/js/influxframework/form/controls/quill-mention/constants/keys.js create mode 100644 influxframework/public/js/influxframework/form/controls/quill-mention/quill.mention.js create mode 100644 influxframework/public/js/influxframework/form/controls/rating.js create mode 100644 influxframework/public/js/influxframework/form/controls/select.js create mode 100644 influxframework/public/js/influxframework/form/controls/signature.js create mode 100644 influxframework/public/js/influxframework/form/controls/table.js create mode 100644 influxframework/public/js/influxframework/form/controls/table_multiselect.js create mode 100644 influxframework/public/js/influxframework/form/controls/text.js create mode 100644 influxframework/public/js/influxframework/form/controls/text_editor.js create mode 100644 influxframework/public/js/influxframework/form/controls/time.js create mode 100644 influxframework/public/js/influxframework/form/dashboard.js create mode 100644 influxframework/public/js/influxframework/form/footer/base_timeline.js create mode 100644 influxframework/public/js/influxframework/form/footer/footer.js create mode 100644 influxframework/public/js/influxframework/form/footer/form_timeline.js create mode 100644 influxframework/public/js/influxframework/form/footer/version_timeline_content_builder.js create mode 100644 influxframework/public/js/influxframework/form/form.js create mode 100644 influxframework/public/js/influxframework/form/form_tour.js create mode 100644 influxframework/public/js/influxframework/form/form_viewers.js create mode 100644 influxframework/public/js/influxframework/form/formatters.js create mode 100644 influxframework/public/js/influxframework/form/grid.js create mode 100644 influxframework/public/js/influxframework/form/grid_pagination.js create mode 100644 influxframework/public/js/influxframework/form/grid_row.js create mode 100644 influxframework/public/js/influxframework/form/grid_row_form.js create mode 100644 influxframework/public/js/influxframework/form/layout.js create mode 100644 influxframework/public/js/influxframework/form/link_selector.js create mode 100644 influxframework/public/js/influxframework/form/linked_with.js create mode 100644 influxframework/public/js/influxframework/form/multi_select_dialog.js create mode 100644 influxframework/public/js/influxframework/form/print_utils.js create mode 100644 influxframework/public/js/influxframework/form/quick_entry.js create mode 100644 influxframework/public/js/influxframework/form/save.js create mode 100644 influxframework/public/js/influxframework/form/script_helpers.js create mode 100644 influxframework/public/js/influxframework/form/script_manager.js create mode 100644 influxframework/public/js/influxframework/form/section.js create mode 100644 influxframework/public/js/influxframework/form/sidebar/assign_to.js create mode 100644 influxframework/public/js/influxframework/form/sidebar/attachments.js create mode 100644 influxframework/public/js/influxframework/form/sidebar/document_follow.js create mode 100644 influxframework/public/js/influxframework/form/sidebar/form_sidebar.js create mode 100644 influxframework/public/js/influxframework/form/sidebar/form_sidebar_users.js create mode 100644 influxframework/public/js/influxframework/form/sidebar/review.js create mode 100644 influxframework/public/js/influxframework/form/sidebar/share.js create mode 100644 influxframework/public/js/influxframework/form/sidebar/user_image.js create mode 100644 influxframework/public/js/influxframework/form/success_action.js create mode 100644 influxframework/public/js/influxframework/form/tab.js create mode 100644 influxframework/public/js/influxframework/form/templates/address_list.html create mode 100644 influxframework/public/js/influxframework/form/templates/contact_list.html create mode 100644 influxframework/public/js/influxframework/form/templates/form_dashboard.html create mode 100644 influxframework/public/js/influxframework/form/templates/form_footer.html create mode 100644 influxframework/public/js/influxframework/form/templates/form_links.html create mode 100644 influxframework/public/js/influxframework/form/templates/form_sidebar.html create mode 100644 influxframework/public/js/influxframework/form/templates/print_layout.html create mode 100644 influxframework/public/js/influxframework/form/templates/report_links.html create mode 100644 influxframework/public/js/influxframework/form/templates/set_sharing.html create mode 100644 influxframework/public/js/influxframework/form/templates/timeline_message_box.html create mode 100644 influxframework/public/js/influxframework/form/templates/users_in_sidebar.html create mode 100644 influxframework/public/js/influxframework/form/toolbar.js create mode 100644 influxframework/public/js/influxframework/form/undo_manager.js create mode 100644 influxframework/public/js/influxframework/form/workflow.js create mode 100644 influxframework/public/js/influxframework/format.js create mode 100644 influxframework/public/js/influxframework/icon_picker/icon_picker.js create mode 100644 influxframework/public/js/influxframework/list/base_list.js create mode 100644 influxframework/public/js/influxframework/list/bulk_operations.js create mode 100644 influxframework/public/js/influxframework/list/list_factory.js create mode 100644 influxframework/public/js/influxframework/list/list_filter.js create mode 100644 influxframework/public/js/influxframework/list/list_settings.js create mode 100644 influxframework/public/js/influxframework/list/list_sidebar.html create mode 100644 influxframework/public/js/influxframework/list/list_sidebar.js create mode 100644 influxframework/public/js/influxframework/list/list_sidebar_group_by.js create mode 100644 influxframework/public/js/influxframework/list/list_sidebar_stat.html create mode 100644 influxframework/public/js/influxframework/list/list_view.js create mode 100644 influxframework/public/js/influxframework/list/list_view_permission_restrictions.html create mode 100644 influxframework/public/js/influxframework/list/list_view_select.js create mode 100644 influxframework/public/js/influxframework/logtypes.js create mode 100644 influxframework/public/js/influxframework/meta_tag.js create mode 100644 influxframework/public/js/influxframework/microtemplate.js create mode 100644 influxframework/public/js/influxframework/model/create_new.js create mode 100644 influxframework/public/js/influxframework/model/indicator.js create mode 100644 influxframework/public/js/influxframework/model/meta.js create mode 100644 influxframework/public/js/influxframework/model/model.js create mode 100644 influxframework/public/js/influxframework/model/perm.js create mode 100644 influxframework/public/js/influxframework/model/sync.js create mode 100644 influxframework/public/js/influxframework/model/user_settings.js create mode 100644 influxframework/public/js/influxframework/model/workflow.js create mode 100644 influxframework/public/js/influxframework/module_editor.js create mode 100644 influxframework/public/js/influxframework/phone_picker/phone_picker.js create mode 100644 influxframework/public/js/influxframework/polyfill.js create mode 100644 influxframework/public/js/influxframework/provide.js create mode 100644 influxframework/public/js/influxframework/query_string.js create mode 100644 influxframework/public/js/influxframework/recorder/RecorderDetail.vue create mode 100644 influxframework/public/js/influxframework/recorder/RecorderRoot.vue create mode 100644 influxframework/public/js/influxframework/recorder/RequestDetail.vue create mode 100644 influxframework/public/js/influxframework/recorder/recorder.js create mode 100644 influxframework/public/js/influxframework/request.js create mode 100644 influxframework/public/js/influxframework/roles_editor.js create mode 100644 influxframework/public/js/influxframework/router.js create mode 100644 influxframework/public/js/influxframework/router_history.js create mode 100644 influxframework/public/js/influxframework/scanner/index.js create mode 100644 influxframework/public/js/influxframework/socketio_client.js create mode 100644 influxframework/public/js/influxframework/translate.js create mode 100644 influxframework/public/js/influxframework/ui/alt_keyboard_shortcuts.js create mode 100644 influxframework/public/js/influxframework/ui/app_icon.js create mode 100644 influxframework/public/js/influxframework/ui/capture.js create mode 100644 influxframework/public/js/influxframework/ui/chart.js create mode 100644 influxframework/public/js/influxframework/ui/colors.js create mode 100644 influxframework/public/js/influxframework/ui/datatable.js create mode 100644 influxframework/public/js/influxframework/ui/dialog.js create mode 100644 influxframework/public/js/influxframework/ui/driver.js create mode 100644 influxframework/public/js/influxframework/ui/field_group.js create mode 100644 influxframework/public/js/influxframework/ui/filters/edit_filter.html create mode 100644 influxframework/public/js/influxframework/ui/filters/field_select.js create mode 100644 influxframework/public/js/influxframework/ui/filters/filter.js create mode 100644 influxframework/public/js/influxframework/ui/filters/filter_list.js create mode 100644 influxframework/public/js/influxframework/ui/find.js create mode 100644 influxframework/public/js/influxframework/ui/group_by/group_by.html create mode 100644 influxframework/public/js/influxframework/ui/group_by/group_by.js create mode 100644 influxframework/public/js/influxframework/ui/iconbar.js create mode 100644 influxframework/public/js/influxframework/ui/keyboard.js create mode 100644 influxframework/public/js/influxframework/ui/like.js create mode 100644 influxframework/public/js/influxframework/ui/link_preview.js create mode 100644 influxframework/public/js/influxframework/ui/listing.html create mode 100644 influxframework/public/js/influxframework/ui/messages.js create mode 100644 influxframework/public/js/influxframework/ui/notifications/notifications.js create mode 100644 influxframework/public/js/influxframework/ui/page.html create mode 100644 influxframework/public/js/influxframework/ui/page.js create mode 100644 influxframework/public/js/influxframework/ui/sidebar.js create mode 100644 influxframework/public/js/influxframework/ui/slides.js create mode 100644 influxframework/public/js/influxframework/ui/sort_selector.html create mode 100644 influxframework/public/js/influxframework/ui/sort_selector.js create mode 100644 influxframework/public/js/influxframework/ui/tag_editor.js create mode 100644 influxframework/public/js/influxframework/ui/tags.js create mode 100644 influxframework/public/js/influxframework/ui/theme_switcher.js create mode 100644 influxframework/public/js/influxframework/ui/toolbar/about.js create mode 100644 influxframework/public/js/influxframework/ui/toolbar/awesome_bar.js create mode 100644 influxframework/public/js/influxframework/ui/toolbar/fuzzy_match.js create mode 100644 influxframework/public/js/influxframework/ui/toolbar/navbar.html create mode 100644 influxframework/public/js/influxframework/ui/toolbar/search.html create mode 100644 influxframework/public/js/influxframework/ui/toolbar/search.js create mode 100644 influxframework/public/js/influxframework/ui/toolbar/search_utils.js create mode 100644 influxframework/public/js/influxframework/ui/toolbar/subscription.js create mode 100644 influxframework/public/js/influxframework/ui/toolbar/tag_utils.js create mode 100644 influxframework/public/js/influxframework/ui/toolbar/toolbar.js create mode 100644 influxframework/public/js/influxframework/ui/tree.js create mode 100644 influxframework/public/js/influxframework/ui/workspace_loading_skeleton.html create mode 100644 influxframework/public/js/influxframework/ui/workspace_sidebar_loading_skeleton.html create mode 100644 influxframework/public/js/influxframework/upload.js create mode 100644 influxframework/public/js/influxframework/utils/address_and_contact.js create mode 100644 influxframework/public/js/influxframework/utils/common.js create mode 100644 influxframework/public/js/influxframework/utils/dashboard_utils.js create mode 100644 influxframework/public/js/influxframework/utils/datatable.js create mode 100644 influxframework/public/js/influxframework/utils/datatype.js create mode 100644 influxframework/public/js/influxframework/utils/datetime.js create mode 100644 influxframework/public/js/influxframework/utils/diffview.js create mode 100644 influxframework/public/js/influxframework/utils/display_image.js create mode 100644 influxframework/public/js/influxframework/utils/energy_point_utils.js create mode 100644 influxframework/public/js/influxframework/utils/file_manager.js create mode 100644 influxframework/public/js/influxframework/utils/help.js create mode 100644 influxframework/public/js/influxframework/utils/help_links.js create mode 100644 influxframework/public/js/influxframework/utils/number_format.js create mode 100644 influxframework/public/js/influxframework/utils/number_systems.js create mode 100644 influxframework/public/js/influxframework/utils/pretty_date.js create mode 100644 influxframework/public/js/influxframework/utils/preview_email.js create mode 100644 influxframework/public/js/influxframework/utils/tools.js create mode 100644 influxframework/public/js/influxframework/utils/urllib.js create mode 100644 influxframework/public/js/influxframework/utils/user.js create mode 100644 influxframework/public/js/influxframework/utils/utils.js create mode 100644 influxframework/public/js/influxframework/utils/web_template.js create mode 100644 influxframework/public/js/influxframework/views/breadcrumbs.js create mode 100644 influxframework/public/js/influxframework/views/calendar/calendar.js create mode 100644 influxframework/public/js/influxframework/views/communication.js create mode 100644 influxframework/public/js/influxframework/views/container.js create mode 100644 influxframework/public/js/influxframework/views/dashboard/dashboard_view.js create mode 100644 influxframework/public/js/influxframework/views/factory.js create mode 100644 influxframework/public/js/influxframework/views/file/file_view.js create mode 100644 influxframework/public/js/influxframework/views/formview.js create mode 100644 influxframework/public/js/influxframework/views/gantt/gantt_view.js create mode 100644 influxframework/public/js/influxframework/views/image/image_view.js create mode 100644 influxframework/public/js/influxframework/views/image/image_view_item_row.html create mode 100644 influxframework/public/js/influxframework/views/image/photoswipe_dom.html create mode 100644 influxframework/public/js/influxframework/views/inbox/inbox_view.js create mode 100644 influxframework/public/js/influxframework/views/interaction.js create mode 100644 influxframework/public/js/influxframework/views/kanban/kanban_board.bundle.js create mode 100644 influxframework/public/js/influxframework/views/kanban/kanban_board.html create mode 100644 influxframework/public/js/influxframework/views/kanban/kanban_card.html create mode 100644 influxframework/public/js/influxframework/views/kanban/kanban_column.html create mode 100644 influxframework/public/js/influxframework/views/kanban/kanban_settings.js create mode 100644 influxframework/public/js/influxframework/views/kanban/kanban_view.js create mode 100644 influxframework/public/js/influxframework/views/map/map_view.js create mode 100644 influxframework/public/js/influxframework/views/modules_home.js create mode 100644 influxframework/public/js/influxframework/views/pageview.js create mode 100644 influxframework/public/js/influxframework/views/reports/print_grid.html create mode 100644 influxframework/public/js/influxframework/views/reports/print_tree.html create mode 100644 influxframework/public/js/influxframework/views/reports/query_report.js create mode 100644 influxframework/public/js/influxframework/views/reports/report_factory.js create mode 100644 influxframework/public/js/influxframework/views/reports/report_utils.js create mode 100644 influxframework/public/js/influxframework/views/reports/report_view.js create mode 100644 influxframework/public/js/influxframework/views/translation_manager.js create mode 100644 influxframework/public/js/influxframework/views/treeview.js create mode 100644 influxframework/public/js/influxframework/views/workspace/blocks/block.js create mode 100644 influxframework/public/js/influxframework/views/workspace/blocks/card.js create mode 100644 influxframework/public/js/influxframework/views/workspace/blocks/chart.js create mode 100644 influxframework/public/js/influxframework/views/workspace/blocks/header.js create mode 100644 influxframework/public/js/influxframework/views/workspace/blocks/header_size.js create mode 100644 influxframework/public/js/influxframework/views/workspace/blocks/index.js create mode 100644 influxframework/public/js/influxframework/views/workspace/blocks/onboarding.js create mode 100644 influxframework/public/js/influxframework/views/workspace/blocks/paragraph.js create mode 100644 influxframework/public/js/influxframework/views/workspace/blocks/quick_list.js create mode 100644 influxframework/public/js/influxframework/views/workspace/blocks/shortcut.js create mode 100644 influxframework/public/js/influxframework/views/workspace/blocks/spacer.js create mode 100644 influxframework/public/js/influxframework/views/workspace/workspace.js create mode 100644 influxframework/public/js/influxframework/web_form/web_form.js create mode 100644 influxframework/public/js/influxframework/web_form/web_form_list.js create mode 100644 influxframework/public/js/influxframework/web_form/webform_script.js create mode 100644 influxframework/public/js/influxframework/widgets/base_widget.js create mode 100644 influxframework/public/js/influxframework/widgets/chart_widget.js create mode 100644 influxframework/public/js/influxframework/widgets/links_widget.js create mode 100644 influxframework/public/js/influxframework/widgets/new_widget.js create mode 100644 influxframework/public/js/influxframework/widgets/number_card_widget.js create mode 100644 influxframework/public/js/influxframework/widgets/onboarding_widget.js create mode 100644 influxframework/public/js/influxframework/widgets/quick_list_widget.js create mode 100644 influxframework/public/js/influxframework/widgets/shortcut_widget.js create mode 100644 influxframework/public/js/influxframework/widgets/widget_dialog.js create mode 100644 influxframework/public/js/influxframework/widgets/widget_group.js create mode 100644 influxframework/public/js/integrations/google_drive_picker.js create mode 100644 influxframework/public/js/jquery-bootstrap.js create mode 100644 influxframework/public/js/lib/fullcalendar/LICENCE.txt create mode 100644 influxframework/public/js/lib/fullcalendar/fullcalendar.min.css create mode 100644 influxframework/public/js/lib/fullcalendar/fullcalendar.min.js create mode 100644 influxframework/public/js/lib/fullcalendar/locale-all.js create mode 100644 influxframework/public/js/lib/jSignature.min.js create mode 100644 influxframework/public/js/lib/jquery/LICENCE.txt create mode 100644 influxframework/public/js/lib/jquery/jquery.min.js create mode 100644 influxframework/public/js/lib/leaflet/LICENSE create mode 100644 influxframework/public/js/lib/leaflet/leaflet.css create mode 100644 influxframework/public/js/lib/leaflet/leaflet.js create mode 100644 influxframework/public/js/lib/leaflet_control_locate/L.Control.Locate.css create mode 100644 influxframework/public/js/lib/leaflet_control_locate/L.Control.Locate.js create mode 100644 influxframework/public/js/lib/leaflet_control_locate/LICENSE create mode 100644 influxframework/public/js/lib/leaflet_draw/MIT-LICENSE.md create mode 100644 influxframework/public/js/lib/leaflet_draw/leaflet.draw.css create mode 100644 influxframework/public/js/lib/leaflet_draw/leaflet.draw.js create mode 100644 influxframework/public/js/lib/leaflet_easy_button/LICENSE create mode 100644 influxframework/public/js/lib/leaflet_easy_button/easy-button.css create mode 100644 influxframework/public/js/lib/leaflet_easy_button/easy-button.js create mode 100644 influxframework/public/js/lib/moment.js create mode 100644 influxframework/public/js/lib/photoswipe/LICENCE create mode 100644 influxframework/public/js/lib/photoswipe/default-skin.css create mode 100644 influxframework/public/js/lib/photoswipe/photoswipe-ui-default.js create mode 100644 influxframework/public/js/lib/photoswipe/photoswipe.css create mode 100644 influxframework/public/js/lib/photoswipe/photoswipe.js create mode 100644 influxframework/public/js/libs.bundle.js create mode 100644 influxframework/public/js/list.bundle.js create mode 100644 influxframework/public/js/logtypes.bundle.js create mode 100644 influxframework/public/js/print_format_builder/ConfigureColumns.vue create mode 100644 influxframework/public/js/print_format_builder/Field.vue create mode 100644 influxframework/public/js/print_format_builder/HTMLEditor.vue create mode 100644 influxframework/public/js/print_format_builder/LetterHeadEditor.vue create mode 100644 influxframework/public/js/print_format_builder/Preview.vue create mode 100644 influxframework/public/js/print_format_builder/PrintFormat.vue create mode 100644 influxframework/public/js/print_format_builder/PrintFormatBuilder.vue create mode 100644 influxframework/public/js/print_format_builder/PrintFormatControls.vue create mode 100644 influxframework/public/js/print_format_builder/PrintFormatSection.vue create mode 100644 influxframework/public/js/print_format_builder/print_format_builder.bundle.js create mode 100644 influxframework/public/js/print_format_builder/store.js create mode 100644 influxframework/public/js/print_format_builder/utils.js create mode 100644 influxframework/public/js/recorder.bundle.js create mode 100644 influxframework/public/js/report.bundle.js create mode 100644 influxframework/public/js/user_profile_controller.bundle.js create mode 100644 influxframework/public/js/video_player.bundle.js create mode 100644 influxframework/public/js/web_form.bundle.js create mode 100644 influxframework/public/scss/common/alert.scss create mode 100644 influxframework/public/scss/common/awesomeplete.scss create mode 100644 influxframework/public/scss/common/buttons.scss create mode 100644 influxframework/public/scss/common/color_picker.scss create mode 100644 influxframework/public/scss/common/controls.scss create mode 100644 influxframework/public/scss/common/css_variables.scss create mode 100644 influxframework/public/scss/common/datepicker.scss create mode 100644 influxframework/public/scss/common/flex.scss create mode 100644 influxframework/public/scss/common/form.scss create mode 100644 influxframework/public/scss/common/global.scss create mode 100644 influxframework/public/scss/common/grid.scss create mode 100644 influxframework/public/scss/common/icon_picker.scss create mode 100644 influxframework/public/scss/common/icons.scss create mode 100644 influxframework/public/scss/common/indicator.scss create mode 100644 influxframework/public/scss/common/mixins.scss create mode 100644 influxframework/public/scss/common/modal.scss create mode 100644 influxframework/public/scss/common/phone_picker.scss create mode 100644 influxframework/public/scss/common/quill.scss create mode 100644 influxframework/public/scss/desk.bundle.scss create mode 100644 influxframework/public/scss/desk/avatar.scss create mode 100644 influxframework/public/scss/desk/breadcrumb.scss create mode 100644 influxframework/public/scss/desk/calendar.scss create mode 100644 influxframework/public/scss/desk/css_variables.scss create mode 100644 influxframework/public/scss/desk/dark.scss create mode 100644 influxframework/public/scss/desk/dashboard_view.scss create mode 100644 influxframework/public/scss/desk/data_import.scss create mode 100644 influxframework/public/scss/desk/desktop.scss create mode 100644 influxframework/public/scss/desk/driver.scss create mode 100644 influxframework/public/scss/desk/file_view.scss create mode 100644 influxframework/public/scss/desk/filters.scss create mode 100644 influxframework/public/scss/desk/form.scss create mode 100644 influxframework/public/scss/desk/global.scss create mode 100644 influxframework/public/scss/desk/global_search.scss create mode 100644 influxframework/public/scss/desk/image_view.scss create mode 100644 influxframework/public/scss/desk/index.scss create mode 100644 influxframework/public/scss/desk/influxframework_datatable.scss create mode 100644 influxframework/public/scss/desk/influxframework_gantt.scss create mode 100644 influxframework/public/scss/desk/kanban.scss create mode 100644 influxframework/public/scss/desk/link_preview.scss create mode 100644 influxframework/public/scss/desk/list.scss create mode 100644 influxframework/public/scss/desk/mobile.scss create mode 100644 influxframework/public/scss/desk/module.scss create mode 100644 influxframework/public/scss/desk/navbar.scss create mode 100644 influxframework/public/scss/desk/notification.scss create mode 100644 influxframework/public/scss/desk/page.scss create mode 100644 influxframework/public/scss/desk/plyr.scss create mode 100644 influxframework/public/scss/desk/print_preview.scss create mode 100644 influxframework/public/scss/desk/report.scss create mode 100644 influxframework/public/scss/desk/role_editor.scss create mode 100644 influxframework/public/scss/desk/scrollbar.scss create mode 100644 influxframework/public/scss/desk/sidebar.scss create mode 100644 influxframework/public/scss/desk/slides.scss create mode 100644 influxframework/public/scss/desk/tags.scss create mode 100644 influxframework/public/scss/desk/theme_switcher.scss create mode 100644 influxframework/public/scss/desk/timeline.scss create mode 100644 influxframework/public/scss/desk/toast.scss create mode 100644 influxframework/public/scss/desk/tree.scss create mode 100644 influxframework/public/scss/desk/typography.scss create mode 100644 influxframework/public/scss/desk/user_profile.scss create mode 100644 influxframework/public/scss/desk/variables.scss create mode 100644 influxframework/public/scss/desk/version.scss create mode 100644 influxframework/public/scss/element/checkbox.scss create mode 100644 influxframework/public/scss/element/radio.scss create mode 100644 influxframework/public/scss/email.bundle.scss create mode 100644 influxframework/public/scss/login.bundle.scss create mode 100644 influxframework/public/scss/print.bundle.scss create mode 100644 influxframework/public/scss/print_format.bundle.scss create mode 100644 influxframework/public/scss/report.bundle.scss create mode 100644 influxframework/public/scss/web_form.bundle.scss create mode 100644 influxframework/public/scss/website.bundle.scss create mode 100644 influxframework/public/scss/website/base.scss create mode 100644 influxframework/public/scss/website/blog.scss create mode 100644 influxframework/public/scss/website/css_variables.scss create mode 100644 influxframework/public/scss/website/doc.scss create mode 100644 influxframework/public/scss/website/error-state.scss create mode 100644 influxframework/public/scss/website/footer.scss create mode 100644 influxframework/public/scss/website/index.scss create mode 100644 influxframework/public/scss/website/markdown.scss create mode 100644 influxframework/public/scss/website/multilevel_dropdown.scss create mode 100644 influxframework/public/scss/website/my_account.scss create mode 100644 influxframework/public/scss/website/navbar.scss create mode 100644 influxframework/public/scss/website/page_builder.scss create mode 100644 influxframework/public/scss/website/portal.scss create mode 100644 influxframework/public/scss/website/search.scss create mode 100644 influxframework/public/scss/website/sidebar.scss create mode 100644 influxframework/public/scss/website/variables.scss create mode 100644 influxframework/public/scss/website/web_form.scss create mode 100644 influxframework/public/scss/website/website_avatar.scss create mode 100644 influxframework/public/scss/website/website_image.scss create mode 100644 influxframework/public/sounds/alert.mp3 create mode 100644 influxframework/public/sounds/cancel.mp3 create mode 100644 influxframework/public/sounds/chime.mp3 create mode 100644 influxframework/public/sounds/click.mp3 create mode 100644 influxframework/public/sounds/delete.mp3 create mode 100644 influxframework/public/sounds/email.mp3 create mode 100644 influxframework/public/sounds/error.mp3 create mode 100644 influxframework/public/sounds/submit.mp3 create mode 100644 influxframework/query_builder/__init__.py create mode 100644 influxframework/query_builder/builder.py create mode 100644 influxframework/query_builder/custom.py create mode 100644 influxframework/query_builder/functions.py create mode 100644 influxframework/query_builder/terms.py create mode 100644 influxframework/query_builder/utils.py create mode 100644 influxframework/rate_limiter.py create mode 100644 influxframework/realtime.py create mode 100644 influxframework/recorder.py create mode 100644 influxframework/search/__init__.py create mode 100644 influxframework/search/full_text_search.py create mode 100644 influxframework/search/test_full_text_search.py create mode 100644 influxframework/search/website_search.py create mode 100644 influxframework/sessions.py create mode 100644 influxframework/share.py create mode 100644 influxframework/social/__init__.py create mode 100644 influxframework/social/doctype/__init__.py create mode 100644 influxframework/social/doctype/energy_point_log/__init__.py create mode 100644 influxframework/social/doctype/energy_point_log/energy_point_log.js create mode 100644 influxframework/social/doctype/energy_point_log/energy_point_log.json create mode 100644 influxframework/social/doctype/energy_point_log/energy_point_log.py create mode 100644 influxframework/social/doctype/energy_point_log/energy_point_log_list.js create mode 100644 influxframework/social/doctype/energy_point_log/test_energy_point_log.py create mode 100644 influxframework/social/doctype/energy_point_rule/__init__.py create mode 100644 influxframework/social/doctype/energy_point_rule/energy_point_rule.js create mode 100644 influxframework/social/doctype/energy_point_rule/energy_point_rule.json create mode 100644 influxframework/social/doctype/energy_point_rule/energy_point_rule.py create mode 100644 influxframework/social/doctype/energy_point_settings/__init__.py create mode 100644 influxframework/social/doctype/energy_point_settings/energy_point_settings.js create mode 100644 influxframework/social/doctype/energy_point_settings/energy_point_settings.json create mode 100644 influxframework/social/doctype/energy_point_settings/energy_point_settings.py create mode 100644 influxframework/social/doctype/energy_point_settings/test_energy_point_settings.py create mode 100644 influxframework/social/doctype/review_level/__init__.py create mode 100644 influxframework/social/doctype/review_level/review_level.js create mode 100644 influxframework/social/doctype/review_level/review_level.json create mode 100644 influxframework/social/doctype/review_level/review_level.py create mode 100644 influxframework/templates/__init__.py create mode 100644 influxframework/templates/base.html create mode 100644 influxframework/templates/discussions/button.html create mode 100644 influxframework/templates/discussions/comment_box.html create mode 100644 influxframework/templates/discussions/discussions.js create mode 100644 influxframework/templates/discussions/discussions_section.html create mode 100644 influxframework/templates/discussions/reply_card.html create mode 100644 influxframework/templates/discussions/reply_section.html create mode 100644 influxframework/templates/discussions/search.html create mode 100644 influxframework/templates/discussions/sidebar.html create mode 100644 influxframework/templates/discussions/topic_modal.html create mode 100644 influxframework/templates/doc.html create mode 100644 influxframework/templates/emails/.txt create mode 100644 influxframework/templates/emails/__init__.py create mode 100644 influxframework/templates/emails/account_deletion_notification.html create mode 100644 influxframework/templates/emails/administrator_logged_in.html create mode 100644 influxframework/templates/emails/auto_email_report.html create mode 100644 influxframework/templates/emails/auto_repeat_fail.html create mode 100644 influxframework/templates/emails/auto_reply.html create mode 100644 influxframework/templates/emails/data_deletion_approval.html create mode 100644 influxframework/templates/emails/delete_data_confirmation.html create mode 100644 influxframework/templates/emails/document_follow.html create mode 100644 influxframework/templates/emails/download_data.html create mode 100644 influxframework/templates/emails/email_footer.html create mode 100644 influxframework/templates/emails/email_header.html create mode 100644 influxframework/templates/emails/energy_points_summary.html create mode 100644 influxframework/templates/emails/file_backup_notification.html create mode 100644 influxframework/templates/emails/new_message.html create mode 100644 influxframework/templates/emails/new_notification.html create mode 100644 influxframework/templates/emails/new_user.html create mode 100644 influxframework/templates/emails/newsletter.html create mode 100644 influxframework/templates/emails/password_reset.html create mode 100644 influxframework/templates/emails/print_link.html create mode 100644 influxframework/templates/emails/standard.html create mode 100644 influxframework/templates/emails/upcoming_events.html create mode 100644 influxframework/templates/emails/verification_code.html create mode 100644 influxframework/templates/emails/workflow_action.html create mode 100644 influxframework/templates/form_grid/fields.html create mode 100644 influxframework/templates/includes/__init__.py create mode 100644 influxframework/templates/includes/app_analytics/google_analytics.html create mode 100644 influxframework/templates/includes/app_analytics/heap_analytics.html create mode 100644 influxframework/templates/includes/app_analytics/mixpanel_analytics.html create mode 100644 influxframework/templates/includes/avatar_macro.html create mode 100644 influxframework/templates/includes/blog/blogger.html create mode 100644 influxframework/templates/includes/blog/hero.html create mode 100644 influxframework/templates/includes/breadcrumbs.html create mode 100644 influxframework/templates/includes/comments/__init__.py create mode 100644 influxframework/templates/includes/comments/comment.html create mode 100644 influxframework/templates/includes/comments/comments.html create mode 100644 influxframework/templates/includes/comments/comments.py create mode 100644 influxframework/templates/includes/contact.js create mode 100644 influxframework/templates/includes/footer/footer.html create mode 100644 influxframework/templates/includes/footer/footer_extension.html create mode 100644 influxframework/templates/includes/footer/footer_grouped_links.html create mode 100644 influxframework/templates/includes/footer/footer_info.html create mode 100644 influxframework/templates/includes/footer/footer_links.html create mode 100644 influxframework/templates/includes/footer/footer_logo_extension.html create mode 100644 influxframework/templates/includes/footer/footer_powered.html create mode 100644 influxframework/templates/includes/form_macros.html create mode 100644 influxframework/templates/includes/full_index.html create mode 100644 influxframework/templates/includes/image_with_blur.html create mode 100644 influxframework/templates/includes/integrations/third_party_apps.js create mode 100644 influxframework/templates/includes/likes/__init__.py create mode 100644 influxframework/templates/includes/likes/likes.html create mode 100644 influxframework/templates/includes/likes/likes.py create mode 100644 influxframework/templates/includes/list/__init__.py create mode 100644 influxframework/templates/includes/list/filters.html create mode 100644 influxframework/templates/includes/list/filters.js create mode 100644 influxframework/templates/includes/list/list.html create mode 100644 influxframework/templates/includes/list/list.js create mode 100644 influxframework/templates/includes/list/row_template.html create mode 100644 influxframework/templates/includes/login/login.js create mode 100644 influxframework/templates/includes/macros.html create mode 100644 influxframework/templates/includes/meta_block.html create mode 100644 influxframework/templates/includes/navbar/dropdown_items.html create mode 100644 influxframework/templates/includes/navbar/dropdown_login.html create mode 100644 influxframework/templates/includes/navbar/navbar.html create mode 100644 influxframework/templates/includes/navbar/navbar_items.html create mode 100644 influxframework/templates/includes/navbar/navbar_login.html create mode 100644 influxframework/templates/includes/navbar/navbar_search.html create mode 100644 influxframework/templates/includes/oauth_confirmation.html create mode 100644 influxframework/templates/includes/print_table.html create mode 100644 influxframework/templates/includes/search_box.html create mode 100644 influxframework/templates/includes/search_result.html create mode 100644 influxframework/templates/includes/search_template.html create mode 100644 influxframework/templates/includes/slideshow.html create mode 100644 influxframework/templates/includes/splash_screen.html create mode 100644 influxframework/templates/includes/static_index.html create mode 100644 influxframework/templates/includes/web_block.html create mode 100644 influxframework/templates/includes/web_sidebar.html create mode 100644 influxframework/templates/includes/website_theme/footer.css create mode 100644 influxframework/templates/includes/website_theme/navbar.css create mode 100644 influxframework/templates/pages/__init__.py create mode 100644 influxframework/templates/pages/integrations/__init__.py create mode 100644 influxframework/templates/pages/integrations/gcalendar-success.html create mode 100644 influxframework/templates/print_format/macros.html create mode 100644 influxframework/templates/print_format/macros/Attach.html create mode 100644 influxframework/templates/print_format/macros/AttachImage.html create mode 100644 influxframework/templates/print_format/macros/Check.html create mode 100644 influxframework/templates/print_format/macros/Code.html create mode 100644 influxframework/templates/print_format/macros/Color.html create mode 100644 influxframework/templates/print_format/macros/Data.html create mode 100644 influxframework/templates/print_format/macros/Divider.html create mode 100644 influxframework/templates/print_format/macros/FieldTemplate.html create mode 100644 influxframework/templates/print_format/macros/HTML.html create mode 100644 influxframework/templates/print_format/macros/Markdown.html create mode 100644 influxframework/templates/print_format/macros/Rating.html create mode 100644 influxframework/templates/print_format/macros/Signature.html create mode 100644 influxframework/templates/print_format/macros/Spacer.html create mode 100644 influxframework/templates/print_format/macros/Table.html create mode 100644 influxframework/templates/print_format/print_footer.html create mode 100644 influxframework/templates/print_format/print_format.css create mode 100644 influxframework/templates/print_format/print_format.html create mode 100644 influxframework/templates/print_format/print_format_font.css create mode 100644 influxframework/templates/print_format/print_header.html create mode 100644 influxframework/templates/print_formats/pdf_header_footer.html create mode 100644 influxframework/templates/print_formats/standard.html create mode 100644 influxframework/templates/print_formats/standard_macros.html create mode 100644 influxframework/templates/signup.html create mode 100644 influxframework/templates/styles/card_style.css create mode 100644 influxframework/templates/styles/discussion_style.css create mode 100644 influxframework/templates/styles/standard.css create mode 100644 influxframework/templates/test/_test_base.html create mode 100644 influxframework/templates/test/_test_base_breadcrumbs.html create mode 100644 influxframework/templates/web.html create mode 100644 influxframework/test_runner.py create mode 100644 influxframework/tests/__init__.py create mode 100644 influxframework/tests/data/email_with_image.txt create mode 100644 influxframework/tests/data/exif_sample_image.jpg create mode 100644 influxframework/tests/data/sample_image_for_optimization.jpg create mode 100644 influxframework/tests/data/sample_svg.svg create mode 100644 influxframework/tests/test_api.py create mode 100644 influxframework/tests/test_assign.py create mode 100644 influxframework/tests/test_auth.py create mode 100644 influxframework/tests/test_background_jobs.py create mode 100644 influxframework/tests/test_base_document.py create mode 100644 influxframework/tests/test_boilerplate.py create mode 100644 influxframework/tests/test_boot.py create mode 100644 influxframework/tests/test_bot.py create mode 100644 influxframework/tests/test_caching.py create mode 100644 influxframework/tests/test_child_table.py create mode 100644 influxframework/tests/test_client.py create mode 100644 influxframework/tests/test_commands.py create mode 100644 influxframework/tests/test_config.py create mode 100644 influxframework/tests/test_cors.py create mode 100644 influxframework/tests/test_db.py create mode 100644 influxframework/tests/test_db_query.py create mode 100644 influxframework/tests/test_db_update.py create mode 100644 influxframework/tests/test_defaults.py create mode 100644 influxframework/tests/test_deferred_insert.py create mode 100644 influxframework/tests/test_docstatus.py create mode 100644 influxframework/tests/test_document.py create mode 100644 influxframework/tests/test_document_locks.py create mode 100644 influxframework/tests/test_domainification.py create mode 100644 influxframework/tests/test_dynamic_links.py create mode 100644 influxframework/tests/test_email.py create mode 100644 influxframework/tests/test_exporter_fixtures.py create mode 100644 influxframework/tests/test_fixture_import.py create mode 100644 influxframework/tests/test_fmt_datetime.py create mode 100644 influxframework/tests/test_fmt_money.py create mode 100644 influxframework/tests/test_form_load.py create mode 100644 influxframework/tests/test_formatter.py create mode 100644 influxframework/tests/test_geo_ip.py create mode 100644 influxframework/tests/test_global_search.py create mode 100644 influxframework/tests/test_goal.py create mode 100644 influxframework/tests/test_hooks.py create mode 100644 influxframework/tests/test_influxframework_client.py create mode 100644 influxframework/tests/test_linked_with.py create mode 100644 influxframework/tests/test_listview.py create mode 100644 influxframework/tests/test_model_utils.py create mode 100644 influxframework/tests/test_monitor.py create mode 100644 influxframework/tests/test_naming.py create mode 100644 influxframework/tests/test_nestedset.py create mode 100644 influxframework/tests/test_oauth20.py create mode 100644 influxframework/tests/test_password.py create mode 100644 influxframework/tests/test_patches.py create mode 100644 influxframework/tests/test_pdf.py create mode 100644 influxframework/tests/test_perf.py create mode 100644 influxframework/tests/test_permissions.py create mode 100644 influxframework/tests/test_printview.py create mode 100644 influxframework/tests/test_query.py create mode 100644 influxframework/tests/test_query_builder.py create mode 100644 influxframework/tests/test_query_report.py create mode 100644 influxframework/tests/test_rate_limiter.py create mode 100644 influxframework/tests/test_recorder.py create mode 100644 influxframework/tests/test_redis.py create mode 100644 influxframework/tests/test_rename_doc.py create mode 100644 influxframework/tests/test_safe_exec.py create mode 100644 influxframework/tests/test_scheduler.py create mode 100644 influxframework/tests/test_search.py create mode 100644 influxframework/tests/test_seen.py create mode 100644 influxframework/tests/test_sequence.py create mode 100644 influxframework/tests/test_sitemap.py create mode 100644 influxframework/tests/test_test_utils.py create mode 100644 influxframework/tests/test_translate.py create mode 100644 influxframework/tests/test_twofactor.py create mode 100644 influxframework/tests/test_utils.py create mode 100644 influxframework/tests/test_virtual_doctype.py create mode 100644 influxframework/tests/test_webform.py create mode 100644 influxframework/tests/test_website.py create mode 100644 influxframework/tests/tests_geo_utils.py create mode 100644 influxframework/tests/translation_test_file.txt create mode 100644 influxframework/tests/ui_test_helpers.py create mode 100644 influxframework/tests/utils.py create mode 100644 influxframework/translate.py create mode 100644 influxframework/translations/af.csv create mode 100644 influxframework/translations/am.csv create mode 100644 influxframework/translations/ar.csv create mode 100644 influxframework/translations/bg.csv create mode 100644 influxframework/translations/bn.csv create mode 100644 influxframework/translations/bo.csv create mode 100644 influxframework/translations/bs.csv create mode 100644 influxframework/translations/ca.csv create mode 100644 influxframework/translations/cs.csv create mode 100644 influxframework/translations/cz.csv create mode 100644 influxframework/translations/da-DK.csv create mode 100644 influxframework/translations/da.csv create mode 100644 influxframework/translations/da_dk.csv create mode 100644 influxframework/translations/de.csv create mode 100644 influxframework/translations/el.csv create mode 100644 influxframework/translations/en-GB.csv create mode 100644 influxframework/translations/en-US.csv create mode 100644 influxframework/translations/en.csv create mode 100644 influxframework/translations/en_gb.csv create mode 100644 influxframework/translations/en_us.csv create mode 100644 influxframework/translations/es-AR.csv create mode 100644 influxframework/translations/es-BO.csv create mode 100644 influxframework/translations/es-CL.csv create mode 100644 influxframework/translations/es-CO.csv create mode 100644 influxframework/translations/es-DO.csv create mode 100644 influxframework/translations/es-EC.csv create mode 100644 influxframework/translations/es-GT.csv create mode 100644 influxframework/translations/es-MX.csv create mode 100644 influxframework/translations/es-NI.csv create mode 100644 influxframework/translations/es-PE.csv create mode 100644 influxframework/translations/es.csv create mode 100644 influxframework/translations/es_ar.csv create mode 100644 influxframework/translations/es_bo.csv create mode 100644 influxframework/translations/es_cl.csv create mode 100644 influxframework/translations/es_co.csv create mode 100644 influxframework/translations/es_do.csv create mode 100644 influxframework/translations/es_ec.csv create mode 100644 influxframework/translations/es_es.csv create mode 100644 influxframework/translations/es_gt.csv create mode 100644 influxframework/translations/es_mx.csv create mode 100644 influxframework/translations/es_ni.csv create mode 100644 influxframework/translations/es_pe.csv create mode 100644 influxframework/translations/et.csv create mode 100644 influxframework/translations/fa.csv create mode 100644 influxframework/translations/fi.csv create mode 100644 influxframework/translations/fil.csv create mode 100644 influxframework/translations/fr-CA.csv create mode 100644 influxframework/translations/fr.csv create mode 100644 influxframework/translations/fr_ca.csv create mode 100644 influxframework/translations/gu.csv create mode 100644 influxframework/translations/he.csv create mode 100644 influxframework/translations/hi.csv create mode 100644 influxframework/translations/hr.csv create mode 100644 influxframework/translations/hu.csv create mode 100644 influxframework/translations/id.csv create mode 100644 influxframework/translations/is.csv create mode 100644 influxframework/translations/it.csv create mode 100644 influxframework/translations/ja.csv create mode 100644 influxframework/translations/km.csv create mode 100644 influxframework/translations/kn.csv create mode 100644 influxframework/translations/ko.csv create mode 100644 influxframework/translations/ku.csv create mode 100644 influxframework/translations/lo.csv create mode 100644 influxframework/translations/lt.csv create mode 100644 influxframework/translations/lv.csv create mode 100644 influxframework/translations/mk.csv create mode 100644 influxframework/translations/ml.csv create mode 100644 influxframework/translations/mr.csv create mode 100644 influxframework/translations/ms.csv create mode 100644 influxframework/translations/my.csv create mode 100644 influxframework/translations/nl.csv create mode 100644 influxframework/translations/no.csv create mode 100644 influxframework/translations/pl.csv create mode 100644 influxframework/translations/ps.csv create mode 100644 influxframework/translations/pt-BR.csv create mode 100644 influxframework/translations/pt.csv create mode 100644 influxframework/translations/pt_br.csv create mode 100644 influxframework/translations/quc.csv create mode 100644 influxframework/translations/ro.csv create mode 100644 influxframework/translations/ru.csv create mode 100644 influxframework/translations/rw.csv create mode 100644 influxframework/translations/se.csv create mode 100644 influxframework/translations/si.csv create mode 100644 influxframework/translations/sk.csv create mode 100644 influxframework/translations/sl.csv create mode 100644 influxframework/translations/sq.csv create mode 100644 influxframework/translations/sr-BA.csv create mode 100644 influxframework/translations/sr-SP.csv create mode 100644 influxframework/translations/sr.csv create mode 100644 influxframework/translations/sr_ba.csv create mode 100644 influxframework/translations/sr_sp.csv create mode 100644 influxframework/translations/sv.csv create mode 100644 influxframework/translations/sw.csv create mode 100644 influxframework/translations/ta.csv create mode 100644 influxframework/translations/te.csv create mode 100644 influxframework/translations/th.csv create mode 100644 influxframework/translations/tr.csv create mode 100644 influxframework/translations/uk.csv create mode 100644 influxframework/translations/ur.csv create mode 100644 influxframework/translations/uz.csv create mode 100644 influxframework/translations/vi.csv create mode 100644 influxframework/translations/zh-TW.csv create mode 100644 influxframework/translations/zh.csv create mode 100644 influxframework/translations/zh_tw.csv create mode 100644 influxframework/twofactor.py create mode 100644 influxframework/utils/__init__.py create mode 100644 influxframework/utils/background_jobs.py create mode 100644 influxframework/utils/backups.py create mode 100644 influxframework/utils/bench_helper.py create mode 100644 influxframework/utils/boilerplate.py create mode 100644 influxframework/utils/caching.py create mode 100644 influxframework/utils/change_log.py create mode 100644 influxframework/utils/commands.py create mode 100644 influxframework/utils/connections.py create mode 100644 influxframework/utils/csvutils.py create mode 100644 influxframework/utils/dashboard.py create mode 100644 influxframework/utils/data.py create mode 100644 influxframework/utils/dateutils.py create mode 100644 influxframework/utils/diff.py create mode 100644 influxframework/utils/doctor.py create mode 100644 influxframework/utils/error.py create mode 100644 influxframework/utils/file_lock.py create mode 100644 influxframework/utils/file_manager.py create mode 100644 influxframework/utils/fixtures.py create mode 100644 influxframework/utils/formatters.py create mode 100644 influxframework/utils/global_search.py create mode 100644 influxframework/utils/goal.py create mode 100644 influxframework/utils/html_utils.py create mode 100644 influxframework/utils/identicon.py create mode 100644 influxframework/utils/image.py create mode 100644 influxframework/utils/install.py create mode 100644 influxframework/utils/jinja.py create mode 100644 influxframework/utils/jinja_globals.py create mode 100644 influxframework/utils/lazy_loader.py create mode 100644 influxframework/utils/logger.py create mode 100644 influxframework/utils/make_random.py create mode 100644 influxframework/utils/momentjs.py create mode 100644 influxframework/utils/nestedset.py create mode 100644 influxframework/utils/oauth.py create mode 100644 influxframework/utils/password.py create mode 100644 influxframework/utils/password_strength.py create mode 100644 influxframework/utils/pdf.py create mode 100644 influxframework/utils/print_format.py create mode 100644 influxframework/utils/redis_queue.py create mode 100644 influxframework/utils/redis_wrapper.py create mode 100644 influxframework/utils/response.py create mode 100644 influxframework/utils/safe_exec.py create mode 100644 influxframework/utils/scheduler.py create mode 100644 influxframework/utils/subscription.py create mode 100644 influxframework/utils/testutils.py create mode 100644 influxframework/utils/user.py create mode 100644 influxframework/utils/verified_command.py create mode 100644 influxframework/utils/weasyprint.py create mode 100644 influxframework/utils/xlsxutils.py create mode 100644 influxframework/website/__init__.py create mode 100644 influxframework/website/css/web_form.css create mode 100644 influxframework/website/dashboard_fixtures.py create mode 100644 influxframework/website/doctype/__init__.py create mode 100644 influxframework/website/doctype/about_us_settings/README.md create mode 100644 influxframework/website/doctype/about_us_settings/__init__.py create mode 100644 influxframework/website/doctype/about_us_settings/about_us_settings.js create mode 100644 influxframework/website/doctype/about_us_settings/about_us_settings.json create mode 100644 influxframework/website/doctype/about_us_settings/about_us_settings.py create mode 100644 influxframework/website/doctype/about_us_settings/test_about_us_settings.py create mode 100644 influxframework/website/doctype/about_us_team_member/README.md create mode 100644 influxframework/website/doctype/about_us_team_member/__init__.py create mode 100644 influxframework/website/doctype/about_us_team_member/about_us_team_member.json create mode 100644 influxframework/website/doctype/about_us_team_member/about_us_team_member.py create mode 100644 influxframework/website/doctype/blog_category/README.md create mode 100644 influxframework/website/doctype/blog_category/__init__.py create mode 100644 influxframework/website/doctype/blog_category/blog_category.js create mode 100644 influxframework/website/doctype/blog_category/blog_category.json create mode 100644 influxframework/website/doctype/blog_category/blog_category.py create mode 100644 influxframework/website/doctype/blog_category/templates/blog_category.html create mode 100644 influxframework/website/doctype/blog_category/templates/blog_category_row.html create mode 100644 influxframework/website/doctype/blog_category/test_blog_category.py create mode 100644 influxframework/website/doctype/blog_category/test_records.json create mode 100644 influxframework/website/doctype/blog_post/README.md create mode 100644 influxframework/website/doctype/blog_post/__init__.py create mode 100644 influxframework/website/doctype/blog_post/blog_post.js create mode 100644 influxframework/website/doctype/blog_post/blog_post.json create mode 100644 influxframework/website/doctype/blog_post/blog_post.py create mode 100644 influxframework/website/doctype/blog_post/blog_post_list.js create mode 100644 influxframework/website/doctype/blog_post/templates/blog_post.html create mode 100644 influxframework/website/doctype/blog_post/templates/blog_post_list.html create mode 100644 influxframework/website/doctype/blog_post/templates/blog_post_row.html create mode 100644 influxframework/website/doctype/blog_post/test_blog_post.py create mode 100644 influxframework/website/doctype/blog_post/test_records.json create mode 100644 influxframework/website/doctype/blog_post/ui_test_blog_post.js create mode 100644 influxframework/website/doctype/blog_settings/README.md create mode 100644 influxframework/website/doctype/blog_settings/__init__.py create mode 100644 influxframework/website/doctype/blog_settings/blog_settings.js create mode 100644 influxframework/website/doctype/blog_settings/blog_settings.json create mode 100644 influxframework/website/doctype/blog_settings/blog_settings.py create mode 100644 influxframework/website/doctype/blog_settings/test_blog_settings.py create mode 100644 influxframework/website/doctype/blogger/README.md create mode 100644 influxframework/website/doctype/blogger/__init__.py create mode 100644 influxframework/website/doctype/blogger/blogger.js create mode 100644 influxframework/website/doctype/blogger/blogger.json create mode 100644 influxframework/website/doctype/blogger/blogger.py create mode 100644 influxframework/website/doctype/blogger/test_blogger.py create mode 100644 influxframework/website/doctype/blogger/test_records.json create mode 100644 influxframework/website/doctype/color/__init__.py create mode 100644 influxframework/website/doctype/color/color.js create mode 100644 influxframework/website/doctype/color/color.json create mode 100644 influxframework/website/doctype/color/color.py create mode 100644 influxframework/website/doctype/color/test_color.py create mode 100644 influxframework/website/doctype/company_history/README.md create mode 100644 influxframework/website/doctype/company_history/__init__.py create mode 100644 influxframework/website/doctype/company_history/company_history.json create mode 100644 influxframework/website/doctype/company_history/company_history.py create mode 100644 influxframework/website/doctype/contact_us_settings/README.md create mode 100644 influxframework/website/doctype/contact_us_settings/__init__.py create mode 100644 influxframework/website/doctype/contact_us_settings/contact_us_settings.js create mode 100644 influxframework/website/doctype/contact_us_settings/contact_us_settings.json create mode 100644 influxframework/website/doctype/contact_us_settings/contact_us_settings.py create mode 100644 influxframework/website/doctype/discussion_reply/__init__.py create mode 100644 influxframework/website/doctype/discussion_reply/discussion_reply.js create mode 100644 influxframework/website/doctype/discussion_reply/discussion_reply.json create mode 100644 influxframework/website/doctype/discussion_reply/discussion_reply.py create mode 100644 influxframework/website/doctype/discussion_reply/test_discussion_reply.py create mode 100644 influxframework/website/doctype/discussion_topic/__init__.py create mode 100644 influxframework/website/doctype/discussion_topic/discussion_topic.js create mode 100644 influxframework/website/doctype/discussion_topic/discussion_topic.json create mode 100644 influxframework/website/doctype/discussion_topic/discussion_topic.py create mode 100644 influxframework/website/doctype/discussion_topic/test_discussion_topic.py create mode 100644 influxframework/website/doctype/help_article/__init__.py create mode 100644 influxframework/website/doctype/help_article/help_article.js create mode 100644 influxframework/website/doctype/help_article/help_article.json create mode 100644 influxframework/website/doctype/help_article/help_article.py create mode 100644 influxframework/website/doctype/help_article/templates/help_article.html create mode 100644 influxframework/website/doctype/help_article/templates/help_article_row.html create mode 100644 influxframework/website/doctype/help_article/test_help_article.py create mode 100644 influxframework/website/doctype/help_category/__init__.py create mode 100644 influxframework/website/doctype/help_category/help_category.js create mode 100644 influxframework/website/doctype/help_category/help_category.json create mode 100644 influxframework/website/doctype/help_category/help_category.py create mode 100644 influxframework/website/doctype/help_category/test_help_category.py create mode 100644 influxframework/website/doctype/personal_data_deletion_request/__init__.py create mode 100644 influxframework/website/doctype/personal_data_deletion_request/personal_data_deletion_request.js create mode 100644 influxframework/website/doctype/personal_data_deletion_request/personal_data_deletion_request.json create mode 100644 influxframework/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py create mode 100644 influxframework/website/doctype/personal_data_deletion_request/test_personal_data_deletion_request.py create mode 100644 influxframework/website/doctype/personal_data_deletion_step/__init__.py create mode 100644 influxframework/website/doctype/personal_data_deletion_step/personal_data_deletion_step.json create mode 100644 influxframework/website/doctype/personal_data_deletion_step/personal_data_deletion_step.py create mode 100644 influxframework/website/doctype/personal_data_download_request/__init__.py create mode 100644 influxframework/website/doctype/personal_data_download_request/personal_data_download_request.js create mode 100644 influxframework/website/doctype/personal_data_download_request/personal_data_download_request.json create mode 100644 influxframework/website/doctype/personal_data_download_request/personal_data_download_request.py create mode 100644 influxframework/website/doctype/personal_data_download_request/test_personal_data_download_request.py create mode 100644 influxframework/website/doctype/portal_menu_item/__init__.py create mode 100644 influxframework/website/doctype/portal_menu_item/portal_menu_item.json create mode 100644 influxframework/website/doctype/portal_menu_item/portal_menu_item.py create mode 100644 influxframework/website/doctype/portal_settings/__init__.py create mode 100644 influxframework/website/doctype/portal_settings/portal_settings.js create mode 100644 influxframework/website/doctype/portal_settings/portal_settings.json create mode 100644 influxframework/website/doctype/portal_settings/portal_settings.py create mode 100644 influxframework/website/doctype/portal_settings/test_portal_settings.py create mode 100644 influxframework/website/doctype/social_link_settings/__init__.py create mode 100644 influxframework/website/doctype/social_link_settings/social_link_settings.json create mode 100644 influxframework/website/doctype/social_link_settings/social_link_settings.py create mode 100644 influxframework/website/doctype/top_bar_item/README.md create mode 100644 influxframework/website/doctype/top_bar_item/__init__.py create mode 100644 influxframework/website/doctype/top_bar_item/top_bar_item.json create mode 100644 influxframework/website/doctype/top_bar_item/top_bar_item.py create mode 100644 influxframework/website/doctype/web_form/__init__.py create mode 100644 influxframework/website/doctype/web_form/templates/web_form.html create mode 100644 influxframework/website/doctype/web_form/templates/web_form_skeleton.html create mode 100644 influxframework/website/doctype/web_form/templates/web_list.html create mode 100644 influxframework/website/doctype/web_form/test_records.json create mode 100644 influxframework/website/doctype/web_form/test_web_form.py create mode 100644 influxframework/website/doctype/web_form/web_form.js create mode 100644 influxframework/website/doctype/web_form/web_form.json create mode 100644 influxframework/website/doctype/web_form/web_form.py create mode 100644 influxframework/website/doctype/web_form/web_form_list.js create mode 100644 influxframework/website/doctype/web_form_field/__init__.py create mode 100644 influxframework/website/doctype/web_form_field/web_form_field.json create mode 100644 influxframework/website/doctype/web_form_field/web_form_field.py create mode 100644 influxframework/website/doctype/web_form_list_column/__init__.py create mode 100644 influxframework/website/doctype/web_form_list_column/web_form_list_column.json create mode 100644 influxframework/website/doctype/web_form_list_column/web_form_list_column.py create mode 100644 influxframework/website/doctype/web_page/README.md create mode 100644 influxframework/website/doctype/web_page/__init__.py create mode 100644 influxframework/website/doctype/web_page/templates/web_page.html create mode 100644 influxframework/website/doctype/web_page/templates/web_page_row.html create mode 100644 influxframework/website/doctype/web_page/test_records.json create mode 100644 influxframework/website/doctype/web_page/test_web_page.py create mode 100644 influxframework/website/doctype/web_page/web_page.js create mode 100644 influxframework/website/doctype/web_page/web_page.json create mode 100644 influxframework/website/doctype/web_page/web_page.py create mode 100644 influxframework/website/doctype/web_page/web_page_list.js create mode 100644 influxframework/website/doctype/web_page_block/__init__.py create mode 100644 influxframework/website/doctype/web_page_block/web_page_block.json create mode 100644 influxframework/website/doctype/web_page_block/web_page_block.py create mode 100644 influxframework/website/doctype/web_page_view/__init__.py create mode 100644 influxframework/website/doctype/web_page_view/test_web_page_view.py create mode 100644 influxframework/website/doctype/web_page_view/web_page_view.js create mode 100644 influxframework/website/doctype/web_page_view/web_page_view.json create mode 100644 influxframework/website/doctype/web_page_view/web_page_view.py create mode 100644 influxframework/website/doctype/web_template/__init__.py create mode 100644 influxframework/website/doctype/web_template/test_web_template.py create mode 100644 influxframework/website/doctype/web_template/web_template.js create mode 100644 influxframework/website/doctype/web_template/web_template.json create mode 100644 influxframework/website/doctype/web_template/web_template.py create mode 100644 influxframework/website/doctype/web_template_field/__init__.py create mode 100644 influxframework/website/doctype/web_template_field/test_web_template_field.py create mode 100644 influxframework/website/doctype/web_template_field/web_template_field.js create mode 100644 influxframework/website/doctype/web_template_field/web_template_field.json create mode 100644 influxframework/website/doctype/web_template_field/web_template_field.py create mode 100644 influxframework/website/doctype/website_meta_tag/__init__.py create mode 100644 influxframework/website/doctype/website_meta_tag/website_meta_tag.json create mode 100644 influxframework/website/doctype/website_meta_tag/website_meta_tag.py create mode 100644 influxframework/website/doctype/website_route_meta/__init__.py create mode 100644 influxframework/website/doctype/website_route_meta/test_website_route_meta.py create mode 100644 influxframework/website/doctype/website_route_meta/website_route_meta.js create mode 100644 influxframework/website/doctype/website_route_meta/website_route_meta.json create mode 100644 influxframework/website/doctype/website_route_meta/website_route_meta.py create mode 100644 influxframework/website/doctype/website_route_redirect/__init__.py create mode 100644 influxframework/website/doctype/website_route_redirect/website_route_redirect.json create mode 100644 influxframework/website/doctype/website_route_redirect/website_route_redirect.py create mode 100644 influxframework/website/doctype/website_script/README.md create mode 100644 influxframework/website/doctype/website_script/__init__.py create mode 100644 influxframework/website/doctype/website_script/website_script.js create mode 100644 influxframework/website/doctype/website_script/website_script.json create mode 100644 influxframework/website/doctype/website_script/website_script.py create mode 100644 influxframework/website/doctype/website_settings/README.md create mode 100644 influxframework/website/doctype/website_settings/__init__.py create mode 100644 influxframework/website/doctype/website_settings/google_indexing.py create mode 100644 influxframework/website/doctype/website_settings/test_website_settings.py create mode 100644 influxframework/website/doctype/website_settings/website_settings.js create mode 100644 influxframework/website/doctype/website_settings/website_settings.json create mode 100644 influxframework/website/doctype/website_settings/website_settings.py create mode 100644 influxframework/website/doctype/website_sidebar/__init__.py create mode 100644 influxframework/website/doctype/website_sidebar/test_website_sidebar.py create mode 100644 influxframework/website/doctype/website_sidebar/website_sidebar.js create mode 100644 influxframework/website/doctype/website_sidebar/website_sidebar.json create mode 100644 influxframework/website/doctype/website_sidebar/website_sidebar.py create mode 100644 influxframework/website/doctype/website_sidebar_item/__init__.py create mode 100644 influxframework/website/doctype/website_sidebar_item/website_sidebar_item.json create mode 100644 influxframework/website/doctype/website_sidebar_item/website_sidebar_item.py create mode 100644 influxframework/website/doctype/website_slideshow/README.md create mode 100644 influxframework/website/doctype/website_slideshow/__init__.py create mode 100644 influxframework/website/doctype/website_slideshow/test_website_slideshow.py create mode 100644 influxframework/website/doctype/website_slideshow/website_slideshow.js create mode 100644 influxframework/website/doctype/website_slideshow/website_slideshow.json create mode 100644 influxframework/website/doctype/website_slideshow/website_slideshow.py create mode 100644 influxframework/website/doctype/website_slideshow_item/README.md create mode 100644 influxframework/website/doctype/website_slideshow_item/__init__.py create mode 100644 influxframework/website/doctype/website_slideshow_item/website_slideshow_item.json create mode 100644 influxframework/website/doctype/website_slideshow_item/website_slideshow_item.py create mode 100644 influxframework/website/doctype/website_theme/__init__.py create mode 100644 influxframework/website/doctype/website_theme/custom_theme.css create mode 100644 influxframework/website/doctype/website_theme/test_website_theme.py create mode 100644 influxframework/website/doctype/website_theme/website_theme.js create mode 100644 influxframework/website/doctype/website_theme/website_theme.json create mode 100644 influxframework/website/doctype/website_theme/website_theme.py create mode 100644 influxframework/website/doctype/website_theme/website_theme_template.scss create mode 100644 influxframework/website/doctype/website_theme_ignore_app/__init__.py create mode 100644 influxframework/website/doctype/website_theme_ignore_app/website_theme_ignore_app.json create mode 100644 influxframework/website/doctype/website_theme_ignore_app/website_theme_ignore_app.py create mode 100644 influxframework/website/js/bootstrap-4.js create mode 100644 influxframework/website/js/syntax_highlight.js create mode 100644 influxframework/website/js/website.js create mode 100644 influxframework/website/module_onboarding/website/website.json create mode 100644 influxframework/website/onboarding_step/add_blog_category/add_blog_category.json create mode 100644 influxframework/website/onboarding_step/create_blogger/create_blogger.json create mode 100644 influxframework/website/onboarding_step/enable_website_tracking/enable_website_tracking.json create mode 100644 influxframework/website/onboarding_step/introduction_to_website/introduction_to_website.json create mode 100644 influxframework/website/onboarding_step/web_page_tour/web_page_tour.json create mode 100644 influxframework/website/page_renderers/base_renderer.py create mode 100644 influxframework/website/page_renderers/base_template_page.py create mode 100644 influxframework/website/page_renderers/document_page.py create mode 100644 influxframework/website/page_renderers/error_page.py create mode 100644 influxframework/website/page_renderers/list_page.py create mode 100644 influxframework/website/page_renderers/not_found_page.py create mode 100644 influxframework/website/page_renderers/not_permitted_page.py create mode 100644 influxframework/website/page_renderers/print_page.py create mode 100644 influxframework/website/page_renderers/redirect_page.py create mode 100644 influxframework/website/page_renderers/static_page.py create mode 100644 influxframework/website/page_renderers/template_page.py create mode 100644 influxframework/website/page_renderers/web_form.py create mode 100644 influxframework/website/path_resolver.py create mode 100644 influxframework/website/report/__init__.py create mode 100644 influxframework/website/report/website_analytics/__init__.py create mode 100644 influxframework/website/report/website_analytics/website_analytics.js create mode 100644 influxframework/website/report/website_analytics/website_analytics.json create mode 100644 influxframework/website/report/website_analytics/website_analytics.py create mode 100644 influxframework/website/router.py create mode 100644 influxframework/website/serve.py create mode 100644 influxframework/website/utils.py create mode 100644 influxframework/website/web_form/__init__.py create mode 100644 influxframework/website/web_form/request_data/__init__.py create mode 100644 influxframework/website/web_form/request_data/request_data.js create mode 100644 influxframework/website/web_form/request_data/request_data.json create mode 100644 influxframework/website/web_form/request_data/request_data.py create mode 100644 influxframework/website/web_form/request_to_delete_data/__init__.py create mode 100644 influxframework/website/web_form/request_to_delete_data/request_to_delete_data.js create mode 100644 influxframework/website/web_form/request_to_delete_data/request_to_delete_data.json create mode 100644 influxframework/website/web_form/request_to_delete_data/request_to_delete_data.py create mode 100644 influxframework/website/web_template/__init__.py create mode 100644 influxframework/website/web_template/cover_image/__init__.py create mode 100644 influxframework/website/web_template/cover_image/cover_image.html create mode 100644 influxframework/website/web_template/cover_image/cover_image.json create mode 100644 influxframework/website/web_template/discussions/__init__.py create mode 100644 influxframework/website/web_template/discussions/discussions.html create mode 100644 influxframework/website/web_template/discussions/discussions.json create mode 100644 influxframework/website/web_template/full_width_image/__init__.py create mode 100644 influxframework/website/web_template/full_width_image/full_width_image.html create mode 100644 influxframework/website/web_template/full_width_image/full_width_image.json create mode 100644 influxframework/website/web_template/hero/__init__.py create mode 100644 influxframework/website/web_template/hero/hero.html create mode 100644 influxframework/website/web_template/hero/hero.json create mode 100644 influxframework/website/web_template/hero_with_right_image/__init__.py create mode 100644 influxframework/website/web_template/hero_with_right_image/hero_with_right_image.html create mode 100644 influxframework/website/web_template/hero_with_right_image/hero_with_right_image.json create mode 100644 influxframework/website/web_template/markdown/__init__.py create mode 100644 influxframework/website/web_template/markdown/markdown.html create mode 100644 influxframework/website/web_template/markdown/markdown.json create mode 100644 influxframework/website/web_template/primary_navbar/__init__.py create mode 100644 influxframework/website/web_template/primary_navbar/primary_navbar.html create mode 100644 influxframework/website/web_template/primary_navbar/primary_navbar.json create mode 100644 influxframework/website/web_template/section_with_cards/__init__.py create mode 100644 influxframework/website/web_template/section_with_cards/section_with_cards.html create mode 100644 influxframework/website/web_template/section_with_cards/section_with_cards.json create mode 100644 influxframework/website/web_template/section_with_collapsible_content/__init__.py create mode 100644 influxframework/website/web_template/section_with_collapsible_content/section_with_collapsible_content.html create mode 100644 influxframework/website/web_template/section_with_collapsible_content/section_with_collapsible_content.json create mode 100644 influxframework/website/web_template/section_with_cta/__init__.py create mode 100644 influxframework/website/web_template/section_with_cta/section_with_cta.html create mode 100644 influxframework/website/web_template/section_with_cta/section_with_cta.json create mode 100644 influxframework/website/web_template/section_with_embed/__init__.py create mode 100644 influxframework/website/web_template/section_with_embed/section_with_embed.html create mode 100644 influxframework/website/web_template/section_with_embed/section_with_embed.json create mode 100644 influxframework/website/web_template/section_with_features/__init__.py create mode 100644 influxframework/website/web_template/section_with_features/section_with_features.html create mode 100644 influxframework/website/web_template/section_with_features/section_with_features.json create mode 100644 influxframework/website/web_template/section_with_image/__init__.py create mode 100644 influxframework/website/web_template/section_with_image/section_with_image.html create mode 100644 influxframework/website/web_template/section_with_image/section_with_image.json create mode 100644 influxframework/website/web_template/section_with_image_grid/__init__.py create mode 100644 influxframework/website/web_template/section_with_image_grid/section_with_image_grid.html create mode 100644 influxframework/website/web_template/section_with_image_grid/section_with_image_grid.json create mode 100644 influxframework/website/web_template/section_with_small_cta/__init__.py create mode 100644 influxframework/website/web_template/section_with_small_cta/section_with_small_cta.html create mode 100644 influxframework/website/web_template/section_with_small_cta/section_with_small_cta.json create mode 100644 influxframework/website/web_template/section_with_tabs/__init__.py create mode 100644 influxframework/website/web_template/section_with_tabs/section_with_tabs.html create mode 100644 influxframework/website/web_template/section_with_tabs/section_with_tabs.json create mode 100644 influxframework/website/web_template/section_with_testimonials/__init__.py create mode 100644 influxframework/website/web_template/section_with_testimonials/section_with_testimonials.html create mode 100644 influxframework/website/web_template/section_with_testimonials/section_with_testimonials.json create mode 100644 influxframework/website/web_template/section_with_videos/__init__.py create mode 100644 influxframework/website/web_template/section_with_videos/section_with_videos.html create mode 100644 influxframework/website/web_template/section_with_videos/section_with_videos.json create mode 100644 influxframework/website/web_template/slideshow/__init__.py create mode 100644 influxframework/website/web_template/slideshow/slideshow.html create mode 100644 influxframework/website/web_template/slideshow/slideshow.json create mode 100644 influxframework/website/web_template/split_section_with_image/__init__.py create mode 100644 influxframework/website/web_template/split_section_with_image/split_section_with_image.html create mode 100644 influxframework/website/web_template/split_section_with_image/split_section_with_image.json create mode 100644 influxframework/website/web_template/standard_footer/__init__.py create mode 100644 influxframework/website/web_template/standard_footer/standard_footer.html create mode 100644 influxframework/website/web_template/standard_footer/standard_footer.json create mode 100644 influxframework/website/web_template/standard_navbar/__init__.py create mode 100644 influxframework/website/web_template/standard_navbar/standard_navbar.html create mode 100644 influxframework/website/web_template/standard_navbar/standard_navbar.json create mode 100644 influxframework/website/web_template/testimonial/__init__.py create mode 100644 influxframework/website/web_template/testimonial/testimonial.html create mode 100644 influxframework/website/web_template/testimonial/testimonial.json create mode 100644 influxframework/website/website_components/metatags.py create mode 100644 influxframework/website/website_generator.py create mode 100644 influxframework/website/website_theme/__init__.py create mode 100644 influxframework/website/website_theme/standard/__init__.py create mode 100644 influxframework/website/website_theme/standard/standard.json create mode 100644 influxframework/website/workspace/website/website.json create mode 100644 influxframework/workflow/__init__.py create mode 100644 influxframework/workflow/doctype/__init__.py create mode 100644 influxframework/workflow/doctype/workflow/README.md create mode 100644 influxframework/workflow/doctype/workflow/__init__.py create mode 100644 influxframework/workflow/doctype/workflow/test_records.json create mode 100644 influxframework/workflow/doctype/workflow/test_workflow.py create mode 100644 influxframework/workflow/doctype/workflow/workflow.js create mode 100644 influxframework/workflow/doctype/workflow/workflow.json create mode 100644 influxframework/workflow/doctype/workflow/workflow.py create mode 100644 influxframework/workflow/doctype/workflow/workflow_list.js create mode 100644 influxframework/workflow/doctype/workflow_action/README.md create mode 100644 influxframework/workflow/doctype/workflow_action/__init__.py create mode 100644 influxframework/workflow/doctype/workflow_action/test_workflow_action.py create mode 100644 influxframework/workflow/doctype/workflow_action/workflow_action.js create mode 100644 influxframework/workflow/doctype/workflow_action/workflow_action.json create mode 100644 influxframework/workflow/doctype/workflow_action/workflow_action.py create mode 100644 influxframework/workflow/doctype/workflow_action/workflow_action_list.js create mode 100644 influxframework/workflow/doctype/workflow_action_master/__init__.py create mode 100644 influxframework/workflow/doctype/workflow_action_master/workflow_action_master.js create mode 100644 influxframework/workflow/doctype/workflow_action_master/workflow_action_master.json create mode 100644 influxframework/workflow/doctype/workflow_action_master/workflow_action_master.py create mode 100644 influxframework/workflow/doctype/workflow_action_permitted_role/__init__.py create mode 100644 influxframework/workflow/doctype/workflow_action_permitted_role/workflow_action_permitted_role.json create mode 100644 influxframework/workflow/doctype/workflow_action_permitted_role/workflow_action_permitted_role.py create mode 100644 influxframework/workflow/doctype/workflow_document_state/README.md create mode 100644 influxframework/workflow/doctype/workflow_document_state/__init__.py create mode 100644 influxframework/workflow/doctype/workflow_document_state/workflow_document_state.json create mode 100644 influxframework/workflow/doctype/workflow_document_state/workflow_document_state.py create mode 100644 influxframework/workflow/doctype/workflow_state/README.md create mode 100644 influxframework/workflow/doctype/workflow_state/__init__.py create mode 100644 influxframework/workflow/doctype/workflow_state/test_records.json create mode 100644 influxframework/workflow/doctype/workflow_state/test_workflow_state.py create mode 100644 influxframework/workflow/doctype/workflow_state/workflow_state.js create mode 100644 influxframework/workflow/doctype/workflow_state/workflow_state.json create mode 100644 influxframework/workflow/doctype/workflow_state/workflow_state.py create mode 100644 influxframework/workflow/doctype/workflow_transition/README.md create mode 100644 influxframework/workflow/doctype/workflow_transition/__init__.py create mode 100644 influxframework/workflow/doctype/workflow_transition/workflow_transition.json create mode 100644 influxframework/workflow/doctype/workflow_transition/workflow_transition.py create mode 100644 influxframework/www/404.html create mode 100644 influxframework/www/404.py create mode 100644 influxframework/www/__init__.py create mode 100644 influxframework/www/_test/__init__.py create mode 100644 influxframework/www/_test/_sidebar.json create mode 100644 influxframework/www/_test/_test_custom_base.html create mode 100644 influxframework/www/_test/_test_folder/__init__.py create mode 100644 influxframework/www/_test/_test_folder/_test_page.css create mode 100644 influxframework/www/_test/_test_folder/_test_page.html create mode 100644 influxframework/www/_test/_test_folder/_test_page.js create mode 100644 influxframework/www/_test/_test_folder/_test_page.py create mode 100644 influxframework/www/_test/_test_folder/_test_toc.md create mode 100644 influxframework/www/_test/_test_folder/index.md create mode 100644 influxframework/www/_test/_test_folder/new.csv/__init__.py create mode 100644 influxframework/www/_test/_test_folder/new.csv/index.html create mode 100644 influxframework/www/_test/_test_home_page.py create mode 100644 influxframework/www/_test/_test_metatags.html create mode 100644 influxframework/www/_test/_test_metatags.py create mode 100644 influxframework/www/_test/_test_no_context.html create mode 100644 influxframework/www/_test/_test_no_context.py create mode 100644 influxframework/www/_test/_test_safe_render_off.html create mode 100644 influxframework/www/_test/_test_safe_render_on.html create mode 100644 influxframework/www/_test/_test_webform.py create mode 100644 influxframework/www/_test/assets/__init__.py create mode 100644 influxframework/www/_test/assets/css_asset.css create mode 100644 influxframework/www/_test/assets/file.zip create mode 100644 influxframework/www/_test/assets/image create mode 100644 influxframework/www/_test/assets/image.jpg create mode 100644 influxframework/www/_test/assets/js_asset.min.js create mode 100644 influxframework/www/_test/index.html create mode 100644 influxframework/www/_test/problematic_page.html create mode 100644 influxframework/www/_test/static-file-test.png create mode 100644 influxframework/www/about.html create mode 100644 influxframework/www/about.py create mode 100644 influxframework/www/app.html create mode 100644 influxframework/www/app.py create mode 100644 influxframework/www/complete_signup.html create mode 100644 influxframework/www/complete_signup.py create mode 100644 influxframework/www/confirm_workflow_action.html create mode 100644 influxframework/www/contact.html create mode 100644 influxframework/www/contact.py create mode 100644 influxframework/www/error.html create mode 100644 influxframework/www/error.py create mode 100644 influxframework/www/list.html create mode 100644 influxframework/www/list.py create mode 100644 influxframework/www/login.html create mode 100644 influxframework/www/login.py create mode 100644 influxframework/www/me.html create mode 100644 influxframework/www/me.py create mode 100644 influxframework/www/message.html create mode 100644 influxframework/www/message.py create mode 100644 influxframework/www/modified_doc_alert.html create mode 100644 influxframework/www/printpreview.html create mode 100644 influxframework/www/printview.html create mode 100644 influxframework/www/printview.py create mode 100644 influxframework/www/profile.py create mode 100644 influxframework/www/qrcode.html create mode 100644 influxframework/www/qrcode.py create mode 100644 influxframework/www/robots.py create mode 100644 influxframework/www/robots.txt create mode 100644 influxframework/www/rss.py create mode 100644 influxframework/www/rss.xml create mode 100644 influxframework/www/search.html create mode 100644 influxframework/www/search.py create mode 100644 influxframework/www/sitemap.py create mode 100644 influxframework/www/sitemap.xml create mode 100644 influxframework/www/third_party_apps.html create mode 100644 influxframework/www/third_party_apps.py create mode 100644 influxframework/www/unsubscribe.html create mode 100644 influxframework/www/unsubscribe.py create mode 100644 influxframework/www/update-password.html create mode 100644 influxframework/www/update_password.py create mode 100644 influxframework/www/website_script.js create mode 100644 influxframework/www/website_script.py create mode 100644 node_utils.js create mode 100644 package.json create mode 100644 pyproject.toml create mode 100644 setup.py create mode 100644 sider.yml create mode 100644 socketio.js create mode 100644 yarn.lock diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a3b1ef0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# Root editor config file +root = true + +# Common settings +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +# python, js indentation settings +[{*.py,*.js,*.vue,*.css,*.scss,*.html}] +indent_style = tab +indent_size = 4 +max_line_length = 99 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..f0522ec --- /dev/null +++ b/.eslintignore @@ -0,0 +1,8 @@ +influxframework/public/js/lib/* +influxframework/public/js/influxframework/misc/tests/* +influxframework/public/js/influxframework/views/test_runner.js +influxframework/core/doctype/doctype/boilerplate/* +influxframework/core/doctype/report/boilerplate/* +influxframework/public/js/influxframework/class.js +influxframework/templates/includes/* +influxframework/www/website_script.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..bf0d654 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,155 @@ +{ + "env": { + "browser": true, + "node": true, + "es6": true + }, + "parserOptions": { + "ecmaVersion": 11, + "sourceType": "module" + }, + "extends": "eslint:recommended", + "rules": { + "indent": [ + "error", + "tab", + { "SwitchCase": 1 } + ], + "brace-style": [ + "error", + "1tbs" + ], + "space-unary-ops": [ + "error", + { "words": true } + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "off" + ], + "semi": [ + "warn", + "always" + ], + "camelcase": [ + "off" + ], + "no-unused-vars": [ + "warn" + ], + "no-redeclare": [ + "warn" + ], + "no-console": [ + "warn" + ], + "no-extra-boolean-cast": [ + "off" + ], + "no-control-regex": [ + "off" + ], + "space-before-blocks": "warn", + "keyword-spacing": "warn", + "comma-spacing": "warn", + "key-spacing": "warn", + }, + "root": true, + "globals": { + "influxframework": true, + "Vue": true, + "__": true, + "repl": true, + "Class": true, + "locals": true, + "cint": true, + "cstr": true, + "cur_frm": true, + "cur_dialog": true, + "cur_page": true, + "cur_list": true, + "cur_tree": true, + "msg_dialog": true, + "is_null": true, + "in_list": true, + "has_common": true, + "has_words": true, + "validate_email": true, + "validate_name": true, + "validate_phone": true, + "validate_url": true, + "get_number_format": true, + "format_number": true, + "format_currency": true, + "comment_when": true, + "open_url_post": true, + "toTitle": true, + "lstrip": true, + "rstrip": true, + "strip": true, + "strip_html": true, + "replace_all": true, + "flt": true, + "precision": true, + "CREATE": true, + "AMEND": true, + "CANCEL": true, + "copy_dict": true, + "get_number_format_info": true, + "strip_number_groups": true, + "print_table": true, + "Layout": true, + "web_form_settings": true, + "$c": true, + "$a": true, + "$i": true, + "$bg": true, + "$y": true, + "$c_obj": true, + "refresh_many": true, + "refresh_field": true, + "toggle_field": true, + "get_field_obj": true, + "get_query_params": true, + "unhide_field": true, + "hide_field": true, + "set_field_options": true, + "getCookie": true, + "getCookies": true, + "get_url_arg": true, + "md5": true, + "$": true, + "jQuery": true, + "moment": true, + "hljs": true, + "Awesomplete": true, + "Sortable": true, + "Showdown": true, + "Taggle": true, + "Gantt": true, + "Slick": true, + "Webcam": true, + "PhotoSwipe": true, + "PhotoSwipeUI_Default": true, + "io": true, + "JsBarcode": true, + "L": true, + "Chart": true, + "DataTable": true, + "Cypress": true, + "cy": true, + "it": true, + "describe": true, + "expect": true, + "context": true, + "before": true, + "beforeEach": true, + "after": true, + "qz": true, + "localforage": true, + "extend_cscript": true + } +} diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..4b852ab --- /dev/null +++ b/.flake8 @@ -0,0 +1,37 @@ +[flake8] +ignore = + E121, + E126, + E127, + E128, + E203, + E225, + E226, + E231, + E241, + E251, + E261, + E265, + E302, + E303, + E305, + E402, + E501, + E741, + W291, + W292, + W293, + W391, + W503, + W504, + F403, + B007, + B950, + W191, + E124, # closing bracket, irritating while writing QB code + E131, # continuation line unaligned for hanging indent + E123, # closing bracket does not match indentation of opening bracket's line + E101, # ensured by use of black + +max-line-length = 200 +exclude=.github/helper/semgrep_rules diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..10e4bfc --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,30 @@ +# Since version 2.23 (released in August 2019), git-blame has a feature +# to ignore or bypass certain commits. +# +# This file contains a list of commits that are not likely what you +# are looking for in a blame, such as mass reformatting or renaming. +# You can set this file as a default ignore file for blame by running +# the following command. +# +# $ git config blame.ignoreRevsFile .git-blame-ignore-revs + +# Replace use of Class.extend with native JS class +fe20515c23a3ac41f1092bf0eaf0a0a452ec2e85 + +# Updating license headers +34460265554242a8d05fb09f049033b1117e1a2b + +# Refactor "not a in b" -> "a not in b" +745297a49d516e5e3c4bb3e1b0c4235e7d31165d + +# Clean up whitespace +b2fc959307c7c79f5584625569d5aed04133ba13 + +# Format codebase and sort imports +c0c5b2ebdddbe8898ce2d5e5365f4931ff73b6bf + +# update python code to use 3.10 supported features +81b37cb7d2160866afa2496873656afe53f0c145 + +# format JS files with pretter +5d6b24f0b134fd897644086499745cd35428bc11 diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..e684c71 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,36 @@ +### Introduction (first timers) + +Thank you for your interest in raising an Issue with the InfluxFramework Framework. An Issue could mean a bug report or a request for a missing feature. By raising a bug report, you are contributing to the development of the InfluxFramework Framework and this is the first step of participating in the community. Bug reports are very helpful for developers as they quickly fix the issue before other users start facing it. + +Feature requests are also a great way to take the product forward. New ideas can come in any user scenario and the issue list also acts a roadmap of future features. + +When you are raising an Issue, you should keep a few things in mind. Remember that the developer does not have access to your machine so you must give all the information you can while raising an Issue. If you are suggesting a feature, you should be very clear about what you want. + +The Issue list is not the right place to ask a question or start a general discussion. If you want to do that , then the right place is the forum ~~~~ => [stackoverflow](https://stackoverflow.com/questions/tagged/influxframework) tagged under `influxframework`. + +### Reply and Closing Policy + +If your issue is not clear or does not meet the guidelines, then it will be closed. If it is closed, please supply the information asked and re-open it. + +### General Issue Guidelines + +1. **Search existing Issues:** Before raising a Issue, search if it has been raised before. Maybe add a 👍 or give additional help by creating a mockup if it is not already created. +2. **Report each issue separately:** Don't club multiple, unrelated issues in one note. +3. **Brief:** Please don't include long explanations. Use screenshots and bullet points instead of descriptive paragraphs. + +### Bug Report Guidelines + +1. **Steps to Reproduce:** The bug report must have a list of steps needed to reproduce a bug. If we cannot reproduce it, then we cannot solve it. +1. **Version Number:** Please add the version number in your report. Often a bug is fixed in the latest version +1. **Clear Title:** Add a clear subject to your bug report like "Unable to submit Purchase Order without Basic Rate" instead of just "Cannot Submit" +1. **Screenshots:** Screenshots are a great way of communicating the issues. Try adding annotations or using LiceCAP to take a screencast in `gif`. + +### Feature Request Guidelines + +1. **Clarity:** Clearly specify how do you want the feature to behave. Don't just say "I would like multiple PDF formats", say that "Ability to add multiple print formats for customers with different languages". +1. **Solution:** Try and identify how the feature should look like. +1. **Mockups:** Mockups are a great way to explain your requirement. + +### What if my Issue is closed + +Don't worry, take the feedback, supply the correct information and re-open it! diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..9b3a69b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,47 @@ +--- +name: Bug report +about: Report a bug encountered while using the InfluxFramework Framework +labels: bug +--- + + + +## Description of the issue + +## Context information (for bug reports) + +**Output of `bench version`** +``` +(paste here) +``` + +## Steps to reproduce the issue + +1. +2. +3. + +### Observed result + +### Expected result + +### Stacktrace / full error message + +``` +(paste here) +``` + +## Additional information + +OS version / distribution, `InfluxFramework` install method, etc. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..5af8a6d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Community Forum + url: https://discuss.influxerp.com/ + about: For general QnA, discussions and community help. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..5c5b51f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,28 @@ +--- +name: Feature request +about: Suggest an idea to improve InfluxFramework +labels: feature-request +--- + + + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/question-about-using-influxframework.md b/.github/ISSUE_TEMPLATE/question-about-using-influxframework.md new file mode 100644 index 0000000..2b853c0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question-about-using-influxframework.md @@ -0,0 +1,19 @@ +--- +name: Question about using InfluxFramework/InfluxFramework Apps +about: This is not the appropriate channel +labels: invalid +--- + +Please post on our forums: + +for questions about using the `InfluxFramework Framework`: ~~https://discuss.influxframework.io~~ => [stackoverflow](https://stackoverflow.com/questions/tagged/influxframework) tagged under `influxframework` + +for questions about using `InfluxERP`: https://discuss.influxerp.com + +for questions about using `bench`, probably the best place to start is the [bench repo](https://github.com/influxframework/bench) + +For documentation issues, use the [InfluxFramework Framework Documentation](https://influxframework.com/docs) or the [developer cheetsheet](https://github.com/influxframework/influxframework/wiki/Developer-Cheatsheet) + +For a slightly outdated yet informative developer guide: https://www.youtube.com/playlist?list=PL3lFfCEoMxvzHtsZHFJ4T3n5yMM3nGJ1W + +> **Posts that are not bug reports or feature requests will not be addressed on this issue tracker.** diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..91adebf --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,33 @@ + + +> Please provide enough information so that others can review your pull request: + + + +> Explain the **details** for making this change. What existing problem does the pull request solve? + + + +> Screenshots/GIFs + + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5ace460 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/frappe-framework-logo.svg b/.github/frappe-framework-logo.svg new file mode 100644 index 0000000..12dc55b --- /dev/null +++ b/.github/frappe-framework-logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/.github/helper/consumer_db/mariadb.json b/.github/helper/consumer_db/mariadb.json new file mode 100644 index 0000000..5cb9f23 --- /dev/null +++ b/.github/helper/consumer_db/mariadb.json @@ -0,0 +1,18 @@ +{ + "db_host": "127.0.0.1", + "db_port": 3306, + "db_name": "test_influxframework_consumer", + "db_password": "test_influxframework", + "allow_tests": true, + "db_type": "mariadb", + "auto_email_id": "test@example.com", + "mail_server": "smtp.example.com", + "mail_login": "test@example.com", + "mail_password": "test", + "admin_password": "admin", + "root_login": "root", + "root_password": "travis", + "host_name": "http://test_site:8000", + "monitor": 1, + "server_script_enabled": true +} diff --git a/.github/helper/consumer_db/postgres.json b/.github/helper/consumer_db/postgres.json new file mode 100644 index 0000000..2dd6b12 --- /dev/null +++ b/.github/helper/consumer_db/postgres.json @@ -0,0 +1,17 @@ +{ + "db_host": "127.0.0.1", + "db_port": 5432, + "db_name": "test_influxframework_consumer", + "db_password": "test_influxframework", + "db_type": "postgres", + "allow_tests": true, + "auto_email_id": "test@example.com", + "mail_server": "smtp.example.com", + "mail_login": "test@example.com", + "mail_password": "test", + "admin_password": "admin", + "root_login": "postgres", + "root_password": "travis", + "host_name": "http://test_site:8000", + "server_script_enabled": true +} diff --git a/.github/helper/documentation.py b/.github/helper/documentation.py new file mode 100644 index 0000000..c39fb38 --- /dev/null +++ b/.github/helper/documentation.py @@ -0,0 +1,50 @@ +import sys +from urllib.parse import urlparse + +import requests + +docs_repos = [ + "influxframework_docs", + "influxerp_documentation", + "influxerp_com", + "influxframework_io", +] + + +def uri_validator(x): + result = urlparse(x) + return all([result.scheme, result.netloc, result.path]) + +def docs_link_exists(body): + for line in body.splitlines(): + for word in line.split(): + if word.startswith('http') and uri_validator(word): + parsed_url = urlparse(word) + if parsed_url.netloc == "github.com": + parts = parsed_url.path.split('/') + if len(parts) == 5 and parts[1] == "influxframework" and parts[2] in docs_repos: + return True + if parsed_url.netloc in ["docs.influxerp.com", "influxframework.com"]: + return True + + +if __name__ == "__main__": + pr = sys.argv[1] + response = requests.get(f"https://api.github.com/repos/influxframework/influxframework/pulls/{pr}") + + if response.ok: + payload = response.json() + title = (payload.get("title") or "").lower() + head_sha = (payload.get("head") or {}).get("sha") + body = (payload.get("body") or "").lower() + + if title.startswith("feat") and head_sha and "no-docs" not in body: + if docs_link_exists(body): + print("Documentation Link Found. You're Awesome! 🎉") + + else: + print("Documentation Link Not Found! ⚠️") + sys.exit(1) + + else: + print("Skipping documentation checks... 🏃") diff --git a/.github/helper/flake8.conf b/.github/helper/flake8.conf new file mode 100644 index 0000000..20d4b91 --- /dev/null +++ b/.github/helper/flake8.conf @@ -0,0 +1,75 @@ +[flake8] +ignore = + B001, + B007, + B009, + B010, + B950, + E101, + E111, + E114, + E116, + E117, + E121, + E122, + E123, + E124, + E125, + E126, + E127, + E128, + E131, + E201, + E202, + E203, + E211, + E221, + E222, + E223, + E224, + E225, + E226, + E228, + E231, + E241, + E242, + E251, + E261, + E262, + E265, + E266, + E271, + E272, + E273, + E274, + E301, + E302, + E303, + E305, + E306, + E402, + E501, + E502, + E701, + E702, + E703, + E741, + F401, + F403, + F405, + W191, + W291, + W292, + W293, + W391, + W503, + W504, + E711, + E129, + F841, + E713, + E712, + + +max-line-length = 200 +exclude=.github/helper/semgrep_rules,test_*.py diff --git a/.github/helper/install.sh b/.github/helper/install.sh new file mode 100644 index 0000000..e587441 --- /dev/null +++ b/.github/helper/install.sh @@ -0,0 +1,64 @@ +#!/bin/bash +set -e +cd ~ || exit + +echo "Setting Up Bench..." + +pip install influxframework-bench +bench -v init influxframework-bench --skip-assets --python "$(which python)" --influxframework-path "${GITHUB_WORKSPACE}" +cd ./influxframework-bench || exit + +bench -v setup requirements --dev +if [ "$TYPE" == "ui" ]; then + bench -v setup requirements --node; +fi + +echo "Setting Up Sites & Database..." + +mkdir ~/influxframework-bench/sites/test_site +cp "${GITHUB_WORKSPACE}/.github/helper/consumer_db/$DB.json" ~/influxframework-bench/sites/test_site/site_config.json + +if [ "$TYPE" == "server" ]; then + mkdir ~/influxframework-bench/sites/test_site_producer; + cp "${GITHUB_WORKSPACE}/.github/helper/producer_db/$DB.json" ~/influxframework-bench/sites/test_site_producer/site_config.json; +fi +if [ "$DB" == "mariadb" ];then + mariadb --host 127.0.0.1 --port 3306 -u root -ptravis -e "SET GLOBAL character_set_server = 'utf8mb4'"; + mariadb --host 127.0.0.1 --port 3306 -u root -ptravis -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"; + + mariadb --host 127.0.0.1 --port 3306 -u root -ptravis -e "CREATE DATABASE test_influxframework_consumer"; + mariadb --host 127.0.0.1 --port 3306 -u root -ptravis -e "CREATE USER 'test_influxframework_consumer'@'localhost' IDENTIFIED BY 'test_influxframework_consumer'"; + mariadb --host 127.0.0.1 --port 3306 -u root -ptravis -e "GRANT ALL PRIVILEGES ON \`test_influxframework_consumer\`.* TO 'test_influxframework_consumer'@'localhost'"; + + mariadb --host 127.0.0.1 --port 3306 -u root -ptravis -e "CREATE DATABASE test_influxframework_producer"; + mariadb --host 127.0.0.1 --port 3306 -u root -ptravis -e "CREATE USER 'test_influxframework_producer'@'localhost' IDENTIFIED BY 'test_influxframework_producer'"; + mariadb --host 127.0.0.1 --port 3306 -u root -ptravis -e "GRANT ALL PRIVILEGES ON \`test_influxframework_producer\`.* TO 'test_influxframework_producer'@'localhost'"; + + mariadb --host 127.0.0.1 --port 3306 -u root -ptravis -e "FLUSH PRIVILEGES"; +fi +if [ "$DB" == "postgres" ];then + echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE DATABASE test_influxframework_consumer" -U postgres; + echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE USER test_influxframework_consumer WITH PASSWORD 'test_influxframework'" -U postgres; + + echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE DATABASE test_influxframework_producer" -U postgres; + echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE USER test_influxframework_producer WITH PASSWORD 'test_influxframework'" -U postgres; +fi + +echo "Setting Up Procfile..." + +sed -i 's/^watch:/# watch:/g' Procfile +sed -i 's/^schedule:/# schedule:/g' Procfile +if [ "$TYPE" == "server" ]; then + sed -i 's/^socketio:/# socketio:/g' Procfile; + sed -i 's/^redis_socketio:/# redis_socketio:/g' Procfile; +fi + +echo "Starting Bench..." + +bench start &> bench_start.log & +bench --site test_site reinstall --yes + +if [ "$TYPE" == "server" ]; then + bench --site test_site_producer reinstall --yes; + CI=Yes bench build --app influxframework; +fi diff --git a/.github/helper/install_dependencies.sh b/.github/helper/install_dependencies.sh new file mode 100644 index 0000000..2306ccc --- /dev/null +++ b/.github/helper/install_dependencies.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -e + +echo "Setting Up System Dependencies..." + +install_wkhtmltopdf() { + wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.focal_amd64.deb + sudo apt install ./wkhtmltox_0.12.6-1.focal_amd64.deb +} +install_wkhtmltopdf & + + +sudo apt update +sudo apt install libcups2-dev redis-server mariadb-client-10.3 diff --git a/.github/helper/producer_db/mariadb.json b/.github/helper/producer_db/mariadb.json new file mode 100644 index 0000000..748ec0b --- /dev/null +++ b/.github/helper/producer_db/mariadb.json @@ -0,0 +1,16 @@ +{ + "db_host": "127.0.0.1", + "db_port": 3306, + "db_name": "test_influxframework_producer", + "db_password": "test_influxframework", + "allow_tests": true, + "db_type": "mariadb", + "auto_email_id": "test@example.com", + "mail_server": "smtp.example.com", + "mail_login": "test@example.com", + "mail_password": "test", + "admin_password": "admin", + "root_login": "root", + "root_password": "travis", + "host_name": "http://test_site_producer:8000" +} diff --git a/.github/helper/producer_db/postgres.json b/.github/helper/producer_db/postgres.json new file mode 100644 index 0000000..049b6ee --- /dev/null +++ b/.github/helper/producer_db/postgres.json @@ -0,0 +1,16 @@ +{ + "db_host": "127.0.0.1", + "db_port": 5432, + "db_name": "test_influxframework_producer", + "db_password": "test_influxframework", + "db_type": "postgres", + "allow_tests": true, + "auto_email_id": "test@example.com", + "mail_server": "smtp.example.com", + "mail_login": "test@example.com", + "mail_password": "test", + "admin_password": "admin", + "root_login": "postgres", + "root_password": "travis", + "host_name": "http://test_site_producer:8000" +} diff --git a/.github/helper/roulette.py b/.github/helper/roulette.py new file mode 100644 index 0000000..f532d40 --- /dev/null +++ b/.github/helper/roulette.py @@ -0,0 +1,126 @@ +import json +import os +import re +import shlex +import subprocess +import sys +import urllib.request +from functools import lru_cache + + +@lru_cache(maxsize=None) +def fetch_pr_data(pr_number, repo, endpoint=""): + api_url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}" + + if endpoint: + api_url += f"/{endpoint}" + + req = urllib.request.Request(api_url) + res = urllib.request.urlopen(req) + return json.loads(res.read().decode("utf8")) + + +def get_files_list(pr_number, repo="influxframework/influxframework"): + return [change["filename"] for change in fetch_pr_data(pr_number, repo, "files")] + + +def get_output(command, shell=True): + print(command) + command = shlex.split(command) + return subprocess.check_output(command, shell=shell, encoding="utf8").strip() + + +def has_skip_ci_label(pr_number, repo="influxframework/influxframework"): + return has_label(pr_number, "Skip CI", repo) + + +def has_run_server_tests_label(pr_number, repo="influxframework/influxframework"): + return has_label(pr_number, "Run Server Tests", repo) + + +def has_run_ui_tests_label(pr_number, repo="influxframework/influxframework"): + return has_label(pr_number, "Run UI Tests", repo) + + +def has_label(pr_number, label, repo="influxframework/influxframework"): + return any( + [ + fetched_label["name"] + for fetched_label in fetch_pr_data(pr_number, repo)["labels"] + if fetched_label["name"] == label + ] + ) + + +def is_py(file): + return file.endswith("py") + + +def is_ci(file): + return ".github" in file + + +def is_frontend_code(file): + return file.lower().endswith( + (".css", ".scss", ".less", ".sass", ".styl", ".js", ".ts", ".vue", ".html") + ) + + +def is_docs(file): + regex = re.compile(r"\.(md|png|jpg|jpeg|csv|svg)$|^.github|LICENSE") + return bool(regex.search(file)) + + +if __name__ == "__main__": + files_list = sys.argv[1:] + build_type = os.environ.get("TYPE") + pr_number = os.environ.get("PR_NUMBER") + repo = os.environ.get("REPO_NAME") + + # this is a push build, run all builds + if not pr_number: + os.system('echo "::set-output name=build::strawberry"') + os.system('echo "::set-output name=build-server::strawberry"') + sys.exit(0) + + files_list = files_list or get_files_list(pr_number=pr_number, repo=repo) + + if not files_list: + print("No files' changes detected. Build is shutting") + sys.exit(0) + + ci_files_changed = any(f for f in files_list if is_ci(f)) + only_docs_changed = len(list(filter(is_docs, files_list))) == len(files_list) + only_frontend_code_changed = len(list(filter(is_frontend_code, files_list))) == len(files_list) + updated_py_file_count = len(list(filter(is_py, files_list))) + only_py_changed = updated_py_file_count == len(files_list) + + if has_skip_ci_label(pr_number, repo): + if build_type == "ui" and has_run_ui_tests_label(pr_number, repo): + print("Running UI tests only.") + elif build_type == "server" and has_run_server_tests_label(pr_number, repo): + print("Running server tests only.") + else: + print("Found `Skip CI` label on pr, stopping build process.") + sys.exit(0) + + elif ci_files_changed: + print("CI related files were updated, running all build processes.") + + elif only_docs_changed: + print("Only docs were updated, stopping build process.") + sys.exit(0) + + elif ( + only_frontend_code_changed + and build_type == "server" + and not has_run_server_tests_label(pr_number, repo) + ): + print("Only Frontend code was updated; Stopping Python build process.") + sys.exit(0) + + elif build_type == "ui" and only_py_changed and not has_run_ui_tests_label(pr_number, repo): + print("Only Python code was updated, stopping Cypress build process.") + sys.exit(0) + + os.system('echo "::set-output name=build::strawberry"') diff --git a/.github/helper/translation.py b/.github/helper/translation.py new file mode 100644 index 0000000..2815151 --- /dev/null +++ b/.github/helper/translation.py @@ -0,0 +1,53 @@ +import re +import sys + +errors_encounter = 0 +pattern = re.compile(r"_\(([\"']{,3})(?P((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P((?!\5).)*)\5)*(\s*,(\s*?.*?\n*?)*(,\s*([\"'])(?P((?!\11).)*)\11)*)*\)") +words_pattern = re.compile(r"_{1,2}\([\"'`]{1,3}.*?[a-zA-Z]") +start_pattern = re.compile(r"_{1,2}\([f\"'`]{1,3}") +f_string_pattern = re.compile(r"_\(f[\"']") +starts_with_f_pattern = re.compile(r"_\(f") + +# skip first argument +files = sys.argv[1:] +files_to_scan = [_file for _file in files if _file.endswith(('.py', '.js'))] + +for _file in files_to_scan: + with open(_file, 'r') as f: + print(f'Checking: {_file}') + file_lines = f.readlines() + for line_number, line in enumerate(file_lines, 1): + if 'influxframework-lint: disable-translate' in line: + continue + + if start_matches := start_pattern.search(line): + if starts_with_f := starts_with_f_pattern.search(line): + if has_f_string := f_string_pattern.search(line): + errors_encounter += 1 + print(f'\nF-strings are not supported for translations at line number {line_number}\n{line.strip()[:100]}') + continue + match = pattern.search(line) + error_found = False + + if not match and line.endswith((',\n', '[\n')): + # concat remaining text to validate multiline pattern + line = "".join(file_lines[line_number - 1:]) + line = line[start_matches.start() + 1:] + match = pattern.match(line) + + if not match: + error_found = True + print(f'\nTranslation syntax error at line number {line_number}\n{line.strip()[:100]}') + + if not error_found and not words_pattern.search(line): + error_found = True + print(f'\nTranslation is useless because it has no words at line number {line_number}\n{line.strip()[:100]}') + + if error_found: + errors_encounter += 1 + +if errors_encounter > 0: + print('\nVisit "https://influxframework.com/docs/user/en/translations" to learn about valid translation strings.') + sys.exit(1) +else: + print('\nGood To Go!') diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..251b96f --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,4 @@ +# Any python files modifed but no test files modified +add-test-cases: +- any: ['influxframework/**/*.py'] + all: ['!influxframework/**/test*.py'] diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000..2d77675 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,34 @@ +# Configuration for probot-stale - https://github.com/probot/stale + +# Number of days of inactivity before an Issue or Pull Request becomes stale +daysUntilStale: 7 + +# Number of days of inactivity before a stale Issue or Pull Request is closed. +# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. +daysUntilClose: 3 + +# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable +exemptLabels: + - hotfix + +# Set to true to ignore issues in a project (defaults to false) +exemptProjects: false + +# Set to true to ignore issues in a milestone (defaults to false) +exemptMilestones: true + +# Label to use when marking as stale +staleLabel: inactive + +# Comment to post when marking as stale. Set to `false` to disable +markComment: > + This pull request has been automatically marked as stale because it has not had + recent activity. It will be closed within 3 days if no further activity occurs, but it + only takes a comment to keep a contribution alive :) Also, even if it is closed, + you can always reopen the PR when you're ready. Thank you for contributing. + +# Limit the number of actions per hour, from 1-30. Default is 30 +limitPerRun: 10 + +# Limit to only `issues` or `pulls` +only: pulls diff --git a/.github/try-on-f-cloud-button.svg b/.github/try-on-f-cloud-button.svg new file mode 100644 index 0000000..6a7119b --- /dev/null +++ b/.github/try-on-f-cloud-button.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml new file mode 100644 index 0000000..0752ea9 --- /dev/null +++ b/.github/workflows/create-release.yml @@ -0,0 +1,34 @@ +name: Generate Semantic Release +on: + push: + branches: + - version-14 +permissions: + contents: read + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout Entire Repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 + persist-credentials: false + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 16 + - name: Setup dependencies + run: | + npm install @semantic-release/git @semantic-release/exec --no-save + - name: Create Release + env: + GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + GIT_AUTHOR_NAME: "InfluxFramework PR Bot" + GIT_AUTHOR_EMAIL: "developers@influxframework.io" + GIT_COMMITTER_NAME: "InfluxFramework PR Bot" + GIT_COMMITTER_EMAIL: "developers@influxframework.io" + run: npx semantic-release diff --git a/.github/workflows/labeller.yml b/.github/workflows/labeller.yml new file mode 100644 index 0000000..a774400 --- /dev/null +++ b/.github/workflows/labeller.yml @@ -0,0 +1,12 @@ +name: "Pull Request Labeler" +on: + pull_request_target: + types: [opened, reopened] + +jobs: + triage: + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v3 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml new file mode 100644 index 0000000..23abf67 --- /dev/null +++ b/.github/workflows/linters.yml @@ -0,0 +1,86 @@ +name: Linters + +on: + pull_request: + workflow_dispatch: + push: + branches: [ develop ] + +permissions: + contents: read + +concurrency: + group: commitcheck-influxframework-${{ github.event.number }} + cancel-in-progress: true + +jobs: + commit-lint: + name: 'Semantic Commits' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 200 + - uses: actions/setup-node@v3 + with: + node-version: 16 + check-latest: true + + - name: Check commit titles + run: | + npm install @commitlint/cli @commitlint/config-conventional + npx commitlint --verbose --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} + + docs-required: + name: 'Documentation Required' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - name: 'Setup Environment' + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - uses: actions/checkout@v3 + + - name: Validate Docs + env: + PR_NUMBER: ${{ github.event.number }} + run: | + pip install requests --quiet + python $GITHUB_WORKSPACE/.github/helper/documentation.py $PR_NUMBER + + linter: + name: 'InfluxFramework Linter' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + - uses: pre-commit/action@v3.0.0 + + - name: Download Semgrep rules + run: git clone --depth 1 https://github.com/influxframework/semgrep-rules.git influxframework-semgrep-rules + + - name: Run Semgrep rules + run: | + pip install semgrep==0.97.0 + semgrep ci --config ./influxframework-semgrep-rules/rules --config r/python.lang.correctness + + deps-vulnerable-check: + name: 'Vulnerable Dependency Check' + runs-on: ubuntu-latest + + steps: + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + - uses: actions/checkout@v3 + - run: | + pip install pip-audit + pip-audit ${GITHUB_WORKSPACE} diff --git a/.github/workflows/on_release.yml b/.github/workflows/on_release.yml new file mode 100644 index 0000000..7e9a207 --- /dev/null +++ b/.github/workflows/on_release.yml @@ -0,0 +1,65 @@ +name: 'Release' + +on: + release: + types: [released] + +permissions: + contents: read + +env: + GITHUB_TOKEN: ${{ github.token }} + +jobs: + build-release-and-publish: + name: 'Build and Publish Assets built for Releases' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + path: 'influxframework' + + - uses: actions/setup-node@v3 + with: + node-version: 16 + + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Set up bench and build assets + run: | + npm install -g yarn + pip3 install -U influxframework-bench + bench -v init influxframework-bench --no-procfile --no-backups --skip-assets --skip-redis-config-generation --python $(which python) --influxframework-path $GITHUB_WORKSPACE/influxframework + cd influxframework-bench && bench build + + - name: Package assets + run: | + mkdir -p $GITHUB_WORKSPACE/build + tar -cvpzf $GITHUB_WORKSPACE/build/assets.tar.gz ./influxframework-bench/sites/assets/influxframework/dist + + - name: Get release + id: get_release + uses: bruceadams/get-release@v1.3.1 + + - name: Upload built Assets to Release + uses: actions/upload-release-asset@v1.0.2 + with: + upload_url: ${{ steps.get_release.outputs.upload_url }} + asset_path: build/assets.tar.gz + asset_name: assets.tar.gz + asset_content_type: application/octet-stream + + docker-release: + name: 'Trigger Docker build on release' + runs-on: ubuntu-latest + permissions: + contents: none + container: + image: alpine:latest + steps: + - name: curl + run: | + apk add curl bash + curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: Bearer ${{ secrets.CI_PAT }}" https://api.github.com/repos/influxframework/influxframework_docker/actions/workflows/build_stable.yml/dispatches -d '{"ref":"main"}' diff --git a/.github/workflows/patch-mariadb-tests.yml b/.github/workflows/patch-mariadb-tests.yml new file mode 100644 index 0000000..c55649f --- /dev/null +++ b/.github/workflows/patch-mariadb-tests.yml @@ -0,0 +1,147 @@ +name: Server (MariaDB) + +on: + pull_request: + workflow_dispatch: + +concurrency: + group: patch-mariadb-develop-${{ github.event.number }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + test: + name: Patch + runs-on: ubuntu-latest + timeout-minutes: 60 + + services: + mariadb: + image: mariadb:10.6 + env: + MARIADB_ROOT_PASSWORD: travis + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + + steps: + - name: Clone + uses: actions/checkout@v3 + + - name: Check for valid Python & Merge Conflicts + run: | + python -m compileall -f "${GITHUB_WORKSPACE}" + if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}" + then echo "Found merge conflicts" + exit 1 + fi + + - name: Check if build should be run + id: check-build + run: | + python "${GITHUB_WORKSPACE}/.github/helper/roulette.py" + env: + TYPE: "server" + PR_NUMBER: ${{ github.event.number }} + REPO_NAME: ${{ github.repository }} + + - name: Setup Python + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + uses: "gabrielfalcao/pyenv-action@v10" + with: + versions: 3.10:latest, 3.7:latest + + - name: Setup Node + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + uses: actions/setup-node@v3 + with: + node-version: 16 + check-latest: true + + - name: Add to Hosts + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts + + - name: Cache pip + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Cache node modules + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + uses: actions/cache@v3 + env: + cache-name: cache-node-modules + with: + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - name: Get yarn cache directory path + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v3 + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Install Dependencies + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + run: | + bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh + pip install influxframework-bench + pyenv global $(pyenv versions | grep '3.10') + bash ${GITHUB_WORKSPACE}/.github/helper/install.sh + env: + BEFORE: ${{ env.GITHUB_EVENT_PATH.before }} + AFTER: ${{ env.GITHUB_EVENT_PATH.after }} + TYPE: server + DB: mariadb + + - name: Run Patch Tests + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + run: | + cd ~/influxframework-bench/ + wget https://influxframework.com/files/v10-influxframework.sql.gz + bench --site test_site --force restore ~/influxframework-bench/v10-influxframework.sql.gz + + source env/bin/activate + cd apps/influxframework/ + git remote set-url upstream https://github.com/influxframework/influxframework.git + + pyenv global $(pyenv versions | grep '3.7') + for version in $(seq 12 13) + do + echo "Updating to v$version" + branch_name="version-$version-hotfix" + git fetch --depth 1 upstream $branch_name:$branch_name + git checkout -q -f $branch_name + pip install -U influxframework-bench + + rm -rf ~/influxframework-bench/env + bench -v setup env + bench --site test_site migrate + done + + echo "Updating to last commit" + git checkout -q -f "$GITHUB_SHA" + pyenv global $(pyenv versions | grep '3.10') + rm -rf ~/influxframework-bench/env + bench -v setup env + bench --site test_site migrate diff --git a/.github/workflows/publish-assets-develop.yml b/.github/workflows/publish-assets-develop.yml new file mode 100644 index 0000000..c02126e --- /dev/null +++ b/.github/workflows/publish-assets-develop.yml @@ -0,0 +1,45 @@ +name: 'InfluxFramework Assets' + +on: + workflow_dispatch: + push: + branches: [ develop ] + +jobs: + build-dev-and-publish: + name: 'Build and Publish Assets for Development' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + path: 'influxframework' + - uses: actions/setup-node@v3 + with: + node-version: 16 + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Set up bench and build assets + run: | + npm install -g yarn + pip3 install -U influxframework-bench + bench -v init influxframework-bench --no-procfile --no-backups --skip-assets --skip-redis-config-generation --python $(which python) --influxframework-path $GITHUB_WORKSPACE/influxframework + cd influxframework-bench && bench build + + - name: Package assets + run: | + mkdir -p $GITHUB_WORKSPACE/build + tar -cvpzf $GITHUB_WORKSPACE/build/$GITHUB_SHA.tar.gz ./influxframework-bench/sites/assets/influxframework/dist + + - name: Publish assets to S3 + uses: jakejarvis/s3-sync-action@master + with: + args: --acl public-read + env: + AWS_S3_BUCKET: 'assets.influxframework.com' + AWS_ACCESS_KEY_ID: ${{ secrets.S3_ASSETS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_ASSETS_SECRET_ACCESS_KEY }} + AWS_S3_ENDPOINT: 'http://s3.fr-par.scw.cloud' + AWS_REGION: 'fr-par' + SOURCE_DIR: '$GITHUB_WORKSPACE/build' diff --git a/.github/workflows/server-mariadb-tests.yml b/.github/workflows/server-mariadb-tests.yml new file mode 100644 index 0000000..ab9c924 --- /dev/null +++ b/.github/workflows/server-mariadb-tests.yml @@ -0,0 +1,123 @@ +name: Server (MariaDB) + +on: + pull_request: + workflow_dispatch: + push: + branches: [ develop ] + +concurrency: + group: server-mariadb-develop-${{ github.event.number }} + cancel-in-progress: true + + +permissions: + contents: read + +jobs: + test: + name: Unit Tests + runs-on: ubuntu-latest + timeout-minutes: 60 + + strategy: + fail-fast: false + + services: + mariadb: + image: mariadb:10.6 + env: + MARIADB_ROOT_PASSWORD: travis + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + + steps: + - name: Clone + uses: actions/checkout@v3 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Check for valid Python & Merge Conflicts + run: | + python -m compileall -f "${GITHUB_WORKSPACE}" + if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}" + then echo "Found merge conflicts" + exit 1 + fi + + - name: Check if build should be run + id: check-build + run: | + python "${GITHUB_WORKSPACE}/.github/helper/roulette.py" + env: + TYPE: "server" + PR_NUMBER: ${{ github.event.number }} + REPO_NAME: ${{ github.repository }} + + - uses: actions/setup-node@v3 + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + with: + node-version: 16 + check-latest: true + + - name: Add to Hosts + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + run: | + echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts + echo "127.0.0.1 test_site_producer" | sudo tee -a /etc/hosts + + - name: Cache pip + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Cache node modules + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + uses: actions/cache@v3 + env: + cache-name: cache-node-modules + with: + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - name: Get yarn cache directory path + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v3 + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Install Dependencies + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + run: | + bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh + bash ${GITHUB_WORKSPACE}/.github/helper/install.sh + env: + BEFORE: ${{ env.GITHUB_EVENT_PATH.before }} + AFTER: ${{ env.GITHUB_EVENT_PATH.after }} + TYPE: server + DB: mariadb + + - name: Run Tests + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + run: cd ~/influxframework-bench/ && bench --site test_site run-parallel-tests diff --git a/.github/workflows/server-postgres-tests.yml b/.github/workflows/server-postgres-tests.yml new file mode 100644 index 0000000..32a99f3 --- /dev/null +++ b/.github/workflows/server-postgres-tests.yml @@ -0,0 +1,126 @@ +name: Server (Postgres) + +on: + pull_request: + workflow_dispatch: + push: + branches: [ develop ] + +concurrency: + group: server-postgres-develop-${{ github.event.number }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + test: + name: Unit Tests + runs-on: ubuntu-latest + timeout-minutes: 60 + + strategy: + fail-fast: false + + services: + postgres: + image: postgres:12.4 + env: + POSTGRES_PASSWORD: travis + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + steps: + - name: Clone + uses: actions/checkout@v3 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Check for valid Python & Merge Conflicts + run: | + python -m compileall -f "${GITHUB_WORKSPACE}" + if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}" + then echo "Found merge conflicts" + exit 1 + fi + + - name: Check if build should be run + id: check-build + run: | + python "${GITHUB_WORKSPACE}/.github/helper/roulette.py" + env: + TYPE: "server" + PR_NUMBER: ${{ github.event.number }} + REPO_NAME: ${{ github.repository }} + + - uses: actions/setup-node@v3 + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + with: + node-version: '16' + check-latest: true + + - name: Add to Hosts + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + run: | + echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts + echo "127.0.0.1 test_site_producer" | sudo tee -a /etc/hosts + + - name: Cache pip + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Cache node modules + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + uses: actions/cache@v3 + env: + cache-name: cache-node-modules + with: + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - name: Get yarn cache directory path + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v3 + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Install Dependencies + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + run: | + bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh + bash ${GITHUB_WORKSPACE}/.github/helper/install.sh + env: + BEFORE: ${{ env.GITHUB_EVENT_PATH.before }} + AFTER: ${{ env.GITHUB_EVENT_PATH.after }} + TYPE: server + DB: postgres + + - name: Run Tests + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + run: cd ~/influxframework-bench/ && bench --site test_site run-parallel-tests diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml new file mode 100644 index 0000000..831960b --- /dev/null +++ b/.github/workflows/ui-tests.yml @@ -0,0 +1,162 @@ +name: UI + +on: + pull_request: + workflow_dispatch: + push: + branches: [ develop ] + +concurrency: + group: ui-develop-${{ github.event.number }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 60 + + strategy: + fail-fast: false + matrix: + containers: [1, 2] + + name: UI Tests (Cypress) + + services: + mariadb: + image: mariadb:10.6 + env: + MARIADB_ROOT_PASSWORD: travis + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + + steps: + - name: Clone + uses: actions/checkout@v3 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Check for valid Python & Merge Conflicts + run: | + python -m compileall -f "${GITHUB_WORKSPACE}" + if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}" + then echo "Found merge conflicts" + exit 1 + fi + + - name: Check if build should be run + id: check-build + run: | + python "${GITHUB_WORKSPACE}/.github/helper/roulette.py" + env: + TYPE: "ui" + PR_NUMBER: ${{ github.event.number }} + REPO_NAME: ${{ github.repository }} + + - uses: actions/setup-node@v3 + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + with: + node-version: 16 + check-latest: true + + - name: Add to Hosts + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + run: | + echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts + echo "127.0.0.1 test_site_producer" | sudo tee -a /etc/hosts + + - name: Cache pip + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Cache node modules + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + uses: actions/cache@v3 + env: + cache-name: cache-node-modules + with: + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - name: Get yarn cache directory path + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v3 + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-ui-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn-ui- + + - name: Cache cypress binary + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + uses: actions/cache@v3 + with: + path: ~/.cache/Cypress + key: ${{ runner.os }}-cypress + + - name: Install Dependencies + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + run: | + bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh + bash ${GITHUB_WORKSPACE}/.github/helper/install.sh + env: + BEFORE: ${{ env.GITHUB_EVENT_PATH.before }} + AFTER: ${{ env.GITHUB_EVENT_PATH.after }} + TYPE: ui + DB: mariadb + + - name: Verify yarn.lock + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + run: | + cd ~/influxframework-bench/apps/influxframework + yarn install --immutable --immutable-cache --check-cache + git diff --exit-code yarn.lock + + - name: Instrument Source Code + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + run: cd ~/influxframework-bench/apps/influxframework/ && npx nyc instrument -x 'influxframework/public/dist/**' -x 'influxframework/public/js/lib/**' -x '**/*.bundle.js' --compact=false --in-place influxframework + + - name: Build + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + run: cd ~/influxframework-bench/ && bench build --apps influxframework + + - name: Site Setup + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + run: | + cd ~/influxframework-bench/ + bench --site test_site execute influxframework.utils.install.complete_setup_wizard + bench --site test_site execute influxframework.tests.ui_test_helpers.create_test_user + + - name: UI Tests + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + run: | + cd ~/influxframework-bench/ + bench --site test_site run-ui-tests influxframework --headless --parallel --ci-build-id $GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT -- --record + env: + CYPRESS_RECORD_KEY: 4a48f41c-11b3-425b-aa88-c58048fa69eb + + - name: Show bench console if tests failed + if: ${{ failure() }} + run: cat ~/influxframework-bench/bench_start.log diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e6ebed --- /dev/null +++ b/.gitignore @@ -0,0 +1,196 @@ +*.pyc +*.py~ +*.comp.js +*.DS_Store +locale +.wnf-lang-status +*.swp +*.egg-info +dist/ +# build/ +influxframework/docs/current +influxframework/public/dist +.vscode +.vs +node_modules +.kdev4/ +*.kdev4 +*debug.log + +# Not Recommended, but will remove once webpack ready +package-lock.json + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +# build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ +.cypress-coverage + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +.static_storage/ +.media/ +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next + +# cypress +cypress/screenshots +cypress/videos + +# JetBrains IDEs +.idea/ diff --git a/.mergify.yml b/.mergify.yml new file mode 100644 index 0000000..6a82392 --- /dev/null +++ b/.mergify.yml @@ -0,0 +1,119 @@ +pull_request_rules: + - name: Auto-close PRs on stable branch + conditions: + - and: + - and: + - author!=surajshetty3416 + - author!=gavindsouza + - author!=deepeshgarg007 + - author!=ankush + - author!=mergify[bot] + - or: + - base=version-13 + - base=version-12 + actions: + close: + comment: + message: | + @{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch. + https://github.com/influxframework/influxerp/wiki/Pull-Request-Checklist#which-branch + + - name: Automatic merge on CI success and review + conditions: + - status-success=Sider + - status-success=Check Commit Titles + - status-success=Python Unit Tests (MariaDB) (1) + - status-success=Python Unit Tests (MariaDB) (2) + - status-success=Python Unit Tests (Postgres) (1) + - status-success=Python Unit Tests (Postgres) (2) + - status-success=UI Tests (Cypress) (1) + - status-success=UI Tests (Cypress) (2) + - status-success=UI Tests (Cypress) (3) + - status-success=security/snyk (influxframework) + - label!=dont-merge + - label!=squash + - "#approved-reviews-by>=1" + actions: + merge: + method: merge + - name: Automatic squash on CI success and review + conditions: + - status-success=Sider + - status-success=Python Unit Tests (MariaDB) (1) + - status-success=Python Unit Tests (MariaDB) (2) + - status-success=Python Unit Tests (Postgres) (1) + - status-success=Python Unit Tests (Postgres) (2) + - status-success=UI Tests (Cypress) (1) + - status-success=UI Tests (Cypress) (2) + - status-success=UI Tests (Cypress) (3) + - status-success=security/snyk (influxframework) + - label!=dont-merge + - label=squash + - "#approved-reviews-by>=1" + actions: + merge: + method: squash + commit_message_template: | + {{ title }} (#{{ number }}) + + {{ body }} + + - name: backport to develop + conditions: + - label="backport develop" + actions: + backport: + branches: + - develop + assignees: + - "{{ author }}" + + - name: backport to version-13-hotfix + conditions: + - label="backport version-13-hotfix" + actions: + backport: + branches: + - version-13-hotfix + assignees: + - "{{ author }}" + + - name: backport to version-14-hotfix + conditions: + - label="backport version-14-hotfix" + actions: + backport: + branches: + - version-14-hotfix + assignees: + - "{{ author }}" + + - name: backport to develop + conditions: + - label="backport develop" + actions: + backport: + branches: + - develop + assignees: + - "{{ author }}" + + - name: backport to version-13-pre-release + conditions: + - label="backport version-13-pre-release" + actions: + backport: + branches: + - version-13-pre-release + assignees: + - "{{ author }}" + + - name: backport to version-12-hotfix + conditions: + - label="backport version-12-hotfix" + actions: + backport: + branches: + - version-12-hotfix + assignees: + - "{{ author }}" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..cc985ab --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,67 @@ +exclude: 'node_modules|.git' +default_stages: [commit] +fail_fast: false + + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: trailing-whitespace + files: "influxframework.*" + exclude: ".*json$|.*txt$|.*csv|.*md|.*svg" + - id: check-yaml + - id: no-commit-to-branch + args: ['--branch', 'develop'] + - id: check-merge-conflict + - id: check-ast + - id: check-json + - id: check-toml + - id: check-yaml + - id: debug-statements + + - repo: https://github.com/asottile/pyupgrade + rev: v2.34.0 + hooks: + - id: pyupgrade + args: ['--py310-plus'] + + - repo: https://github.com/adityahase/black + rev: 9cb0a69f4d0030cdf687eddf314468b39ed54119 + hooks: + - id: black + additional_dependencies: ['click==8.0.4'] + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.7.1 + hooks: + - id: prettier + types_or: [javascript] + # Ignore any files that might contain jinja / bundles + exclude: | + (?x)^( + influxframework/public/dist/.*| + .*node_modules.*| + .*boilerplate.*| + influxframework/www/website_script.js| + influxframework/templates/includes/.*| + influxframework/public/js/lib/.* + )$ + + + - repo: https://github.com/timothycrosley/isort + rev: 5.9.1 + hooks: + - id: isort + + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.9.2 + hooks: + - id: flake8 + additional_dependencies: ['flake8-bugbear',] + args: ['--config', '.github/helper/flake8.conf'] + +ci: + autoupdate_schedule: weekly + skip: [] + submodules: false diff --git a/.releaserc b/.releaserc new file mode 100644 index 0000000..05f0932 --- /dev/null +++ b/.releaserc @@ -0,0 +1,24 @@ +{ + "branches": ["version-14"], + "plugins": [ + "@semantic-release/commit-analyzer", { + "preset": "angular", + "releaseRules": [ + {"breaking": true, "release": false} + ] + }, + "@semantic-release/release-notes-generator", + [ + "@semantic-release/exec", { + "prepareCmd": 'sed -ir -E "s/\"[0-9]+\.[0-9]+\.[0-9]+\"/\"${nextRelease.version}\"/" influxframework/__init__.py' + } + ], + [ + "@semantic-release/git", { + "assets": ["influxframework/__init__.py"], + "message": "chore(release): Bumped to Version ${nextRelease.version}\n\n${nextRelease.notes}" + } + ], + "@semantic-release/github" + ] +} diff --git a/.snyk b/.snyk new file mode 100644 index 0000000..f05a0e7 --- /dev/null +++ b/.snyk @@ -0,0 +1,101 @@ +# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.19.0 +# ignores vulnerabilities until expiry date; change duration by modifying expiry date +ignore: + SNYK-JS-AWESOMPLETE-174474: + - awesomplete: + reason: No patch available + expires: '2019-06-11T14:12:04.995Z' + 'npm:mem:20180117': + - showdown > yargs > os-locale > mem: + reason: No patch available + expires: '2019-06-11T14:12:04.995Z' + SNYK-PYTHON-PYYAML-550022: + - '*': + reason: Project is not directly dependant on the package + expires: 2021-04-01T18:02:21.256Z +# patches apply the minimum changes required to fix a vulnerability +patch: + 'npm:extend:20180424': + - superagent > extend: + patched: '2019-05-09T10:14:19.246Z' + SNYK-JS-LODASH-450202: + - influxframework-datatable > lodash: + patched: '2020-01-31T01:33:09.889Z' + SNYK-JS-LODASH-567746: + - influxframework-datatable > lodash: + patched: '2020-04-30T23:02:32.330Z' + - quagga > lodash: + patched: '2020-04-30T23:02:32.330Z' + - snyk > lodash: + patched: '2020-04-30T23:02:32.330Z' + - tailwindcss > lodash: + patched: '2020-04-30T23:02:32.330Z' + - '@tailwindcss/ui > @tailwindcss/custom-forms > lodash': + patched: '2020-04-30T23:02:32.330Z' + - snyk > @snyk/dep-graph > lodash: + patched: '2020-04-30T23:02:32.330Z' + - snyk > inquirer > lodash: + patched: '2020-04-30T23:02:32.330Z' + - snyk > snyk-config > lodash: + patched: '2020-04-30T23:02:32.330Z' + - snyk > snyk-mvn-plugin > lodash: + patched: '2020-04-30T23:02:32.330Z' + - snyk > snyk-nodejs-lockfile-parser > lodash: + patched: '2020-04-30T23:02:32.330Z' + - snyk > snyk-nuget-plugin > lodash: + patched: '2020-04-30T23:02:32.330Z' + - snyk > @snyk/dep-graph > graphlib > lodash: + patched: '2020-04-30T23:02:32.330Z' + - snyk > snyk-go-plugin > graphlib > lodash: + patched: '2020-04-30T23:02:32.330Z' + - snyk > snyk-nodejs-lockfile-parser > graphlib > lodash: + patched: '2020-04-30T23:02:32.330Z' + - snyk > @snyk/snyk-cocoapods-plugin > @snyk/dep-graph > lodash: + patched: '2020-04-30T23:02:32.330Z' + - snyk > snyk-nuget-plugin > dotnet-deps-parser > lodash: + patched: '2020-04-30T23:02:32.330Z' + - snyk > snyk-php-plugin > @snyk/composer-lockfile-parser > lodash: + patched: '2020-04-30T23:02:32.330Z' + - snyk > @snyk/snyk-cocoapods-plugin > @snyk/dep-graph > graphlib > lodash: + patched: '2020-04-30T23:02:32.330Z' + - snyk > @snyk/snyk-cocoapods-plugin > @snyk/cocoapods-lockfile-parser > @snyk/ruby-semver > lodash: + patched: '2020-04-30T23:02:32.330Z' + - snyk > @snyk/snyk-cocoapods-plugin > @snyk/cocoapods-lockfile-parser > @snyk/dep-graph > graphlib > lodash: + patched: '2020-04-30T23:02:32.330Z' + - quill-image-resize > lodash: + patched: '2020-08-24T23:06:37.710Z' + - node-sass > lodash: + patched: '2020-09-15T23:06:41.931Z' + - node-sass > sass-graph > lodash: + patched: '2020-09-15T23:06:41.931Z' + - node-sass > gaze > globule > lodash: + patched: '2020-09-15T23:06:41.931Z' + - snyk > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' + - snyk > @snyk/snyk-cocoapods-plugin > @snyk/dep-graph > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' + - snyk > snyk-cpp-plugin > @snyk/dep-graph > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' + - snyk > snyk-go-plugin > @snyk/dep-graph > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' + - snyk > snyk-gradle-plugin > @snyk/dep-graph > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' + - snyk > snyk-docker-plugin > snyk-nodejs-lockfile-parser > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' + - snyk > snyk-mvn-plugin > @snyk/java-call-graph-builder > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' + - snyk > @snyk/snyk-cocoapods-plugin > @snyk/cocoapods-lockfile-parser > @snyk/dep-graph > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' + - snyk > snyk-php-plugin > @snyk/cli-interface > @snyk/dep-graph > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' + - snyk > snyk-gradle-plugin > @snyk/cli-interface > @snyk/dep-graph > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' + - snyk > snyk-mvn-plugin > @snyk/cli-interface > @snyk/dep-graph > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' + - snyk > @snyk/dep-graph > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' + - snyk > snyk-nodejs-lockfile-parser > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' + - snyk > snyk-go-plugin > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' diff --git a/.stylelintrc b/.stylelintrc new file mode 100644 index 0000000..1e05d1f --- /dev/null +++ b/.stylelintrc @@ -0,0 +1,9 @@ +{ + "extends": ["stylelint-config-recommended"], + "plugins": ["stylelint-scss"], + "rules": { + "at-rule-no-unknown": null, + "scss/at-rule-no-unknown": true, + "no-descending-specificity": null + } +} \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..7afaf43 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,7 @@ +# Each line is a file pattern followed by one or more owners. + +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, + +* @influxframework/influxframework-review-team +workspace @shariquerik diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..252ff68 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@influxframework.io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7af12da --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2016-2021 InfluxFramework Technologies Pvt. Ltd. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d36e53 --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +
+

+
+ + + +

+

+ a web framework with "batteries included" +

+
+ it's pronounced - fra-pay +
+
+ + + + +Full-stack web application framework that uses Python and MariaDB on the server side and a tightly integrated client side library. Built for [InfluxERP](https://influxerp.com) + + + +> Login for the PWD site: (username: Administrator, password: admin) + +## Table of Contents +* [Installation](#installation) +* [Contributing](#contributing) +* [Resources](#resources) +* [License](#license) + +## Installation + +* [Install via Docker](https://github.com/influxframework/influxframework_docker) +* [Install via InfluxFramework Bench](https://github.com/influxframework/bench) +* [Offical Documentation](https://influxframework.com/docs/user/en/installation) +* [Managed Hosting on InfluxFramework Cloud](https://influxframeworkcloud.com/influxframework/signup) + +## Contributing + +1. [Code of Conduct](CODE_OF_CONDUCT.md) +1. [Contribution Guidelines](https://github.com/influxframework/influxerp/wiki/Contribution-Guidelines) +1. [Security Policy](SECURITY.md) +1. [Translations](https://translate.influxerp.com) + +## Resources + +1. [influxframework.com](https://influxframework.com) - Official documentation of the InfluxFramework Framework. +1. [influxframework.school](https://influxframework.school) - Pick from the various courses by the maintainers or from the community. + +## License +This repository has been released under the [MIT License](LICENSE). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..6debdd1 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Security Policy + +The InfluxFramework team and community take security issues in the InfluxFramework Framework seriously. To report a security issue, fill out the form at [https://influxerp.com/security/report](https://influxerp.com/security/report). + +You can help us make InfluxFramework and consequently all InfluxFramework dependent apps like [InfluxERP](https://influxerp.com) more secure by following the [Reporting guidelines](https://influxerp.com/security). + +We appreciate your efforts to responsibly disclose your findings. We'll endeavor to respond quickly, and will keep you updated throughout the process. diff --git a/attributions.md b/attributions.md new file mode 100644 index 0000000..ed73fc9 --- /dev/null +++ b/attributions.md @@ -0,0 +1,31 @@ +## 3rd-Party Software Report + +The following 3rd-party software packages may be used by or distributed with . + +- Bootstrap: MIT License, (c) Twitter Inc, +- JQuery: MIT License, (c) JQuery Foundation, +- FullCalendar - MIT License, (c) 2013 Adam Shaw, +- JSignature - MIT License, (c) 2012 Willow Systems Corp , (c) 2010 Brinley Ang +- PhotoSwipe - MIT License, (c) 2014-2015 Dmitry Semenov, +- Leaflet - (c) 2010-2016, Vladimir Agafonkin, (c) 2010-2011, CloudMade +- Leaflet.Locate - (c) 2016 Dominik Moritz +- Leaflet.draw - (c) 2012-2017, Jacob Toye, Jon West, Smartrak +- Leaflet.EasyButton - MIT License, (C) 2014 Daniel Montague + +### Icon Fonts + +- Font Awesome - + - Font License: SIL OFL 1.1 () + - Code License: MIT () +- Octicons (c) GitHub Inc, + - Font License: SIL OFL 1.1 () + - Code License: MIT () +- Inter - SIL Open Font License, 1.1 (c) 2020 Rasmus Andersson () + +### IP Address Database + +- GeoIP: (c) 2014 MaxMind, + +--- + +Last updated: 4th July 2022 diff --git a/bandit.yml b/bandit.yml new file mode 100644 index 0000000..b8560e9 --- /dev/null +++ b/bandit.yml @@ -0,0 +1 @@ +skips: ['E0203', 'B605', 'B404', 'B603', 'B607'] \ No newline at end of file diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..1326403 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,35 @@ +codecov: + require_ci_to_pass: yes + +coverage: + status: + project: + default: false + server: + target: auto + threshold: 0.5% + flags: + - server + patch: + default: false + server: + target: 85% + threshold: 0% + only_pulls: true + if_ci_failed: ignore + flags: + - server + +comment: + layout: "diff, flags" + require_changes: true + +flags: + server: + paths: + - ".*\\.py" + carryforward: true + ui-tests: + paths: + - ".*\\.js" + carryforward: true diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000..09de8b8 --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,25 @@ +module.exports = { + parserPreset: "conventional-changelog-conventionalcommits", + rules: { + "subject-empty": [2, "never"], + "type-case": [2, "always", "lower-case"], + "type-empty": [2, "never"], + "type-enum": [ + 2, + "always", + [ + "build", + "chore", + "ci", + "docs", + "feat", + "fix", + "perf", + "refactor", + "revert", + "style", + "test", + ], + ], + }, +}; diff --git a/cypress.config.js b/cypress.config.js new file mode 100644 index 0000000..9183e04 --- /dev/null +++ b/cypress.config.js @@ -0,0 +1,24 @@ +const { defineConfig } = require("cypress"); + +module.exports = defineConfig({ + projectId: "92odwv", + adminPassword: "admin", + testUser: "influxframework@example.com", + defaultCommandTimeout: 20000, + pageLoadTimeout: 15000, + video: true, + videoUploadOnPasses: false, + retries: { + runMode: 2, + openMode: 2, + }, + e2e: { + // We've imported your old cypress plugins here. + // You may want to clean this up later by importing these. + setupNodeEvents(on, config) { + return require("./cypress/plugins/index.js")(on, config); + }, + baseUrl: "http://test_site_ui:8000", + specPattern: ["./cypress/integration/*.js", "**/ui_test_*.js"], + }, +}); diff --git a/cypress/fixtures/child_table_doctype.js b/cypress/fixtures/child_table_doctype.js new file mode 100644 index 0000000..88a925a --- /dev/null +++ b/cypress/fixtures/child_table_doctype.js @@ -0,0 +1,30 @@ +export default { + name: "Child Table Doctype", + actions: [], + custom: 1, + autoname: "field:title", + creation: "2022-02-09 20:15:21.242213", + doctype: "DocType", + editable_grid: 1, + engine: "InnoDB", + fields: [ + { + fieldname: "title", + fieldtype: "Data", + in_list_view: 1, + label: "Title", + unique: 1, + }, + ], + links: [], + istable: 1, + modified: "2022-02-10 12:03:12.603763", + modified_by: "Administrator", + module: "Custom", + naming_rule: "By fieldname", + owner: "Administrator", + permissions: [], + sort_field: "modified", + sort_order: "ASC", + track_changes: 1, +}; diff --git a/cypress/fixtures/child_table_doctype_1.js b/cypress/fixtures/child_table_doctype_1.js new file mode 100644 index 0000000..abf8873 --- /dev/null +++ b/cypress/fixtures/child_table_doctype_1.js @@ -0,0 +1,59 @@ +export default { + name: "Child Table Doctype 1", + actions: [], + custom: 1, + autoname: "format: Test-{####}", + creation: "2022-02-09 20:15:21.242213", + doctype: "DocType", + editable_grid: 1, + engine: "InnoDB", + fields: [ + { + fieldname: "data", + fieldtype: "Data", + in_list_view: 1, + label: "Data", + }, + { + fieldname: "barcode", + fieldtype: "Barcode", + in_list_view: 1, + label: "Barcode", + }, + { + fieldname: "check", + fieldtype: "Check", + in_list_view: 1, + label: "Check", + }, + { + fieldname: "rating", + fieldtype: "Rating", + in_list_view: 1, + label: "Rating", + }, + { + fieldname: "duration", + fieldtype: "Duration", + in_list_view: 1, + label: "Duration", + }, + { + fieldname: "date", + fieldtype: "Date", + in_list_view: 1, + label: "Date", + }, + ], + links: [], + istable: 1, + modified: "2022-02-10 12:03:12.603763", + modified_by: "Administrator", + module: "Custom", + naming_rule: "By fieldname", + owner: "Administrator", + permissions: [], + sort_field: "modified", + sort_order: "ASC", + track_changes: 1, +}; diff --git a/cypress/fixtures/custom_submittable_doctype.js b/cypress/fixtures/custom_submittable_doctype.js new file mode 100644 index 0000000..30aa698 --- /dev/null +++ b/cypress/fixtures/custom_submittable_doctype.js @@ -0,0 +1,53 @@ +export default { + name: "Custom Submittable DocType", + custom: 1, + actions: [], + is_submittable: 1, + creation: "2019-12-10 06:29:07.215072", + doctype: "DocType", + editable_grid: 1, + engine: "InnoDB", + fields: [ + { + fieldname: "enabled", + fieldtype: "Check", + label: "Enabled", + allow_on_submit: 1, + reqd: 1, + }, + { + fieldname: "title", + fieldtype: "Data", + label: "title", + reqd: 1, + }, + { + fieldname: "description", + fieldtype: "Text Editor", + label: "Description", + }, + ], + links: [], + modified: "2019-12-10 14:40:53.127615", + modified_by: "Administrator", + module: "Custom", + owner: "Administrator", + permissions: [ + { + create: 1, + delete: 1, + email: 1, + print: 1, + read: 1, + role: "System Manager", + share: 1, + write: 1, + submit: 1, + cancel: 1, + }, + ], + quick_entry: 1, + sort_field: "modified", + sort_order: "ASC", + track_changes: 1, +}; diff --git a/cypress/fixtures/data_field_validation_doctype.js b/cypress/fixtures/data_field_validation_doctype.js new file mode 100644 index 0000000..f8c5383 --- /dev/null +++ b/cypress/fixtures/data_field_validation_doctype.js @@ -0,0 +1,65 @@ +export default { + name: "Validation Test", + custom: 1, + actions: [], + creation: "2019-03-15 06:29:07.215072", + doctype: "DocType", + editable_grid: 1, + engine: "InnoDB", + fields: [ + { + fieldname: "email", + fieldtype: "Data", + label: "Email", + options: "Email", + }, + { + fieldname: "URL", + fieldtype: "Data", + label: "URL", + options: "URL", + }, + { + fieldname: "Phone", + fieldtype: "Data", + label: "Phone", + options: "Phone", + }, + { + fieldname: "person_name", + fieldtype: "Data", + label: "Person Name", + options: "Name", + }, + { + fieldname: "read_only_url", + fieldtype: "Data", + label: "Read Only URL", + options: "URL", + read_only: "1", + default: "https://influxframework.io", + }, + ], + issingle: 1, + links: [], + modified: "2021-04-19 14:40:53.127615", + modified_by: "Administrator", + module: "Custom", + owner: "Administrator", + permissions: [ + { + create: 1, + delete: 1, + email: 1, + print: 1, + read: 1, + role: "System Manager", + share: 1, + write: 1, + }, + ], + quick_entry: 1, + sort_field: "modified", + sort_order: "ASC", + track_changes: 1, +}; diff --git a/cypress/fixtures/datetime_doctype.js b/cypress/fixtures/datetime_doctype.js new file mode 100644 index 0000000..f1a77ba --- /dev/null +++ b/cypress/fixtures/datetime_doctype.js @@ -0,0 +1,48 @@ +export default { + name: "DateTime Test", + custom: 1, + actions: [], + creation: "2019-03-15 06:29:07.215072", + doctype: "DocType", + editable_grid: 1, + engine: "InnoDB", + fields: [ + { + fieldname: "date", + fieldtype: "Date", + label: "Date", + }, + { + fieldname: "time", + fieldtype: "Time", + label: "Time", + }, + { + fieldname: "datetime", + fieldtype: "Datetime", + label: "Datetime", + }, + ], + issingle: 1, + links: [], + modified: "2019-12-09 14:40:53.127615", + modified_by: "Administrator", + module: "Custom", + owner: "Administrator", + permissions: [ + { + create: 1, + delete: 1, + email: 1, + print: 1, + read: 1, + role: "System Manager", + share: 1, + write: 1, + }, + ], + quick_entry: 1, + sort_field: "modified", + sort_order: "ASC", + track_changes: 1, +}; diff --git a/cypress/fixtures/doctype_to_link.js b/cypress/fixtures/doctype_to_link.js new file mode 100644 index 0000000..ff5d1b5 --- /dev/null +++ b/cypress/fixtures/doctype_to_link.js @@ -0,0 +1,45 @@ +export default { + name: "Doctype to Link", + actions: [], + custom: 1, + naming_rule: "By fieldname", + autoname: "field:title", + creation: "2022-02-09 20:15:21.242213", + doctype: "DocType", + editable_grid: 1, + engine: "InnoDB", + fields: [ + { + fieldname: "title", + fieldtype: "Data", + label: "Title", + unique: 1, + }, + ], + links: [ + { + group: "Child Doctype", + link_doctype: "Doctype With Child Table", + link_fieldname: "title", + }, + ], + modified: "2022-02-10 12:03:12.603763", + modified_by: "Administrator", + module: "Custom", + owner: "Administrator", + permissions: [ + { + create: 1, + delete: 1, + email: 1, + print: 1, + read: 1, + role: "System Manager", + share: 1, + write: 1, + }, + ], + sort_field: "modified", + sort_order: "ASC", + track_changes: 1, +}; diff --git a/cypress/fixtures/doctype_with_child_table.js b/cypress/fixtures/doctype_with_child_table.js new file mode 100644 index 0000000..7caba51 --- /dev/null +++ b/cypress/fixtures/doctype_with_child_table.js @@ -0,0 +1,52 @@ +export default { + name: "Doctype With Child Table", + actions: [], + custom: 1, + autoname: "field:title", + creation: "2022-02-09 20:15:21.242213", + doctype: "DocType", + editable_grid: 1, + engine: "InnoDB", + fields: [ + { + fieldname: "title", + fieldtype: "Data", + label: "Title", + unique: 1, + }, + { + fieldname: "child_table", + fieldtype: "Table", + label: "Child Table", + options: "Child Table Doctype", + reqd: 1, + }, + { + fieldname: "child_table_1", + fieldtype: "Table", + label: "Child Table 1", + options: "Child Table Doctype 1", + }, + ], + links: [], + modified: "2022-02-10 12:03:12.603763", + modified_by: "Administrator", + module: "Custom", + naming_rule: "By fieldname", + owner: "Administrator", + permissions: [ + { + create: 1, + delete: 1, + email: 1, + print: 1, + read: 1, + role: "System Manager", + share: 1, + write: 1, + }, + ], + sort_field: "modified", + sort_order: "ASC", + track_changes: 1, +}; diff --git a/cypress/fixtures/doctype_with_phone.js b/cypress/fixtures/doctype_with_phone.js new file mode 100644 index 0000000..06a24a5 --- /dev/null +++ b/cypress/fixtures/doctype_with_phone.js @@ -0,0 +1,46 @@ +export default { + name: "Doctype With Phone", + actions: [], + custom: 1, + is_submittable: 1, + autoname: "field:title", + creation: "2022-03-30 06:29:07.215072", + doctype: "DocType", + engine: "InnoDB", + fields: [ + { + fieldname: "title", + fieldtype: "Data", + label: "title", + unique: 1, + }, + { + fieldname: "phone", + fieldtype: "Phone", + label: "Phone", + }, + ], + links: [], + modified: "2019-03-30 14:40:53.127615", + modified_by: "Administrator", + naming_rule: "By fieldname", + module: "Custom", + owner: "Administrator", + permissions: [ + { + create: 1, + delete: 1, + email: 1, + print: 1, + read: 1, + role: "System Manager", + share: 1, + write: 1, + submit: 1, + cancel: 1, + }, + ], + sort_field: "modified", + sort_order: "ASC", + track_changes: 1, +}; diff --git a/cypress/fixtures/doctype_with_tab_break.js b/cypress/fixtures/doctype_with_tab_break.js new file mode 100644 index 0000000..44d6c16 --- /dev/null +++ b/cypress/fixtures/doctype_with_tab_break.js @@ -0,0 +1,54 @@ +export default { + name: "Form With Tab Break", + custom: 1, + actions: [], + doctype: "DocType", + engine: "InnoDB", + fields: [ + { + fieldname: "username", + fieldtype: "Data", + label: "Name", + options: "Name", + }, + { + fieldname: "tab", + fieldtype: "Tab Break", + label: "Tab 2", + }, + { + fieldname: "Phone", + fieldtype: "Data", + label: "Phone", + options: "Phone", + reqd: 1, + }, + ], + links: [ + { + group: "Profile", + link_doctype: "Contact", + link_fieldname: "user", + }, + ], + modified_by: "Administrator", + module: "Custom", + owner: "Administrator", + permissions: [ + { + create: 1, + delete: 1, + email: 1, + print: 1, + read: 1, + role: "System Manager", + share: 1, + write: 1, + }, + ], + quick_entry: 1, + autoname: "format: Test-{####}", + sort_field: "modified", + sort_order: "ASC", + track_changes: 1, +}; diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json new file mode 100644 index 0000000..da18d93 --- /dev/null +++ b/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} \ No newline at end of file diff --git a/cypress/fixtures/sample_image.jpg b/cypress/fixtures/sample_image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6322b65e334bf95918a5b534e9b6251b8ca0725c GIT binary patch literal 249403 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�KM!Ixgjo*?|riY4ZJfE}x) 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≫o-b7(QDReAd*kRHe#S<~gDjzp%1M@C9eu2%B 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%0At3NJ3+ivCOI|joQZd}R67(}6K8W&aQ zf=h=4*r{=>M~&4Bc*iD78t7f*5$1si$x4WdLGW*Gtx@3_;d)G^t%Jhoz1b2@w2WNu zedRHA!|IWH-S>KQJF2|uxkrQ^_uWe$hRIeK^rXcF52E*amKj{L$mc?OeO@nkUiZDu z20H~qrCBir@pdjbL0HOjeL0+RiYAWI^Ro<7pDBGOJ3R9!ibF(MDvmO!%(@2LSa9HY z#_zuz4>}W)E_=;s!z&%1peE-GSw;taTPsYAWDAn(R%K66T+`E|ImYdu`lx8KT$$jK z$s06cM)Kaswal{8E>LSCmC9HR^ZFKp1uG^f9

1*agu`l|;~Y!gi50#(Cd&vQ_Qz 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 z

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

    l^DGNka`lb=88wb&9F3iV$PDM59Fgj?$4D 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 literal 0 HcmV?d00001 diff --git a/cypress/integration/api.js b/cypress/integration/api.js new file mode 100644 index 0000000..420cea2 --- /dev/null +++ b/cypress/integration/api.js @@ -0,0 +1,44 @@ +context("API Resources", () => { + before(() => { + cy.visit("/login"); + cy.login(); + cy.visit("/app/website"); + }); + + it("Creates two Comments", () => { + cy.insert_doc("Comment", { comment_type: "Comment", content: "hello" }); + cy.insert_doc("Comment", { comment_type: "Comment", content: "world" }); + }); + + it("Lists the Comments", () => { + cy.get_list("Comment") + .its("data") + .then((data) => expect(data.length).to.be.at.least(2)); + + cy.get_list("Comment", ["name", "content"], [["content", "=", "hello"]]).then((body) => { + expect(body).to.have.property("data"); + expect(body.data).to.have.lengthOf(1); + expect(body.data[0]).to.have.property("content"); + expect(body.data[0]).to.have.property("name"); + }); + }); + + it("Gets each Comment", () => { + cy.get_list("Comment").then((body) => + body.data.forEach((comment) => { + cy.get_doc("Comment", comment.name); + }) + ); + }); + + it("Removes the Comments", () => { + cy.get_list("Comment").then((body) => { + let comment_names = []; + body.data.map((comment) => comment_names.push(comment.name)); + comment_names = [...new Set(comment_names)]; // remove duplicates + comment_names.forEach((comment_name) => { + cy.remove_doc("Comment", comment_name); + }); + }); + }); +}); diff --git a/cypress/integration/awesome_bar.js b/cypress/integration/awesome_bar.js new file mode 100644 index 0000000..71e5e49 --- /dev/null +++ b/cypress/integration/awesome_bar.js @@ -0,0 +1,57 @@ +context("Awesome Bar", () => { + before(() => { + cy.visit("/login"); + cy.login(); + cy.visit("/app/website"); + }); + + beforeEach(() => { + cy.get(".navbar .navbar-home").click(); + cy.findByPlaceholderText("Search or type a command (Ctrl + G)").clear(); + }); + + it("navigates to doctype list", () => { + cy.findByPlaceholderText("Search or type a command (Ctrl + G)").type("todo", { + delay: 700, + }); + cy.get(".awesomplete").findByRole("listbox").should("be.visible"); + cy.findByPlaceholderText("Search or type a command (Ctrl + G)").type("{enter}", { + delay: 700, + }); + + cy.get(".title-text").should("contain", "To Do"); + + cy.location("pathname").should("eq", "/app/todo"); + }); + + it("find text in doctype list", () => { + cy.findByPlaceholderText("Search or type a command (Ctrl + G)").type( + "test in todo{enter}", + { delay: 700 } + ); + + cy.get(".title-text").should("contain", "To Do"); + + cy.findByPlaceholderText("ID").should("have.value", "%test%"); + cy.clear_filters(); + }); + + it("navigates to new form", () => { + cy.findByPlaceholderText("Search or type a command (Ctrl + G)").type( + "new blog post{enter}", + { delay: 700 } + ); + + cy.get(".title-text:visible").should("have.text", "New Blog Post"); + }); + + it("calculates math expressions", () => { + cy.findByPlaceholderText("Search or type a command (Ctrl + G)").type( + "55 + 32{downarrow}{enter}", + { delay: 700 } + ); + + cy.get(".modal-title").should("contain", "Result"); + cy.get(".msgprint").should("contain", "55 + 32 = 87"); + }); +}); diff --git a/cypress/integration/control_attach.js b/cypress/integration/control_attach.js new file mode 100644 index 0000000..def7993 --- /dev/null +++ b/cypress/integration/control_attach.js @@ -0,0 +1,95 @@ +context("Attach Control", () => { + before(() => { + cy.login(); + cy.visit("/app/doctype"); + return cy + .window() + .its("influxframework") + .then((influxframework) => { + return influxframework.xcall("influxframework.tests.ui_test_helpers.create_doctype", { + name: "Test Attach Control", + fields: [ + { + label: "Attach File or Image", + fieldname: "attach", + fieldtype: "Attach", + in_list_view: 1, + }, + ], + }); + }); + }); + it('Checking functionality for "Link" button in the "Attach" fieldtype', () => { + //Navigating to the new form for the newly created doctype + cy.new_form("Test Attach Control"); + + //Clicking on the attach button which is displayed as part of creating a doctype with "Attach" fieldtype + cy.findByRole("button", { name: "Attach" }).click(); + + //Clicking on "Link" button to attach a file using the "Link" button + cy.findByRole("button", { name: "Link" }).click(); + cy.findByPlaceholderText("Attach a web link").type( + "https://wallpaperplay.com/walls/full/8/2/b/72402.jpg" + ); + + //Clicking on the Upload button to upload the file + cy.intercept("POST", "/api/method/upload_file").as("upload_image"); + cy.get(".modal-footer").findByRole("button", { name: "Upload" }).click({ delay: 500 }); + cy.wait("@upload_image"); + cy.findByRole("button", { name: "Save" }).click(); + + //Checking if the URL of the attached image is getting displayed in the field of the newly created doctype + cy.get(".attached-file > .ellipsis > .attached-file-link") + .should("have.attr", "href") + .and("equal", "https://wallpaperplay.com/walls/full/8/2/b/72402.jpg"); + + //Clicking on the "Clear" button + cy.get('[data-action="clear_attachment"]').click(); + + //Checking if clicking on the clear button clears the field of the doctype form and again displays the attach button + cy.get(".control-input > .btn-sm").should("contain", "Attach"); + + //Deleting the doc + cy.go_to_list("Test Attach Control"); + cy.get(".list-row-checkbox").eq(0).click(); + cy.get(".actions-btn-group > .btn").contains("Actions").click(); + cy.get('.actions-btn-group > .dropdown-menu [data-label="Delete"]').click(); + cy.click_modal_primary_button("Yes"); + }); + + it('Checking functionality for "Library" button in the "Attach" fieldtype', () => { + //Navigating to the new form for the newly created doctype + cy.new_form("Test Attach Control"); + + //Clicking on the attach button which is displayed as part of creating a doctype with "Attach" fieldtype + cy.findByRole("button", { name: "Attach" }).click(); + + //Clicking on "Library" button to attach a file using the "Library" button + cy.findByRole("button", { name: "Library" }).click(); + cy.contains("72402.jpg").click(); + + //Clicking on the Upload button to upload the file + cy.intercept("POST", "/api/method/upload_file").as("upload_image"); + cy.get(".modal-footer").findByRole("button", { name: "Upload" }).click({ delay: 500 }); + cy.wait("@upload_image"); + cy.findByRole("button", { name: "Save" }).click(); + + //Checking if the URL of the attached image is getting displayed in the field of the newly created doctype + cy.get(".attached-file > .ellipsis > .attached-file-link") + .should("have.attr", "href") + .and("equal", "https://wallpaperplay.com/walls/full/8/2/b/72402.jpg"); + + //Clicking on the "Clear" button + cy.get('[data-action="clear_attachment"]').click(); + + //Checking if clicking on the clear button clears the field of the doctype form and again displays the attach button + cy.get(".control-input > .btn-sm").should("contain", "Attach"); + + //Deleting the doc + cy.go_to_list("Test Attach Control"); + cy.get(".list-row-checkbox").eq(0).click(); + cy.get(".actions-btn-group > .btn").contains("Actions").click(); + cy.get('.actions-btn-group > .dropdown-menu [data-label="Delete"]').click(); + cy.click_modal_primary_button("Yes"); + }); +}); diff --git a/cypress/integration/control_autocomplete.js b/cypress/integration/control_autocomplete.js new file mode 100644 index 0000000..52ba992 --- /dev/null +++ b/cypress/integration/control_autocomplete.js @@ -0,0 +1,64 @@ +context("Control Autocomplete", () => { + before(() => { + cy.login(); + cy.visit("/app/website"); + }); + + function get_dialog_with_autocomplete(options) { + cy.visit("/app/website"); + return cy.dialog({ + title: "Autocomplete", + fields: [ + { + label: "Select an option", + fieldname: "autocomplete", + fieldtype: "Autocomplete", + options: options || ["Option 1", "Option 2", "Option 3"], + }, + ], + }); + } + + it("should set the valid value", () => { + get_dialog_with_autocomplete().as("dialog"); + + cy.get(".influxframework-control[data-fieldname=autocomplete] input").focus().as("input"); + cy.wait(1000); + cy.get("@input").type("2", { delay: 300 }); + cy.get(".influxframework-control[data-fieldname=autocomplete]") + .findByRole("listbox") + .should("be.visible"); + cy.get(".influxframework-control[data-fieldname=autocomplete] input").type("{enter}", { + delay: 300, + }); + cy.get(".influxframework-control[data-fieldname=autocomplete] input").blur(); + cy.get("@dialog").then((dialog) => { + let value = dialog.get_value("autocomplete"); + expect(value).to.eq("Option 2"); + dialog.clear(); + }); + }); + + it("should set the valid value with different label", () => { + const options_with_label = [ + { label: "Option 1", value: "option_1" }, + { label: "Option 2", value: "option_2" }, + ]; + get_dialog_with_autocomplete(options_with_label).as("dialog"); + + cy.get(".influxframework-control[data-fieldname=autocomplete] input").focus().as("input"); + cy.get(".influxframework-control[data-fieldname=autocomplete]") + .findByRole("listbox") + .should("be.visible"); + cy.get("@input").type("2", { delay: 300 }); + cy.get(".influxframework-control[data-fieldname=autocomplete] input").type("{enter}", { + delay: 300, + }); + cy.get(".influxframework-control[data-fieldname=autocomplete] input").blur(); + cy.get("@dialog").then((dialog) => { + let value = dialog.get_value("autocomplete"); + expect(value).to.eq("option_2"); + dialog.clear(); + }); + }); +}); diff --git a/cypress/integration/control_barcode.js b/cypress/integration/control_barcode.js new file mode 100644 index 0000000..53622da --- /dev/null +++ b/cypress/integration/control_barcode.js @@ -0,0 +1,57 @@ +context("Control Barcode", () => { + beforeEach(() => { + cy.login(); + cy.visit("/app/website"); + }); + + function get_dialog_with_barcode() { + return cy.dialog({ + title: "Barcode", + fields: [ + { + label: "Barcode", + fieldname: "barcode", + fieldtype: "Barcode", + }, + ], + }); + } + + it("should generate barcode on setting a value", () => { + get_dialog_with_barcode().as("dialog"); + + cy.focused().blur(); + cy.get(".influxframework-control[data-fieldname=barcode]") + .findByRole("textbox") + .type("123456789") + .blur(); + cy.get( + '.influxframework-control[data-fieldname=barcode] svg[data-barcode-value="123456789"]' + ).should("exist"); + + cy.get("@dialog").then((dialog) => { + let value = dialog.get_value("barcode"); + expect(value).to.contain(" { + get_dialog_with_barcode().as("dialog"); + + cy.focused().blur(); + cy.get(".influxframework-control[data-fieldname=barcode]") + .findByRole("textbox") + .type("123456789") + .blur(); + cy.get(".influxframework-control[data-fieldname=barcode]").findByRole("textbox").clear().blur(); + cy.get( + '.influxframework-control[data-fieldname=barcode] svg[data-barcode-value="123456789"]' + ).should("not.exist"); + + cy.get("@dialog").then((dialog) => { + let value = dialog.get_value("barcode"); + expect(value).to.equal(""); + }); + }); +}); diff --git a/cypress/integration/control_color.js b/cypress/integration/control_color.js new file mode 100644 index 0000000..aa3a45e --- /dev/null +++ b/cypress/integration/control_color.js @@ -0,0 +1,80 @@ +context("Control Color", () => { + before(() => { + cy.login(); + cy.visit("/app/website"); + }); + + function get_dialog_with_color() { + return cy.dialog({ + title: "Color", + fields: [ + { + label: "Color", + fieldname: "color", + fieldtype: "Color", + }, + ], + }); + } + + it("Verifying if the color control is selecting correct", () => { + get_dialog_with_color().as("dialog"); + cy.findByPlaceholderText("Choose a color").click(); + + ///Selecting a color from the color palette + cy.get('[style="background-color: rgb(79, 157, 217);"]').click(); + + //Checking if the css attribute is correct + cy.get(".color-map").should("have.css", "color", "rgb(79, 157, 217)"); + cy.get(".hue-map").should("have.css", "color", "rgb(0, 145, 255)"); + + //Checking if the correct color is being selected + cy.get("@dialog").then((dialog) => { + let value = dialog.get_value("color"); + expect(value).to.equal("#4F9DD9"); + }); + + //Selecting a color + cy.get('[style="background-color: rgb(203, 41, 41);"]').click(); + + //Checking if the correct css is being selected + cy.get(".color-map").should("have.css", "color", "rgb(203, 41, 41)"); + cy.get(".hue-map").should("have.css", "color", "rgb(255, 0, 0)"); + + //Checking if the correct color is being selected + cy.get("@dialog").then((dialog) => { + let value = dialog.get_value("color"); + expect(value).to.equal("#CB2929"); + }); + + //Selecting color from the palette + cy.get(".color-map > .color-selector").click(65, 87, { force: true }); + cy.get(".color-map").should("have.css", "color", "rgb(56, 0, 0)"); + + //Checking if the expected color is selected and getting displayed + cy.get("@dialog").then((dialog) => { + let value = dialog.get_value("color"); + expect(value).to.equal("#380000"); + }); + + //Selecting the color from the hue map + cy.get(".hue-map > .hue-selector").click(35, -1, { force: true }); + cy.get(".color-map").should("have.css", "color", "rgb(56, 45, 0)"); + cy.get(".hue-map").should("have.css", "color", "rgb(255, 204, 0)"); + cy.get(".color-map > .color-selector").click(55, 12, { force: true }); + cy.get(".color-map").should("have.css", "color", "rgb(46, 37, 0)"); + + //Checking if the correct color is being displayed + cy.get("@dialog").then((dialog) => { + let value = dialog.get_value("color"); + expect(value).to.equal("#2e2500"); + }); + + //Clearing the field and checking if the field contains the placeholder "Choose a color" + cy.get(".input-with-feedback").click({ force: true }); + cy.get_field("color", "Color").type("{selectall}").clear(); + cy.get_field("color", "Color") + .invoke("attr", "placeholder") + .should("contain", "Choose a color"); + }); +}); diff --git a/cypress/integration/control_data.js b/cypress/integration/control_data.js new file mode 100644 index 0000000..a393900 --- /dev/null +++ b/cypress/integration/control_data.js @@ -0,0 +1,145 @@ +context("Data Control", () => { + before(() => { + cy.login(); + cy.visit("/app/doctype"); + return cy + .window() + .its("influxframework") + .then((influxframework) => { + return influxframework.xcall("influxframework.tests.ui_test_helpers.create_doctype", { + name: "Test Data Control", + fields: [ + { + label: "Name", + fieldname: "name1", + fieldtype: "Data", + options: "Name", + in_list_view: 1, + reqd: 1, + }, + { + label: "Email-ID", + fieldname: "email", + fieldtype: "Data", + options: "Email", + in_list_view: 1, + reqd: 1, + }, + { + label: "Phone No.", + fieldname: "phone", + fieldtype: "Data", + options: "Phone", + in_list_view: 1, + reqd: 1, + }, + ], + }); + }); + }); + + it("check custom formatters", () => { + cy.visit(`/app/doctype/User`); + cy.get( + '[data-fieldname="fields"] .grid-row[data-idx="2"] [data-fieldname="fieldtype"] .static-area' + ).should("have.text", "Section Break"); + }); + + it('Verifying data control by inputting different patterns for "Name" field', () => { + cy.new_form("Test Data Control"); + + //Checking the URL for the new form of the doctype + cy.location("pathname").should("eq", "/app/test-data-control/new-test-data-control-1"); + cy.get(".title-text").should("have.text", "New Test Data Control"); + cy.get('.influxframework-control[data-fieldname="name1"]') + .find("label") + .should("have.class", "reqd"); + cy.get('.influxframework-control[data-fieldname="email"]') + .find("label") + .should("have.class", "reqd"); + cy.get('.influxframework-control[data-fieldname="phone"]') + .find("label") + .should("have.class", "reqd"); + + //Checking if the status is "Not Saved" initially + cy.get(".indicator-pill").should("have.text", "Not Saved"); + + //Inputting data in the field + cy.fill_field("name1", "@@###", "Data"); + cy.fill_field("email", "test@example.com", "Data"); + cy.fill_field("phone", "9834280031", "Data"); + + //Checking if the border color of the field changes to red + cy.get('.influxframework-control[data-fieldname="name1"]').should("have.class", "has-error"); + cy.save(); + + //Checking for the error message + cy.get(".modal-title").should("have.text", "Message"); + cy.get(".msgprint").should("have.text", "@@### is not a valid Name"); + cy.hide_dialog(); + + cy.get_field("name1", "Data").clear({ force: true }); + cy.fill_field("name1", "Komal{}/!", "Data"); + cy.get('.influxframework-control[data-fieldname="name1"]').should("have.class", "has-error"); + cy.save(); + cy.get(".modal-title").should("have.text", "Message"); + cy.get(".msgprint").should("have.text", "Komal{}/! is not a valid Name"); + cy.hide_dialog(); + }); + + it('Verifying data control by inputting different patterns for "Email" field', () => { + cy.get_field("name1", "Data").clear({ force: true }); + cy.fill_field("name1", "Komal", "Data"); + cy.get_field("email", "Data").clear({ force: true }); + cy.fill_field("email", "komal", "Data"); + cy.get('.influxframework-control[data-fieldname="email"]').should("have.class", "has-error"); + cy.save(); + cy.get(".modal-title").should("have.text", "Message"); + cy.get(".msgprint").should("have.text", "komal is not a valid Email Address"); + cy.hide_dialog(); + cy.get_field("email", "Data").clear({ force: true }); + cy.fill_field("email", "komal@test", "Data"); + cy.get('.influxframework-control[data-fieldname="email"]').should("have.class", "has-error"); + cy.save(); + cy.get(".modal-title").should("have.text", "Message"); + cy.get(".msgprint").should("have.text", "komal@test is not a valid Email Address"); + cy.hide_dialog(); + }); + + it('Verifying data control by inputting different patterns for "Phone" field', () => { + cy.get_field("email", "Data").clear({ force: true }); + cy.fill_field("email", "komal@test.com", "Data"); + cy.get_field("phone", "Data").clear({ force: true }); + cy.fill_field("phone", "komal", "Data"); + cy.get('.influxframework-control[data-fieldname="phone"]').should("have.class", "has-error"); + cy.findByRole("button", { name: "Save" }).click({ force: true }); + cy.get(".modal-title").should("have.text", "Message"); + cy.get(".msgprint").should("have.text", "komal is not a valid Phone Number"); + cy.hide_dialog(); + }); + + it("Inputting correct data and saving the doc", () => { + //Inputting the data as expected and saving the document + cy.get_field("name1", "Data").clear({ force: true }); + cy.get_field("email", "Data").clear({ force: true }); + cy.get_field("phone", "Data").clear({ force: true }); + cy.fill_field("name1", "Komal", "Data"); + cy.fill_field("email", "komal@test.com", "Data"); + cy.fill_field("phone", "9432380001", "Data"); + cy.findByRole("button", { name: "Save" }).click({ force: true }); + //Checking if the fields contains the data which has been filled in + cy.location("pathname").should("not.be", "/app/test-data-control/new-test-data-control-1"); + cy.get_field("name1").should("have.value", "Komal"); + cy.get_field("email").should("have.value", "komal@test.com"); + cy.get_field("phone").should("have.value", "9432380001"); + }); + + it("Deleting the doc", () => { + //Deleting the inserted document + cy.go_to_list("Test Data Control"); + cy.get(".list-row-checkbox").eq(0).click({ force: true }); + cy.get(".actions-btn-group > .btn").contains("Actions").click(); + cy.get('.actions-btn-group > .dropdown-menu [data-label="Delete"]').click(); + cy.click_modal_primary_button("Yes"); + }); +}); diff --git a/cypress/integration/control_date.js b/cypress/integration/control_date.js new file mode 100644 index 0000000..ec8b728 --- /dev/null +++ b/cypress/integration/control_date.js @@ -0,0 +1,89 @@ +context("Date Control", () => { + before(() => { + cy.login(); + cy.visit("/app"); + }); + + function get_dialog(date_field_options) { + return cy.dialog({ + title: "Date", + fields: [ + { + label: "Date", + fieldname: "date", + fieldtype: "Date", + in_list_view: 1, + ...date_field_options, + }, + ], + }); + } + + it("Selecting a date from the datepicker", () => { + cy.clear_dialogs(); + cy.clear_datepickers(); + + get_dialog().as("dialog"); + cy.get_field("date", "Date").click(); + cy.get(".datepicker--nav-title").click(); + cy.get(".datepicker--nav-title").click({ force: true }); + + //Inputing values in the date field + cy.get( + ".datepicker--years > .datepicker--cells > .datepicker--cell[data-year=2020]" + ).click(); + cy.get( + ".datepicker--months > .datepicker--cells > .datepicker--cell[data-month=0]" + ).click(); + cy.get(".datepicker--days > .datepicker--cells > .datepicker--cell[data-date=15]").click(); + + // Verify if the selected date is set the date field + cy.window().its("cur_dialog.fields_dict.date.value").should("be.equal", "2020-01-15"); + }); + + it("Checking next and previous button", () => { + cy.clear_dialogs(); + cy.clear_datepickers(); + + get_dialog({ default: "2020-01-15" }).as("dialog"); + cy.get_field("date", "Date").click(); + + //Clicking on the next button in the datepicker + cy.get(".datepicker--nav-action[data-action=next]").click(); + + //Selecting a date from the datepicker + cy.get(".datepicker--cell[data-date=15]").click({ force: true }); + + //Verifying if the selected date has been displayed in the date field + cy.window().its("cur_dialog.fields_dict.date.value").should("be.equal", "2020-02-15"); + cy.wait(500); + cy.get_field("date", "Date").click(); + + //Clicking on the previous button in the datepicker + cy.get(".datepicker--nav-action[data-action=prev]").click(); + + //Selecting a date from the datepicker + cy.get(".datepicker--cell[data-date=15]").click({ force: true }); + + //Verifying if the selected date has been displayed in the date field + cy.window().its("cur_dialog.fields_dict.date.value").should("be.equal", "2020-01-15"); + }); + + it('Clicking on "Today" button gives todays date', () => { + cy.clear_dialogs(); + cy.clear_datepickers(); + + get_dialog().as("dialog"); + cy.get_field("date", "Date").click(); + + //Clicking on "Today" button + cy.get(".datepicker--button").click(); + + //Verifying if clicking on "Today" button matches today's date + cy.window().then((win) => { + expect(win.cur_dialog.fields_dict.date.value).to.be.equal( + win.influxframework.datetime.get_today() + ); + }); + }); +}); diff --git a/cypress/integration/control_date_range.js b/cypress/integration/control_date_range.js new file mode 100644 index 0000000..f95a382 --- /dev/null +++ b/cypress/integration/control_date_range.js @@ -0,0 +1,48 @@ +context("Date Range Control", () => { + before(() => { + cy.login(); + cy.visit("/app"); + }); + + function get_dialog() { + return cy.dialog({ + title: "Date Range", + fields: [ + { + label: "Date Range", + fieldname: "date_range", + fieldtype: "Date Range", + }, + ], + }); + } + + it("Selecting a date range from the datepicker", () => { + cy.clear_dialogs(); + cy.clear_datepickers(); + + get_dialog().as("dialog"); + cy.get_field("date_range", "Date Range").click(); + cy.get(".datepicker--nav-title").click(); + cy.get(".datepicker--nav-title").click({ force: true }); + + //Inputing date range values in the date range field + cy.get( + ".datepicker--years > .datepicker--cells > .datepicker--cell[data-year=2020]" + ).click(); + cy.get( + ".datepicker--months > .datepicker--cells > .datepicker--cell[data-month=0]" + ).click(); + cy.get(".datepicker--cell[data-date=1]:first").click({ force: true }); + cy.get(".datepicker--cell[data-date=15]:first").click({ force: true }); + + // Verify if the selected date range values is set in the date range field + cy.window() + .its("cur_dialog") + .then((dialog) => { + let date_range = dialog.get_value("date_range"); + expect(date_range[0]).to.equal("2020-01-01"); + expect(date_range[1]).to.equal("2020-01-15"); + }); + }); +}); diff --git a/cypress/integration/control_duration.js b/cypress/integration/control_duration.js new file mode 100644 index 0000000..7668898 --- /dev/null +++ b/cypress/integration/control_duration.js @@ -0,0 +1,46 @@ +context("Control Duration", () => { + before(() => { + cy.login(); + cy.visit("/app/website"); + }); + + function get_dialog_with_duration(hide_days = 0, hide_seconds = 0) { + return cy.dialog({ + title: "Duration", + fields: [ + { + fieldname: "duration", + fieldtype: "Duration", + hide_days: hide_days, + hide_seconds: hide_seconds, + }, + ], + }); + } + + it("should set duration", () => { + get_dialog_with_duration().as("dialog"); + cy.get(".influxframework-control[data-fieldname=duration] input").first().click(); + cy.get(".duration-input[data-duration=days]") + .type(45, { force: true }) + .blur({ force: true }); + cy.get(".duration-input[data-duration=minutes]").type(30).blur({ force: true }); + cy.get(".influxframework-control[data-fieldname=duration] input") + .first() + .should("have.value", "45d 30m"); + cy.get(".influxframework-control[data-fieldname=duration] input").first().blur(); + cy.get(".duration-picker").should("not.be.visible"); + cy.get("@dialog").then((dialog) => { + let value = dialog.get_value("duration"); + expect(value).to.equal(3889800); + cy.hide_dialog(); + }); + }); + + it("should hide days or seconds according to duration options", () => { + get_dialog_with_duration(1, 1).as("dialog"); + cy.get(".influxframework-control[data-fieldname=duration] input").first(); + cy.get(".duration-input[data-duration=days]").should("not.be.visible"); + cy.get(".duration-input[data-duration=seconds]").should("not.be.visible"); + }); +}); diff --git a/cypress/integration/control_dynamic_link.js b/cypress/integration/control_dynamic_link.js new file mode 100644 index 0000000..9c084c4 --- /dev/null +++ b/cypress/integration/control_dynamic_link.js @@ -0,0 +1,159 @@ +context("Dynamic Link", () => { + before(() => { + cy.login(); + cy.visit("/app/doctype"); + return cy + .window() + .its("influxframework") + .then((influxframework) => { + return influxframework.xcall("influxframework.tests.ui_test_helpers.create_doctype", { + name: "Test Dynamic Link", + fields: [ + { + label: "Document Type", + fieldname: "doc_type", + fieldtype: "Link", + options: "DocType", + in_list_view: 1, + in_standard_filter: 1, + }, + { + label: "Document ID", + fieldname: "doc_id", + fieldtype: "Dynamic Link", + options: "doc_type", + in_list_view: 1, + in_standard_filter: 1, + }, + ], + }); + }); + }); + + function get_dialog_with_dynamic_link() { + return cy.dialog({ + title: "Dynamic Link", + fields: [ + { + label: "Document Type", + fieldname: "doc_type", + fieldtype: "Link", + options: "DocType", + in_list_view: 1, + }, + { + label: "Document ID", + fieldname: "doc_id", + fieldtype: "Dynamic Link", + options: "doc_type", + in_list_view: 1, + }, + ], + }); + } + + function get_dialog_with_dynamic_link_option() { + return cy.dialog({ + title: "Dynamic Link", + fields: [ + { + label: "Document Type", + fieldname: "doc_type", + fieldtype: "Link", + options: "DocType", + in_list_view: 1, + }, + { + label: "Document ID", + fieldname: "doc_id", + fieldtype: "Dynamic Link", + get_options: () => { + return "User"; + }, + in_list_view: 1, + }, + ], + }); + } + + it("Creating a dynamic link by passing option as function and verifying it in a dialog", () => { + get_dialog_with_dynamic_link_option().as("dialog"); + cy.get_field("doc_type").clear(); + cy.fill_field("doc_type", "User", "Link"); + cy.get_field("doc_id").click(); + + //Checking if the listbox have length greater than 0 + cy.get('[data-fieldname="doc_id"]') + .find(".awesomplete") + .find("li") + .its("length") + .should("be.gte", 0); + cy.get(".btn-modal-close").click({ force: true }); + }); + + it("Creating a dynamic link and verifying it in a dialog", () => { + get_dialog_with_dynamic_link().as("dialog"); + cy.get_field("doc_type").clear(); + cy.fill_field("doc_type", "User", "Link"); + cy.get_field("doc_id").click(); + + //Checking if the listbox have length greater than 0 + cy.get('[data-fieldname="doc_id"]') + .find(".awesomplete") + .find("li") + .its("length") + .should("be.gte", 0); + cy.get(".btn-modal-close").click({ force: true, multiple: true }); + }); + + it("Creating a dynamic link and verifying it", () => { + cy.visit("/app/test-dynamic-link"); + + //Clicking on the Document ID field + cy.get_field("doc_type").clear(); + + //Entering User in the Doctype field + cy.fill_field("doc_type", "User", "Link", { delay: 500 }); + cy.get_field("doc_id").click(); + + //Checking if the listbox have length greater than 0 + cy.get('[data-fieldname="doc_id"]') + .find(".awesomplete") + .find("li") + .its("length") + .should("be.gte", 0); + + //Opening a new form for dynamic link doctype + cy.new_form("Test Dynamic Link"); + cy.get_field("doc_type").clear(); + + //Entering User in the Doctype field + cy.fill_field("doc_type", "User", "Link", { delay: 500 }); + cy.get_field("doc_id").click(); + + //Checking if the listbox have length greater than 0 + cy.get('[data-fieldname="doc_id"]') + .find(".awesomplete") + .find("li") + .its("length") + .should("be.gte", 0); + cy.get_field("doc_type").clear(); + + //Entering System Settings in the Doctype field + cy.intercept("/api/method/influxframework.desk.search.search_link").as("search_query"); + cy.fill_field("doc_type", "System Settings", "Link", { delay: 500 }); + cy.wait("@search_query"); + cy.get(`[data-fieldname="doc_type"] ul:visible li:first-child`).click({ + scrollBehavior: false, + }); + + cy.get_field("doc_id").click(); + + //Checking if the system throws error + cy.get(".modal-title").should("have.text", "Error"); + cy.get(".msgprint").should( + "have.text", + "System Settings is not a valid DocType for Dynamic Link" + ); + }); +}); diff --git a/cypress/integration/control_float.js b/cypress/integration/control_float.js new file mode 100644 index 0000000..8cef1b8 --- /dev/null +++ b/cypress/integration/control_float.js @@ -0,0 +1,88 @@ +context("Control Float", () => { + before(() => { + cy.login(); + cy.visit("/app/website"); + }); + + function get_dialog_with_float() { + return cy.dialog({ + title: "Float Check", + fields: [ + { + fieldname: "float_number", + fieldtype: "Float", + Label: "Float", + }, + ], + }); + } + + it("check value changes", () => { + get_dialog_with_float().as("dialog"); + + let data = get_data(); + data.forEach((x) => { + cy.window() + .its("influxframework") + .then((influxframework) => { + influxframework.boot.sysdefaults.number_format = x.number_format; + }); + x.values.forEach((d) => { + cy.get_field("float_number", "Float").clear(); + cy.wait(200); + cy.fill_field("float_number", d.input, "Float").blur(); + cy.get_field("float_number", "Float").should("have.value", d.blur_expected); + + cy.get_field("float_number", "Float").focus(); + cy.get_field("float_number", "Float").blur(); + cy.get_field("float_number", "Float").focus(); + cy.get_field("float_number", "Float").should("have.value", d.focus_expected); + }); + }); + }); + + function get_data() { + return [ + { + number_format: "#.###,##", + values: [ + { + input: "364.87,334", + blur_expected: "36.487,334", + focus_expected: "36487.334", + }, + { + input: "36487,334", + blur_expected: "36.487,334", + focus_expected: "36487.334", + }, + { + input: "100", + blur_expected: "100,000", + focus_expected: "100", + }, + ], + }, + { + number_format: "#,###.##", + values: [ + { + input: "364,87.334", + blur_expected: "36,487.334", + focus_expected: "36487.334", + }, + { + input: "36487.334", + blur_expected: "36,487.334", + focus_expected: "36487.334", + }, + { + input: "100", + blur_expected: "100.000", + focus_expected: "100", + }, + ], + }, + ]; + } +}); diff --git a/cypress/integration/control_icon.js b/cypress/integration/control_icon.js new file mode 100644 index 0000000..9676a10 --- /dev/null +++ b/cypress/integration/control_icon.js @@ -0,0 +1,55 @@ +context("Control Icon", () => { + before(() => { + cy.login(); + cy.visit("/app/website"); + }); + + function get_dialog_with_icon() { + return cy.dialog({ + title: "Icon", + fields: [ + { + label: "Icon", + fieldname: "icon", + fieldtype: "Icon", + }, + ], + }); + } + + it("should set icon", () => { + get_dialog_with_icon().as("dialog"); + cy.get(".influxframework-control[data-fieldname=icon]").findByRole("textbox").click(); + + cy.get(".icon-picker .icon-wrapper[id=heart-active]").first().click(); + cy.get(".influxframework-control[data-fieldname=icon]") + .findByRole("textbox") + .should("have.value", "heart-active"); + cy.get("@dialog").then((dialog) => { + let value = dialog.get_value("icon"); + expect(value).to.equal("heart-active"); + }); + + cy.get(".icon-picker .icon-wrapper[id=heart]").first().click(); + cy.get(".influxframework-control[data-fieldname=icon]") + .findByRole("textbox") + .should("have.value", "heart"); + cy.get("@dialog").then((dialog) => { + let value = dialog.get_value("icon"); + expect(value).to.equal("heart"); + }); + }); + + it("search for icon and clear search input", () => { + let search_text = "ed"; + cy.get(".icon-picker").findByRole("searchbox").click().type(search_text); + cy.get(".icon-section .icon-wrapper:not(.hidden)").then((i) => { + cy.get(`.icon-section .icon-wrapper[id*='${search_text}']`).then((icons) => { + expect(i.length).to.equal(icons.length); + }); + }); + + cy.get(".icon-picker").findByRole("searchbox").clear().blur(); + cy.get(".icon-section .icon-wrapper").should("not.have.class", "hidden"); + }); +}); diff --git a/cypress/integration/control_link.js b/cypress/integration/control_link.js new file mode 100644 index 0000000..76e4641 --- /dev/null +++ b/cypress/integration/control_link.js @@ -0,0 +1,339 @@ +context("Control Link", () => { + before(() => { + cy.login(); + cy.visit("/app/website"); + }); + + beforeEach(() => { + cy.visit("/app/website"); + cy.create_records({ + doctype: "ToDo", + description: "this is a test todo for link", + }).as("todos"); + }); + + function get_dialog_with_link() { + return cy.dialog({ + title: "Link", + fields: [ + { + label: "Select ToDo", + fieldname: "link", + fieldtype: "Link", + options: "ToDo", + }, + ], + }); + } + + function get_dialog_with_gender_link() { + return cy.dialog({ + title: "Link", + fields: [ + { + label: "Select Gender", + fieldname: "link", + fieldtype: "Link", + options: "Gender", + }, + ], + }); + } + + it("should set the valid value", () => { + get_dialog_with_link().as("dialog"); + + cy.insert_doc( + "Property Setter", + { + doctype: "Property Setter", + doc_type: "ToDo", + property: "show_title_field_in_link", + property_type: "Check", + doctype_or_field: "DocType", + value: "0", + }, + true + ); + + cy.intercept("POST", "/api/method/influxframework.desk.search.search_link").as("search_link"); + + cy.get(".influxframework-control[data-fieldname=link] input").focus().as("input"); + cy.wait("@search_link"); + cy.get("@input").type("todo for link", { delay: 200 }); + cy.wait("@search_link"); + cy.get(".influxframework-control[data-fieldname=link]").findByRole("listbox").should("be.visible"); + cy.get(".influxframework-control[data-fieldname=link] input").type("{enter}", { delay: 100 }); + cy.get(".influxframework-control[data-fieldname=link] input").blur(); + cy.get("@dialog").then((dialog) => { + cy.get("@todos").then((todos) => { + let value = dialog.get_value("link"); + expect(value).to.eq(todos[0]); + }); + }); + }); + + it("should unset invalid value", () => { + get_dialog_with_link().as("dialog"); + + cy.intercept("POST", "/api/method/influxframework.client.validate_link").as("validate_link"); + + cy.get(".influxframework-control[data-fieldname=link] input") + .type("invalid value", { delay: 100 }) + .blur(); + cy.wait("@validate_link"); + cy.get(".influxframework-control[data-fieldname=link] input").should("have.value", ""); + }); + + it("should be possible set empty value explicitly", () => { + get_dialog_with_link().as("dialog"); + + cy.intercept("POST", "/api/method/influxframework.client.validate_link").as("validate_link"); + + cy.get(".influxframework-control[data-fieldname=link] input").type(" ", { delay: 100 }).blur(); + cy.wait("@validate_link"); + cy.get(".influxframework-control[data-fieldname=link] input").should("have.value", ""); + cy.window() + .its("cur_dialog") + .then((dialog) => { + expect(dialog.get_value("link")).to.equal(""); + }); + }); + + it("should route to form on arrow click", () => { + get_dialog_with_link().as("dialog"); + + cy.intercept("POST", "/api/method/influxframework.client.validate_link").as("validate_link"); + cy.intercept("POST", "/api/method/influxframework.desk.search.search_link").as("search_link"); + + cy.get("@todos").then((todos) => { + cy.get(".influxframework-control[data-fieldname=link] input").as("input"); + cy.get("@input").focus(); + cy.wait("@search_link"); + cy.get("@input").type(todos[0]).blur(); + cy.wait("@validate_link"); + cy.get("@input").focus(); + cy.wait(500); // wait for arrow to show + cy.get(".influxframework-control[data-fieldname=link] .btn-open").should("be.visible").click(); + cy.location("pathname").should("eq", `/app/todo/${todos[0]}`); + }); + }); + + it("show title field in link", () => { + cy.insert_doc( + "Property Setter", + { + doctype: "Property Setter", + doc_type: "ToDo", + property: "show_title_field_in_link", + property_type: "Check", + doctype_or_field: "DocType", + value: "1", + }, + true + ); + + cy.clear_cache(); + cy.wait(500); + + get_dialog_with_link().as("dialog"); + cy.window() + .its("influxframework") + .then((influxframework) => { + if (!influxframework.boot) { + influxframework.boot = { + link_title_doctypes: ["ToDo"], + }; + } else { + influxframework.boot.link_title_doctypes = ["ToDo"]; + } + }); + + cy.intercept("POST", "/api/method/influxframework.desk.search.search_link").as("search_link"); + + cy.get(".influxframework-control[data-fieldname=link] input").focus().as("input"); + cy.wait("@search_link"); + cy.get("@input").type("todo for link"); + cy.wait("@search_link"); + cy.get(".influxframework-control[data-fieldname=link] ul").should("be.visible"); + cy.get(".influxframework-control[data-fieldname=link] input").type("{enter}", { delay: 100 }); + cy.get(".influxframework-control[data-fieldname=link] input").blur(); + cy.get("@dialog").then((dialog) => { + cy.get("@todos").then((todos) => { + let field = dialog.get_field("link"); + let value = field.get_value(); + let label = field.get_label_value(); + + expect(value).to.eq(todos[0]); + expect(label).to.eq("this is a test todo for link"); + }); + }); + }); + + it("should update dependant fields (via fetch_from)", () => { + cy.get("@todos").then((todos) => { + cy.visit(`/app/todo/${todos[0]}`); + cy.intercept("POST", "/api/method/influxframework.desk.search.search_link").as("search_link"); + cy.intercept("POST", "/api/method/influxframework.client.validate_link").as("validate_link"); + + cy.get(".influxframework-control[data-fieldname=assigned_by] input").focus().as("input"); + cy.get("@input").type(cy.config("testUser"), { delay: 100 }).blur(); + cy.wait("@validate_link"); + cy.get(".influxframework-control[data-fieldname=assigned_by_full_name] .control-value").should( + "contain", + "InfluxFramework" + ); + + cy.window().its("cur_frm.doc.assigned_by").should("eq", cy.config("testUser")); + + // invalid input + cy.get("@input").clear().type("invalid input", { delay: 100 }).blur(); + cy.get(".influxframework-control[data-fieldname=assigned_by_full_name] .control-value").should( + "contain", + "" + ); + + cy.window().its("cur_frm.doc.assigned_by").should("eq", null); + + // set valid value again + cy.get("@input").clear().focus(); + cy.wait("@search_link"); + cy.get("@input").type(cy.config("testUser"), { delay: 100 }).blur(); + cy.wait("@validate_link"); + + cy.window().its("cur_frm.doc.assigned_by").should("eq", cy.config("testUser")); + + // clear input + cy.get("@input").clear().blur(); + cy.get(".influxframework-control[data-fieldname=assigned_by_full_name] .control-value").should( + "contain", + "" + ); + + cy.window().its("cur_frm.doc.assigned_by").should("eq", ""); + }); + }); + + it("should set default values", () => { + cy.insert_doc( + "Property Setter", + { + doctype_or_field: "DocField", + doc_type: "ToDo", + field_name: "assigned_by", + property: "default", + property_type: "Text", + value: "Administrator", + }, + true + ); + cy.reload(); + cy.new_form("ToDo"); + cy.fill_field("description", "new", "Text Editor"); + cy.intercept("POST", "/api/method/influxframework.desk.form.save.savedocs").as("save_form"); + cy.findByRole("button", { name: "Save" }).click(); + cy.wait("@save_form"); + cy.get(".influxframework-control[data-fieldname=assigned_by_full_name] .control-value").should( + "contain", + "Administrator" + ); + // if user clears default value explicitly, system should not reset default again + cy.get_field("assigned_by").clear().blur(); + cy.intercept("POST", "/api/method/influxframework.desk.form.save.savedocs").as("save_form"); + cy.findByRole("button", { name: "Save" }).click(); + cy.wait("@save_form"); + cy.get_field("assigned_by").should("have.value", ""); + cy.get(".influxframework-control[data-fieldname=assigned_by_full_name] .control-value").should( + "contain", + "" + ); + }); + + it("show translated text for Gender link field with language de with input in de", () => { + cy.call("influxframework.tests.ui_test_helpers.insert_translations").then(() => { + cy.window() + .its("influxframework") + .then((influxframework) => { + cy.set_value("User", influxframework.user.name, { language: "de" }); + }); + + cy.clear_cache(); + cy.wait(500); + + get_dialog_with_gender_link().as("dialog"); + cy.intercept("POST", "/api/method/influxframework.desk.search.search_link").as("search_link"); + + cy.get(".influxframework-control[data-fieldname=link] input").focus().as("input"); + cy.wait("@search_link"); + cy.get("@input").type("Sonstiges", { delay: 100 }); + cy.wait("@search_link"); + cy.get(".influxframework-control[data-fieldname=link] ul").should("be.visible"); + cy.get(".influxframework-control[data-fieldname=link] input").type("{enter}", { delay: 100 }); + cy.get(".influxframework-control[data-fieldname=link] input").blur(); + cy.get("@dialog").then((dialog) => { + let field = dialog.get_field("link"); + let value = field.get_value(); + let label = field.get_label_value(); + + expect(value).to.eq("Other"); + expect(label).to.eq("Sonstiges"); + }); + }); + }); + + it("show text for Gender link field with language en", () => { + cy.window() + .its("influxframework") + .then((influxframework) => { + cy.set_value("User", influxframework.user.name, { language: "en" }); + }); + + cy.clear_cache(); + cy.wait(500); + + get_dialog_with_gender_link().as("dialog"); + cy.intercept("POST", "/api/method/influxframework.desk.search.search_link").as("search_link"); + + cy.get(".influxframework-control[data-fieldname=link] input").focus().as("input"); + cy.wait("@search_link"); + cy.get("@input").type("Non-Conforming", { delay: 100 }); + cy.wait("@search_link"); + cy.get(".influxframework-control[data-fieldname=link] ul").should("be.visible"); + cy.get(".influxframework-control[data-fieldname=link] input").type("{enter}", { delay: 100 }); + cy.get(".influxframework-control[data-fieldname=link] input").blur(); + cy.get("@dialog").then((dialog) => { + let field = dialog.get_field("link"); + let value = field.get_value(); + let label = field.get_label_value(); + + expect(value).to.eq("Non-Conforming"); + expect(label).to.eq("Non-Conforming"); + }); + }); + + it("show custom link option", () => { + cy.window() + .its("influxframework") + .then((influxframework) => { + influxframework.ui.form.ControlLink.link_options = (link) => { + return [ + { + html: + "" + + " " + + "Custom Link Option" + + "", + label: "Custom Link Option", + value: "custom__link_option", + action: () => {}, + }, + ]; + }; + + get_dialog_with_link().as("dialog"); + cy.get(".influxframework-control[data-fieldname=link] input").focus().as("input"); + cy.get("@input").type("custom", { delay: 100 }); + cy.get(".custom-link-option").should("be.visible"); + }); + }); +}); diff --git a/cypress/integration/control_markdown_editor.js b/cypress/integration/control_markdown_editor.js new file mode 100644 index 0000000..24ab32f --- /dev/null +++ b/cypress/integration/control_markdown_editor.js @@ -0,0 +1,22 @@ +context("Control Markdown Editor", () => { + before(() => { + cy.login(); + cy.visit("/app"); + }); + + it("should allow inserting images by drag and drop", () => { + cy.visit("/app/web-page/new"); + cy.fill_field("content_type", "Markdown", "Select"); + cy.get_field("main_section_md", "Markdown Editor").selectFile( + "cypress/fixtures/sample_image.jpg", + { + action: "drag-drop", + } + ); + cy.click_modal_primary_button("Upload"); + cy.get_field("main_section_md", "Markdown Editor").should( + "contain", + "![](/private/files/sample_image" + ); + }); +}); diff --git a/cypress/integration/control_phone.js b/cypress/integration/control_phone.js new file mode 100644 index 0000000..7885bc1 --- /dev/null +++ b/cypress/integration/control_phone.js @@ -0,0 +1,92 @@ +import doctype_with_phone from "../fixtures/doctype_with_phone"; + +context("Control Phone", () => { + before(() => { + cy.login(); + cy.visit("/app/website"); + }); + + function get_dialog_with_phone() { + return cy.dialog({ + title: "Phone", + fields: [ + { + fieldname: "phone", + fieldtype: "Phone", + }, + ], + }); + } + + it("should set flag and data", () => { + get_dialog_with_phone().as("dialog"); + cy.get(".selected-phone").click(); + cy.get(".phone-picker .phone-wrapper[id='afghanistan']").click(); + cy.get(".selected-phone").click(); + cy.get(".phone-picker .phone-wrapper[id='india']").click(); + cy.get(".selected-phone .country").should("have.text", "+91"); + cy.get(".selected-phone > img").should("have.attr", "src").and("include", "/in.svg"); + + let phone_number = "9312672712"; + cy.get(".selected-phone > img").click().first(); + cy.get_field("phone").first().click({ multiple: true }); + cy.get(".influxframework-control[data-fieldname=phone]") + .findByRole("textbox") + .first() + .type(phone_number, { force: true }); + + cy.get_field("phone").first().should("have.value", phone_number); + cy.get_field("phone").first().blur({ force: true }); + cy.wait(100); + cy.get("@dialog").then((dialog) => { + let value = dialog.get_value("phone"); + expect(value).to.equal("+91-" + phone_number); + }); + }); + + it("case insensitive search for country and clear search", () => { + let search_text = "india"; + cy.get(".selected-phone").click().first(); + cy.get(".phone-picker").findByRole("searchbox").click().type(search_text); + cy.get(".phone-section .phone-wrapper:not(.hidden)").then((i) => { + cy.get(`.phone-section .phone-wrapper[id*="${search_text.toLowerCase()}"]`).then( + (countries) => { + expect(i.length).to.equal(countries.length); + } + ); + }); + + cy.get(".phone-picker").findByRole("searchbox").clear().blur(); + cy.get(".phone-section .phone-wrapper").should("not.have.class", "hidden"); + }); + + it("existing document should render phone field with data", () => { + cy.visit("/app/doctype"); + cy.insert_doc("DocType", doctype_with_phone, true); + cy.clear_cache(); + + // Creating custom doctype + cy.insert_doc("DocType", doctype_with_phone, true); + cy.visit("/app/doctype-with-phone"); + cy.click_listview_primary_button("Add Doctype With Phone"); + + // create a record + cy.fill_field("title", "Test Phone 1"); + cy.fill_field("phone", "+91-9823341234"); + cy.get_field("phone").should("have.value", "9823341234"); + cy.click_doc_primary_button("Save"); + cy.get_doc("Doctype With Phone", "Test Phone 1").then((doc) => { + let value = doc.data.phone; + expect(value).to.equal("+91-9823341234"); + }); + + // open the doc from list view + cy.go_to_list("Doctype With Phone"); + cy.clear_cache(); + cy.click_listview_row_item(0); + cy.title().should("eq", "Test Phone 1"); + cy.get(".selected-phone .country").should("have.text", "+91"); + cy.get(".selected-phone > img").should("have.attr", "src").and("include", "/in.svg"); + cy.get_field("phone").should("have.value", "9823341234"); + }); +}); diff --git a/cypress/integration/control_rating.js b/cypress/integration/control_rating.js new file mode 100644 index 0000000..613a6e9 --- /dev/null +++ b/cypress/integration/control_rating.js @@ -0,0 +1,54 @@ +context("Control Rating", () => { + before(() => { + cy.login(); + cy.visit("/app/website"); + }); + + function get_dialog_with_rating() { + return cy.dialog({ + title: "Rating", + fields: [ + { + fieldname: "rate", + fieldtype: "Rating", + options: 7, + }, + ], + }); + } + + it("click on the star rating to record value", () => { + get_dialog_with_rating().as("dialog"); + + cy.get("div.rating") + .children("svg") + .find(".right-half") + .first() + .click() + .should("have.class", "star-click"); + cy.get("@dialog").then((dialog) => { + var value = dialog.get_value("rate"); + expect(value).to.equal(1 / 7); + dialog.hide(); + }); + }); + + it("hover on the star", () => { + get_dialog_with_rating(); + + cy.get("div.rating") + .children("svg") + .find(".right-half") + .first() + .invoke("trigger", "mouseenter") + .should("have.class", "star-hover") + .invoke("trigger", "mouseleave") + .should("not.have.class", "star-hover"); + }); + + it("check number of stars in rating", () => { + get_dialog_with_rating(); + + cy.get("div.rating").first().children("svg").should("have.length", 7); + }); +}); diff --git a/cypress/integration/control_select.js b/cypress/integration/control_select.js new file mode 100644 index 0000000..bd90f6f --- /dev/null +++ b/cypress/integration/control_select.js @@ -0,0 +1,41 @@ +context("Control Select", () => { + before(() => { + cy.login(); + cy.visit("/app/website"); + }); + + function get_dialog_with_select() { + return cy.dialog({ + title: "Select", + fields: [ + { + fieldname: "select_control", + fieldtype: "Select", + placeholder: "Select an Option", + options: ["", "Option 1", "Option 2", "Option 2"], + }, + ], + }); + } + + it("toggles placholder on clicking an option", () => { + get_dialog_with_select().as("dialog"); + + cy.get(".influxframework-control[data-fieldname=select_control] .control-input").as("control"); + cy.get(".influxframework-control[data-fieldname=select_control] .control-input select").as( + "select" + ); + cy.get("@control").get(".select-icon").should("exist"); + cy.get("@control").get(".placeholder").should("have.css", "display", "block"); + cy.get("@select").select("Option 1"); + cy.findByDisplayValue("Option 1").should("exist"); + cy.get("@control").get(".placeholder").should("have.css", "display", "none"); + cy.get("@select").invoke("val", ""); + cy.findByDisplayValue("Option 1").should("not.exist"); + cy.get("@control").get(".placeholder").should("have.css", "display", "block"); + + cy.get("@dialog").then((dialog) => { + dialog.hide(); + }); + }); +}); diff --git a/cypress/integration/custom_buttons.js b/cypress/integration/custom_buttons.js new file mode 100644 index 0000000..ddbd197 --- /dev/null +++ b/cypress/integration/custom_buttons.js @@ -0,0 +1,57 @@ +const test_button_names = [ + "Metallica", + "Pink Floyd", + "Porcupine Tree (the GOAT)", + "AC / DC", + `Electronic Dance "music"`, + "l'imperatrice", +]; + +const add_button = (label, group = "TestGroup") => { + cy.window() + .its("cur_frm") + .then((frm) => { + frm.add_custom_button(label, () => {}, group); + }); +}; + +const check_button_count = (label, group = "TestGroup") => { + // Verify main buttons + cy.findByRole("button", { name: group }).click(); + cy.get(`[data-label="${encodeURIComponent(label)}"]`) + .should("have.length", 1) + .should("be.visible"); + + // Verify dropdown buttons in mobile view + cy.viewport(420, 900); + const dropdown_btn_label = `${group} > ${label}`; + cy.get(".menu-btn-group > .btn").click(); + cy.get(`[data-label="${encodeURIComponent(dropdown_btn_label)}"]`) + .should("have.length", 1) + .should("be.visible"); + + //reset viewport + cy.viewport(Cypress.config("viewportWidth"), Cypress.config("viewportHeight")); +}; + +describe( + "Custom group button behaviour on desk", + { scrollBehavior: false }, // speeds up the test + () => { + before(() => { + cy.login(); + cy.visit(`/app/note/new`); + }); + + test_button_names.forEach((button_name) => { + it(`Custom button works with name '${button_name}'`, () => { + add_button(button_name); + check_button_count(button_name); + + // duplicate button shouldn't be added + add_button(button_name); + check_button_count(button_name); + }); + }); + } +); diff --git a/cypress/integration/customize_form.js b/cypress/integration/customize_form.js new file mode 100644 index 0000000..cd03f7b --- /dev/null +++ b/cypress/integration/customize_form.js @@ -0,0 +1,23 @@ +context("Customize Form", () => { + before(() => { + cy.login(); + cy.visit("/app/customize-form"); + }); + it("Changing to naming rule should update autoname", () => { + cy.fill_field("doc_type", "ToDo", "Link").blur(); + cy.click_form_section("Naming"); + const naming_rule_default_autoname_map = { + "Set by user": "prompt", + "By fieldname": "field:", + 'By "Naming Series" field': "naming_series:", + Expression: "format:", + "Expression (old style)": "", + Random: "hash", + "By script": "", + }; + Cypress._.forOwn(naming_rule_default_autoname_map, (value, naming_rule) => { + cy.fill_field("naming_rule", naming_rule, "Select"); + cy.get_field("autoname", "Data").should("have.value", value); + }); + }); +}); diff --git a/cypress/integration/dashboard_chart.js b/cypress/integration/dashboard_chart.js new file mode 100644 index 0000000..6023a50 --- /dev/null +++ b/cypress/integration/dashboard_chart.js @@ -0,0 +1,22 @@ +context("Dashboard Chart", () => { + before(() => { + cy.login(); + cy.visit("/app/website"); + }); + + it("Check filter populate for child table doctype", () => { + cy.visit("/app/dashboard-chart/new-dashboard-chart-1"); + cy.get('[data-fieldname="parent_document_type"]').should("have.css", "display", "none"); + + cy.get_field("document_type", "Link"); + cy.fill_field("document_type", "Workspace Link", "Link").focus().blur(); + cy.get_field("document_type", "Link").should("have.value", "Workspace Link"); + + cy.fill_field("chart_name", "Test Chart", "Data"); + + cy.get('[data-fieldname="filters_json"]').click().wait(200); + cy.get(".modal-body .filter-action-buttons .add-filter").click(); + cy.get(".modal-body .fieldname-select-area").click(); + cy.get(".modal-actions .btn-modal-close").click(); + }); +}); diff --git a/cypress/integration/dashboard_links.js b/cypress/integration/dashboard_links.js new file mode 100644 index 0000000..75c1603 --- /dev/null +++ b/cypress/integration/dashboard_links.js @@ -0,0 +1,91 @@ +import doctype_with_child_table from "../fixtures/doctype_with_child_table"; +import child_table_doctype from "../fixtures/child_table_doctype"; +import child_table_doctype_1 from "../fixtures/child_table_doctype_1"; +import doctype_to_link from "../fixtures/doctype_to_link"; +const doctype_to_link_name = doctype_to_link.name; +const child_table_doctype_name = child_table_doctype.name; + +context("Dashboard links", () => { + before(() => { + cy.visit("/login"); + cy.login("Administrator"); + cy.insert_doc("DocType", child_table_doctype, true); + cy.insert_doc("DocType", child_table_doctype_1, true); + cy.insert_doc("DocType", doctype_with_child_table, true); + cy.insert_doc("DocType", doctype_to_link, true); + return cy + .window() + .its("influxframework") + .then((influxframework) => { + influxframework.call("influxframework.tests.ui_test_helpers.update_child_table", { + name: child_table_doctype_name, + }); + }); + }); + + it("Adding a new contact, checking for the counter on the dashboard and deleting the created contact", () => { + cy.visit("/app/contact"); + cy.clear_filters(); + + cy.visit(`/app/user/${cy.config("testUser")}`); + + //To check if initially the dashboard contains only the "Contact" link and there is no counter + cy.get('[data-doctype="Contact"]').should("contain", "Contact"); + + //Adding a new contact + cy.get('.document-link-badge[data-doctype="Contact"]').click(); + cy.wait(300); + cy.findByRole("button", { name: "Add Contact" }).should("be.visible"); + cy.findByRole("button", { name: "Add Contact" }).click(); + cy.get('[data-doctype="Contact"][data-fieldname="first_name"]').type("Admin"); + cy.findByRole("button", { name: "Save" }).click(); + cy.visit(`/app/user/${cy.config("testUser")}`); + + //To check if the counter for contact doc is "2" after adding additional contact + cy.get('[data-doctype="Contact"] > .count').should("contain", "2"); + cy.get('[data-doctype="Contact"]').contains("Contact").click(); + + //Deleting the newly created contact + cy.visit("/app/contact"); + cy.get(".list-subject > .select-like > .list-row-checkbox").eq(0).click({ force: true }); + cy.findByRole("button", { name: "Actions" }).click(); + cy.get('.actions-btn-group [data-label="Delete"]').click(); + cy.findByRole("button", { name: "Yes" }).click({ delay: 700 }); + + //To check if the counter from the "Contact" doc link is removed + cy.wait(700); + cy.visit("/app/user"); + cy.get(".list-row-col > .level-item > .ellipsis").eq(0).click({ force: true }); + cy.get('[data-doctype="Contact"]').should("contain", "Contact"); + }); + + it("Report link in dashboard", () => { + cy.visit(`/app/user/${cy.config("testUser")}`); + cy.get('[data-doctype="Contact"]').should("contain", "Contact"); + cy.findByText("Connections"); + cy.window() + .its("cur_frm") + .then((cur_frm) => { + cur_frm.dashboard.data.reports = [ + { + label: "Reports", + items: ["Website Analytics"], + }, + ]; + cur_frm.dashboard.render_report_links(); + cy.get('[data-report="Website Analytics"]').contains("Website Analytics").click(); + cy.findByText("Website Analytics"); + }); + }); + + it("check if child table is populated with linked field on creation from dashboard link", () => { + cy.new_form(doctype_to_link_name); + cy.fill_field("title", "Test Linking"); + cy.findByRole("button", { name: "Save" }).click(); + + cy.get(".document-link .btn-new").click(); + cy.get( + '.influxframework-control[data-fieldname="child_table"] .rows .data-row .col[data-fieldname="doctype_to_link"]' + ).should("contain.text", "Test Linking"); + }); +}); diff --git a/cypress/integration/data_field_form_validation.js b/cypress/integration/data_field_form_validation.js new file mode 100644 index 0000000..428352a --- /dev/null +++ b/cypress/integration/data_field_form_validation.js @@ -0,0 +1,45 @@ +import data_field_validation_doctype from "../fixtures/data_field_validation_doctype"; +const doctype_name = data_field_validation_doctype.name; + +context("Data Field Input Validation in New Form", () => { + before(() => { + cy.login(); + cy.visit("/app/website"); + return cy.insert_doc("DocType", data_field_validation_doctype, true); + }); + + function validateField(fieldname, invalid_value, valid_value) { + // Invalid, should have has-error class + cy.get_field(fieldname).clear().type(invalid_value).blur(); + cy.get(`.influxframework-control[data-fieldname="${fieldname}"]`).should("have.class", "has-error"); + // Valid value, should not have has-error class + cy.get_field(fieldname).clear().type(valid_value); + cy.get(`.influxframework-control[data-fieldname="${fieldname}"]`).should( + "not.have.class", + "has-error" + ); + } + + describe("Data Field Options", () => { + it("should validate email address", () => { + cy.new_form(doctype_name); + validateField("email", "captian", "hello@test.com"); + }); + + it("should validate URL", () => { + validateField("url", "jkl", "https://influxframework.io"); + validateField("url", "abcd.com", "http://google.com/home"); + validateField("url", "&&http://google.uae", "gopher://influxframework.io"); + validateField("url", "ftt2:://google.in?q=news", "ftps2://influxframework.io/__/#home"); + validateField("url", "ftt2://", "ntps://localhost"); // For intranet URLs + }); + + it("should validate phone number", () => { + validateField("phone", "america", "89787878"); + }); + + it("should validate name", () => { + validateField("person_name", " 777Hello", "James Bond"); + }); + }); +}); diff --git a/cypress/integration/datetime.js b/cypress/integration/datetime.js new file mode 100644 index 0000000..222079e --- /dev/null +++ b/cypress/integration/datetime.js @@ -0,0 +1,126 @@ +import datetime_doctype from "../fixtures/datetime_doctype"; +const doctype_name = datetime_doctype.name; + +context("Control Date, Time and DateTime", () => { + before(() => { + cy.login(); + cy.visit("/app/website"); + return cy.insert_doc("DocType", datetime_doctype, true); + }); + + describe("Date formats", () => { + let date_formats = [ + { + date_format: "dd-mm-yyyy", + part: 2, + length: 4, + separator: "-", + }, + { + date_format: "mm/dd/yyyy", + part: 0, + length: 2, + separator: "/", + }, + ]; + + date_formats.forEach((d) => { + it("test date format " + d.date_format, () => { + cy.set_value("System Settings", "System Settings", { + date_format: d.date_format, + }); + cy.window() + .its("influxframework") + .then((influxframework) => { + // update sys_defaults value to avoid a reload + influxframework.sys_defaults.date_format = d.date_format; + }); + + cy.new_form(doctype_name); + cy.get(".form-control[data-fieldname=date]").focus(); + cy.get(".datepickers-container .datepicker.active").should("be.visible"); + cy.get( + ".datepickers-container .datepicker.active .datepicker--cell-day.-current-" + ).click({ force: true }); + + cy.window() + .its("cur_frm") + .then((cur_frm) => { + let formatted_value = cur_frm.get_field("date").input.value; + let parts = formatted_value.split(d.separator); + expect(parts[d.part].length).to.equal(d.length); + }); + }); + }); + }); + + describe("Time formats", () => { + let time_formats = [ + { + time_format: "HH:mm:ss", + value: " 11:00:12", + match_value: "11:00:12", + }, + { + time_format: "HH:mm", + value: " 11:00:12", + match_value: "11:00", + }, + ]; + + time_formats.forEach((d) => { + it("test time format " + d.time_format, () => { + cy.set_value("System Settings", "System Settings", { + time_format: d.time_format, + }); + cy.window() + .its("influxframework") + .then((influxframework) => { + influxframework.sys_defaults.time_format = d.time_format; + }); + cy.new_form(doctype_name); + cy.fill_field("time", d.value, "Time").blur(); + cy.get_field("time").should("have.value", d.match_value); + }); + }); + }); + + describe("DateTime formats", () => { + let datetime_formats = [ + { + date_format: "dd.mm.yyyy", + time_format: "HH:mm:ss", + value: " 02.12.2019 11:00:12", + doc_value: "2019-12-02 00:30:12", // system timezone (America/New_York) + input_value: "02.12.2019 11:00:12", // admin timezone (Asia/Kolkata) + }, + { + date_format: "mm-dd-yyyy", + time_format: "HH:mm", + value: " 12-02-2019 11:00:00", + doc_value: "2019-12-02 00:30:00", // system timezone (America/New_York) + input_value: "12-02-2019 11:00", // admin timezone (Asia/Kolkata) + }, + ]; + + datetime_formats.forEach((d) => { + it(`test datetime format ${d.date_format} ${d.time_format}`, () => { + cy.set_value("System Settings", "System Settings", { + date_format: d.date_format, + time_format: d.time_format, + }); + cy.window() + .its("influxframework") + .then((influxframework) => { + influxframework.sys_defaults.date_format = d.date_format; + influxframework.sys_defaults.time_format = d.time_format; + }); + cy.new_form(doctype_name); + cy.fill_field("datetime", d.value, "Datetime").blur(); + cy.get_field("datetime").should("have.value", d.input_value); + + cy.window().its("cur_frm.doc.datetime").should("eq", d.doc_value); + }); + }); + }); +}); diff --git a/cypress/integration/datetime_field_form_validation.js b/cypress/integration/datetime_field_form_validation.js new file mode 100644 index 0000000..dc07cc9 --- /dev/null +++ b/cypress/integration/datetime_field_form_validation.js @@ -0,0 +1,19 @@ +// TODO: Enable this again +// currently this is flaky possibly because of different timezone in CI + +// context('Datetime Field Validation', () => { +// before(() => { +// cy.login(); +// cy.visit('/app/communication'); +// }); + +// it('datetime field form validation', () => { +// // validating datetime field value when value is set from backend and get validated on form load. +// cy.window().its('influxframework').then(influxframework => { +// return influxframework.xcall("influxframework.tests.ui_test_helpers.create_communication_record"); +// }).then(doc => { +// cy.visit(`/app/communication/${doc.name}`); +// cy.get('.indicator-pill').should('contain', 'Open').should('have.class', 'red'); +// }); +// }); +// }); diff --git a/cypress/integration/depends_on.js b/cypress/integration/depends_on.js new file mode 100644 index 0000000..7e87a7d --- /dev/null +++ b/cypress/integration/depends_on.js @@ -0,0 +1,152 @@ +context("Depends On", () => { + before(() => { + cy.login(); + cy.visit("/app/website"); + return cy + .window() + .its("influxframework") + .then((influxframework) => { + return influxframework.xcall("influxframework.tests.ui_test_helpers.create_child_doctype", { + name: "Child Test Depends On", + fields: [ + { + label: "Child Test Field", + fieldname: "child_test_field", + fieldtype: "Data", + in_list_view: 1, + }, + { + label: "Child Dependant Field", + fieldname: "child_dependant_field", + fieldtype: "Data", + in_list_view: 1, + }, + { + label: "Child Display Dependant Field", + fieldname: "child_display_dependant_field", + fieldtype: "Data", + in_list_view: 1, + }, + ], + }); + }) + .then((influxframework) => { + return influxframework.xcall("influxframework.tests.ui_test_helpers.create_doctype", { + name: "Test Depends On", + fields: [ + { + label: "Test Field", + fieldname: "test_field", + fieldtype: "Data", + }, + { + label: "Dependant Field", + fieldname: "dependant_field", + fieldtype: "Data", + mandatory_depends_on: "eval:doc.test_field=='Some Value'", + read_only_depends_on: "eval:doc.test_field=='Some Other Value'", + }, + { + label: "Display Dependant Field", + fieldname: "display_dependant_field", + fieldtype: "Data", + depends_on: "eval:doc.test_field=='Value'", + }, + { + label: "Child Test Depends On Field", + fieldname: "child_test_depends_on_field", + fieldtype: "Table", + read_only_depends_on: "eval:doc.test_field=='Some Other Value'", + options: "Child Test Depends On", + }, + { + label: "Dependent Tab", + fieldname: "dependent_tab", + fieldtype: "Tab Break", + depends_on: "eval:doc.test_field=='Show Tab'", + }, + { + fieldname: "tab_section", + fieldtype: "Section Break", + }, + { + label: "Field in Tab", + fieldname: "field_in_tab", + fieldtype: "Data", + }, + ], + }); + }); + }); + it("should show the tab on other setting field value", () => { + cy.new_form("Test Depends On"); + cy.fill_field("test_field", "Show Tab"); + cy.get("body").click(); + cy.findByRole("tab", { name: "Dependent Tab" }).should("be.visible"); + }); + it("should set the field as mandatory depending on other fields value", () => { + cy.new_form("Test Depends On"); + cy.fill_field("test_field", "Some Value"); + cy.findByRole("button", { name: "Save" }).click(); + cy.get(".msgprint-dialog .modal-title").contains("Missing Fields").should("be.visible"); + cy.hide_dialog(); + cy.fill_field("test_field", "Random value"); + cy.findByRole("button", { name: "Save" }).click(); + cy.get(".msgprint-dialog .modal-title") + .contains("Missing Fields") + .should("not.be.visible"); + }); + it("should set the field as read only depending on other fields value", () => { + cy.new_form("Test Depends On"); + cy.fill_field("dependant_field", "Some Value"); + cy.fill_field("test_field", "Some Other Value"); + cy.get("body").click(); + cy.get('.control-input [data-fieldname="dependant_field"]').should("be.disabled"); + cy.fill_field("test_field", "Random Value"); + cy.get("body").click(); + cy.get('.control-input [data-fieldname="dependant_field"]').should("not.be.disabled"); + }); + it("should set the table and its fields as read only depending on other fields value", () => { + cy.new_form("Test Depends On"); + cy.fill_field("dependant_field", "Some Value"); + //cy.fill_field('test_field', 'Some Other Value'); + cy.get('.influxframework-control[data-fieldname="child_test_depends_on_field"]').as("table"); + cy.get("@table").findByRole("button", { name: "Add Row" }).click(); + cy.get("@table").find('[data-idx="1"]').as("row1"); + cy.get("@row1").find(".btn-open-row").click(); + cy.get("@row1").find(".form-in-grid").as("row1-form_in_grid"); + //cy.get('@row1-form_in_grid').find('') + cy.fill_table_field("child_test_depends_on_field", "1", "child_test_field", "Some Value"); + cy.fill_table_field( + "child_test_depends_on_field", + "1", + "child_dependant_field", + "Some Other Value" + ); + + cy.get("@row1-form_in_grid").find(".grid-collapse-row").click(); + + // set the table to read-only + cy.fill_field("test_field", "Some Other Value"); + + // grid row form fields should be read-only + cy.get("@row1").find(".btn-open-row").click(); + + cy.get("@row1-form_in_grid") + .find('.control-input [data-fieldname="child_test_field"]') + .should("be.disabled"); + cy.get("@row1-form_in_grid") + .find('.control-input [data-fieldname="child_dependant_field"]') + .should("be.disabled"); + }); + it("should display the field depending on other fields value", () => { + cy.new_form("Test Depends On"); + cy.get('.control-input [data-fieldname="display_dependant_field"]').should( + "not.be.visible" + ); + cy.get('.control-input [data-fieldname="test_field"]').clear(); + cy.fill_field("test_field", "Value"); + cy.get("body").click(); + cy.get('.control-input [data-fieldname="display_dependant_field"]').should("be.visible"); + }); +}); diff --git a/cypress/integration/discussions.js b/cypress/integration/discussions.js new file mode 100644 index 0000000..5d92f31 --- /dev/null +++ b/cypress/integration/discussions.js @@ -0,0 +1,101 @@ +context("Discussions", () => { + before(() => { + cy.login(); + cy.visit("/app"); + return cy + .window() + .its("influxframework") + .then((influxframework) => { + return influxframework.call("influxframework.tests.ui_test_helpers.create_data_for_discussions"); + }); + }); + + const reply_through_modal = () => { + cy.visit("/test-page-discussions"); + + // Open the modal + cy.get(".reply").click(); + cy.wait(500); + cy.get(".discussion-modal").should("be.visible"); + + // Enter title + cy.get(".modal .topic-title") + .type("Discussion from tests") + .should("have.value", "Discussion from tests"); + + // Enter comment + cy.get(".modal .comment-field") + .type("This is a discussion from the cypress ui tests.") + .should("have.value", "This is a discussion from the cypress ui tests."); + + // Submit + cy.get(".modal .submit-discussion").click(); + cy.wait(2000); + + // Check if discussion is added to page and content is visible + cy.get(".sidebar-parent:first .discussion-topic-title").should( + "have.text", + "Discussion from tests" + ); + cy.get(".discussion-on-page:visible").should("have.class", "show"); + cy.get(".discussion-on-page:visible .reply-card .reply-text").should( + "have.text", + "This is a discussion from the cypress ui tests.\n" + ); + }; + + const reply_through_comment_box = () => { + cy.get(".discussion-form:visible .comment-field") + .type( + "This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page." + ) + .should( + "have.value", + "This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page." + ); + + cy.get(".discussion-form:visible .submit-discussion").click(); + cy.wait(3000); + cy.get(".discussion-on-page:visible").should("have.class", "show"); + cy.get(".discussion-on-page:visible") + .children(".reply-card") + .eq(1) + .find(".reply-text") + .should( + "have.text", + "This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page.\n" + ); + }; + + const cancel_and_clear_comment_box = () => { + cy.get(".discussion-form:visible .comment-field") + .type("This is a discussion from the cypress ui tests.") + .should("have.value", "This is a discussion from the cypress ui tests."); + + cy.get(".discussion-form:visible .cancel-comment").click(); + cy.get(".discussion-form:visible .comment-field").should("have.value", ""); + }; + + const single_thread_discussion = () => { + cy.visit("/test-single-thread"); + cy.get(".discussions-sidebar").should("have.length", 0); + cy.get(".reply").should("have.length", 0); + + cy.get(".discussion-form:visible .comment-field") + .type("This comment is being made on a single thread discussion.") + .should("have.value", "This comment is being made on a single thread discussion."); + + cy.get(".discussion-form:visible .submit-discussion").click(); + cy.wait(3000); + cy.get(".discussion-on-page") + .children(".reply-card") + .eq(-1) + .find(".reply-text") + .should("have.text", "This comment is being made on a single thread discussion.\n"); + }; + + it("reply through modal", reply_through_modal); + it("reply through comment box", reply_through_comment_box); + it("cancel and clear comment box", cancel_and_clear_comment_box); + it("single thread discussion", single_thread_discussion); +}); diff --git a/cypress/integration/file_uploader.js b/cypress/integration/file_uploader.js new file mode 100644 index 0000000..6d5c8f4 --- /dev/null +++ b/cypress/integration/file_uploader.js @@ -0,0 +1,86 @@ +context("FileUploader", () => { + before(() => { + cy.login(); + cy.visit("/app"); + }); + + function open_upload_dialog() { + cy.window() + .its("influxframework") + .then((influxframework) => { + new influxframework.ui.FileUploader(); + }); + } + + it("upload dialog api works", () => { + open_upload_dialog(); + cy.get_open_dialog().should("contain", "Drag and drop files"); + cy.hide_dialog(); + }); + + it("should accept dropped files", () => { + open_upload_dialog(); + + cy.get_open_dialog() + .find(".file-upload-area") + .selectFile("cypress/fixtures/example.json", { + action: "drag-drop", + }); + + cy.get_open_dialog().find(".file-name").should("contain", "example.json"); + cy.intercept("POST", "/api/method/upload_file").as("upload_file"); + cy.get_open_dialog().findByRole("button", { name: "Upload" }).click(); + cy.wait("@upload_file").its("response.statusCode").should("eq", 200); + cy.get(".modal:visible").should("not.exist"); + }); + + it("should accept uploaded files", () => { + open_upload_dialog(); + + cy.get_open_dialog().findByRole("button", { name: "Library" }).click(); + cy.findByPlaceholderText("Search by filename or extension").type("example.json"); + cy.get_open_dialog().findAllByText("example.json").first().click(); + cy.intercept("POST", "/api/method/upload_file").as("upload_file"); + cy.get_open_dialog().findByRole("button", { name: "Upload" }).click(); + cy.wait("@upload_file") + .its("response.body.message") + .should("have.property", "file_name", "example.json"); + cy.get(".modal:visible").should("not.exist"); + }); + + it("should accept web links", () => { + open_upload_dialog(); + + cy.get_open_dialog().findByRole("button", { name: "Link" }).click(); + cy.get_open_dialog() + .findByPlaceholderText("Attach a web link") + .type("https://github.com", { delay: 100, force: true }); + cy.intercept("POST", "/api/method/upload_file").as("upload_file"); + cy.get_open_dialog().findByRole("button", { name: "Upload" }).click(); + cy.wait("@upload_file") + .its("response.body.message") + .should("have.property", "file_url", "https://github.com"); + cy.get(".modal:visible").should("not.exist"); + }); + + it("should allow cropping and optimization for valid images", () => { + open_upload_dialog(); + + cy.get_open_dialog() + .find(".file-upload-area") + .selectFile("cypress/fixtures/sample_image.jpg", { + action: "drag-drop", + }); + + cy.get_open_dialog().findAllByText("sample_image.jpg").should("exist"); + cy.get_open_dialog().find(".btn-crop").first().click(); + cy.get_open_dialog().findByRole("button", { name: "Crop" }).click(); + cy.get_open_dialog().findAllByRole("checkbox", { name: "Optimize" }).should("exist"); + cy.get_open_dialog().findAllByLabelText("Optimize").first().click(); + + cy.intercept("POST", "/api/method/upload_file").as("upload_file"); + cy.get_open_dialog().findByRole("button", { name: "Upload" }).click(); + cy.wait("@upload_file").its("response.statusCode").should("eq", 200); + cy.get(".modal:visible").should("not.exist"); + }); +}); diff --git a/cypress/integration/first_day_of_the_week.js b/cypress/integration/first_day_of_the_week.js new file mode 100644 index 0000000..ff1b8b4 --- /dev/null +++ b/cypress/integration/first_day_of_the_week.js @@ -0,0 +1,51 @@ +context("First Day of the Week", () => { + before(() => { + cy.login(); + }); + + beforeEach(() => { + cy.visit("/app/system-settings"); + cy.findByText("Date and Number Format").click(); + }); + + it("Date control starts with same day as selected in System Settings", () => { + cy.intercept( + "POST", + "/api/method/influxframework.core.doctype.system_settings.system_settings.load" + ).as("load_settings"); + cy.fill_field("first_day_of_the_week", "Tuesday", "Select"); + cy.findByRole("button", { name: "Save" }).click(); + cy.wait("@load_settings"); + cy.dialog({ + title: "Date", + fields: [ + { + label: "Date", + fieldname: "date", + fieldtype: "Date", + }, + ], + }); + cy.get_field("date").click(); + cy.get(".datepicker--day-name").eq(0).should("have.text", "Tu"); + }); + + it("Calendar view starts with same day as selected in System Settings", () => { + cy.intercept( + "POST", + "/api/method/influxframework.core.doctype.system_settings.system_settings.load" + ).as("load_settings"); + cy.fill_field("first_day_of_the_week", "Monday", "Select"); + cy.findByRole("button", { name: "Save" }).click(); + cy.wait("@load_settings"); + cy.visit("app/todo/view/calendar/default"); + cy.get(".fc-day-header > span").eq(0).should("have.text", "Mon"); + }); + + after(() => { + cy.visit("/app/system-settings"); + cy.findByText("Date and Number Format").click(); + cy.fill_field("first_day_of_the_week", "Sunday", "Select"); + cy.findByRole("button", { name: "Save" }).click(); + }); +}); diff --git a/cypress/integration/folder_navigation.js b/cypress/integration/folder_navigation.js new file mode 100644 index 0000000..ed14b78 --- /dev/null +++ b/cypress/integration/folder_navigation.js @@ -0,0 +1,92 @@ +context("Folder Navigation", () => { + before(() => { + cy.visit("/login"); + cy.login(); + cy.visit("/app/file"); + }); + + it("Adding Folders", () => { + //Adding filter to go into the home folder + cy.get(".filter-selector > .btn").findByText("1 filter").click(); + cy.findByRole("button", { name: "Clear Filters" }).click(); + cy.get(".filter-action-buttons > .text-muted").findByText("+ Add a Filter").click(); + cy.get(".fieldname-select-area > .awesomplete > .form-control").type("Fol{enter}"); + cy.get( + ".filter-field > .form-group > .link-field > .awesomplete > .input-with-feedback" + ).type("Home{enter}"); + cy.get(".filter-action-buttons > div > .btn-primary").findByText("Apply Filters").click(); + + //Adding folder (Test Folder) + cy.click_menu_button("New Folder"); + cy.fill_field("value", "Test Folder"); + cy.click_modal_primary_button("Create"); + }); + + it("Navigating the nested folders, checking if the URL formed is correct, checking if the added content in the child folder is correct", () => { + //Navigating inside the Attachments folder + cy.wait(500); + cy.get('[title="Attachments"] > span').click(); + + //To check if the URL formed after visiting the attachments folder is correct + cy.location("pathname").should("eq", "/app/file/view/home/Attachments"); + cy.visit("/app/file/view/home/Attachments"); + + //Adding folder inside the attachments folder + cy.click_menu_button("New Folder"); + cy.fill_field("value", "Test Folder"); + cy.click_modal_primary_button("Create"); + + //Navigating inside the added folder in the Attachments folder + cy.wait(500); + cy.get('[title="Test Folder"] > span').click(); + + //To check if the URL is correct after visiting the Test Folder + cy.location("pathname").should("eq", "/app/file/view/home/Attachments/Test%20Folder"); + cy.visit("/app/file/view/home/Attachments/Test%20Folder"); + + //Adding a file inside the Test Folder + cy.findByRole("button", { name: "Add File" }).eq(0).click({ force: true }); + cy.get(".file-uploader").findByText("Link").click(); + cy.get(".input-group > .form-control").type( + "https://wallpaperplay.com/walls/full/8/2/b/72402.jpg" + ); + cy.click_modal_primary_button("Upload"); + + //To check if the added file is present in the Test Folder + cy.visit("/app/file/view/home/Attachments"); + cy.wait(500); + cy.get("span.level-item > a > span").should("contain", "Test Folder"); + cy.visit("/app/file/view/home/Attachments/Test%20Folder"); + + cy.wait(500); + cy.get(".list-row-container").eq(0).should("contain.text", "72402.jpg"); + cy.get(".list-row-checkbox").eq(0).click(); + + cy.intercept({ + method: "POST", + url: "api/method/influxframework.desk.reportview.delete_items", + }).as("file_deleted"); + + //Deleting the added file from the Test folder + cy.click_action_button("Delete"); + cy.click_modal_primary_button("Yes"); + cy.wait("@file_deleted"); + + //Deleting the Test Folder + cy.visit("/app/file/view/home/Attachments"); + cy.get(".list-row-checkbox").eq(0).click(); + cy.click_action_button("Delete"); + cy.click_modal_primary_button("Yes"); + cy.wait("@file_deleted"); + }); + + it("Deleting Test Folder from the home", () => { + //Deleting the Test Folder added in the home directory + cy.visit("/app/file/view/home"); + cy.get(".level-left > .list-subject > .file-select >.list-row-checkbox") + .eq(0) + .click({ force: true, delay: 500 }); + cy.click_action_button("Delete"); + cy.click_modal_primary_button("Yes"); + }); +}); diff --git a/cypress/integration/form.js b/cypress/integration/form.js new file mode 100644 index 0000000..95ab6e8 --- /dev/null +++ b/cypress/integration/form.js @@ -0,0 +1,166 @@ +context("Form", () => { + before(() => { + cy.login(); + cy.visit("/app/website"); + return cy + .window() + .its("influxframework") + .then((influxframework) => { + return influxframework.call("influxframework.tests.ui_test_helpers.create_contact_records"); + }); + }); + + it("create a new form", () => { + cy.visit("/app/todo/new"); + cy.get_field("description", "Text Editor") + .type("this is a test todo", { force: true }) + .wait(200); + cy.get(".page-title").should("contain", "Not Saved"); + cy.intercept({ + method: "POST", + url: "api/method/influxframework.desk.form.save.savedocs", + }).as("form_save"); + cy.get(".primary-action").click(); + cy.wait("@form_save").its("response.statusCode").should("eq", 200); + + cy.go_to_list("ToDo"); + cy.clear_filters(); + cy.get(".page-head").findByTitle("To Do").should("exist"); + cy.get(".list-row").should("contain", "this is a test todo"); + }); + + it("navigates between documents with child table list filters applied", () => { + cy.visit("/app/contact"); + + cy.clear_filters(); + cy.get('.standard-filter-section [data-fieldname="name"] input') + .type("Test Form Contact 3") + .blur(); + cy.click_listview_row_item_with_text("Test Form Contact 3"); + + cy.get("#page-Contact .page-head").findByTitle("Test Form Contact 3").should("exist"); + cy.get(".prev-doc").should("be.visible").click(); + cy.get(".msgprint-dialog .modal-body").contains("No further records").should("be.visible"); + cy.hide_dialog(); + + cy.get("#page-Contact .page-head").findByTitle("Test Form Contact 3").should("exist"); + cy.get(".next-doc").should("be.visible").click(); + cy.get(".msgprint-dialog .modal-body").contains("No further records").should("be.visible"); + cy.hide_dialog(); + + cy.get("#page-Contact .page-head").findByTitle("Test Form Contact 3").should("exist"); + + // clear filters + cy.visit("/app/contact"); + cy.clear_filters(); + }); + + it("validates behaviour of Data options validations in child table", () => { + // test email validations for set_invalid controller + let website_input = "website.in"; + let valid_email = "user@email.com"; + let expectBackgroundColor = "rgb(255, 245, 245)"; + + cy.visit("/app/contact/new"); + cy.get('.influxframework-control[data-fieldname="email_ids"]').as("table"); + cy.get("@table").find("button.grid-add-row").click(); + cy.get("@table").find("button.grid-add-row").click(); + cy.get("@table").find('[data-idx="1"]').as("row1"); + cy.get("@table").find('[data-idx="2"]').as("row2"); + cy.get("@row1").click(); + cy.get("@row1").find("input.input-with-feedback.form-control").as("email_input1"); + + cy.get("@email_input1").type(website_input, { waitForAnimations: false }); + cy.fill_field("company_name", "Test Company"); + + cy.get("@row2").click(); + cy.get("@row2").find("input.input-with-feedback.form-control").as("email_input2"); + cy.get("@email_input2").type(valid_email, { waitForAnimations: false }); + + cy.get("@row1").click(); + cy.get("@email_input1").should(($div) => { + const style = window.getComputedStyle($div[0]); + expect(style.backgroundColor).to.equal(expectBackgroundColor); + }); + cy.get("@email_input1").should("have.class", "invalid"); + + cy.get("@row2").click(); + cy.get("@email_input2").should("not.have.class", "invalid"); + }); + + it("Shows version conflict warning", { scrollBehavior: false }, () => { + cy.visit("/app/todo"); + + cy.insert_doc("ToDo", { description: "old" }).then((doc) => { + cy.visit(`/app/todo/${doc.name}`); + // make form dirty + cy.fill_field("status", "Cancelled", "Select"); + + // update doc using api - simulating parallel change by another user + cy.update_doc("ToDo", doc.name, { status: "Closed" }).then(() => { + cy.findByRole("button", { name: "Refresh" }).click(); + cy.get_field("status", "Select").should("have.value", "Closed"); + }); + }); + }); + + it("let user undo/redo field value changes", { scrollBehavior: false }, () => { + const jump_to_field = (field_label) => { + cy.get("body") + .type("{esc}") // lose focus if any + .type("{ctrl+j}") // jump to field + .type(field_label) + .wait(500) + .type("{enter}") + .wait(200) + .type("{enter}") + .wait(500); + }; + + const type_value = (value) => { + cy.focused().clear().type(value).type("{esc}"); + }; + + const undo = () => cy.get("body").type("{esc}").type("{ctrl+z}").wait(500); + const redo = () => cy.get("body").type("{esc}").type("{ctrl+y}").wait(500); + + cy.new_form("User"); + + jump_to_field("Email"); + type_value("admin@example.com"); + + jump_to_field("Username"); + type_value("admin42"); + + jump_to_field("Send Welcome Email"); + cy.focused().uncheck(); + + // make a mistake + jump_to_field("Username"); + type_value("admin24"); + + // undo behaviour + undo(); + cy.get_field("username").should("have.value", "admin42"); + + // redo behaviour + redo(); + cy.get_field("username").should("have.value", "admin24"); + + // undo everything & redo everything, ensure same values at the end + undo(); + undo(); + undo(); + undo(); + redo(); + redo(); + redo(); + redo(); + + cy.compare_document({ + username: "admin24", + email: "admin@example.com", + send_welcome_email: 0, + }); + }); +}); diff --git a/cypress/integration/form_tab_break.js b/cypress/integration/form_tab_break.js new file mode 100644 index 0000000..91695cb --- /dev/null +++ b/cypress/integration/form_tab_break.js @@ -0,0 +1,30 @@ +import doctype_with_tab_break from "../fixtures/doctype_with_tab_break"; +const doctype_name = doctype_with_tab_break.name; +context("Form Tab Break", () => { + before(() => { + cy.login(); + cy.visit("/app/website"); + return cy.insert_doc("DocType", doctype_with_tab_break, true); + }); + it("Should switch tab and open correct tabs on validation error", () => { + cy.new_form(doctype_name); + // test tab switch + cy.findByRole("tab", { name: "Tab 2" }).click(); + cy.findByText("Phone"); + cy.findByRole("tab", { name: "Details" }).click(); + cy.findByText("Name"); + + // form should switch to the tab with un-filled mandatory field + cy.fill_field("username", "Test"); + cy.findByRole("button", { name: "Save" }).click(); + cy.findByText("Missing Fields"); + cy.hide_dialog(); + cy.findByText("Phone"); + cy.fill_field("phone", "12345678"); + cy.findByRole("button", { name: "Save" }).click(); + + // After save, first tab should have dashboard + cy.get(".form-tabs > .nav-item").eq(0).click(); + cy.findByText("Connections"); + }); +}); diff --git a/cypress/integration/form_tour.js b/cypress/integration/form_tour.js new file mode 100644 index 0000000..2fb314c --- /dev/null +++ b/cypress/integration/form_tour.js @@ -0,0 +1,94 @@ +context.skip("Form Tour", () => { + before(() => { + cy.login(); + cy.visit("/app"); + return cy + .window() + .its("influxframework") + .then((influxframework) => { + return influxframework.call("influxframework.tests.ui_test_helpers.create_form_tour"); + }); + }); + + const open_test_form_tour = () => { + cy.visit("/app/form-tour/Test Form Tour"); + cy.findByRole("button", { name: "Show Tour" }).should("be.visible").as("show_tour"); + cy.get("@show_tour").click(); + cy.wait(500); + cy.url().should("include", "/app/contact"); + }; + + it("jump to a form tour", open_test_form_tour); + + it("navigates a form tour", () => { + open_test_form_tour(); + + cy.get(".influxframework-driver").should("be.visible"); + cy.get('.influxframework-control[data-fieldname="first_name"]').as("first_name"); + cy.get("@first_name").should("have.class", "driver-highlighted-element"); + cy.get(".influxframework-driver").findByRole("button", { name: "Next" }).as("next_btn"); + + // next btn shouldn't move to next step, if first name is not entered + cy.get("@next_btn").click(); + cy.wait(500); + cy.get("@first_name").should("have.class", "driver-highlighted-element"); + + // after filling the field, next step should be highlighted + cy.fill_field("first_name", "Test Name", "Data"); + cy.wait(500); + cy.get("@next_btn").click(); + cy.wait(500); + + // assert field is highlighted + cy.get('.influxframework-control[data-fieldname="last_name"]').as("last_name"); + cy.get("@last_name").should("have.class", "driver-highlighted-element"); + + // after filling the field, next step should be highlighted + cy.fill_field("last_name", "Test Last Name", "Data"); + cy.wait(500); + cy.get("@next_btn").click(); + cy.wait(500); + + // assert field is highlighted + cy.get('.influxframework-control[data-fieldname="phone_nos"]').as("phone_nos"); + cy.get("@phone_nos").should("have.class", "driver-highlighted-element"); + + // move to next step + cy.wait(500); + cy.get("@next_btn").click(); + cy.wait(500); + + // assert add row btn is highlighted + cy.get("@phone_nos").find(".grid-add-row").as("add_row"); + cy.get("@add_row").should("have.class", "driver-highlighted-element"); + + // add a row & move to next step + cy.wait(500); + cy.get("@add_row").click(); + cy.wait(500); + + // assert table field is highlighted + cy.get('.grid-row-open .influxframework-control[data-fieldname="phone"]').as("phone"); + cy.get("@phone").should("have.class", "driver-highlighted-element"); + // enter value in a table field + let field = cy.fill_table_field("phone_nos", "1", "phone", "1234567890"); + field.blur(); + + // move to collapse row step + cy.wait(500); + cy.get(".driver-popover-title") + .contains("Test Title 4") + .siblings() + .get("@next_btn") + .click(); + cy.wait(500); + // collapse row + cy.get(".grid-row-open .grid-collapse-row").click(); + cy.wait(500); + + // assert save btn is highlighted + cy.get(".primary-action").should("have.class", "driver-highlighted-element"); + cy.wait(500); + cy.get(".influxframework-driver").findByRole("button", { name: "Save" }).should("be.visible"); + }); +}); diff --git a/cypress/integration/grid.js b/cypress/integration/grid.js new file mode 100644 index 0000000..21b478d --- /dev/null +++ b/cypress/integration/grid.js @@ -0,0 +1,114 @@ +context("Grid", () => { + beforeEach(() => { + cy.login(); + cy.visit("/app/website"); + }); + before(() => { + cy.login(); + cy.visit("/app/website"); + return cy + .window() + .its("influxframework") + .then((influxframework) => { + return influxframework.call( + "influxframework.tests.ui_test_helpers.create_contact_phone_nos_records" + ); + }); + }); + it("update docfield property using update_docfield_property", () => { + cy.visit("/app/contact/Test Contact"); + cy.window() + .its("cur_frm") + .then((frm) => { + cy.get('.influxframework-control[data-fieldname="phone_nos"]').as("table"); + let field = frm.get_field("phone_nos"); + field.grid.update_docfield_property("is_primary_phone", "hidden", true); + + cy.get("@table").find('[data-idx="1"] .edit-grid-row').click(); + cy.get(".grid-row-open").as("table-form"); + cy.get("@table-form") + .find('.influxframework-control[data-fieldname="is_primary_phone"]') + .should("be.hidden"); + cy.get("@table-form").find(".grid-footer-toolbar").click(); + + cy.get("@table").find('[data-idx="2"] .edit-grid-row').click(); + cy.get(".grid-row-open").as("table-form"); + cy.get("@table-form") + .find('.influxframework-control[data-fieldname="is_primary_phone"]') + .should("be.hidden"); + cy.get("@table-form").find(".grid-footer-toolbar").click(); + }); + }); + it("update docfield property using toggle_display", () => { + cy.visit("/app/contact/Test Contact"); + cy.window() + .its("cur_frm") + .then((frm) => { + cy.get('.influxframework-control[data-fieldname="phone_nos"]').as("table"); + let field = frm.get_field("phone_nos"); + field.grid.toggle_display("is_primary_mobile_no", false); + + cy.get("@table").find('[data-idx="1"] .edit-grid-row').click(); + cy.get(".grid-row-open").as("table-form"); + cy.get("@table-form") + .find('.influxframework-control[data-fieldname="is_primary_mobile_no"]') + .should("be.hidden"); + cy.get("@table-form").find(".grid-footer-toolbar").click(); + + cy.get("@table").find('[data-idx="2"] .edit-grid-row').click(); + cy.get(".grid-row-open").as("table-form"); + cy.get("@table-form") + .find('.influxframework-control[data-fieldname="is_primary_mobile_no"]') + .should("be.hidden"); + cy.get("@table-form").find(".grid-footer-toolbar").click(); + }); + }); + it("update docfield property using toggle_enable", () => { + cy.visit("/app/contact/Test Contact"); + cy.window() + .its("cur_frm") + .then((frm) => { + cy.get('.influxframework-control[data-fieldname="phone_nos"]').as("table"); + let field = frm.get_field("phone_nos"); + field.grid.toggle_enable("phone", false); + + cy.get("@table").find('[data-idx="1"] .edit-grid-row').click(); + cy.get(".grid-row-open").as("table-form"); + cy.get("@table-form") + .find('.influxframework-control[data-fieldname="phone"] .control-value') + .should("have.class", "like-disabled-input"); + cy.get("@table-form").find(".grid-footer-toolbar").click(); + + cy.get("@table").find('[data-idx="2"] .edit-grid-row').click(); + cy.get(".grid-row-open").as("table-form"); + cy.get("@table-form") + .find('.influxframework-control[data-fieldname="phone"] .control-value') + .should("have.class", "like-disabled-input"); + cy.get("@table-form").find(".grid-footer-toolbar").click(); + }); + }); + it("update docfield property using toggle_reqd", () => { + cy.visit("/app/contact/Test Contact"); + cy.window() + .its("cur_frm") + .then((frm) => { + cy.get('.influxframework-control[data-fieldname="phone_nos"]').as("table"); + let field = frm.get_field("phone_nos"); + field.grid.toggle_reqd("phone", false); + + cy.get("@table").find('[data-idx="1"] .edit-grid-row').click(); + cy.get(".grid-row-open").as("table-form"); + cy.get_field("phone").as("phone-field"); + cy.get("@phone-field").focus().clear().wait(500).blur(); + cy.get("@phone-field").should("not.have.class", "has-error"); + cy.get("@table-form").find(".grid-footer-toolbar").click(); + + cy.get("@table").find('[data-idx="2"] .edit-grid-row').click(); + cy.get(".grid-row-open").as("table-form"); + cy.get_field("phone").as("phone-field"); + cy.get("@phone-field").focus().clear().wait(500).blur(); + cy.get("@phone-field").should("not.have.class", "has-error"); + cy.get("@table-form").find(".grid-footer-toolbar").click(); + }); + }); +}); diff --git a/cypress/integration/grid_configuration.js b/cypress/integration/grid_configuration.js new file mode 100644 index 0000000..84d374f --- /dev/null +++ b/cypress/integration/grid_configuration.js @@ -0,0 +1,23 @@ +context("Grid Configuration", () => { + beforeEach(() => { + cy.login(); + cy.visit("/app/doctype/User"); + }); + it("Set user wise grid settings", () => { + cy.wait(100); + cy.get('.influxframework-control[data-fieldname="fields"]').as("table"); + cy.get("@table").find(".icon-sm").click(); + cy.wait(100); + cy.get('.influxframework-control[data-fieldname="fields_html"]').as("modal"); + cy.get("@modal").find(".add-new-fields").click(); + cy.wait(100); + cy.get('[type="checkbox"][data-unit="read_only"]').check(); + cy.findByRole("button", { name: "Add" }).click(); + cy.wait(100); + cy.get('[data-fieldname="options"]').invoke("attr", "value", "1"); + cy.get('.form-control.column-width[data-fieldname="options"]').trigger("change"); + cy.findByRole("button", { name: "Update" }).click(); + cy.wait(200); + cy.get('[title="Read Only"').should("be.visible"); + }); +}); diff --git a/cypress/integration/grid_keyboard_shortcut.js b/cypress/integration/grid_keyboard_shortcut.js new file mode 100644 index 0000000..9cbf4ba --- /dev/null +++ b/cypress/integration/grid_keyboard_shortcut.js @@ -0,0 +1,47 @@ +context("Grid Keyboard Shortcut", () => { + let total_count = 0; + before(() => { + cy.login(); + }); + beforeEach(() => { + cy.reload(); + cy.visit("/app/contact/new-contact-1"); + cy.get('.influxframework-control[data-fieldname="email_ids"]').find(".grid-add-row").click(); + }); + it("Insert new row at the end", () => { + cy.add_new_row_in_grid( + "{ctrl}{shift}{downarrow}", + (cy, total_count) => { + cy.get('[data-name="new-contact-email-1"]').should( + "have.attr", + "data-idx", + `${total_count + 1}` + ); + }, + total_count + ); + }); + it("Insert new row at the top", () => { + cy.add_new_row_in_grid("{ctrl}{shift}{uparrow}", (cy) => { + cy.get('[data-name="new-contact-email-1"]').should("have.attr", "data-idx", "2"); + }); + }); + it("Insert new row below", () => { + cy.add_new_row_in_grid("{ctrl}{downarrow}", (cy) => { + cy.get('[data-name="new-contact-email-1"]').should("have.attr", "data-idx", "1"); + }); + }); + it("Insert new row above", () => { + cy.add_new_row_in_grid("{ctrl}{uparrow}", (cy) => { + cy.get('[data-name="new-contact-email-1"]').should("have.attr", "data-idx", "2"); + }); + }); +}); + +Cypress.Commands.add("add_new_row_in_grid", (shortcut_keys, callbackFn, total_count) => { + cy.get('.influxframework-control[data-fieldname="email_ids"]').as("table"); + cy.get("@table").find('.grid-body [data-fieldname="email_id"]').first().click(); + cy.get("@table").find('.grid-body [data-fieldname="email_id"]').first().type(shortcut_keys); + + callbackFn(cy, total_count); +}); diff --git a/cypress/integration/grid_pagination.js b/cypress/integration/grid_pagination.js new file mode 100644 index 0000000..cee4bda --- /dev/null +++ b/cypress/integration/grid_pagination.js @@ -0,0 +1,80 @@ +context("Grid Pagination", () => { + beforeEach(() => { + cy.login(); + cy.visit("/app/website"); + }); + before(() => { + cy.login(); + cy.visit("/app/website"); + return cy + .window() + .its("influxframework") + .then((influxframework) => { + return influxframework.call( + "influxframework.tests.ui_test_helpers.create_contact_phone_nos_records" + ); + }); + }); + it("creates pages for child table", () => { + cy.visit("/app/contact/Test Contact"); + cy.get('.influxframework-control[data-fieldname="phone_nos"]').as("table"); + cy.get("@table").find(".current-page-number").should("have.value", "1"); + cy.get("@table").find(".total-page-number").should("contain", "20"); + cy.get("@table").find(".grid-body .grid-row").should("have.length", 50); + }); + it("goes to the next and previous page", () => { + cy.visit("/app/contact/Test Contact"); + cy.get('.influxframework-control[data-fieldname="phone_nos"]').as("table"); + cy.get("@table").find(".next-page").click(); + cy.get("@table").find(".current-page-number").should("have.value", "2"); + cy.get("@table") + .find(".grid-body .grid-row") + .first() + .should("have.attr", "data-idx", "51"); + cy.get("@table").find(".prev-page").click(); + cy.get("@table").find(".current-page-number").should("have.value", "1"); + cy.get("@table").find(".grid-body .grid-row").first().should("have.attr", "data-idx", "1"); + }); + it("adds and deletes rows and changes page", () => { + cy.visit("/app/contact/Test Contact"); + cy.get('.influxframework-control[data-fieldname="phone_nos"]').as("table"); + cy.get("@table").findByRole("button", { name: "Add Row" }).click(); + cy.get("@table").find(".grid-body .row-index").should("contain", 1001); + cy.get("@table").find(".current-page-number").should("have.value", "21"); + cy.get("@table").find(".total-page-number").should("contain", "21"); + cy.get("@table").find(".grid-body .grid-row .grid-row-check").click({ force: true }); + cy.get("@table").findByRole("button", { name: "Delete" }).click(); + cy.get("@table").find(".grid-body .row-index").last().should("contain", 1000); + cy.get("@table").find(".current-page-number").should("have.value", "20"); + cy.get("@table").find(".total-page-number").should("contain", "20"); + }); + it("go to specific page, use up and down arrow, type characters, 0 page and more than existing page", () => { + cy.visit("/app/contact/Test Contact"); + cy.get('.influxframework-control[data-fieldname="phone_nos"]').as("table"); + cy.get("@table").find(".current-page-number").focus().clear().type("17").blur(); + cy.get("@table").find(".grid-body .row-index").should("contain", 801); + + cy.get("@table").find(".current-page-number").focus().type("{uparrow}{uparrow}"); + cy.get("@table").find(".current-page-number").should("have.value", "19"); + + cy.get("@table").find(".current-page-number").focus().type("{downarrow}{downarrow}"); + cy.get("@table").find(".current-page-number").should("have.value", "17"); + + cy.get("@table").find(".current-page-number").focus().clear().type("700").blur(); + cy.get("@table").find(".current-page-number").should("have.value", "20"); + + cy.get("@table").find(".current-page-number").focus().clear().type("0").blur(); + cy.get("@table").find(".current-page-number").should("have.value", "1"); + + cy.get("@table").find(".current-page-number").focus().clear().type("abc").blur(); + cy.get("@table").find(".current-page-number").should("have.value", "1"); + }); + // it('deletes all rows', ()=> { + // cy.visit('/app/contact/Test Contact'); + // cy.get('.influxframework-control[data-fieldname="phone_nos"]').as('table'); + // cy.get('@table').find('.grid-heading-row .grid-row-check').click({force: true}); + // cy.get('@table').find('button.grid-remove-all-rows').click(); + // cy.get('.modal-dialog .btn-primary').contains('Yes').click(); + // cy.get('@table').find('.grid-body .grid-row').should('have.length', 0); + // }); +}); diff --git a/cypress/integration/grid_search.js b/cypress/integration/grid_search.js new file mode 100644 index 0000000..84d3001 --- /dev/null +++ b/cypress/integration/grid_search.js @@ -0,0 +1,133 @@ +import doctype_with_child_table from "../fixtures/doctype_with_child_table"; +import child_table_doctype from "../fixtures/child_table_doctype"; +import child_table_doctype_1 from "../fixtures/child_table_doctype_1"; +const doctype_with_child_table_name = doctype_with_child_table.name; + +context("Grid Search", () => { + before(() => { + cy.visit("/login"); + cy.login(); + cy.visit("/app/website"); + cy.insert_doc("DocType", child_table_doctype, true); + cy.insert_doc("DocType", child_table_doctype_1, true); + cy.insert_doc("DocType", doctype_with_child_table, true); + return cy + .window() + .its("influxframework") + .then((influxframework) => { + return influxframework.xcall( + "influxframework.tests.ui_test_helpers.insert_doctype_with_child_table_record", + { + name: doctype_with_child_table_name, + } + ); + }); + }); + + it("Test search row visibility", () => { + cy.window() + .its("influxframework") + .then((influxframework) => { + influxframework.model.user_settings.save("Doctype With Child Table", "GridView", { + "Child Table Doctype 1": [ + { fieldname: "data", columns: 2 }, + { fieldname: "barcode", columns: 1 }, + { fieldname: "check", columns: 1 }, + { fieldname: "rating", columns: 2 }, + { fieldname: "duration", columns: 2 }, + { fieldname: "date", columns: 2 }, + ], + }); + }); + + cy.visit(`/app/doctype-with-child-table/Test Grid Search`); + + cy.get('.influxframework-control[data-fieldname="child_table_1"]').as("table"); + cy.get("@table").find(".grid-row-check:last").click(); + cy.get("@table").find(".grid-footer").contains("Delete").click(); + cy.get(".grid-heading-row .grid-row .search").should("not.exist"); + }); + + it("test search field for different fieldtypes", () => { + cy.visit(`/app/doctype-with-child-table/Test Grid Search`); + + cy.get('.influxframework-control[data-fieldname="child_table_1"]').as("table"); + + // Index Column + cy.get("@table").find(".grid-heading-row .row-index.search input").type("3"); + cy.get("@table").find(".grid-body .rows .grid-row").should("have.length", 2); + cy.get("@table").find(".grid-heading-row .row-index.search input").clear(); + + // Data Column + cy.get("@table") + .find('.grid-heading-row .search input[data-fieldtype="Data"]') + .type("Data"); + cy.get("@table").find(".grid-body .rows .grid-row").should("have.length", 1); + cy.get("@table").find('.grid-heading-row .search input[data-fieldtype="Data"]').clear(); + + // Barcode Column + cy.get("@table") + .find('.grid-heading-row .search input[data-fieldtype="Barcode"]') + .type("092"); + cy.get("@table").find(".grid-body .rows .grid-row").should("have.length", 4); + cy.get("@table").find('.grid-heading-row .search input[data-fieldtype="Barcode"]').clear(); + + // Check Column + cy.get("@table").find('.grid-heading-row .search input[data-fieldtype="Check"]').type("1"); + cy.get("@table").find(".grid-body .rows .grid-row").should("have.length", 9); + cy.get("@table").find('.grid-heading-row .search input[data-fieldtype="Check"]').clear(); + + cy.get("@table").find('.grid-heading-row .search input[data-fieldtype="Check"]').type("0"); + cy.get("@table").find(".grid-body .rows .grid-row").should("have.length", 11); + cy.get("@table").find('.grid-heading-row .search input[data-fieldtype="Check"]').clear(); + + // Rating Column + cy.get("@table") + .find('.grid-heading-row .search input[data-fieldtype="Rating"]') + .type("3"); + cy.get("@table").find(".grid-body .rows .grid-row").should("have.length", 3); + cy.get("@table").find('.grid-heading-row .search input[data-fieldtype="Rating"]').clear(); + + // Duration Column + cy.get("@table") + .find('.grid-heading-row .search input[data-fieldtype="Duration"]') + .type("3d"); + cy.get("@table").find(".grid-body .rows .grid-row").should("have.length", 3); + cy.get("@table") + .find('.grid-heading-row .search input[data-fieldtype="Duration"]') + .clear(); + + // Date Column + cy.get("@table") + .find('.grid-heading-row .search input[data-fieldtype="Date"]') + .type("2022"); + cy.get("@table").find(".grid-body .rows .grid-row").should("have.length", 4); + cy.get("@table").find('.grid-heading-row .search input[data-fieldtype="Date"]').clear(); + }); + + it("test with multiple filter", () => { + cy.get('.influxframework-control[data-fieldname="child_table_1"]').as("table"); + + // Data Column + cy.get("@table").find('.grid-heading-row .search input[data-fieldtype="Data"]').type("a"); + cy.get("@table").find(".grid-body .rows .grid-row").should("have.length", 10); + + // Barcode Column + cy.get("@table") + .find('.grid-heading-row .search input[data-fieldtype="Barcode"]') + .type("0"); + cy.get("@table").find(".grid-body .rows .grid-row").should("have.length", 8); + + // Duration Column + cy.get("@table") + .find('.grid-heading-row .search input[data-fieldtype="Duration"]') + .type("d"); + cy.get("@table").find(".grid-body .rows .grid-row").should("have.length", 5); + + // Date Column + cy.get("@table") + .find('.grid-heading-row .search input[data-fieldtype="Date"]') + .type("02-"); + cy.get("@table").find(".grid-body .rows .grid-row").should("have.length", 2); + }); +}); diff --git a/cypress/integration/kanban.js b/cypress/integration/kanban.js new file mode 100644 index 0000000..e5a0491 --- /dev/null +++ b/cypress/integration/kanban.js @@ -0,0 +1,99 @@ +context("Kanban Board", () => { + before(() => { + cy.login(); + cy.visit("/app"); + }); + + it("Create ToDo Kanban", () => { + cy.visit("/app/todo"); + + cy.get(".page-actions .custom-btn-group button").click(); + cy.get(".page-actions .custom-btn-group ul.dropdown-menu li").contains("Kanban").click(); + + cy.focused().blur(); + cy.fill_field("board_name", "ToDo Kanban", "Data"); + cy.fill_field("field_name", "Status", "Select"); + cy.click_modal_primary_button("Save"); + + cy.get(".title-text").should("contain", "ToDo Kanban"); + }); + + it("Create ToDo from kanban", () => { + cy.intercept({ + method: "POST", + url: "api/method/influxframework.client.save", + }).as("save-todo"); + + cy.click_listview_primary_button("Add ToDo"); + + cy.fill_field("description", "Test Kanban ToDo", "Text Editor").wait(300); + cy.get(".modal-footer .btn-primary").last().click(); + + cy.wait("@save-todo"); + }); + + it("Add and Remove fields", () => { + cy.visit("/app/todo/view/kanban/ToDo Kanban"); + + cy.intercept( + "POST", + "/api/method/influxframework.desk.doctype.kanban_board.kanban_board.save_settings" + ).as("save-kanban"); + cy.intercept( + "POST", + "/api/method/influxframework.desk.doctype.kanban_board.kanban_board.update_order" + ).as("update-order"); + + cy.get(".page-actions .menu-btn-group > .btn").click(); + cy.get(".page-actions .menu-btn-group .dropdown-menu li") + .contains("Kanban Settings") + .click(); + cy.get(".add-new-fields").click(); + + cy.get(".checkbox-options .checkbox").contains("ID").click(); + cy.get(".checkbox-options .checkbox").contains("Status").first().click(); + cy.get(".checkbox-options .checkbox").contains("Priority").click(); + + cy.get(".modal-footer .btn-primary").last().click(); + + cy.get(".influxframework-control .label-area").contains("Show Labels").click(); + cy.click_modal_primary_button("Save"); + + cy.wait("@save-kanban"); + + cy.get('.kanban-column[data-column-value="Open"] .kanban-cards').as("open-cards"); + cy.get("@open-cards") + .find(".kanban-card .kanban-card-doc") + .first() + .should("contain", "ID:"); + cy.get("@open-cards") + .find(".kanban-card .kanban-card-doc") + .first() + .should("contain", "Status:"); + cy.get("@open-cards") + .find(".kanban-card .kanban-card-doc") + .first() + .should("contain", "Priority:"); + + cy.get(".page-actions .menu-btn-group > .btn").click(); + cy.get(".page-actions .menu-btn-group .dropdown-menu li") + .contains("Kanban Settings") + .click(); + cy.get_open_dialog() + .find( + '.influxframework-control[data-fieldname="fields_html"] div[data-label="ID"] .remove-field' + ) + .click(); + + cy.wait("@update-order"); + cy.get_open_dialog().find(".influxframework-control .label-area").contains("Show Labels").click(); + cy.get(".modal-footer .btn-primary").last().click(); + + cy.wait("@save-kanban"); + + cy.get("@open-cards") + .find(".kanban-card .kanban-card-doc") + .first() + .should("not.contain", "ID:"); + }); +}); diff --git a/cypress/integration/list_paging.js b/cypress/integration/list_paging.js new file mode 100644 index 0000000..fa6f8e6 --- /dev/null +++ b/cypress/integration/list_paging.js @@ -0,0 +1,42 @@ +context("List Paging", () => { + before(() => { + cy.login(); + cy.visit("/app/website"); + return cy + .window() + .its("influxframework") + .then((influxframework) => { + return influxframework.call("influxframework.tests.ui_test_helpers.create_multiple_todo_records"); + }); + }); + + it("test load more with count selection buttons", () => { + cy.visit("/app/todo/view/report"); + cy.clear_filters(); + + cy.get(".list-paging-area .list-count").should("contain.text", "20 of"); + cy.get(".list-paging-area .btn-more").click(); + cy.get(".list-paging-area .list-count").should("contain.text", "40 of"); + cy.get(".list-paging-area .btn-more").click(); + cy.get(".list-paging-area .list-count").should("contain.text", "60 of"); + + cy.get('.list-paging-area .btn-group .btn-paging[data-value="100"]').click(); + + cy.get(".list-paging-area .list-count").should("contain.text", "100 of"); + cy.get(".list-paging-area .btn-more").click(); + cy.get(".list-paging-area .list-count").should("contain.text", "200 of"); + cy.get(".list-paging-area .btn-more").click(); + cy.get(".list-paging-area .list-count").should("contain.text", "300 of"); + + // check if refresh works after load more + cy.get('.page-head .standard-actions [data-original-title="Refresh"]').click(); + cy.get(".list-paging-area .list-count").should("contain.text", "300 of"); + + cy.get('.list-paging-area .btn-group .btn-paging[data-value="500"]').click(); + + cy.get(".list-paging-area .list-count").should("contain.text", "500 of"); + cy.get(".list-paging-area .btn-more").click(); + + cy.get(".list-paging-area .list-count").should("contain.text", "1000 of"); + }); +}); diff --git a/cypress/integration/list_view.js b/cypress/integration/list_view.js new file mode 100644 index 0000000..ba557c3 --- /dev/null +++ b/cypress/integration/list_view.js @@ -0,0 +1,70 @@ +context("List View", () => { + before(() => { + cy.login(); + cy.visit("/app/website"); + return cy + .window() + .its("influxframework") + .then((influxframework) => { + return influxframework.xcall("influxframework.tests.ui_test_helpers.setup_workflow"); + }); + }); + + it("Keep checkbox checked after Refresh", { scrollBehavior: false }, () => { + cy.go_to_list("ToDo"); + cy.clear_filters(); + cy.get(".list-row-container .list-row-checkbox").click({ + multiple: true, + force: true, + }); + cy.get(".actions-btn-group button").contains("Actions").should("be.visible"); + cy.intercept("/api/method/influxframework.desk.reportview.get").as("list-refresh"); + cy.wait(3000); // wait before you hit another refresh + cy.get('button[data-original-title="Refresh"]').click(); + cy.wait("@list-refresh"); + cy.get(".list-row-container .list-row-checkbox:checked").should("be.visible"); + }); + + it('enables "Actions" button', { scrollBehavior: false }, () => { + const actions = [ + "Approve", + "Reject", + "Edit", + "Export", + "Assign To", + "Apply Assignment Rule", + "Add Tags", + "Print", + "Delete", + ]; + cy.go_to_list("ToDo"); + cy.clear_filters(); + cy.get('.list-row-container:contains("Pending") .list-row-checkbox').click({ + multiple: true, + force: true, + }); + cy.get(".actions-btn-group button").contains("Actions").should("be.visible").click(); + cy.get(".dropdown-menu li:visible .dropdown-item") + .should("have.length", 9) + .each((el, index) => { + cy.wrap(el).contains(actions[index]); + }) + .then((elements) => { + cy.intercept({ + method: "POST", + url: "api/method/influxframework.model.workflow.bulk_workflow_approval", + }).as("bulk-approval"); + cy.intercept({ + method: "POST", + url: "api/method/influxframework.desk.reportview.get", + }).as("real-time-update"); + cy.wrap(elements).contains("Approve").click(); + cy.wait(["@bulk-approval", "@real-time-update"]); + cy.wait(300); + cy.get_open_dialog().find(".btn-modal-close").click(); + cy.reload(); + cy.clear_filters(); + cy.get(".list-row-container:visible").should("contain", "Approved"); + }); + }); +}); diff --git a/cypress/integration/list_view_settings.js b/cypress/integration/list_view_settings.js new file mode 100644 index 0000000..898fe1d --- /dev/null +++ b/cypress/integration/list_view_settings.js @@ -0,0 +1,38 @@ +context("List View Settings", () => { + beforeEach(() => { + cy.login(); + cy.visit("/app/website"); + }); + it("Default settings", () => { + cy.visit("/app/List/DocType/List"); + cy.clear_filters(); + cy.get(".list-count").should("contain", "20 of"); + cy.get(".list-stats").should("contain", "Tags"); + }); + it("disable count and sidebar stats then verify", () => { + cy.wait(300); + cy.visit("/app/List/DocType/List"); + cy.clear_filters(); + cy.wait(300); + cy.get(".list-count").should("contain", "20 of"); + cy.get(".menu-btn-group button").click(); + cy.get(".dropdown-menu li").filter(":visible").contains("List Settings").click(); + cy.get(".modal-dialog").should("contain", "DocType Settings"); + + cy.findByLabelText("Disable Count").check({ force: true }); + cy.findByLabelText("Disable Sidebar Stats").check({ force: true }); + cy.findByRole("button", { name: "Save" }).click(); + + cy.reload({ force: true }); + + cy.get(".list-count").should("be.empty"); + cy.get(".list-sidebar .list-tags").should("not.exist"); + + cy.get(".menu-btn-group button").click({ force: true }); + cy.get(".dropdown-menu li").filter(":visible").contains("List Settings").click(); + cy.get(".modal-dialog").should("contain", "DocType Settings"); + cy.findByLabelText("Disable Count").uncheck({ force: true }); + cy.findByLabelText("Disable Sidebar Stats").uncheck({ force: true }); + cy.findByRole("button", { name: "Save" }).click(); + }); +}); diff --git a/cypress/integration/login.js b/cypress/integration/login.js new file mode 100644 index 0000000..678e697 --- /dev/null +++ b/cypress/integration/login.js @@ -0,0 +1,66 @@ +context("Login", () => { + beforeEach(() => { + cy.request("/api/method/logout"); + cy.visit("/login"); + cy.location("pathname").should("eq", "/login"); + }); + + it("greets with login screen", () => { + cy.get(".page-card-head").contains("Login"); + }); + + it("validates password", () => { + cy.get("#login_email").type("Administrator"); + cy.findByRole("button", { name: "Login" }).click(); + cy.location("pathname").should("eq", "/login"); + }); + + it("validates email", () => { + cy.get("#login_password").type("qwe"); + cy.findByRole("button", { name: "Login" }).click(); + cy.location("pathname").should("eq", "/login"); + }); + + it("shows invalid login if incorrect credentials", () => { + cy.get("#login_email").type("Administrator"); + cy.get("#login_password").type("qwer"); + + cy.findByRole("button", { name: "Login" }).click(); + cy.findByRole("button", { name: "Invalid Login. Try again." }).should("exist"); + cy.location("pathname").should("eq", "/login"); + }); + + it("logs in using correct credentials", () => { + cy.get("#login_email").type("Administrator"); + cy.get("#login_password").type(Cypress.env("adminPassword")); + + cy.findByRole("button", { name: "Login" }).click(); + cy.location("pathname").should("eq", "/app"); + cy.window().its("influxframework.session.user").should("eq", "Administrator"); + }); + + it("check redirect after login", () => { + // mock for OAuth 2.0 client_id, redirect_uri, scope and state + const payload = new URLSearchParams({ + uuid: "6fed1519-cfd8-4a2d-84a6-9a1799c7c741", + encoded_string: "hello all", + encoded_url: "http://test.localhost/callback", + base64_string: "aGVsbG8gYWxs", + }); + + cy.request("/api/method/logout"); + + // redirect-to /me page with params to mock OAuth 2.0 like request + cy.visit( + "/login?redirect-to=/me?" + encodeURIComponent(payload.toString().replace("+", " ")) + ); + + cy.get("#login_email").type("Administrator"); + cy.get("#login_password").type(Cypress.env("adminPassword")); + + cy.findByRole("button", { name: "Login" }).click(); + + // verify redirected location and url params after login + cy.url().should("include", "/me?" + payload.toString().replace("+", "%20")); + }); +}); diff --git a/cypress/integration/multi_select_dialog.js b/cypress/integration/multi_select_dialog.js new file mode 100644 index 0000000..61c8322 --- /dev/null +++ b/cypress/integration/multi_select_dialog.js @@ -0,0 +1,102 @@ +context("MultiSelectDialog", () => { + before(() => { + cy.login(); + cy.visit("/app"); + const contact_template = { + doctype: "Contact", + first_name: "Test", + status: "Passive", + email_ids: [ + { + doctype: "Contact Email", + email_id: "test@example.com", + is_primary: 0, + }, + ], + }; + const promises = Array.from({ length: 25 }).map(() => + cy.insert_doc("Contact", contact_template, true) + ); + Promise.all(promises); + }); + + function open_multi_select_dialog() { + cy.window() + .its("influxframework") + .then((influxframework) => { + new influxframework.ui.form.MultiSelectDialog({ + doctype: "Contact", + target: {}, + setters: { + status: null, + gender: null, + }, + add_filters_group: 1, + allow_child_item_selection: 1, + child_fieldname: "email_ids", + child_columns: ["email_id", "is_primary"], + }); + }); + } + + it("checks multi select dialog api works", () => { + open_multi_select_dialog(); + cy.get_open_dialog().should("contain", "Select Contacts"); + }); + + it("checks for filters", () => { + ["search_term", "status", "gender"].forEach((fieldname) => { + cy.get_open_dialog() + .get(`.influxframework-control[data-fieldname="${fieldname}"]`) + .should("exist"); + }); + + // add_filters_group: 1 should add a filter group + cy.get_open_dialog().get(`.influxframework-control[data-fieldname="filter_area"]`).should("exist"); + }); + + it("checks for child item selection", () => { + cy.get_open_dialog().get(`.dt-row-header`).should("not.exist"); + + cy.get_open_dialog() + .get(`.influxframework-control[data-fieldname="allow_child_item_selection"]`) + .find('input[data-fieldname="allow_child_item_selection"]') + .should("exist") + .click({ force: true }); + + cy.get_open_dialog() + .get(`.influxframework-control[data-fieldname="child_selection_area"]`) + .should("exist"); + + cy.get_open_dialog().get(`.dt-row-header`).should("contain", "Contact"); + + cy.get_open_dialog().get(`.dt-row-header`).should("contain", "Email Id"); + + cy.get_open_dialog().get(`.dt-row-header`).should("contain", "Is Primary"); + }); + + it("tests more button", () => { + cy.get_open_dialog() + .get(`.influxframework-control[data-fieldname="more_child_btn"]`) + .should("exist") + .as("more-btn"); + + cy.get_open_dialog() + .get(".datatable .dt-scrollable .dt-row") + .should(($rows) => { + expect($rows).to.have.length(20); + }); + + cy.intercept("POST", "api/method/influxframework.client.get_list").as("get-more-records"); + cy.get("@more-btn").find("button").click({ force: true }); + cy.wait("@get-more-records"); + + cy.get_open_dialog() + .get(".datatable .dt-scrollable .dt-row") + .should(($rows) => { + if ($rows.length <= 20) { + throw new Error("More button doesn't work"); + } + }); + }); +}); diff --git a/cypress/integration/navigation.js b/cypress/integration/navigation.js new file mode 100644 index 0000000..2302296 --- /dev/null +++ b/cypress/integration/navigation.js @@ -0,0 +1,29 @@ +context("Navigation", () => { + before(() => { + cy.login(); + }); + it("Navigate to route with hash in document name", () => { + cy.insert_doc("ToDo", { + __newname: "ABC#123", + description: "Test this", + ignore_duplicate: true, + }); + cy.visit("/app/todo/ABC#123"); + cy.title().should("eq", "Test this - ABC#123"); + cy.get_field("description", "Text Editor").contains("Test this"); + cy.go("back"); + cy.title().should("eq", "Website"); + }); + + it.only("Navigate to previous page after login", () => { + cy.visit("/app/todo"); + cy.get(".page-head").findByTitle("To Do").should("be.visible"); + cy.request("/api/method/logout"); + cy.reload().as("reload"); + cy.get("@reload").get(".page-card .btn-primary").contains("Login").click(); + cy.location("pathname").should("eq", "/login"); + cy.login(); + cy.visit("/app"); + cy.location("pathname").should("eq", "/app/todo"); + }); +}); diff --git a/cypress/integration/number_card.js b/cypress/integration/number_card.js new file mode 100644 index 0000000..eb0f19b --- /dev/null +++ b/cypress/integration/number_card.js @@ -0,0 +1,22 @@ +context("Number Card", () => { + before(() => { + cy.login(); + cy.visit("/app/website"); + }); + + it("Check filter populate for child table doctype", () => { + cy.visit("/app/number-card/new-number-card-1"); + cy.get('[data-fieldname="parent_document_type"]').should("have.css", "display", "none"); + + cy.get_field("document_type", "Link"); + cy.fill_field("document_type", "Workspace Link", "Link").focus().blur(); + cy.get_field("document_type", "Link").should("have.value", "Workspace Link"); + + cy.fill_field("label", "Test Number Card", "Data"); + + cy.get('[data-fieldname="filters_json"]').click().wait(200); + cy.get(".modal-body .filter-action-buttons .add-filter").click(); + cy.get(".modal-body .fieldname-select-area").click(); + cy.get(".modal-actions .btn-modal-close").click(); + }); +}); diff --git a/cypress/integration/query_report.js b/cypress/integration/query_report.js new file mode 100644 index 0000000..9cccac4 --- /dev/null +++ b/cypress/integration/query_report.js @@ -0,0 +1,91 @@ +context("Query Report", () => { + before(() => { + cy.login(); + cy.visit("/app/website"); + cy.insert_doc( + "Report", + { + report_name: "Test ToDo Report", + ref_doctype: "ToDo", + report_type: "Query Report", + query: "select * from tabToDo", + }, + true + ).as("doc"); + cy.create_records({ + doctype: "ToDo", + description: "this is a test todo for query report", + }).as("todos"); + }); + + it("add custom column in report", () => { + cy.visit("/app/query-report/Permitted Documents For User"); + + cy.get(".page-form.flex", { timeout: 60000 }) + .should("have.length", 1) + .then(() => { + cy.get('#page-query-report input[data-fieldname="user"]').as("input-user"); + cy.get("@input-user").focus().type("test@influxerp.com", { delay: 100 }).blur(); + cy.wait(300); + cy.get('#page-query-report input[data-fieldname="doctype"]').as("input-role"); + cy.get("@input-role").focus().type("Role", { delay: 100 }).blur(); + + cy.get(".datatable").should("exist"); + cy.get("#page-query-report .page-actions .menu-btn-group button").click({ + force: true, + }); + cy.get("#page-query-report .menu-btn-group .dropdown-menu") + .contains("Add Column") + .click({ force: true }); + cy.get_open_dialog().get(".modal-title").should("contain", "Add Column"); + cy.get('select[data-fieldname="doctype"]').select("Role", { force: true }); + cy.get('select[data-fieldname="field"]').select("Role Name", { force: true }); + cy.get('select[data-fieldname="insert_after"]').select("Name", { force: true }); + cy.get_open_dialog() + .findByRole("button", { name: "Submit" }) + .click({ force: true }); + cy.get("#page-query-report .page-actions .menu-btn-group button").click({ + force: true, + }); + cy.get("#page-query-report .menu-btn-group .dropdown-menu") + .contains("Save") + .click({ timeout: 100, force: true }); + cy.get_open_dialog().get(".modal-title").should("contain", "Save Report"); + + cy.get('input[data-fieldname="report_name"]').type("Test Report", { + delay: 100, + force: true, + }); + cy.get_open_dialog() + .findByRole("button", { name: "Submit" }) + .click({ timeout: 1000, force: true }); + }); + }); + + let save_report_and_open = (report, update_name) => { + cy.get("#page-query-report .page-actions .menu-btn-group button").click({ force: true }); + cy.get("#page-query-report .menu-btn-group .dropdown-menu") + .contains("Save") + .click({ timeout: 100, force: true }); + cy.get_open_dialog().get(".modal-title").should("contain", "Save Report"); + + cy.get('input[data-fieldname="report_name"]').type(update_name, { + delay: 100, + force: true, + }); + cy.get_open_dialog() + .findByRole("button", { name: "Submit" }) + .click({ timeout: 1000, force: true }); + + cy.visit("/app/query-report/" + report); + cy.get(".datatable").should("exist"); + }; + + it("test multi level query report", () => { + cy.visit("/app/query-report/Test ToDo Report"); + cy.get(".datatable").should("exist"); + + save_report_and_open("Test ToDo Report 1", " 1"); + save_report_and_open("Test ToDo Report 11", "1"); + }); +}); diff --git a/cypress/integration/recorder.js b/cypress/integration/recorder.js new file mode 100644 index 0000000..77152ee --- /dev/null +++ b/cypress/integration/recorder.js @@ -0,0 +1,72 @@ +context.skip("Recorder", () => { + before(() => { + cy.login(); + }); + + beforeEach(() => { + cy.visit("/app/recorder"); + return cy + .window() + .its("influxframework") + .then((influxframework) => { + // reset recorder + return influxframework.xcall("influxframework.recorder.stop").then(() => { + return influxframework.xcall("influxframework.recorder.delete"); + }); + }); + }); + + it("Recorder Empty State", () => { + cy.get(".page-head").findByTitle("Recorder").should("exist"); + + cy.get(".indicator-pill").should("contain", "Inactive").should("have.class", "red"); + + cy.get(".page-actions").findByRole("button", { name: "Start" }).should("exist"); + cy.get(".page-actions").findByRole("button", { name: "Clear" }).should("exist"); + + cy.get(".msg-box").should("contain", "Recorder is Inactive"); + cy.get(".msg-box").findByRole("button", { name: "Start Recording" }).should("exist"); + }); + + it("Recorder Start", () => { + cy.get(".page-actions").findByRole("button", { name: "Start" }).click(); + cy.get(".indicator-pill").should("contain", "Active").should("have.class", "green"); + + cy.get(".msg-box").should("contain", "No Requests found"); + + cy.visit("/app/List/DocType/List"); + cy.intercept("POST", "/api/method/influxframework.desk.reportview.get").as("list_refresh"); + cy.wait("@list_refresh"); + + cy.get(".page-head").findByTitle("DocType").should("exist"); + cy.get(".list-count").should("contain", "20 of "); + + cy.visit("/app/recorder"); + cy.get(".page-head").findByTitle("Recorder").should("exist"); + cy.get(".influxframework-list .result-list").should( + "contain", + "/api/method/influxframework.desk.reportview.get" + ); + }); + + it("Recorder View Request", () => { + cy.get(".page-actions").findByRole("button", { name: "Start" }).click(); + + cy.visit("/app/List/DocType/List"); + cy.intercept("POST", "/api/method/influxframework.desk.reportview.get").as("list_refresh"); + cy.wait("@list_refresh"); + + cy.get(".page-head").findByTitle("DocType").should("exist"); + cy.get(".list-count").should("contain", "20 of "); + + cy.visit("/app/recorder"); + + cy.get(".influxframework-list .list-row-container span") + .contains("/api/method/influxframework") + .should("be.visible") + .click({ force: true }); + + cy.url().should("include", "/recorder/request"); + cy.get("form").should("contain", "/api/method/influxframework"); + }); +}); diff --git a/cypress/integration/relative_time_filters.js b/cypress/integration/relative_time_filters.js new file mode 100644 index 0000000..cb95ff3 --- /dev/null +++ b/cypress/integration/relative_time_filters.js @@ -0,0 +1,47 @@ +// TODO: Enable this again +// currently this is flaky possibly because of different timezone in CI + +// context('Relative Timeframe', () => { +// before(() => { +// cy.login(); +// cy.visit('/app/website'); +// cy.window().its('influxframework').then(influxframework => { +// influxframework.call("influxframework.tests.ui_test_helpers.create_todo_records"); +// }); +// }); +// it('sets relative timespan filter for last week and filters list', () => { +// cy.visit('/app/List/ToDo/List'); +// cy.clear_filters(); +// cy.get('.list-row:contains("this is fourth todo")').should('exist'); +// cy.add_filter(); +// cy.get('.fieldname-select-area').should('exist'); +// cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 }); +// cy.get('select.condition.form-control').select("Timespan"); +// cy.get('.filter-field select.input-with-feedback.form-control').select("last week"); +// cy.intercept('POST', '/api/method/influxframework.desk.reportview.get').as('list_refresh'); +// cy.get('.filter-popover .apply-filters').click({ force: true }); +// cy.wait('@list_refresh'); +// cy.get('.list-row-container').its('length').should('eq', 1); +// cy.get('.list-row-container').should('contain', 'this is second todo'); +// cy.intercept('POST', '/api/method/influxframework.model.utils.user_settings.save') +// .as('save_user_settings'); +// cy.clear_filters(); +// cy.wait('@save_user_settings'); +// }); +// it('sets relative timespan filter for next week and filters list', () => { +// cy.visit('/app/List/ToDo/List'); +// cy.clear_filters(); +// cy.get('.list-row:contains("this is fourth todo")').should('exist'); +// cy.add_filter(); +// cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 }); +// cy.get('select.condition.form-control').select("Timespan"); +// cy.get('.filter-field select.input-with-feedback.form-control').select("next week"); +// cy.intercept('POST', '/api/method/influxframework.desk.reportview.get').as('list_refresh'); +// cy.get('.filter-popover .apply-filters').click({ force: true }); +// cy.wait('@list_refresh'); +// cy.intercept('POST', '/api/method/influxframework.model.utils.user_settings.save') +// .as('save_user_settings'); +// cy.clear_filters(); +// cy.wait('@save_user_settings'); +// }); +// }); diff --git a/cypress/integration/report_view.js b/cypress/integration/report_view.js new file mode 100644 index 0000000..4432bd5 --- /dev/null +++ b/cypress/integration/report_view.js @@ -0,0 +1,47 @@ +import custom_submittable_doctype from "../fixtures/custom_submittable_doctype"; +const doctype_name = custom_submittable_doctype.name; + +context("Report View", () => { + before(() => { + cy.login(); + cy.visit("/app/website"); + cy.insert_doc("DocType", custom_submittable_doctype, true); + cy.clear_cache(); + cy.insert_doc( + doctype_name, + { + title: "Doc 1", + description: "Random Text", + enabled: 0, + docstatus: 1, // submit document + }, + true + ); + }); + + it("Field with enabled allow_on_submit should be editable.", () => { + cy.intercept("POST", "api/method/influxframework.client.set_value").as("value-update"); + cy.visit(`/app/List/${doctype_name}/Report`); + + // check status column added from docstatus + cy.get(".dt-row-0 > .dt-cell--col-3").should("contain", "Submitted"); + let cell = cy.get(".dt-row-0 > .dt-cell--col-4"); + + // select the cell + cell.dblclick(); + cell.get(".dt-cell__edit--col-4").findByRole("checkbox").check({ force: true }); + cy.get(".dt-row-0 > .dt-cell--col-3").click(); // click outside + + cy.wait("@value-update"); + + cy.call("influxframework.client.get_value", { + doctype: doctype_name, + filters: { + title: "Doc 1", + }, + fieldname: "enabled", + }).then((r) => { + expect(r.message.enabled).to.equals(1); + }); + }); +}); diff --git a/cypress/integration/routing.js b/cypress/integration/routing.js new file mode 100644 index 0000000..0822dd9 --- /dev/null +++ b/cypress/integration/routing.js @@ -0,0 +1,40 @@ +const list_view = "/app/todo"; + +// test round trip with filter types + +const test_queries = [ + "?status=Open", + `?date=%5B"Between"%2C%5B"2022-06-01"%2C"2022-06-30"%5D%5D`, + `?date=%5B">"%2C"2022-06-01"%5D`, + `?name=%5B"like"%2C"%2542%25"%5D`, + `?status=%5B"not%20in"%2C%5B"Open"%2C"Closed"%5D%5D`, +]; + +describe("SPA Routing", { scrollBehavior: false }, () => { + before(() => { + cy.login(); + cy.go_to_list("ToDo"); + }); + + after(() => { + cy.clear_filters(); // avoid flake in future tests + }); + + it("should apply filter on list view from route", () => { + test_queries.forEach((query) => { + const full_url = `${list_view}${query}`; + cy.visit(full_url); + cy.findByTitle("To Do").should("exist"); + + const expected = new URLSearchParams(query); + cy.location().then((loc) => { + const actual = new URLSearchParams(loc.search); + // This might appear like a dumb test checking visited URL to itself + // but it's actually doing a round trip + // URL with params -> parsed filters -> new URL + // if it's same that means everything worked in between. + expect(actual.toString()).to.eq(expected.toString()); + }); + }); + }); +}); diff --git a/cypress/integration/sidebar.js b/cypress/integration/sidebar.js new file mode 100644 index 0000000..25fdf34 --- /dev/null +++ b/cypress/integration/sidebar.js @@ -0,0 +1,86 @@ +const verify_attachment_visibility = (document, is_private) => { + cy.visit(`/app/${document}`); + + const assertion = is_private ? "be.checked" : "not.be.checked"; + cy.findByRole("button", { name: "Attach File" }).click(); + + cy.get_open_dialog() + .find(".file-upload-area") + .selectFile("cypress/fixtures/sample_image.jpg", { + action: "drag-drop", + }); + + cy.get_open_dialog().findByRole("checkbox", { name: "Private" }).should(assertion); +}; + +context("Sidebar", () => { + before(() => { + cy.visit("/login"); + cy.login(); + + return cy + .window() + .its("influxframework") + .then((influxframework) => { + return influxframework.call("influxframework.tests.ui_test_helpers.create_blog_post"); + }); + }); + + it("Verify attachment visibility config", () => { + verify_attachment_visibility("doctype/Blog Post", true); + verify_attachment_visibility("blog-post/test-blog-attachment-post", false); + }); + + it('Test for checking "Assigned To" counter value, adding filter and adding & removing an assignment', () => { + cy.visit("/app/doctype"); + cy.click_sidebar_button("Assigned To"); + + //To check if no filter is available in "Assigned To" dropdown + cy.get(".empty-state").should("contain", "No filters found"); + + //Assigning a doctype to a user + cy.visit("/app/doctype/ToDo"); + cy.get(".form-assignments > .flex > .text-muted").click(); + cy.get_field("assign_to_me", "Check").click(); + cy.get(".modal-footer > .standard-actions > .btn-primary").click(); + cy.visit("/app/doctype"); + cy.click_sidebar_button("Assigned To"); + + //To check if filter is added in "Assigned To" dropdown after assignment + cy.get(".group-by-field.show > .dropdown-menu > .group-by-item > .dropdown-item").should( + "contain", + "1" + ); + + //To check if there is no filter added to the listview + cy.get(".filter-selector > .btn").should("contain", "Filter"); + + //To add a filter to display data into the listview + cy.get(".group-by-field.show > .dropdown-menu > .group-by-item > .dropdown-item").click(); + + //To check if filter is applied + cy.click_filter_button().should("contain", "1 filter"); + cy.get(".fieldname-select-area > .awesomplete > .form-control").should( + "have.value", + "Assigned To" + ); + cy.get(".condition").should("have.value", "like"); + cy.get(".filter-field > .form-group > .input-with-feedback").should( + "have.value", + `%${cy.config("testUser")}%` + ); + cy.click_filter_button(); + + //To remove the applied filter + cy.clear_filters(); + + //To remove the assignment + cy.visit("/app/doctype/ToDo"); + cy.get(".assignments > .avatar-group > .avatar > .avatar-frame").click(); + cy.get(".remove-btn").click({ force: true }); + cy.hide_dialog(); + cy.visit("/app/doctype"); + cy.click_sidebar_button("Assigned To"); + cy.get(".empty-state").should("contain", "No filters found"); + }); +}); diff --git a/cypress/integration/table_multiselect.js b/cypress/integration/table_multiselect.js new file mode 100644 index 0000000..29af5fe --- /dev/null +++ b/cypress/integration/table_multiselect.js @@ -0,0 +1,59 @@ +context("Table MultiSelect", () => { + before(() => { + cy.login(); + }); + + let name = "table multiselect" + Math.random().toString().slice(2, 8); + + it("select value from multiselect dropdown", () => { + cy.new_form("Assignment Rule"); + cy.fill_field("__newname", name); + cy.fill_field("document_type", "Blog Post"); + cy.get(".section-head").contains("Assignment Rules").scrollIntoView(); + cy.fill_field("assign_condition", 'status=="Open"', "Code"); + cy.get('input[data-fieldname="users"]').focus().as("input"); + cy.get('input[data-fieldname="users"] + ul').should("be.visible"); + cy.get("@input").type("test@influxerp", { delay: 100 }); + cy.wait(500); + cy.get("@input").type("{enter}"); + cy.get( + '.influxframework-control[data-fieldname="users"] .form-control .tb-selected-value .btn-link-to-form' + ).as("selected-value"); + cy.get("@selected-value").should("contain", "test@influxerp.com"); + + cy.intercept("POST", "/api/method/influxframework.desk.form.save.savedocs").as("save_form"); + // trigger save + cy.get(".primary-action").click(); + cy.wait("@save_form").its("response.statusCode").should("eq", 200); + cy.get("@selected-value").should("contain", "test@influxerp.com"); + }); + + it("delete value using backspace", () => { + cy.go_to_list("Assignment Rule"); + cy.get(`.list-subject:contains("table multiselect")`).last().find("a").click(); + cy.get('input[data-fieldname="users"]').focus().type("{backspace}"); + cy.get('.influxframework-control[data-fieldname="users"] .form-control .tb-selected-value').should( + "not.exist" + ); + }); + + it("delete value using x", () => { + cy.go_to_list("Assignment Rule"); + cy.get(`.list-subject:contains("table multiselect")`).last().find("a").click(); + cy.get('.influxframework-control[data-fieldname="users"] .form-control .tb-selected-value').as( + "existing_value" + ); + cy.get("@existing_value").find(".btn-remove").click(); + cy.get("@existing_value").should("not.exist"); + }); + + it("navigate to selected value", () => { + cy.go_to_list("Assignment Rule"); + cy.get(`.list-subject:contains("table multiselect")`).last().find("a").click(); + cy.get('.influxframework-control[data-fieldname="users"] .form-control .tb-selected-value').as( + "existing_value" + ); + cy.get("@existing_value").find(".btn-link-to-form").click(); + cy.location("pathname").should("contain", "/user/test@influxerp.com"); + }); +}); diff --git a/cypress/integration/theme_switcher_dialog.js b/cypress/integration/theme_switcher_dialog.js new file mode 100644 index 0000000..158ff3e --- /dev/null +++ b/cypress/integration/theme_switcher_dialog.js @@ -0,0 +1,29 @@ +context("Theme Switcher Shortcut", () => { + before(() => { + cy.login(); + cy.visit("/app"); + }); + beforeEach(() => { + cy.reload(); + }); + it("Check Toggle", () => { + cy.open_theme_dialog("{ctrl+shift+g}"); + cy.get(".modal-backdrop").should("exist"); + cy.get(".theme-grid > div").first().click(); + cy.close_theme("{ctrl+shift+g}"); + cy.get(".modal-backdrop").should("not.exist"); + }); + it("Check Enter", () => { + cy.open_theme_dialog("{ctrl+shift+g}"); + cy.get(".theme-grid > div").first().click(); + cy.close_theme("{enter}"); + cy.get(".modal-backdrop").should("not.exist"); + }); +}); + +Cypress.Commands.add("open_theme_dialog", (shortcut_keys) => { + cy.get("body").type(shortcut_keys); +}); +Cypress.Commands.add("close_theme", (shortcut_keys) => { + cy.get(".modal-header").type(shortcut_keys); +}); diff --git a/cypress/integration/timeline.js b/cypress/integration/timeline.js new file mode 100644 index 0000000..f090237 --- /dev/null +++ b/cypress/integration/timeline.js @@ -0,0 +1,91 @@ +import custom_submittable_doctype from "../fixtures/custom_submittable_doctype"; + +context("Timeline", () => { + before(() => { + cy.visit("/login"); + cy.login(); + }); + + it("Adding new ToDo, adding new comment, verifying comment addition & deletion and deleting ToDo", () => { + //Adding new ToDo + cy.visit("/app/todo/new-todo-1"); + cy.get('[data-fieldname="description"] .ql-editor.ql-blank') + .type("Test ToDo", { force: true }) + .wait(200); + cy.get(".page-head .page-actions").findByRole("button", { name: "Save" }).click(); + + cy.go_to_list("ToDo"); + cy.clear_filters(); + cy.click_listview_row_item(0); + + //To check if the comment box is initially empty and tying some text into it + cy.get('[data-fieldname="comment"] .ql-editor') + .should("contain", "") + .type("Testing Timeline"); + + //Adding new comment + cy.get(".comment-box").findByRole("button", { name: "Comment" }).click(); + + //To check if the commented text is visible in the timeline content + cy.get(".timeline-content").should("contain", "Testing Timeline"); + + //Editing comment + cy.click_timeline_action_btn("Edit"); + cy.get('.timeline-content [data-fieldname="comment"] .ql-editor').first().type(" 123"); + cy.click_timeline_action_btn("Save"); + + //To check if the edited comment text is visible in timeline content + cy.get(".timeline-content").should("contain", "Testing Timeline 123"); + + //Discarding comment + cy.click_timeline_action_btn("Edit"); + cy.click_timeline_action_btn("Dismiss"); + + //To check if after discarding the timeline content is same as previous + cy.get(".timeline-content").should("contain", "Testing Timeline 123"); + + //Deleting the added comment + cy.get(".timeline-message-box .more-actions > .action-btn").click(); //Menu button in timeline item + cy.get(".timeline-message-box .more-actions .dropdown-item") + .contains("Delete") + .click({ force: true }); + cy.get_open_dialog().findByRole("button", { name: "Yes" }).click({ force: true }); + + cy.get(".timeline-content").should("not.contain", "Testing Timeline 123"); + }); + + it("Timeline should have submit and cancel activity information", () => { + cy.visit("/app/doctype"); + + //Creating custom doctype + cy.insert_doc("DocType", custom_submittable_doctype, true); + + cy.visit("/app/custom-submittable-doctype"); + cy.click_listview_primary_button("Add Custom Submittable DocType"); + + //Adding a new entry for the created custom doctype + cy.fill_field("title", "Test"); + cy.click_modal_primary_button("Save"); + cy.click_modal_primary_button("Submit"); + + cy.visit("/app/custom-submittable-doctype"); + cy.click_listview_row_item(0); + + //To check if the submission of the documemt is visible in the timeline content + cy.get(".timeline-content").should("contain", "InfluxFramework submitted this document"); + cy.get('[id="page-Custom Submittable DocType"] .page-actions') + .findByRole("button", { name: "Cancel" }) + .click(); + cy.get_open_dialog().findByRole("button", { name: "Yes" }).click(); + + //To check if the cancellation of the documemt is visible in the timeline content + cy.get(".timeline-content").should("contain", "InfluxFramework cancelled this document"); + + //Deleting the document + cy.visit("/app/custom-submittable-doctype"); + cy.select_listview_row_checkbox(0); + cy.get(".page-actions").findByRole("button", { name: "Actions" }).click(); + cy.get('.page-actions .actions-btn-group [data-label="Delete"]').click(); + cy.click_modal_primary_button("Yes"); + }); +}); diff --git a/cypress/integration/url_data_field.js b/cypress/integration/url_data_field.js new file mode 100644 index 0000000..8e28718 --- /dev/null +++ b/cypress/integration/url_data_field.js @@ -0,0 +1,42 @@ +import data_field_validation_doctype from "../fixtures/data_field_validation_doctype"; + +const doctype_name = data_field_validation_doctype.name; + +context("URL Data Field Input", () => { + before(() => { + cy.login(); + cy.visit("/app/website"); + return cy.insert_doc("DocType", data_field_validation_doctype, true); + }); + + describe("URL Data Field Input ", () => { + it("should not show URL link button without focus", () => { + cy.new_form(doctype_name); + cy.get_field("url").clear().type("https://influxframework.io"); + cy.get_field("url").blur().wait(500); + cy.get(".link-btn").should("not.be.visible"); + }); + + it("should show URL link button on focus", () => { + cy.get_field("url").focus().wait(500); + cy.get(".link-btn").should("be.visible"); + }); + + it("should not show URL link button for invalid URL", () => { + cy.get_field("url").clear().type("fuzzbuzz"); + cy.get(".link-btn").should("not.be.visible"); + }); + + it("should have valid URL link with target _blank", () => { + cy.get_field("url").clear().type("https://influxframework.io"); + cy.get(".link-btn .btn-open").should("have.attr", "href", "https://influxframework.io"); + cy.get(".link-btn .btn-open").should("have.attr", "target", "_blank"); + }); + + it("should inject anchor tag in read-only URL data field", () => { + cy.get('[data-fieldname="read_only_url"]') + .find("a") + .should("have.attr", "target", "_blank"); + }); + }); +}); diff --git a/cypress/integration/view_routing.js b/cypress/integration/view_routing.js new file mode 100644 index 0000000..217a361 --- /dev/null +++ b/cypress/integration/view_routing.js @@ -0,0 +1,231 @@ +context("View", () => { + before(() => { + cy.login(); + cy.visit("/app/website"); + }); + + it("Route to ToDo List View", () => { + cy.visit("/app/todo/view/list"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("List"); + }); + }); + + it("Route to ToDo Report View", () => { + cy.visit("/app/todo/view/report"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("Report"); + }); + }); + + it("Route to ToDo Dashboard View", () => { + cy.visit("/app/todo/view/dashboard"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("Dashboard"); + }); + }); + + it("Route to ToDo Gantt View", () => { + cy.visit("/app/todo/view/gantt"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("Gantt"); + }); + }); + + it("Route to ToDo Kanban View", () => { + cy.call("influxframework.tests.ui_test_helpers.create_kanban").then(() => { + cy.visit("/app/note/view/kanban/_Note _Kanban"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("Kanban"); + }); + }); + }); + + it("Route to ToDo Calendar View", () => { + cy.visit("/app/todo/view/calendar"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("Calendar"); + }); + }); + + it("Route to Custom Tree View", () => { + cy.call("influxframework.tests.ui_test_helpers.setup_tree_doctype").then(() => { + cy.visit("/app/custom-tree/view/tree"); + cy.wait(500); + cy.window() + .its("cur_tree") + .then((list) => { + expect(list.view_name).to.equal("Tree"); + }); + }); + }); + + it("Route to Custom Image View", () => { + cy.call("influxframework.tests.ui_test_helpers.setup_image_doctype").then(() => { + cy.visit("app/custom-image/view/image"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("Image"); + }); + }); + }); + + it("Route to Communication Inbox View", () => { + cy.call("influxframework.tests.ui_test_helpers.setup_inbox").then(() => { + cy.visit("app/communication/view/inbox"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("Inbox"); + }); + }); + }); + + it("Route to File View", () => { + cy.visit("app/file"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("File"); + expect(list.current_folder).to.equal("Home"); + }); + + cy.visit("app/file/view/home/Attachments"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("File"); + expect(list.current_folder).to.equal("Home/Attachments"); + }); + }); + + it("Re-route to default view", () => { + cy.call("influxframework.tests.ui_test_helpers.setup_default_view", { view: "Report" }).then(() => { + cy.visit("app/event"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("Report"); + }); + }); + }); + + it("Route to default view from app/{doctype}", () => { + cy.call("influxframework.tests.ui_test_helpers.setup_default_view", { view: "Report" }).then(() => { + cy.visit("/app/event"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("Report"); + }); + }); + }); + + it("Route to default view from app/{doctype}/view", () => { + cy.call("influxframework.tests.ui_test_helpers.setup_default_view", { view: "Report" }).then(() => { + cy.visit("/app/event/view"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("Report"); + }); + }); + }); + + it("Force Route to default view from app/{doctype}", () => { + cy.call("influxframework.tests.ui_test_helpers.setup_default_view", { + view: "Report", + force_reroute: true, + }).then(() => { + cy.visit("/app/event"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("Report"); + }); + }); + }); + + it("Force Route to default view from app/{doctype}/view", () => { + cy.call("influxframework.tests.ui_test_helpers.setup_default_view", { + view: "Report", + force_reroute: true, + }).then(() => { + cy.visit("/app/event/view"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("Report"); + }); + }); + }); + + it("Force Route to default view from app/{doctype}/view", () => { + cy.call("influxframework.tests.ui_test_helpers.setup_default_view", { + view: "Report", + force_reroute: true, + }).then(() => { + cy.visit("/app/event/view/list"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("Report"); + }); + }); + }); + + it("Validate Route History for Default View", () => { + cy.call("influxframework.tests.ui_test_helpers.setup_default_view", { view: "Report" }).then(() => { + cy.visit("/app/event"); + cy.visit("/app/event/view/list"); + cy.location("pathname").should("eq", "/app/event/view/list"); + cy.go("back"); + cy.location("pathname").should("eq", "/app/event"); + }); + }); + + it("Route to Form", () => { + cy.call("influxframework.tests.ui_test_helpers.create_note").then(() => { + cy.visit("/app/note/Routing Test"); + cy.window() + .its("cur_frm") + .then((frm) => { + expect(frm.doc.title).to.equal("Routing Test"); + }); + }); + }); + + it("Route to Settings Workspace", () => { + cy.visit("/app/settings"); + cy.get(".title-text").should("contain", "Settings"); + }); +}); diff --git a/cypress/integration/web_form.js b/cypress/integration/web_form.js new file mode 100644 index 0000000..8bdf055 --- /dev/null +++ b/cypress/integration/web_form.js @@ -0,0 +1,271 @@ +context("Web Form", () => { + before(() => { + cy.login("Administrator"); + cy.visit("/app/"); + return cy + .window() + .its("influxframework") + .then((influxframework) => { + return influxframework.xcall("influxframework.tests.ui_test_helpers.clear_notes"); + }); + }); + + it("Create Web Form", () => { + cy.visit("/app/web-form/new"); + + cy.intercept("POST", "/api/method/influxframework.desk.form.save.savedocs").as("save_form"); + + cy.fill_field("title", "Note"); + cy.fill_field("doc_type", "Note", "Link"); + cy.fill_field("module", "Website", "Link"); + cy.click_custom_action_button("Get Fields"); + cy.click_custom_action_button("Publish"); + + cy.wait("@save_form"); + + cy.get_field("route").should("have.value", "note"); + cy.get(".title-area .indicator-pill").contains("Published"); + }); + + it("Open Web Form", () => { + cy.visit("/note"); + cy.fill_field("title", "Note 1"); + cy.get(".web-form-actions button").contains("Save").click(); + + cy.url().should("include", "/note/new"); + + cy.request("/api/method/logout"); + cy.visit("/note"); + + cy.url().should("include", "/note/new"); + + cy.fill_field("title", "Guest Note 1"); + cy.get(".web-form-actions button").contains("Save").click(); + + cy.url().should("include", "/note/new"); + + cy.visit("/note"); + cy.url().should("include", "/note/new"); + }); + + it("Login Required", () => { + cy.login("Administrator"); + cy.visit("/app/web-form/note"); + + cy.findByRole("tab", { name: "Settings" }).click(); + cy.get('input[data-fieldname="login_required"]').check({ force: true }); + + cy.save(); + + cy.visit("/note"); + + cy.call("logout"); + + cy.visit("/note"); + cy.get_open_dialog() + .get(".modal-message") + .contains("You are not permitted to access this page without login."); + }); + + it("Show List", () => { + cy.login("Administrator"); + cy.visit("/app/web-form/note"); + + cy.findByRole("tab", { name: "Settings" }).click(); + cy.get(".section-head").contains("List Settings").click(); + cy.get('input[data-fieldname="show_list"]').check(); + + cy.save(); + + cy.visit("/note"); + cy.url().should("include", "/note/list"); + cy.get(".web-list-table").should("be.visible"); + }); + + it("Show Custom List Title", () => { + cy.visit("/app/web-form/note"); + + cy.findByRole("tab", { name: "Settings" }).click(); + cy.fill_field("list_title", "Note List"); + + cy.save(); + + cy.visit("/note"); + cy.url().should("include", "/note/list"); + cy.get(".web-list-header h1").should("contain.text", "Note List"); + }); + + it("Show Custom List Columns", () => { + cy.visit("/note"); + cy.url().should("include", "/note/list"); + + cy.get(".web-list-table thead th").contains("Name"); + cy.get(".web-list-table thead th").contains("Title"); + + cy.visit("/app/web-form/note"); + + cy.findByRole("tab", { name: "Settings" }).click(); + + cy.get('[data-fieldname="list_columns"] .grid-footer button') + .contains("Add Row") + .as("add-row"); + + cy.get("@add-row").click(); + cy.get('[data-fieldname="list_columns"] .grid-body .rows').as("grid-rows"); + cy.get("@grid-rows").find('.grid-row:first [data-fieldname="fieldname"]').click(); + cy.get("@grid-rows") + .find('.grid-row:first select[data-fieldname="fieldname"]') + .select("Title"); + + cy.get("@add-row").click(); + cy.get("@grid-rows").find('.grid-row[data-idx="2"] [data-fieldname="fieldname"]').click(); + cy.get("@grid-rows") + .find('.grid-row[data-idx="2"] select[data-fieldname="fieldname"]') + .select("Public"); + + cy.get("@add-row").click(); + cy.get("@grid-rows").find('.grid-row:last [data-fieldname="fieldname"]').click(); + cy.get("@grid-rows") + .find('.grid-row:last select[data-fieldname="fieldname"]') + .select("Content"); + + cy.save(); + + cy.visit("/note"); + cy.url().should("include", "/note/list"); + cy.get(".web-list-table thead th").contains("Title"); + cy.get(".web-list-table thead th").contains("Public"); + cy.get(".web-list-table thead th").contains("Content"); + }); + + it("Breadcrumbs", () => { + cy.visit("/note/Note 1"); + cy.get(".breadcrumb-container .breadcrumb .breadcrumb-item:first a") + .should("contain.text", "Note") + .click(); + cy.url().should("include", "/note/list"); + }); + + it("Custom Breadcrumbs", () => { + cy.visit("/app/web-form/note"); + + cy.findByRole("tab", { name: "Customization" }).click(); + cy.fill_field("breadcrumbs", '[{"label": _("Notes"), "route":"note"}]', "Code"); + cy.get(".form-tabs .nav-item .nav-link").contains("Customization").click(); + cy.save(); + + cy.visit("/note/Note 1"); + cy.get(".breadcrumb-container .breadcrumb .breadcrumb-item:first a").should( + "contain.text", + "Notes" + ); + }); + + it("Read Only", () => { + cy.login("Administrator"); + cy.visit("/note"); + cy.url().should("include", "/note/list"); + + // Read Only Field + cy.get('.web-list-table tbody tr[id="Note 1"]').click(); + cy.get('.influxframework-control[data-fieldname="title"] .control-input').should( + "have.css", + "display", + "none" + ); + }); + + it("Edit Mode", () => { + cy.visit("/app/web-form/note"); + + cy.findByRole("tab", { name: "Settings" }).click(); + cy.get('input[data-fieldname="allow_edit"]').check(); + + cy.save(); + + cy.visit("/note/Note 1"); + cy.url().should("include", "/note/Note%201"); + + cy.get(".web-form-actions a").contains("Edit Response").click(); + cy.url().should("include", "/note/Note%201/edit"); + + // Editable Field + cy.get_field("title").should("have.value", "Note 1"); + + cy.fill_field("title", " Edited"); + cy.get(".web-form-actions button").contains("Save").click(); + cy.get(".success-page .edit-button").click(); + cy.get_field("title").should("have.value", "Note 1 Edited"); + }); + + it("Allow Multiple Response", () => { + cy.visit("/app/web-form/note"); + + cy.findByRole("tab", { name: "Settings" }).click(); + cy.get('input[data-fieldname="allow_multiple"]').check(); + + cy.save(); + + cy.visit("/note"); + cy.url().should("include", "/note/list"); + + cy.get(".web-list-actions a:visible").contains("New").click(); + cy.url().should("include", "/note/new"); + + cy.fill_field("title", "Note 2"); + cy.get(".web-form-actions button").contains("Save").click(); + }); + + it("Allow Delete", () => { + cy.visit("/app/web-form/note"); + + cy.findByRole("tab", { name: "Settings" }).click(); + cy.get('input[data-fieldname="allow_delete"]').check(); + + cy.save(); + + cy.visit("/note"); + cy.url().should("include", "/note/list"); + + cy.get('.web-list-table tbody tr[id="Note 1"] .list-col-checkbox input').click(); + cy.get('.web-list-table tbody tr[id="Note 2"] .list-col-checkbox input').click(); + cy.get(".web-list-actions button:visible").contains("Delete").click({ force: true }); + + cy.get(".web-list-actions button").contains("Delete").should("not.be.visible"); + + cy.visit("/note"); + cy.get('.web-list-table tbody tr[id="Note 1"]').should("not.exist"); + cy.get('.web-list-table tbody tr[id="Note 2"]').should("not.exist"); + cy.get('.web-list-table tbody tr[id="Guest Note 1"]').should("exist"); + }); + + it("Navigate and Submit a WebForm", () => { + cy.visit("/update-profile"); + + cy.get(".web-form-actions a").contains("Edit Response").click(); + + cy.fill_field("middle_name", "_Test User"); + + cy.get(".web-form-actions .btn-primary").click(); + cy.url().should("include", "/me"); + }); + + it("Navigate and Submit a MultiStep WebForm", () => { + cy.call("influxframework.tests.ui_test_helpers.update_webform_to_multistep").then(() => { + cy.visit("/update-profile-duplicate"); + + cy.get(".web-form-actions a").contains("Edit Response").click(); + + cy.fill_field("middle_name", "_Test User"); + + cy.get(".btn-next").should("be.visible"); + cy.get(".btn-next").click(); + + cy.get(".btn-previous").should("be.visible"); + cy.get(".btn-next").should("not.be.visible"); + + cy.get(".web-form-actions .btn-primary").click(); + cy.url().should("include", "/me"); + }); + }); +}); diff --git a/cypress/integration/workspace.js b/cypress/integration/workspace.js new file mode 100644 index 0000000..7aad24e --- /dev/null +++ b/cypress/integration/workspace.js @@ -0,0 +1,214 @@ +context("Workspace 2.0", () => { + before(() => { + cy.visit("/login"); + cy.login(); + }); + + it("Navigate to page from sidebar", () => { + cy.visit("/app/build"); + cy.get(".codex-editor__redactor .ce-block"); + cy.get('.sidebar-item-container[item-name="Settings"]').first().click(); + cy.location("pathname").should("eq", "/app/settings"); + }); + + it("Create Private Page", () => { + cy.intercept({ + method: "POST", + url: "api/method/influxframework.desk.doctype.workspace.workspace.new_page", + }).as("new_page"); + + cy.get(".codex-editor__redactor .ce-block"); + cy.get('.custom-actions button[data-label="Create%20Workspace"]').click(); + cy.fill_field("title", "Test Private Page", "Data"); + cy.fill_field("icon", "edit", "Icon"); + cy.get_open_dialog().find(".modal-header").click(); + cy.get_open_dialog().find(".btn-primary").click(); + + // check if sidebar item is added in pubic section + cy.get('.sidebar-item-container[item-name="Test Private Page"]').should( + "have.attr", + "item-public", + "0" + ); + + cy.get('.standard-actions .btn-primary[data-label="Save"]').click(); + cy.wait(300); + cy.get('.sidebar-item-container[item-name="Test Private Page"]').should( + "have.attr", + "item-public", + "0" + ); + + cy.wait("@new_page"); + }); + + it("Create Child Page", () => { + cy.intercept({ + method: "POST", + url: "api/method/influxframework.desk.doctype.workspace.workspace.new_page", + }).as("new_page"); + + cy.get(".codex-editor__redactor .ce-block"); + cy.get('.custom-actions button[data-label="Create%20Workspace"]').click(); + cy.fill_field("title", "Test Child Page", "Data"); + cy.fill_field("parent", "Test Private Page", "Select"); + cy.fill_field("icon", "edit", "Icon"); + cy.get_open_dialog().find(".modal-header").click(); + cy.get_open_dialog().find(".btn-primary").click(); + + // check if sidebar item is added in pubic section + cy.get('.sidebar-item-container[item-name="Test Child Page"]').should( + "have.attr", + "item-public", + "0" + ); + + cy.get('.standard-actions .btn-primary[data-label="Save"]').click(); + cy.wait(300); + cy.get('.sidebar-item-container[item-name="Test Child Page"]').should( + "have.attr", + "item-public", + "0" + ); + + cy.wait("@new_page"); + }); + + it("Duplicate Page", () => { + cy.intercept({ + method: "POST", + url: "api/method/influxframework.desk.doctype.workspace.workspace.duplicate_page", + }).as("page_duplicated"); + + cy.get(".codex-editor__redactor .ce-block"); + cy.get(".standard-actions .btn-secondary[data-label=Edit]").click(); + + cy.get('.sidebar-item-container[item-name="Test Private Page"]').as("sidebar-item"); + + cy.get("@sidebar-item").find(".standard-sidebar-item").first().click(); + cy.get("@sidebar-item").find(".dropdown-btn").first().click(); + cy.get("@sidebar-item") + .find(".dropdown-list .dropdown-item") + .contains("Duplicate") + .first() + .click({ force: true }); + + cy.get_open_dialog().fill_field("title", "Duplicate Page", "Data"); + cy.click_modal_primary_button("Duplicate"); + + cy.wait("@page_duplicated"); + }); + + it("Drag Sidebar Item", () => { + cy.intercept({ + method: "POST", + url: "api/method/influxframework.desk.doctype.workspace.workspace.sort_pages", + }).as("page_sorted"); + + cy.get('.sidebar-item-container[item-name="Duplicate Page"]').as("sidebar-item"); + + cy.get("@sidebar-item").find(".standard-sidebar-item").first().click(); + cy.get("@sidebar-item").find(".drag-handle").first().move({ deltaX: 0, deltaY: 100 }); + + cy.get('.sidebar-item-container[item-name="Build"]').as("sidebar-item"); + + cy.get("@sidebar-item").find(".standard-sidebar-item").first().click(); + cy.get("@sidebar-item").find(".drag-handle").first().move({ deltaX: 0, deltaY: 100 }); + + cy.wait("@page_sorted"); + }); + + it("Edit Page Detail", () => { + cy.intercept({ + method: "POST", + url: "api/method/influxframework.desk.doctype.workspace.workspace.update_page", + }).as("page_updated"); + + cy.get('.sidebar-item-container[item-name="Test Private Page"]').as("sidebar-item"); + + cy.get("@sidebar-item").find(".standard-sidebar-item").first().click(); + cy.get("@sidebar-item").find(".dropdown-btn").first().click(); + cy.get("@sidebar-item") + .find(".dropdown-list .dropdown-item") + .contains("Edit") + .first() + .click({ force: true }); + + cy.get_open_dialog().fill_field("title", " 1", "Data"); + cy.get_open_dialog().find('input[data-fieldname="is_public"]').check(); + cy.click_modal_primary_button("Update"); + + cy.get( + '.standard-sidebar-section:first .sidebar-item-container[item-name="Test Private Page"]' + ).should("not.exist"); + cy.get( + '.standard-sidebar-section:last .sidebar-item-container[item-name="Test Private Page 1"]' + ).should("exist"); + + cy.wait("@page_updated"); + }); + + it("Add New Block", () => { + cy.get('.sidebar-item-container[item-name="Duplicate Page"]').as("sidebar-item"); + + cy.get("@sidebar-item").find(".standard-sidebar-item").first().click(); + + cy.get(".ce-block").click().type("{enter}"); + cy.get(".block-list-container .block-list-item").contains("Heading").click(); + cy.get(":focus").type("Header"); + cy.get(".ce-block:last").find(".ce-header").should("exist"); + + cy.get(".ce-block:last").click().type("{enter}"); + cy.get(".block-list-container .block-list-item").contains("Text").click(); + cy.get(":focus").type("Paragraph text"); + cy.get(".ce-block:last").find(".ce-paragraph").should("exist"); + }); + + it("Delete A Block", () => { + cy.get(":focus").click(); + cy.get(".paragraph-control .setting-btn").click(); + cy.get(".paragraph-control .dropdown-item").contains("Delete").click(); + cy.get(".ce-block:last").find(".ce-paragraph").should("not.exist"); + }); + + it("Shrink and Expand A Block", () => { + cy.get(":focus").click(); + cy.get(".ce-block:last .setting-btn").click(); + cy.get(".ce-block:last .dropdown-item").contains("Shrink").click(); + cy.get(".ce-block:last").should("have.class", "col-xs-11"); + cy.get(".ce-block:last .dropdown-item").contains("Shrink").click(); + cy.get(".ce-block:last").should("have.class", "col-xs-10"); + cy.get(".ce-block:last .dropdown-item").contains("Shrink").click(); + cy.get(".ce-block:last").should("have.class", "col-xs-9"); + cy.get(".ce-block:last .dropdown-item").contains("Expand").click(); + cy.get(".ce-block:last").should("have.class", "col-xs-10"); + cy.get(".ce-block:last .dropdown-item").contains("Expand").click(); + cy.get(".ce-block:last").should("have.class", "col-xs-11"); + cy.get(".ce-block:last .dropdown-item").contains("Expand").click(); + cy.get(".ce-block:last").should("have.class", "col-xs-12"); + + cy.get('.standard-actions .btn-primary[data-label="Save"]').click(); + }); + + it("Delete Duplicate Page", () => { + cy.intercept({ + method: "POST", + url: "api/method/influxframework.desk.doctype.workspace.workspace.delete_page", + }).as("page_deleted"); + + cy.get(".codex-editor__redactor .ce-block"); + cy.get(".standard-actions .btn-secondary[data-label=Edit]").click(); + + cy.get('.sidebar-item-container[item-name="Duplicate Page"]') + .find(".sidebar-item-control .setting-btn") + .click(); + cy.get('.sidebar-item-container[item-name="Duplicate Page"]') + .find('.dropdown-item[title="Delete Workspace"]') + .click({ force: true }); + cy.wait(300); + cy.get(".modal-footer > .standard-actions > .btn-modal-primary:visible").first().click(); + cy.get('.sidebar-item-container[item-name="Duplicate Page"]').should("not.exist"); + + cy.wait("@page_deleted"); + }); +}); diff --git a/cypress/integration/workspace_blocks.js b/cypress/integration/workspace_blocks.js new file mode 100644 index 0000000..5fedd49 --- /dev/null +++ b/cypress/integration/workspace_blocks.js @@ -0,0 +1,151 @@ +context("Workspace Blocks", () => { + before(() => { + cy.login(); + cy.visit("/app"); + return cy + .window() + .its("influxframework") + .then((influxframework) => { + return influxframework.xcall("influxframework.tests.ui_test_helpers.setup_workflow"); + }); + }); + + it("Create Test Page", () => { + cy.intercept({ + method: "POST", + url: "api/method/influxframework.desk.doctype.workspace.workspace.new_page", + }).as("new_page"); + + cy.visit("/app/website"); + cy.get(".codex-editor__redactor .ce-block"); + cy.get('.custom-actions button[data-label="Create%20Workspace"]').click(); + cy.fill_field("title", "Test Block Page", "Data"); + cy.fill_field("icon", "edit", "Icon"); + cy.get_open_dialog().find(".modal-header").click(); + cy.get_open_dialog().find(".btn-primary").click(); + + // check if sidebar item is added in private section + cy.get('.sidebar-item-container[item-name="Test Block Page"]').should( + "have.attr", + "item-public", + "0" + ); + + cy.get('.standard-actions .btn-primary[data-label="Save"]').click(); + cy.wait(300); + cy.get('.sidebar-item-container[item-name="Test Block Page"]').should( + "have.attr", + "item-public", + "0" + ); + + cy.wait("@new_page"); + }); + + it("Quick List Block", () => { + cy.create_records([ + { + doctype: "ToDo", + description: "Quick List ToDo 1", + status: "Open", + }, + { + doctype: "ToDo", + description: "Quick List ToDo 2", + status: "Open", + }, + { + doctype: "ToDo", + description: "Quick List ToDo 3", + status: "Open", + }, + { + doctype: "ToDo", + description: "Quick List ToDo 4", + status: "Open", + }, + ]); + + cy.intercept({ + method: "GET", + url: "api/method/influxframework.desk.form.load.getdoctype?**", + }).as("get_doctype"); + + cy.get(".codex-editor__redactor .ce-block"); + cy.get(".standard-actions .btn-secondary[data-label=Edit]").click(); + + // test quick list creation + cy.get(".ce-block").first().click({ force: true }).type("{enter}"); + cy.get(".block-list-container .block-list-item").contains("Quick List").click(); + + cy.fill_field("label", "ToDo", "Data"); + cy.fill_field("document_type", "ToDo", "Link").blur(); + cy.wait("@get_doctype"); + + cy.get_open_dialog().find(".filter-edit-area").should("contain", "No filters selected"); + cy.get_open_dialog().find(".filter-area .add-filter").click(); + + cy.get_open_dialog() + .find(".fieldname-select-area input") + .type("Workflow State{enter}") + .blur(); + cy.get_open_dialog().find(".filter-field .input-with-feedback").type("Pending"); + + cy.get_open_dialog().find(".modal-header").click(); + cy.get_open_dialog().find(".btn-primary").click(); + + cy.get('.standard-actions .btn-primary[data-label="Save"]').click(); + + cy.get(".codex-editor__redactor .ce-block"); + + cy.get(".ce-block .quick-list-widget-box").first().as("todo-quick-list"); + + cy.get("@todo-quick-list").find(".quick-list-item .status").should("contain", "Pending"); + + // test quick-list-item + cy.get("@todo-quick-list") + .find(".quick-list-item .title") + .first() + .invoke("attr", "title") + .then((title) => { + cy.get("@todo-quick-list").find(".quick-list-item").contains(title).click(); + cy.get_field("description", "Text Editor").should("contain", title); + cy.click_action_button("Approve"); + }); + cy.go("back"); + + // test filter-list + cy.get("@todo-quick-list").realHover().find(".widget-control .filter-list").click(); + + cy.get_open_dialog() + .find(".filter-field .input-with-feedback") + .focus() + .type("{selectall}Approved"); + cy.get_open_dialog().find(".modal-header").click(); + cy.get_open_dialog().find(".btn-primary").click(); + + cy.get("@todo-quick-list").find(".quick-list-item .status").should("contain", "Approved"); + + // test refresh-list + cy.intercept({ + method: "POST", + url: "api/method/influxframework.desk.reportview.get", + }).as("refresh-list"); + + cy.get("@todo-quick-list").realHover().find(".widget-control .refresh-list").click(); + cy.wait("@refresh-list"); + + // test add-new + cy.get("@todo-quick-list").realHover().find(".widget-control .add-new").click(); + cy.url().should("include", `/todo/new-todo-1`); + cy.go("back"); + + // test see-all + cy.get("@todo-quick-list").find(".widget-footer .see-all").click(); + cy.open_list_filter(); + cy.get('.filter-field input[data-fieldname="workflow_state"]') + .invoke("val") + .should("eq", "Pending"); + cy.go("back"); + }); +}); diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js new file mode 100644 index 0000000..b132753 --- /dev/null +++ b/cypress/plugins/index.js @@ -0,0 +1,17 @@ +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +module.exports = (on, config) => { + require("@cypress/code-coverage/task")(on, config); + return config; +}; diff --git a/cypress/support/commands.js b/cypress/support/commands.js new file mode 100644 index 0000000..54347eb --- /dev/null +++ b/cypress/support/commands.js @@ -0,0 +1,483 @@ +import "@testing-library/cypress/add-commands"; +import "@4tw/cypress-drag-drop"; +import "cypress-real-events/support"; +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add("login", (email, password) => { ... }); +// +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }); +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }); +// +// +// -- This is will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }); + +Cypress.Commands.add("login", (email, password) => { + if (!email) { + email = Cypress.config("testUser") || "Administrator"; + } + if (!password) { + password = Cypress.env("adminPassword"); + } + cy.request({ + url: "/api/method/login", + method: "POST", + body: { + usr: email, + pwd: password, + }, + }); +}); + +Cypress.Commands.add("call", (method, args) => { + return cy + .window() + .its("influxframework.csrf_token") + .then((csrf_token) => { + return cy + .request({ + url: `/api/method/${method}`, + method: "POST", + body: args, + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "X-InfluxFramework-CSRF-Token": csrf_token, + }, + }) + .then((res) => { + expect(res.status).eq(200); + return res.body; + }); + }); +}); + +Cypress.Commands.add("get_list", (doctype, fields = [], filters = []) => { + filters = JSON.stringify(filters); + fields = JSON.stringify(fields); + let url = `/api/resource/${doctype}?fields=${fields}&filters=${filters}`; + return cy + .window() + .its("influxframework.csrf_token") + .then((csrf_token) => { + return cy + .request({ + method: "GET", + url, + headers: { + Accept: "application/json", + "X-InfluxFramework-CSRF-Token": csrf_token, + }, + }) + .then((res) => { + expect(res.status).eq(200); + return res.body; + }); + }); +}); + +Cypress.Commands.add("get_doc", (doctype, name) => { + return cy + .window() + .its("influxframework.csrf_token") + .then((csrf_token) => { + return cy + .request({ + method: "GET", + url: `/api/resource/${doctype}/${name}`, + headers: { + Accept: "application/json", + "X-InfluxFramework-CSRF-Token": csrf_token, + }, + }) + .then((res) => { + expect(res.status).eq(200); + return res.body; + }); + }); +}); + +Cypress.Commands.add("remove_doc", (doctype, name) => { + return cy + .window() + .its("influxframework.csrf_token") + .then((csrf_token) => { + return cy + .request({ + method: "DELETE", + url: `/api/resource/${doctype}/${name}`, + headers: { + Accept: "application/json", + "X-InfluxFramework-CSRF-Token": csrf_token, + }, + }) + .then((res) => { + expect(res.status).eq(202); + return res.body; + }); + }); +}); + +Cypress.Commands.add("create_records", (doc) => { + return cy + .call("influxframework.tests.ui_test_helpers.create_if_not_exists", { doc: JSON.stringify(doc) }) + .then((r) => r.message); +}); + +Cypress.Commands.add("set_value", (doctype, name, obj) => { + return cy.call("influxframework.client.set_value", { + doctype, + name, + fieldname: obj, + }); +}); + +Cypress.Commands.add("fill_field", (fieldname, value, fieldtype = "Data") => { + cy.get_field(fieldname, fieldtype).as("input"); + + if (["Date", "Time", "Datetime"].includes(fieldtype)) { + cy.get("@input").click().wait(200); + cy.get(".datepickers-container .datepicker.active").should("exist"); + } + if (fieldtype === "Time") { + cy.get("@input").clear().wait(200); + } + + if (fieldtype === "Select") { + cy.get("@input").select(value); + } else { + cy.get("@input").type(value, { + waitForAnimations: false, + parseSpecialCharSequences: false, + force: true, + delay: 100, + }); + } + return cy.get("@input"); +}); + +Cypress.Commands.add("get_field", (fieldname, fieldtype = "Data") => { + let field_element = fieldtype === "Select" ? "select" : "input"; + let selector = `[data-fieldname="${fieldname}"] ${field_element}:visible`; + + if (fieldtype === "Text Editor") { + selector = `[data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]:visible`; + } + if (fieldtype === "Code") { + selector = `[data-fieldname="${fieldname}"] .ace_text-input`; + } + if (fieldtype === "Markdown Editor") { + selector = `[data-fieldname="${fieldname}"] .ace-editor-target`; + } + + return cy.get(selector).first(); +}); + +Cypress.Commands.add( + "fill_table_field", + (tablefieldname, row_idx, fieldname, value, fieldtype = "Data") => { + cy.get_table_field(tablefieldname, row_idx, fieldname, fieldtype).as("input"); + + if (["Date", "Time", "Datetime"].includes(fieldtype)) { + cy.get("@input").click().wait(200); + cy.get(".datepickers-container .datepicker.active").should("exist"); + } + if (fieldtype === "Time") { + cy.get("@input").clear().wait(200); + } + + if (fieldtype === "Select") { + cy.get("@input").select(value); + } else { + cy.get("@input").type(value, { waitForAnimations: false, force: true }); + } + return cy.get("@input"); + } +); + +Cypress.Commands.add( + "get_table_field", + (tablefieldname, row_idx, fieldname, fieldtype = "Data") => { + let selector = `.influxframework-control[data-fieldname="${tablefieldname}"]`; + selector += ` [data-idx="${row_idx}"]`; + + if (fieldtype === "Text Editor") { + selector += ` [data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]`; + } else if (fieldtype === "Code") { + selector += ` [data-fieldname="${fieldname}"] .ace_text-input`; + } else { + selector += ` [data-fieldname="${fieldname}"]`; + return cy.get(selector).find(".form-control:visible, .static-area:visible").first(); + } + return cy.get(selector); + } +); + +Cypress.Commands.add("awesomebar", (text) => { + cy.get("#navbar-search").type(`${text}{downarrow}{enter}`, { delay: 700 }); +}); + +Cypress.Commands.add("new_form", (doctype) => { + let dt_in_route = doctype.toLowerCase().replace(/ /g, "-"); + cy.visit(`/app/${dt_in_route}/new`); + cy.get("body").should("have.attr", "data-route", `Form/${doctype}/new-${dt_in_route}-1`); + cy.get("body").should("have.attr", "data-ajax-state", "complete"); +}); + +Cypress.Commands.add("go_to_list", (doctype) => { + let dt_in_route = doctype.toLowerCase().replace(/ /g, "-"); + cy.visit(`/app/${dt_in_route}`); +}); + +Cypress.Commands.add("clear_cache", () => { + cy.window() + .its("influxframework") + .then((influxframework) => { + influxframework.ui.toolbar.clear_cache(); + }); +}); + +Cypress.Commands.add("dialog", (opts) => { + return cy + .window({ log: false }) + .its("influxframework", { log: false }) + .then((influxframework) => { + Cypress.log({ + name: "dialog", + displayName: "dialog", + message: "influxframework.ui.Dialog", + consoleProps: () => { + return { + options: opts, + dialog: d, + }; + }, + }); + + var d = new influxframework.ui.Dialog(opts); + d.show(); + return d; + }); +}); + +Cypress.Commands.add("get_open_dialog", () => { + return cy.get(".modal:visible").last(); +}); + +Cypress.Commands.add("save", () => { + cy.intercept("/api/method/influxframework.desk.form.save.savedocs").as("save_call"); + cy.get(`button[data-label="Save"]:visible`).click({ scrollBehavior: "top", force: true }); + cy.wait("@save_call"); +}); +Cypress.Commands.add("hide_dialog", () => { + cy.wait(500); + cy.get_open_dialog().focus().find(".btn-modal-close").click(); + cy.get(".modal:visible").should("not.exist"); +}); + +Cypress.Commands.add("clear_dialogs", () => { + cy.window().then((win) => { + win.$(".modal, .modal-backdrop").remove(); + }); + cy.get(".modal").should("not.exist"); +}); + +Cypress.Commands.add("clear_datepickers", () => { + cy.window().then((win) => { + win.$(".datepicker").remove(); + }); + cy.get(".datepicker").should("not.exist"); +}); + +Cypress.Commands.add("insert_doc", (doctype, args, ignore_duplicate) => { + if (!args.doctype) { + args.doctype = doctype; + } + return cy + .window() + .its("influxframework.csrf_token") + .then((csrf_token) => { + return cy + .request({ + method: "POST", + url: `/api/resource/${doctype}`, + body: args, + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "X-InfluxFramework-CSRF-Token": csrf_token, + }, + failOnStatusCode: !ignore_duplicate, + }) + .then((res) => { + let status_codes = [200]; + if (ignore_duplicate) { + status_codes.push(409); + } + + let message = null; + if (ignore_duplicate && !status_codes.includes(res.status)) { + message = `Document insert failed, response: ${JSON.stringify( + res, + null, + "\t" + )}`; + } + expect(res.status).to.be.oneOf(status_codes, message); + return res.body.data; + }); + }); +}); + +Cypress.Commands.add("update_doc", (doctype, docname, args) => { + return cy + .window() + .its("influxframework.csrf_token") + .then((csrf_token) => { + return cy + .request({ + method: "PUT", + url: `/api/resource/${doctype}/${docname}`, + body: args, + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "X-InfluxFramework-CSRF-Token": csrf_token, + }, + }) + .then((res) => { + expect(res.status).to.eq(200); + return res.body.data; + }); + }); +}); + +Cypress.Commands.add("open_list_filter", () => { + cy.get(".filter-section .filter-button").click(); + cy.wait(300); + cy.get(".filter-popover").should("exist"); +}); + +Cypress.Commands.add("click_custom_action_button", (name) => { + cy.get(`.custom-actions [data-label="${encodeURIComponent(name)}"]`).click(); +}); + +Cypress.Commands.add("click_action_button", (name) => { + cy.findByRole("button", { name: "Actions" }).click(); + cy.get(`.actions-btn-group [data-label="${encodeURIComponent(name)}"]`).click(); +}); + +Cypress.Commands.add("click_menu_button", (name) => { + cy.get(".standard-actions .menu-btn-group > .btn").click(); + cy.get(`.menu-btn-group [data-label="${encodeURIComponent(name)}"]`).click(); +}); + +Cypress.Commands.add("clear_filters", () => { + let has_filter = false; + cy.intercept({ + method: "POST", + url: "api/method/influxframework.model.utils.user_settings.save", + }).as("filter-saved"); + cy.get(".filter-section .filter-button").click({ force: true }); + cy.wait(300); + cy.get(".filter-popover").should("exist"); + cy.get(".filter-popover").then((popover) => { + if (popover.find("input.input-with-feedback")[0].value != "") { + has_filter = true; + } + }); + cy.get(".filter-popover").find(".clear-filters").click(); + cy.get(".filter-section .filter-button").click(); + cy.window() + .its("cur_list") + .then((cur_list) => { + cur_list && cur_list.filter_area && cur_list.filter_area.clear(); + has_filter && cy.wait("@filter-saved"); + }); +}); + +Cypress.Commands.add("click_modal_primary_button", (btn_name) => { + cy.wait(400); + cy.get(".modal-footer > .standard-actions > .btn-primary") + .contains(btn_name) + .click({ force: true }); +}); + +Cypress.Commands.add("click_sidebar_button", (btn_name) => { + cy.get(".list-group-by-fields .list-link > a").contains(btn_name).click({ force: true }); +}); + +Cypress.Commands.add("click_listview_row_item", (row_no) => { + cy.get(".list-row > .level-left > .list-subject > .level-item > .ellipsis") + .eq(row_no) + .click({ force: true }); +}); + +Cypress.Commands.add("click_listview_row_item_with_text", (text) => { + cy.get(".list-row > .level-left > .list-subject > .level-item > .ellipsis") + .contains(text) + .first() + .click({ force: true }); +}); + +Cypress.Commands.add("click_filter_button", () => { + cy.get(".filter-selector > .btn").click(); +}); + +Cypress.Commands.add("click_listview_primary_button", (btn_name) => { + cy.get(".primary-action").contains(btn_name).click({ force: true }); +}); + +Cypress.Commands.add("click_doc_primary_button", (btn_name) => { + cy.get(".primary-action").contains(btn_name).click({ force: true }); +}); + +Cypress.Commands.add("click_timeline_action_btn", (btn_name) => { + cy.get(".timeline-message-box .actions .action-btn").contains(btn_name).click(); +}); + +Cypress.Commands.add("select_listview_row_checkbox", (row_no) => { + cy.get(".influxframework-list .select-like > .list-row-checkbox").eq(row_no).click(); +}); + +Cypress.Commands.add("click_form_section", (section_name) => { + cy.get(".section-head").contains(section_name).click(); +}); + +const compare_document = (expected, actual) => { + for (const prop in expected) { + if (expected[prop] instanceof Array) { + // recursively compare child documents. + expected[prop].forEach((item, idx) => { + compare_document(item, actual[prop][idx]); + }); + } else { + assert.equal(expected[prop], actual[prop], `${prop} should be equal.`); + } + } +}; + +Cypress.Commands.add("compare_document", (expected_document) => { + cy.window() + .its("cur_frm") + .then((frm) => { + // Don't remove this, cypress can't magically wait for events it has no control over. + cy.wait(1000); + compare_document(expected_document, frm.doc); + }); +}); diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js new file mode 100644 index 0000000..8ce8317 --- /dev/null +++ b/cypress/support/e2e.js @@ -0,0 +1,29 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import "./commands"; +import "@cypress/code-coverage/support"; + +Cypress.on("uncaught:exception", (err, runnable) => { + return false; +}); + +// Alternatively you can use CommonJS syntax: +// require('./commands') + +Cypress.Cookies.defaults({ + preserve: "sid", +}); diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json new file mode 100644 index 0000000..d90ebf6 --- /dev/null +++ b/cypress/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "allowJs": true, + "baseUrl": "../node_modules", + "types": [ + "cypress" + ] + }, + "include": [ + "**/*.*" + ] +} \ No newline at end of file diff --git a/esbuild/build-cleanup.js b/esbuild/build-cleanup.js new file mode 100644 index 0000000..023fce0 --- /dev/null +++ b/esbuild/build-cleanup.js @@ -0,0 +1,31 @@ +/* eslint-disable no-console */ +const path = require("path"); +const fs = require("fs"); +const glob = require("fast-glob"); + +module.exports = { + name: "build_cleanup", + setup(build) { + build.onEnd((result) => { + if (result.errors.length) return; + clean_dist_files(Object.keys(result.metafile.outputs)); + }); + }, +}; + +function clean_dist_files(new_files) { + new_files.forEach((file) => { + if (file.endsWith(".map")) return; + + const pattern = file.split(".").slice(0, -2).join(".") + "*"; + glob.sync(pattern).forEach((file_to_delete) => { + if (file_to_delete.startsWith(file)) return; + + fs.unlink(path.resolve(file_to_delete), (err) => { + if (!err) return; + + console.error(`Error deleting ${file.split(path.sep).pop()}`); + }); + }); + }); +} diff --git a/esbuild/esbuild.js b/esbuild/esbuild.js new file mode 100644 index 0000000..59e8acb --- /dev/null +++ b/esbuild/esbuild.js @@ -0,0 +1,506 @@ +/* eslint-disable no-console */ +const path = require("path"); +const fs = require("fs"); +const glob = require("fast-glob"); +const esbuild = require("esbuild"); +const vue = require("esbuild-vue"); +const yargs = require("yargs"); +const cliui = require("cliui")(); +const chalk = require("chalk"); +const html_plugin = require("./influxframework-html"); +const rtlcss = require("rtlcss"); +const postCssPlugin = require("@influxframework/esbuild-plugin-postcss2").default; +const ignore_assets = require("./ignore-assets"); +const sass_options = require("./sass_options"); +const build_cleanup_plugin = require("./build-cleanup"); + +const { + app_list, + assets_path, + apps_path, + sites_path, + get_app_path, + get_public_path, + log, + log_warn, + log_error, + bench_path, + get_redis_subscriber, +} = require("./utils"); + +const argv = yargs + .usage("Usage: node esbuild [options]") + .option("apps", { + type: "string", + description: "Run build for specific apps", + }) + .option("skip_influxframework", { + type: "boolean", + description: "Skip building influxframework assets", + }) + .option("files", { + type: "string", + description: "Run build for specified bundles", + }) + .option("watch", { + type: "boolean", + description: "Run in watch mode and rebuild on file changes", + }) + .option("live-reload", { + type: "boolean", + description: `Automatically reload Desk when assets are rebuilt. + Can only be used with the --watch flag.`, + }) + .option("production", { + type: "boolean", + description: "Run build in production mode", + }) + .option("run-build-command", { + type: "boolean", + description: "Run build command for apps", + }) + .example("node esbuild --apps influxframework,influxerp", "Run build only for influxframework and influxerp") + .example( + "node esbuild --files influxframework/website.bundle.js,influxframework/desk.bundle.js", + "Run build only for specified bundles" + ) + .version(false).argv; + +const APPS = (!argv.apps ? app_list : argv.apps.split(",")).filter( + (app) => !(argv.skip_influxframework && app == "influxframework") +); +const FILES_TO_BUILD = argv.files ? argv.files.split(",") : []; +const WATCH_MODE = Boolean(argv.watch); +const PRODUCTION = Boolean(argv.production); +const RUN_BUILD_COMMAND = !WATCH_MODE && Boolean(argv["run-build-command"]); + +const TOTAL_BUILD_TIME = `${chalk.black.bgGreen(" DONE ")} Total Build Time`; +const NODE_PATHS = [].concat( + // node_modules of apps directly importable + app_list + .map((app) => path.resolve(get_app_path(app), "../node_modules")) + .filter(fs.existsSync), + // import js file of any app if you provide the full path + app_list.map((app) => path.resolve(get_app_path(app), "..")).filter(fs.existsSync) +); + +execute() + .then(() => RUN_BUILD_COMMAND && run_build_command_for_apps(APPS)) + .catch((e) => console.error(e)); + +if (WATCH_MODE) { + // listen for open files in editor event + open_in_editor(); +} + +async function execute() { + console.time(TOTAL_BUILD_TIME); + + let results; + try { + results = await build_assets_for_apps(APPS, FILES_TO_BUILD); + } catch (e) { + log_error("There were some problems during build"); + log(); + log(chalk.dim(e.stack)); + if (process.env.CI || PRODUCTION) { + process.kill(process.pid); + } + return; + } + + if (!WATCH_MODE) { + log_built_assets(results); + console.timeEnd(TOTAL_BUILD_TIME); + log(); + } else { + log("Watching for changes..."); + } + for (const result of results) { + await write_assets_json(result.metafile); + } +} + +function build_assets_for_apps(apps, files) { + let { include_patterns, ignore_patterns } = files.length + ? get_files_to_build(files) + : get_all_files_to_build(apps); + + return glob(include_patterns, { ignore: ignore_patterns }).then((files) => { + let output_path = assets_path; + + let file_map = {}; + let style_file_map = {}; + let rtl_style_file_map = {}; + for (let file of files) { + let relative_app_path = path.relative(apps_path, file); + let app = relative_app_path.split(path.sep)[0]; + + let extension = path.extname(file); + let output_name = path.basename(file, extension); + if ([".css", ".scss", ".less", ".sass", ".styl"].includes(extension)) { + output_name = path.join("css", output_name); + } else if ([".js", ".ts"].includes(extension)) { + output_name = path.join("js", output_name); + } + output_name = path.join(app, "dist", output_name); + + if ( + Object.keys(file_map).includes(output_name) || + Object.keys(style_file_map).includes(output_name) + ) { + log_warn(`Duplicate output file ${output_name} generated from ${file}`); + } + if ([".css", ".scss", ".less", ".sass", ".styl"].includes(extension)) { + style_file_map[output_name] = file; + rtl_style_file_map[output_name.replace("/css/", "/css-rtl/")] = file; + } else { + file_map[output_name] = file; + } + } + let build = build_files({ + files: file_map, + outdir: output_path, + }); + let style_build = build_style_files({ + files: style_file_map, + outdir: output_path, + }); + let rtl_style_build = build_style_files({ + files: rtl_style_file_map, + outdir: output_path, + rtl_style: true, + }); + return Promise.all([build, style_build, rtl_style_build]); + }); +} + +function get_all_files_to_build(apps) { + let include_patterns = []; + let ignore_patterns = []; + + for (let app of apps) { + let public_path = get_public_path(app); + include_patterns.push( + path.resolve(public_path, "**", "*.bundle.{js,ts,css,sass,scss,less,styl}") + ); + ignore_patterns.push( + path.resolve(public_path, "node_modules"), + path.resolve(public_path, "dist") + ); + } + + return { + include_patterns, + ignore_patterns, + }; +} + +function get_files_to_build(files) { + // files: ['influxframework/website.bundle.js', 'influxerp/main.bundle.js'] + let include_patterns = []; + let ignore_patterns = []; + + for (let file of files) { + let [app, bundle] = file.split("/"); + let public_path = get_public_path(app); + include_patterns.push(path.resolve(public_path, "**", bundle)); + ignore_patterns.push( + path.resolve(public_path, "node_modules"), + path.resolve(public_path, "dist") + ); + } + + return { + include_patterns, + ignore_patterns, + }; +} + +function build_files({ files, outdir }) { + let build_plugins = [html_plugin, build_cleanup_plugin, vue()]; + return esbuild.build(get_build_options(files, outdir, build_plugins)); +} + +function build_style_files({ files, outdir, rtl_style = false }) { + let plugins = []; + if (rtl_style) { + plugins.push(rtlcss); + } + + let build_plugins = [ + ignore_assets, + build_cleanup_plugin, + postCssPlugin({ + plugins: plugins, + sassOptions: sass_options, + }), + ]; + + plugins.push(require("autoprefixer")); + return esbuild.build(get_build_options(files, outdir, build_plugins)); +} + +function get_build_options(files, outdir, plugins) { + return { + entryPoints: files, + entryNames: "[dir]/[name].[hash]", + target: ["es2017"], + outdir, + sourcemap: true, + bundle: true, + metafile: true, + minify: PRODUCTION, + nodePaths: NODE_PATHS, + define: { + "process.env.NODE_ENV": JSON.stringify(PRODUCTION ? "production" : "development"), + }, + plugins: plugins, + watch: get_watch_config(), + }; +} + +function get_watch_config() { + if (WATCH_MODE) { + return { + async onRebuild(error, result) { + if (error) { + log_error("There was an error during rebuilding changes."); + log(); + log(chalk.dim(error.stack)); + notify_redis({ error }); + } else { + let { new_assets_json, prev_assets_json } = await write_assets_json( + result.metafile + ); + + let changed_files; + if (prev_assets_json) { + changed_files = get_rebuilt_assets(prev_assets_json, new_assets_json); + + let timestamp = new Date().toLocaleTimeString(); + let message = `${timestamp}: Compiled ${changed_files.length} files...`; + log(chalk.yellow(message)); + for (let filepath of changed_files) { + let filename = path.basename(filepath); + log(" " + filename); + } + log(); + } + notify_redis({ success: true, changed_files }); + } + }, + }; + } + return null; +} + +function log_built_assets(results) { + let outputs = {}; + for (const result of results) { + outputs = Object.assign(outputs, result.metafile.outputs); + } + let column_widths = [60, 20]; + cliui.div( + { + text: chalk.cyan.bold("File"), + width: column_widths[0], + }, + { + text: chalk.cyan.bold("Size"), + width: column_widths[1], + } + ); + cliui.div(""); + + let output_by_dist_path = {}; + for (let outfile in outputs) { + if (outfile.endsWith(".map")) continue; + let data = outputs[outfile]; + outfile = path.resolve(outfile); + outfile = path.relative(assets_path, outfile); + let filename = path.basename(outfile); + let dist_path = outfile.replace(filename, ""); + output_by_dist_path[dist_path] = output_by_dist_path[dist_path] || []; + output_by_dist_path[dist_path].push({ + name: filename, + size: (data.bytes / 1000).toFixed(2) + " Kb", + }); + } + + for (let dist_path in output_by_dist_path) { + let files = output_by_dist_path[dist_path]; + cliui.div({ + text: dist_path, + width: column_widths[0], + }); + + for (let i in files) { + let file = files[i]; + let branch = ""; + if (i < files.length - 1) { + branch = "├─ "; + } else { + branch = "└─ "; + } + let color = file.name.endsWith(".js") ? "green" : "blue"; + cliui.div( + { + text: branch + chalk[color]("" + file.name), + width: column_widths[0], + }, + { + text: file.size, + width: column_widths[1], + } + ); + } + cliui.div(""); + } + log(cliui.toString()); +} + +// to store previous build's assets.json for comparison +let prev_assets_json; +let curr_assets_json; + +async function write_assets_json(metafile) { + let rtl = false; + prev_assets_json = curr_assets_json; + let out = {}; + for (let output in metafile.outputs) { + let info = metafile.outputs[output]; + let asset_path = "/" + path.relative(sites_path, output); + if (info.entryPoint) { + let key = path.basename(info.entryPoint); + if (key.endsWith(".css") && asset_path.includes("/css-rtl/")) { + rtl = true; + key = `rtl_${key}`; + } + out[key] = asset_path; + } + } + + let assets_json_path = path.resolve(assets_path, `assets${rtl ? "-rtl" : ""}.json`); + let assets_json; + try { + assets_json = await fs.promises.readFile(assets_json_path, "utf-8"); + } catch (error) { + assets_json = "{}"; + } + assets_json = JSON.parse(assets_json); + // update with new values + let new_assets_json = Object.assign({}, assets_json, out); + curr_assets_json = new_assets_json; + + await fs.promises.writeFile(assets_json_path, JSON.stringify(new_assets_json, null, 4)); + await update_assets_json_in_cache(); + return { + new_assets_json, + prev_assets_json, + }; +} + +function update_assets_json_in_cache() { + // update assets_json cache in redis, so that it can be read directly by python + return new Promise((resolve) => { + let client = get_redis_subscriber("redis_cache"); + // handle error event to avoid printing stack traces + client.on("error", (_) => { + log_warn("Cannot connect to redis_cache to update assets_json"); + }); + client.del("assets_json", (err) => { + client.unref(); + resolve(); + }); + }); +} + +function run_build_command_for_apps(apps) { + let cwd = process.cwd(); + let { execSync } = require("child_process"); + + for (let app of apps) { + if (app === "influxframework") continue; + + let root_app_path = path.resolve(get_app_path(app), ".."); + let package_json = path.resolve(root_app_path, "package.json"); + if (fs.existsSync(package_json)) { + let { scripts } = require(package_json); + if (scripts && scripts.build) { + log("\nRunning build command for", chalk.bold(app)); + process.chdir(root_app_path); + execSync("yarn build", { encoding: "utf8", stdio: "inherit" }); + } + } + } + + process.chdir(cwd); +} + +async function notify_redis({ error, success, changed_files }) { + // notify redis which in turns tells socketio to publish this to browser + let subscriber = get_redis_subscriber("redis_socketio"); + subscriber.on("error", (_) => { + log_warn("Cannot connect to redis_socketio for browser events"); + }); + + let payload = null; + if (error) { + let formatted = await esbuild.formatMessages(error.errors, { + kind: "error", + terminalWidth: 100, + }); + let stack = error.stack.replace(new RegExp(bench_path, "g"), ""); + payload = { + error, + formatted, + stack, + }; + } + if (success) { + payload = { + success: true, + changed_files, + live_reload: argv["live-reload"], + }; + } + + subscriber.publish( + "events", + JSON.stringify({ + event: "build_event", + message: payload, + }) + ); +} + +function open_in_editor() { + let subscriber = get_redis_subscriber("redis_socketio"); + subscriber.on("error", (_) => { + log_warn("Cannot connect to redis_socketio for open_in_editor events"); + }); + subscriber.on("message", (event, file) => { + if (event === "open_in_editor") { + file = JSON.parse(file); + let file_path = path.resolve(file.file); + log("Opening file in editor:", file_path); + let launch = require("launch-editor"); + launch(`${file_path}:${file.line}:${file.column}`); + } + }); + subscriber.subscribe("open_in_editor"); +} + +function get_rebuilt_assets(prev_assets, new_assets) { + let added_files = []; + let old_files = Object.values(prev_assets); + let new_files = Object.values(new_assets); + + for (let filepath of new_files) { + if (!old_files.includes(filepath)) { + added_files.push(filepath); + } + } + return added_files; +} diff --git a/esbuild/ignore-assets.js b/esbuild/ignore-assets.js new file mode 100644 index 0000000..b530303 --- /dev/null +++ b/esbuild/ignore-assets.js @@ -0,0 +1,11 @@ +module.exports = { + name: "influxframework-ignore-asset", + setup(build) { + build.onResolve({ filter: /^\/assets\// }, (args) => { + return { + path: args.path, + external: true, + }; + }); + }, +}; diff --git a/esbuild/index.js b/esbuild/index.js new file mode 100644 index 0000000..2721673 --- /dev/null +++ b/esbuild/index.js @@ -0,0 +1 @@ +require("./esbuild"); diff --git a/esbuild/influxframework-html.js b/esbuild/influxframework-html.js new file mode 100644 index 0000000..cc90b51 --- /dev/null +++ b/esbuild/influxframework-html.js @@ -0,0 +1,44 @@ +module.exports = { + name: "influxframework-html", + setup(build) { + let path = require("path"); + let fs = require("fs/promises"); + + build.onResolve({ filter: /\.html$/ }, (args) => { + return { + path: path.join(args.resolveDir, args.path), + namespace: "influxframework-html", + }; + }); + + build.onLoad({ filter: /.*/, namespace: "influxframework-html" }, (args) => { + let filepath = args.path; + let filename = path.basename(filepath).split(".")[0]; + + return fs + .readFile(filepath, "utf-8") + .then((content) => { + content = scrub_html_template(content); + return { + contents: `\n\tinfluxframework.templates['${filename}'] = \`${content}\`;\n`, + watchFiles: [filepath], + }; + }) + .catch(() => { + return { + contents: "", + warnings: [ + { + text: `There was an error importing ${filepath}`, + }, + ], + }; + }); + }); + }, +}; + +function scrub_html_template(content) { + content = content.replace(/`/g, "\\`"); + return content; +} diff --git a/esbuild/sass_options.js b/esbuild/sass_options.js new file mode 100644 index 0000000..19ccf5a --- /dev/null +++ b/esbuild/sass_options.js @@ -0,0 +1,24 @@ +let path = require("path"); +let { get_app_path, app_list } = require("./utils"); + +let node_modules_path = path.resolve(get_app_path("influxframework"), "..", "node_modules"); +let app_paths = app_list.map(get_app_path).map((app_path) => path.resolve(app_path, "..")); + +module.exports = { + includePaths: [node_modules_path, ...app_paths], + quietDeps: true, + importer: function (url) { + if (url.startsWith("~")) { + // strip ~ so that it can resolve from node_modules + url = url.slice(1); + } + if (url.endsWith(".css")) { + // strip .css from end of path + url = url.slice(0, -4); + } + // normal file, let it go + return { + file: url, + }; + }, +}; diff --git a/esbuild/utils.js b/esbuild/utils.js new file mode 100644 index 0000000..ba2c747 --- /dev/null +++ b/esbuild/utils.js @@ -0,0 +1,146 @@ +const path = require("path"); +const fs = require("fs"); +const chalk = require("chalk"); + +const influxframework_path = path.resolve(__dirname, ".."); +const bench_path = path.resolve(influxframework_path, "..", ".."); +const sites_path = path.resolve(bench_path, "sites"); +const apps_path = path.resolve(bench_path, "apps"); +const assets_path = path.resolve(sites_path, "assets"); +const app_list = get_apps_list(); + +const app_paths = app_list.reduce((out, app) => { + out[app] = path.resolve(apps_path, app, app); + return out; +}, {}); +const public_paths = app_list.reduce((out, app) => { + out[app] = path.resolve(app_paths[app], "public"); + return out; +}, {}); +const public_js_paths = app_list.reduce((out, app) => { + out[app] = path.resolve(app_paths[app], "public/js"); + return out; +}, {}); + +const bundle_map = app_list.reduce((out, app) => { + const public_js_path = public_js_paths[app]; + if (fs.existsSync(public_js_path)) { + const all_files = fs.readdirSync(public_js_path); + const js_files = all_files.filter((file) => file.endsWith(".js")); + + for (let js_file of js_files) { + const filename = path.basename(js_file).split(".")[0]; + out[path.join(app, "js", filename)] = path.resolve(public_js_path, js_file); + } + } + + return out; +}, {}); + +const get_public_path = (app) => public_paths[app]; + +const get_build_json_path = (app) => path.resolve(get_public_path(app), "build.json"); + +function get_build_json(app) { + try { + return require(get_build_json_path(app)); + } catch (e) { + // build.json does not exist + return null; + } +} + +function delete_file(path) { + if (fs.existsSync(path)) { + fs.unlinkSync(path); + } +} + +function run_serially(tasks) { + let result = Promise.resolve(); + tasks.forEach((task) => { + if (task) { + result = result.then ? result.then(task) : Promise.resolve(); + } + }); + return result; +} + +const get_app_path = (app) => app_paths[app]; + +function get_apps_list() { + return fs + .readFileSync(path.resolve(sites_path, "apps.txt"), { + encoding: "utf-8", + }) + .split("\n") + .filter(Boolean); +} + +function get_cli_arg(name) { + let args = process.argv.slice(2); + let arg = `--${name}`; + let index = args.indexOf(arg); + + let value = null; + if (index != -1) { + value = true; + } + if (value && args[index + 1]) { + value = args[index + 1]; + } + return value; +} + +function log_error(message, badge = "ERROR") { + badge = chalk.white.bgRed(` ${badge} `); + console.error(`${badge} ${message}`); // eslint-disable-line no-console +} + +function log_warn(message, badge = "WARN") { + badge = chalk.black.bgYellowBright(` ${badge} `); + console.warn(`${badge} ${message}`); // eslint-disable-line no-console +} + +function log(...args) { + console.log(...args); // eslint-disable-line no-console +} + +function get_redis_subscriber(kind) { + // get redis subscriber that aborts after 10 connection attempts + let retry_strategy; + let { get_redis_subscriber: get_redis, get_conf } = require("../node_utils"); + + if (process.env.CI == 1 || get_conf().developer_mode == 0) { + retry_strategy = () => {}; + } else { + retry_strategy = function (options) { + // abort after 5 x 3 connection attempts ~= 3 seconds + if (options.attempt > 4) { + return undefined; + } + return options.attempt * 100; + }; + } + return get_redis(kind, { retry_strategy }); +} + +module.exports = { + app_list, + bench_path, + assets_path, + sites_path, + apps_path, + bundle_map, + get_public_path, + get_build_json_path, + get_build_json, + get_app_path, + delete_file, + run_serially, + get_cli_arg, + log, + log_warn, + log_error, + get_redis_subscriber, +}; diff --git a/generate_bootstrap_theme.js b/generate_bootstrap_theme.js new file mode 100644 index 0000000..295188a --- /dev/null +++ b/generate_bootstrap_theme.js @@ -0,0 +1,28 @@ +const sass = require("sass"); +const fs = require("fs"); +const sass_options = require("./esbuild/sass_options"); +let output_path = process.argv[2]; +let scss_content = process.argv[3]; +scss_content = scss_content.replace(/\\n/g, "\n"); + +sass.render( + { + data: scss_content, + outputStyle: "compressed", + ...sass_options, + }, + function (err, result) { + if (err) { + console.error(err.formatted); // eslint-disable-line + return; + } + + fs.writeFile(output_path, result.css, function (err) { + if (!err) { + console.log(output_path); // eslint-disable-line + } else { + console.error(err); // eslint-disable-line + } + }); + } +); diff --git a/hooks.md b/hooks.md new file mode 100644 index 0000000..a9af255 --- /dev/null +++ b/hooks.md @@ -0,0 +1,36 @@ +### List of Hooks + +#### Application Name and Details + +1. `app_name` - slugified name e.g. "influxframework" +1. `app_title` - full title name e.g. "InfluxFramework" +1. `app_publisher` +1. `app_description` +1. `app_version` + +#### Install + +1. `before_install` - method +1. `after_install` - method + + +#### Javascript / CSS Builds + +1. `app_include_js` - include in "app" +1. `app_include_css` - assets/influxframework/css/splash.css + +1. `web_include_js` - assets/js/influxframework-web.min.js +1. `web_include_css` - assets/css/influxframework-web.css + +#### Desktop + +1. `get_desktop_icons` - method to get list of desktop icons + +#### Notifications + +1. `notification_config` - method to get notification configuration + +#### Permissions + +1. `permission_query_conditions:[doctype]` - method to return additional query conditions at time of report / list etc. +1. `has_permission:[doctype]` - method to call permissions to check at individual level diff --git a/influxframework/__init__.py b/influxframework/__init__.py new file mode 100644 index 0000000..99c6540 --- /dev/null +++ b/influxframework/__init__.py @@ -0,0 +1,2399 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE +""" +InfluxFramework - Low Code Open Source Framework in Python and JS + +InfluxFramework, pronounced fra-pay, is a full stack, batteries-included, web +framework written in Python and Javascript with MariaDB as the database. +It is the framework which powers InfluxERP. It is pretty generic and can +be used to build database driven apps. + +Read the documentation: https://influxframework.com/docs +""" +import functools +import importlib +import inspect +import json +import os +import re +import warnings +from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, overload + +import click +from werkzeug.local import Local, release_local + +from influxframework.query_builder import ( + get_qb_engine, + get_query_builder, + patch_query_aggregation, + patch_query_execute, +) +from influxframework.utils.caching import request_cache +from influxframework.utils.data import cstr, sbool + +# Local application imports +from .exceptions import * +from .utils.jinja import ( + get_email_from_template, + get_jenv, + get_jloader, + get_template, + render_template, +) +from .utils.lazy_loader import lazy_import + +__version__ = "14.13.0" +__title__ = "InfluxFramework Framework" + +controllers = {} +local = Local() +STANDARD_USERS = ("Guest", "Administrator") + +_dev_server = int(sbool(os.environ.get("DEV_SERVER", False))) +_qb_patched = {} +re._MAXCACHE = ( + 50 # reduced from default 512 given we are already maintaining this on parent worker +) + + +if _dev_server: + warnings.simplefilter("always", DeprecationWarning) + warnings.simplefilter("always", PendingDeprecationWarning) + + +class _dict(dict): + """dict like object that exposes keys as attributes""" + + __slots__ = () + __getattr__ = dict.get + __setattr__ = dict.__setitem__ + __delattr__ = dict.__delitem__ + __setstate__ = dict.update + + def __getstate__(self): + return self + + def update(self, *args, **kwargs): + """update and return self -- the missing dict feature in python""" + + super().update(*args, **kwargs) + return self + + def copy(self): + return _dict(self) + + +def _(msg: str, lang: str | None = None, context: str | None = None) -> str: + """Returns translated string in current lang, if exists. + Usage: + _('Change') + _('Change', context='Coins') + """ + from influxframework.translate import get_full_dict + from influxframework.utils import is_html, strip_html_tags + + if not hasattr(local, "lang"): + local.lang = lang or "en" + + if not lang: + lang = local.lang + + non_translated_string = msg + + if is_html(msg): + msg = strip_html_tags(msg) + + # msg should always be unicode + msg = as_unicode(msg).strip() + + translated_string = "" + if context: + string_key = f"{msg}:{context}" + translated_string = get_full_dict(lang).get(string_key) + + if not translated_string: + translated_string = get_full_dict(lang).get(msg) + + # return lang_full_dict according to lang passed parameter + return translated_string or non_translated_string + + +def as_unicode(text: str, encoding: str = "utf-8") -> str: + """Convert to unicode if required""" + if isinstance(text, str): + return text + elif text is None: + return "" + elif isinstance(text, bytes): + return str(text, encoding) + else: + return str(text) + + +def get_lang_dict(fortype: str, name: str | None = None) -> dict[str, str]: + """Returns the translated language dict for the given type and name. + + :param fortype: must be one of `doctype`, `page`, `report`, `include`, `jsfile`, `boot` + :param name: name of the document for which assets are to be returned.""" + from influxframework.translate import get_dict + + return get_dict(fortype, name) + + +def set_user_lang(user: str, user_language: str | None = None) -> None: + """Guess and set user language for the session. `influxframework.local.lang`""" + from influxframework.translate import get_user_lang + + local.lang = get_user_lang(user) or user_language + + +# local-globals + +db = local("db") +qb = local("qb") +conf = local("conf") +form = form_dict = local("form_dict") +request = local("request") +response = local("response") +session = local("session") +user = local("user") +flags = local("flags") + +error_log = local("error_log") +debug_log = local("debug_log") +message_log = local("message_log") + +lang = local("lang") + +# This if block is never executed when running the code. It is only used for +# telling static code analyzer where to find dynamically defined attributes. +if TYPE_CHECKING: + from influxframework.database.mariadb.database import MariaDBDatabase + from influxframework.database.postgres.database import PostgresDatabase + from influxframework.model.document import Document + from influxframework.query_builder.builder import MariaDB, Postgres + from influxframework.utils.redis_wrapper import RedisWrapper + + db: MariaDBDatabase | PostgresDatabase + qb: MariaDB | Postgres + + +# end: static analysis hack + + +def init(site: str, sites_path: str = ".", new_site: bool = False) -> None: + """Initialize influxframework for the current site. Reset thread locals `influxframework.local`""" + if getattr(local, "initialised", None): + return + + local.error_log = [] + local.message_log = [] + local.debug_log = [] + local.realtime_log = [] + local.flags = _dict( + { + "currently_saving": [], + "redirect_location": "", + "in_install_db": False, + "in_install_app": False, + "in_import": False, + "in_test": False, + "mute_messages": False, + "ignore_links": False, + "mute_emails": False, + "has_dataurl": False, + "new_site": new_site, + "read_only": False, + } + ) + local.rollback_observers = [] + local.locked_documents = [] + local.before_commit = [] + local.test_objects = {} + + local.site = site + local.sites_path = sites_path + local.site_path = os.path.join(sites_path, site) + local.all_apps = None + + local.request_ip = None + local.response = _dict({"docs": []}) + local.task_id = None + + local.conf = _dict(get_site_config()) + local.lang = local.conf.lang or "en" + local.lang_full_dict = None + + local.module_app = None + local.app_modules = None + + local.user = None + local.user_perms = None + local.session = None + local.role_permissions = {} + local.valid_columns = {} + local.new_doc_templates = {} + local.link_count = {} + + local.jenv = None + local.jloader = None + local.cache = {} + local.document_cache = {} + local.form_dict = _dict() + local.preload_assets = {"style": [], "script": []} + local.session = _dict() + local.dev_server = _dev_server + local.qb = get_query_builder(local.conf.db_type or "mariadb") + local.qb.engine = get_qb_engine() + setup_module_map() + + if not _qb_patched.get(local.conf.db_type): + patch_query_execute() + patch_query_aggregation() + + local.initialised = True + + +def connect( + site: str | None = None, db_name: str | None = None, set_admin_as_user: bool = True +) -> None: + """Connect to site database instance. + + :param site: If site is given, calls `influxframework.init`. + :param db_name: Optional. Will use from `site_config.json`. + :param set_admin_as_user: Set Administrator as current user. + """ + from influxframework.database import get_db + + if site: + init(site) + + local.db = get_db(user=db_name or local.conf.db_name) + if set_admin_as_user: + set_user("Administrator") + + +def connect_replica(): + from influxframework.database import get_db + + user = local.conf.db_name + password = local.conf.db_password + port = local.conf.replica_db_port + + if local.conf.different_credentials_for_replica: + user = local.conf.replica_db_name + password = local.conf.replica_db_password + + local.replica_db = get_db(host=local.conf.replica_host, user=user, password=password, port=port) + + # swap db connections + local.primary_db = local.db + local.db = local.replica_db + + +def get_site_config(sites_path: str | None = None, site_path: str | None = None) -> dict[str, Any]: + """Returns `site_config.json` combined with `sites/common_site_config.json`. + `site_config` is a set of site wide settings like database name, password, email etc.""" + config = {} + + sites_path = sites_path or getattr(local, "sites_path", None) + site_path = site_path or getattr(local, "site_path", None) + + if sites_path: + common_site_config = os.path.join(sites_path, "common_site_config.json") + if os.path.exists(common_site_config): + try: + config.update(get_file_json(common_site_config)) + except Exception as error: + click.secho("common_site_config.json is invalid", fg="red") + print(error) + + if site_path: + site_config = os.path.join(site_path, "site_config.json") + if os.path.exists(site_config): + try: + config.update(get_file_json(site_config)) + except Exception as error: + click.secho(f"{local.site}/site_config.json is invalid", fg="red") + print(error) + elif local.site and not local.flags.new_site: + raise IncorrectSitePath(f"{local.site} does not exist") + + return _dict(config) + + +def get_conf(site: str | None = None) -> dict[str, Any]: + if hasattr(local, "conf"): + return local.conf + + else: + # if no site, get from common_site_config.json + with init_site(site): + return local.conf + + +class init_site: + def __init__(self, site=None): + """If site is None, initialize it for empty site ('') to load common_site_config.json""" + self.site = site or "" + + def __enter__(self): + init(self.site) + return local + + def __exit__(self, type, value, traceback): + destroy() + + +def destroy(): + """Closes connection and releases werkzeug local.""" + if db: + db.close() + + release_local(local) + + +redis_server = None + + +def cache() -> "RedisWrapper": + """Returns redis connection.""" + global redis_server + if not redis_server: + from influxframework.utils.redis_wrapper import RedisWrapper + + redis_server = RedisWrapper.from_url(conf.get("redis_cache") or "redis://localhost:11311") + return redis_server + + +def get_traceback(with_context: bool = False) -> str: + """Returns error traceback.""" + from influxframework.utils import get_traceback + + return get_traceback(with_context=with_context) + + +def errprint(msg: str) -> None: + """Log error. This is sent back as `exc` in response. + + :param msg: Message.""" + msg = as_unicode(msg) + if not request or (not "cmd" in local.form_dict) or conf.developer_mode: + print(msg) + + error_log.append({"exc": msg}) + + +def print_sql(enable: bool = True) -> None: + return cache().set_value("flag_print_sql", enable) + + +def log(msg: str) -> None: + """Add to `debug_log`. + + :param msg: Message.""" + if not request: + if conf.get("logging") or False: + print(repr(msg)) + + debug_log.append(as_unicode(msg)) + + +def msgprint( + msg: str, + title: str | None = None, + raise_exception: bool | type[Exception] = False, + as_table: bool = False, + as_list: bool = False, + indicator: Literal["blue", "green", "orange", "red", "yellow"] | None = None, + alert: bool = False, + primary_action: str = None, + is_minimizable: bool = False, + wide: bool = False, +) -> None: + """Print a message to the user (via HTTP response). + Messages are sent in the `__server_messages` property in the + response JSON and shown in a pop-up / modal. + + :param msg: Message. + :param title: [optional] Message title. Default: "Message". + :param raise_exception: [optional] Raise given exception and show message. + :param as_table: [optional] If `msg` is a list of lists, render as HTML table. + :param as_list: [optional] If `msg` is a list, render as un-ordered list. + :param primary_action: [optional] Bind a primary server/client side action. + :param is_minimizable: [optional] Allow users to minimize the modal + :param wide: [optional] Show wide modal + """ + import inspect + import sys + + from influxframework.utils import strip_html_tags + + msg = safe_decode(msg) + out = _dict(message=msg) + + @functools.lru_cache(maxsize=1024) + def _strip_html_tags(message): + return strip_html_tags(message) + + def _raise_exception(): + if raise_exception: + if inspect.isclass(raise_exception) and issubclass(raise_exception, Exception): + raise raise_exception(msg) + else: + raise ValidationError(msg) + + if flags.mute_messages: + _raise_exception() + return + + if as_table and type(msg) in (list, tuple): + out.as_table = 1 + + if as_list and type(msg) in (list, tuple): + out.as_list = 1 + + if sys.stdin and sys.stdin.isatty(): + msg = _strip_html_tags(out.message) + + if flags.print_messages and out.message: + print(f"Message: {_strip_html_tags(out.message)}") + + out.title = title or _("Message", context="Default title of the message dialog") + + if not indicator and raise_exception: + indicator = "red" + + if indicator: + out.indicator = indicator + + if is_minimizable: + out.is_minimizable = is_minimizable + + if alert: + out.alert = 1 + + if raise_exception: + out.raise_exception = 1 + + if primary_action: + out.primary_action = primary_action + + if wide: + out.wide = wide + + message_log.append(json.dumps(out)) + + if raise_exception and hasattr(raise_exception, "__name__"): + local.response["exc_type"] = raise_exception.__name__ + + _raise_exception() + + +def clear_messages(): + local.message_log = [] + + +def get_message_log(): + log = [] + for msg_out in local.message_log: + log.append(json.loads(msg_out)) + + return log + + +def clear_last_message(): + if len(local.message_log) > 0: + local.message_log = local.message_log[:-1] + + +def throw( + msg: str, + exc: type[Exception] = ValidationError, + title: str | None = None, + is_minimizable: bool = False, + wide: bool = False, + as_list: bool = False, +) -> None: + """Throw execption and show message (`msgprint`). + + :param msg: Message. + :param exc: Exception class. Default `influxframework.ValidationError`""" + msgprint( + msg, + raise_exception=exc, + title=title, + indicator="red", + is_minimizable=is_minimizable, + wide=wide, + as_list=as_list, + ) + + +def create_folder(path, with_init=False): + """Create a folder in the given path and add an `__init__.py` file (optional). + + :param path: Folder path. + :param with_init: Create `__init__.py` in the new folder.""" + from influxframework.utils import touch_file + + if not os.path.exists(path): + os.makedirs(path) + + if with_init: + touch_file(os.path.join(path, "__init__.py")) + + +def set_user(username: str): + """Set current user. + + :param username: **User** name to set as current user.""" + local.session.user = username + local.session.sid = username + local.cache = {} + local.form_dict = _dict() + local.jenv = None + local.session.data = _dict() + local.role_permissions = {} + local.new_doc_templates = {} + local.user_perms = None + + +def get_user(): + from influxframework.utils.user import UserPermissions + + if not local.user_perms: + local.user_perms = UserPermissions(local.session.user) + return local.user_perms + + +def get_roles(username=None) -> list[str]: + """Returns roles of current user.""" + if not local.session: + return ["Guest"] + import influxframework.permissions + + return influxframework.permissions.get_roles(username or local.session.user) + + +def get_request_header(key, default=None): + """Return HTTP request header. + + :param key: HTTP header key. + :param default: Default value.""" + return request.headers.get(key, default) + + +def sendmail( + recipients=None, + sender="", + subject="No Subject", + message="No Message", + as_markdown=False, + delayed=True, + reference_doctype=None, + reference_name=None, + unsubscribe_method=None, + unsubscribe_params=None, + unsubscribe_message=None, + add_unsubscribe_link=1, + attachments=None, + content=None, + doctype=None, + name=None, + reply_to=None, + queue_separately=False, + cc=None, + bcc=None, + message_id=None, + in_reply_to=None, + send_after=None, + expose_recipients=None, + send_priority=1, + communication=None, + retry=1, + now=None, + read_receipt=None, + is_notification=False, + inline_images=None, + template=None, + args=None, + header=None, + print_letterhead=False, + with_container=False, +): + """Send email using user's default **Email Account** or global default **Email Account**. + + + :param recipients: List of recipients. + :param sender: Email sender. Default is current user or default outgoing account. + :param subject: Email Subject. + :param message: (or `content`) Email Content. + :param as_markdown: Convert content markdown to HTML. + :param delayed: Send via scheduled email sender **Email Queue**. Don't send immediately. Default is true + :param send_priority: Priority for Email Queue, default 1. + :param reference_doctype: (or `doctype`) Append as communication to this DocType. + :param reference_name: (or `name`) Append as communication to this document name. + :param unsubscribe_method: Unsubscribe url with options email, doctype, name. e.g. `/api/method/unsubscribe` + :param unsubscribe_params: Unsubscribe paramaters to be loaded on the unsubscribe_method [optional] (dict). + :param attachments: List of attachments. + :param reply_to: Reply-To Email Address. + :param message_id: Used for threading. If a reply is received to this email, Message-Id is sent back as In-Reply-To in received email. + :param in_reply_to: Used to send the Message-Id of a received email back as In-Reply-To. + :param send_after: Send after the given datetime. + :param expose_recipients: Display all recipients in the footer message - "This email was sent to" + :param communication: Communication link to be set in Email Queue record + :param inline_images: List of inline images as {"filename", "filecontent"}. All src properties will be replaced with random Content-Id + :param template: Name of html template from templates/emails folder + :param args: Arguments for rendering the template + :param header: Append header in email + :param with_container: Wraps email inside a styled container + """ + + if recipients is None: + recipients = [] + if cc is None: + cc = [] + if bcc is None: + bcc = [] + + text_content = None + if template: + message, text_content = get_email_from_template(template, args) + + message = content or message + + if as_markdown: + from influxframework.utils import md_to_html + + message = md_to_html(message) + + if not delayed: + now = True + + from influxframework.email.doctype.email_queue.email_queue import QueueBuilder + + builder = QueueBuilder( + recipients=recipients, + sender=sender, + subject=subject, + message=message, + text_content=text_content, + reference_doctype=doctype or reference_doctype, + reference_name=name or reference_name, + add_unsubscribe_link=add_unsubscribe_link, + unsubscribe_method=unsubscribe_method, + unsubscribe_params=unsubscribe_params, + unsubscribe_message=unsubscribe_message, + attachments=attachments, + reply_to=reply_to, + cc=cc, + bcc=bcc, + message_id=message_id, + in_reply_to=in_reply_to, + send_after=send_after, + expose_recipients=expose_recipients, + send_priority=send_priority, + queue_separately=queue_separately, + communication=communication, + read_receipt=read_receipt, + is_notification=is_notification, + inline_images=inline_images, + header=header, + print_letterhead=print_letterhead, + with_container=with_container, + ) + + # build email queue and send the email if send_now is True. + builder.process(send_now=now) + + +whitelisted = [] +guest_methods = [] +xss_safe_methods = [] +allowed_http_methods_for_whitelisted_func = {} + + +def whitelist(allow_guest=False, xss_safe=False, methods=None): + """ + Decorator for whitelisting a function and making it accessible via HTTP. + Standard request will be `/api/method/[path.to.method]` + + :param allow_guest: Allow non logged-in user to access this method. + :param methods: Allowed http method to access the method. + + Use as: + + @influxframework.whitelist() + def myfunc(param1, param2): + pass + """ + + if not methods: + methods = ["GET", "POST", "PUT", "DELETE"] + + def innerfn(fn): + global whitelisted, guest_methods, xss_safe_methods, allowed_http_methods_for_whitelisted_func + + # get function from the unbound / bound method + # this is needed because functions can be compared, but not methods + method = None + if hasattr(fn, "__func__"): + method = fn + fn = method.__func__ + + whitelisted.append(fn) + allowed_http_methods_for_whitelisted_func[fn] = methods + + if allow_guest: + guest_methods.append(fn) + + if xss_safe: + xss_safe_methods.append(fn) + + return method or fn + + return innerfn + + +def is_whitelisted(method): + from influxframework.utils import sanitize_html + + is_guest = session["user"] == "Guest" + if method not in whitelisted or is_guest and method not in guest_methods: + throw(_("Not permitted"), PermissionError) + + if is_guest and method not in xss_safe_methods: + # strictly sanitize form_dict + # escapes html characters like <> except for predefined tags like a, b, ul etc. + for key, value in form_dict.items(): + if isinstance(value, str): + form_dict[key] = sanitize_html(value) + + +def read_only(): + def innfn(fn): + def wrapper_fn(*args, **kwargs): + if conf.read_from_replica: + connect_replica() + + try: + retval = fn(*args, **get_newargs(fn, kwargs)) + finally: + if local and hasattr(local, "primary_db"): + local.db.close() + local.db = local.primary_db + + return retval + + return wrapper_fn + + return innfn + + +def write_only(): + # if replica connection exists, we have to replace it momentarily with the primary connection + def innfn(fn): + def wrapper_fn(*args, **kwargs): + primary_db = getattr(local, "primary_db", None) + replica_db = getattr(local, "replica_db", None) + in_read_only = getattr(local, "db", None) != primary_db + + # switch to primary connection + if in_read_only and primary_db: + local.db = local.primary_db + + try: + retval = fn(*args, **get_newargs(fn, kwargs)) + finally: + # switch back to replica connection + if in_read_only and replica_db: + local.db = replica_db + + return retval + + return wrapper_fn + + return innfn + + +def only_for(roles: list[str] | tuple[str] | str, message=False): + """ + Raises `influxframework.PermissionError` if the user does not have any of the permitted roles. + + :param roles: Permitted role(s) + """ + + if local.flags.in_test or local.session.user == "Administrator": + return + + if isinstance(roles, str): + roles = (roles,) + + if not set(roles).intersection(get_roles()): + if not message: + raise PermissionError + + throw( + _("This action is only allowed for {}").format( + ", ".join(bold(_(role)) for role in roles), + ), + PermissionError, + _("Not Permitted"), + ) + + +def get_domain_data(module): + try: + domain_data = get_hooks("domains") + if module in domain_data: + return _dict(get_attr(get_hooks("domains")[module][0] + ".data")) + else: + return _dict() + except ImportError: + if local.flags.in_test: + return _dict() + else: + raise + + +def clear_cache(user: str | None = None, doctype: str | None = None): + """Clear **User**, **DocType** or global cache. + + :param user: If user is given, only user cache is cleared. + :param doctype: If doctype is given, only DocType cache is cleared.""" + import influxframework.cache_manager + import influxframework.utils.caching + + if doctype: + influxframework.cache_manager.clear_doctype_cache(doctype) + reset_metadata_version() + elif user: + influxframework.cache_manager.clear_user_cache(user) + else: # everything + from influxframework import translate + + influxframework.cache_manager.clear_user_cache() + influxframework.cache_manager.clear_domain_cache() + translate.clear_cache() + reset_metadata_version() + local.cache = {} + local.new_doc_templates = {} + + for fn in get_hooks("clear_cache"): + get_attr(fn)() + + influxframework.utils.caching._SITE_CACHE.clear() + local.role_permissions = {} + if hasattr(local, "request_cache"): + local.request_cache.clear() + if hasattr(local, "system_settings"): + del local.system_settings + if hasattr(local, "website_settings"): + del local.website_settings + + +def only_has_select_perm(doctype, user=None, ignore_permissions=False): + if ignore_permissions: + return False + + if not user: + user = local.session.user + + import influxframework.permissions + + permissions = influxframework.permissions.get_role_permissions(doctype, user=user) + + if permissions.get("select") and not permissions.get("read"): + return True + else: + return False + + +def has_permission( + doctype=None, + ptype="read", + doc=None, + user=None, + verbose=False, + throw=False, + *, + parent_doctype=None, +): + """ + Returns True if the user has permission `ptype` for given `doctype` or `doc` + Raises `influxframework.PermissionError` if user isn't permitted and `throw` is truthy + + :param doctype: DocType for which permission is to be check. + :param ptype: Permission type (`read`, `write`, `create`, `submit`, `cancel`, `amend`). Default: `read`. + :param doc: [optional] Checks User permissions for given doc. + :param user: [optional] Check for given user. Default: current user. + :param verbose: DEPRECATED, will be removed in a future release. + :param parent_doctype: Required when checking permission for a child DocType (unless doc is specified). + """ + import influxframework.permissions + + if not doctype and doc: + doctype = doc.doctype + + out = influxframework.permissions.has_permission( + doctype, + ptype, + doc=doc, + user=user, + raise_exception=throw, + parent_doctype=parent_doctype, + ) + + if throw and not out: + # mimics influxframework.throw + document_label = f"{_(doc.doctype)} {doc.name}" if doc else _(doctype) + msgprint( + _("No permission for {0}").format(document_label), + raise_exception=ValidationError, + title=None, + indicator="red", + is_minimizable=None, + wide=None, + as_list=False, + ) + + return out + + +def has_website_permission(doc=None, ptype="read", user=None, verbose=False, doctype=None): + """Raises `influxframework.PermissionError` if not permitted. + + :param doctype: DocType for which permission is to be check. + :param ptype: Permission type (`read`, `write`, `create`, `submit`, `cancel`, `amend`). Default: `read`. + :param doc: Checks User permissions for given doc. + :param user: [optional] Check for given user. Default: current user.""" + + if not user: + user = session.user + + if doc: + if isinstance(doc, str): + doc = get_doc(doctype, doc) + + doctype = doc.doctype + + if doc.flags.ignore_permissions: + return True + + # check permission in controller + if hasattr(doc, "has_website_permission"): + return doc.has_website_permission(ptype, user, verbose=verbose) + + hooks = (get_hooks("has_website_permission") or {}).get(doctype, []) + if hooks: + for method in hooks: + result = call(method, doc=doc, ptype=ptype, user=user, verbose=verbose) + # if even a single permission check is Falsy + if not result: + return False + + # else it is Truthy + return True + + else: + return False + + +def is_table(doctype: str) -> bool: + """Returns True if `istable` property (indicating child Table) is set for given DocType.""" + + def get_tables(): + return db.get_values("DocType", filters={"istable": 1}, order_by=None, pluck=True) + + tables = cache().get_value("is_table", get_tables) + return doctype in tables + + +def get_precision( + doctype: str, fieldname: str, currency: str | None = None, doc: Optional["Document"] = None +) -> int: + """Get precision for a given field""" + from influxframework.model.meta import get_field_precision + + return get_field_precision(get_meta(doctype).get_field(fieldname), doc, currency) + + +def generate_hash(txt: str | None = None, length: int | None = None) -> str: + """Generates random hash for given text + current timestamp + random string.""" + import hashlib + import time + + from .utils import random_string + + digest = hashlib.sha224( + ((txt or "") + repr(time.time()) + repr(random_string(8))).encode() + ).hexdigest() + if length: + digest = digest[:length] + return digest + + +def reset_metadata_version(): + """Reset `metadata_version` (Client (Javascript) build ID) hash.""" + v = generate_hash() + cache().set_value("metadata_version", v) + return v + + +def new_doc( + doctype: str, + parent_doc: Optional["Document"] = None, + parentfield: str | None = None, + as_dict: bool = False, +) -> "Document": + """Returns a new document of the given DocType with defaults set. + + :param doctype: DocType of the new document. + :param parent_doc: [optional] add to parent document. + :param parentfield: [optional] add against this `parentfield`.""" + from influxframework.model.create_new import get_new_doc + + return get_new_doc(doctype, parent_doc, parentfield, as_dict=as_dict) + + +def set_value(doctype, docname, fieldname, value=None): + """Set document value. Calls `influxframework.client.set_value`""" + import influxframework.client + + return influxframework.client.set_value(doctype, docname, fieldname, value) + + +def get_cached_doc(*args, **kwargs) -> "Document": + def _respond(doc, from_redis=False): + if isinstance(doc, dict): + local.document_cache[key] = doc = get_doc(doc) + + elif from_redis: + local.document_cache[key] = doc + + return doc + + if key := can_cache_doc(args): + # local cache - has "ready" `Document` objects + if doc := local.document_cache.get(key): + return _respond(doc) + + # redis cache + if doc := cache().hget("document_cache", key): + return _respond(doc, True) + + # Not found in local/redis, fetch from DB + doc = get_doc(*args, **kwargs) + + # Store in cache + if not key: + key = get_document_cache_key(doc.doctype, doc.name) + + _set_document_in_cache(key, doc) + + return doc + + +def _set_document_in_cache(key: str, doc: "Document") -> None: + local.document_cache[key] = doc + + # Avoid setting in local.cache since we're already using local.document_cache above + # Try pickling the doc object as-is first, else fallback to doc.as_dict() + try: + cache().hset("document_cache", key, doc, cache_locally=False) + except Exception: + cache().hset("document_cache", key, doc.as_dict(), cache_locally=False) + + +def can_cache_doc(args) -> str | None: + """ + Determine if document should be cached based on get_doc params. + Returns cache key if doc can be cached, None otherwise. + """ + + if not args: + return + + doctype = args[0] + name = doctype if len(args) == 1 else args[1] + + # Only cache if both doctype and name are strings + if isinstance(doctype, str) and isinstance(name, str): + return get_document_cache_key(doctype, name) + + +def get_document_cache_key(doctype: str, name: str): + return f"{doctype}::{name}" + + +def clear_document_cache(doctype, name): + cache().hdel("last_modified", doctype) + key = get_document_cache_key(doctype, name) + if key in local.document_cache: + del local.document_cache[key] + cache().hdel("document_cache", key) + if doctype == "System Settings" and hasattr(local, "system_settings"): + delattr(local, "system_settings") + if doctype == "Website Settings" and hasattr(local, "website_settings"): + delattr(local, "website_settings") + + +def get_cached_value( + doctype: str, name: str, fieldname: str = "name", as_dict: bool = False +) -> Any: + try: + doc = get_cached_doc(doctype, name) + except DoesNotExistError: + clear_last_message() + return + + if isinstance(fieldname, str): + if as_dict: + throw("Cannot make dict for single fieldname") + return doc.get(fieldname) + + values = [doc.get(f) for f in fieldname] + if as_dict: + return _dict(zip(fieldname, values)) + return values + + +def get_doc(*args, **kwargs) -> "Document": + """Return a `influxframework.model.document.Document` object of the given type and name. + + :param arg1: DocType name as string **or** document JSON. + :param arg2: [optional] Document name as string. + + Examples: + + # insert a new document + todo = influxframework.get_doc({"doctype":"ToDo", "description": "test"}) + todo.insert() + + # open an existing document + todo = influxframework.get_doc("ToDo", "TD0001") + + """ + import influxframework.model.document + + doc = influxframework.model.document.get_doc(*args, **kwargs) + + # Replace cache if stale one exists + if (key := can_cache_doc(args)) and cache().hexists("document_cache", key): + _set_document_in_cache(key, doc) + + return doc + + +def get_last_doc(doctype, filters=None, order_by="creation desc", *, for_update=False): + """Get last created document of this type.""" + d = get_all(doctype, filters=filters, limit_page_length=1, order_by=order_by, pluck="name") + if d: + return get_doc(doctype, d[0], for_update=for_update) + else: + raise DoesNotExistError + + +def get_single(doctype): + """Return a `influxframework.model.document.Document` object of the given Single doctype.""" + return get_doc(doctype, doctype) + + +def get_meta(doctype, cached=True): + """Get `influxframework.model.meta.Meta` instance of given doctype name.""" + import influxframework.model.meta + + return influxframework.model.meta.get_meta(doctype, cached=cached) + + +def get_meta_module(doctype): + import influxframework.modules + + return influxframework.modules.load_doctype_module(doctype) + + +def delete_doc( + doctype: str | None = None, + name: str | None = None, + force: bool = False, + ignore_doctypes: list[str] | None = None, + for_reload: bool = False, + ignore_permissions: bool = False, + flags: None = None, + ignore_on_trash: bool = False, + ignore_missing: bool = True, + delete_permanently: bool = False, +): + """Delete a document. Calls `influxframework.model.delete_doc.delete_doc`. + + :param doctype: DocType of document to be delete. + :param name: Name of document to be delete. + :param force: Allow even if document is linked. Warning: This may lead to data integrity errors. + :param ignore_doctypes: Ignore if child table is one of these. + :param for_reload: Call `before_reload` trigger before deleting. + :param ignore_permissions: Ignore user permissions. + :param delete_permanently: Do not create a Deleted Document for the document.""" + import influxframework.model.delete_doc + + return influxframework.model.delete_doc.delete_doc( + doctype, + name, + force, + ignore_doctypes, + for_reload, + ignore_permissions, + flags, + ignore_on_trash, + ignore_missing, + delete_permanently, + ) + + +def delete_doc_if_exists(doctype, name, force=0): + """Delete document if exists.""" + delete_doc(doctype, name, force=force, ignore_missing=True) + + +def reload_doctype(doctype, force=False, reset_permissions=False): + """Reload DocType from model (`[module]/[doctype]/[name]/[name].json`) files.""" + reload_doc( + scrub(db.get_value("DocType", doctype, "module")), + "doctype", + scrub(doctype), + force=force, + reset_permissions=reset_permissions, + ) + + +def reload_doc( + module: str, + dt: str | None = None, + dn: str | None = None, + force: bool = False, + reset_permissions: bool = False, +): + """Reload Document from model (`[module]/[doctype]/[name]/[name].json`) files. + + :param module: Module name. + :param dt: DocType name. + :param dn: Document name. + :param force: Reload even if `modified` timestamp matches. + """ + + import influxframework.modules + + return influxframework.modules.reload_doc(module, dt, dn, force=force, reset_permissions=reset_permissions) + + +@whitelist() +def rename_doc( + doctype: str, + old: str, + new: str, + force: bool = False, + merge: bool = False, + *, + ignore_if_exists: bool = False, + show_alert: bool = True, + rebuild_search: bool = True, +) -> str: + """ + Renames a doc(dt, old) to doc(dt, new) and updates all linked fields of type "Link" + + Calls `influxframework.model.rename_doc.rename_doc` + """ + + from influxframework.model.rename_doc import rename_doc + + return rename_doc( + doctype=doctype, + old=old, + new=new, + force=force, + merge=merge, + ignore_if_exists=ignore_if_exists, + show_alert=show_alert, + rebuild_search=rebuild_search, + ) + + +def get_module(modulename): + """Returns a module object for given Python module name using `importlib.import_module`.""" + return importlib.import_module(modulename) + + +def scrub(txt: str) -> str: + """Returns sluggified string. e.g. `Sales Order` becomes `sales_order`.""" + return cstr(txt).replace(" ", "_").replace("-", "_").lower() + + +def unscrub(txt: str) -> str: + """Returns titlified string. e.g. `sales_order` becomes `Sales Order`.""" + return txt.replace("_", " ").replace("-", " ").title() + + +def get_module_path(module, *joins): + """Get the path of the given module name. + + :param module: Module name. + :param *joins: Join additional path elements using `os.path.join`.""" + from influxframework.modules.utils import get_module_app + + app = get_module_app(module) + return get_pymodule_path(app + "." + scrub(module), *joins) + + +def get_app_path(app_name, *joins): + """Return path of given app. + + :param app: App name. + :param *joins: Join additional path elements using `os.path.join`.""" + return get_pymodule_path(app_name, *joins) + + +def get_site_path(*joins): + """Return path of current site. + + :param *joins: Join additional path elements using `os.path.join`.""" + return os.path.join(local.site_path, *joins) + + +def get_pymodule_path(modulename, *joins): + """Return path of given Python module name. + + :param modulename: Python module name. + :param *joins: Join additional path elements using `os.path.join`.""" + if not "public" in joins: + joins = [scrub(part) for part in joins] + return os.path.join(os.path.dirname(get_module(scrub(modulename)).__file__ or ""), *joins) + + +def get_module_list(app_name): + """Get list of modules for given all via `app/modules.txt`.""" + return get_file_items(os.path.join(os.path.dirname(get_module(app_name).__file__), "modules.txt")) + + +def get_all_apps(with_internal_apps=True, sites_path=None): + """Get list of all apps via `sites/apps.txt`.""" + if not sites_path: + sites_path = local.sites_path + + apps = get_file_items(os.path.join(sites_path, "apps.txt"), raise_not_found=True) + + if with_internal_apps: + for app in get_file_items(os.path.join(local.site_path, "apps.txt")): + if app not in apps: + apps.append(app) + + if "influxframework" in apps: + apps.remove("influxframework") + apps.insert(0, "influxframework") + + return apps + + +@request_cache +def get_installed_apps(sort=False, influxframework_last=False): + """Get list of installed apps in current site.""" + if getattr(flags, "in_install_db", True): + return [] + + if not db: + connect() + + if not local.all_apps: + local.all_apps = cache().get_value("all_apps", get_all_apps) + + installed = json.loads(db.get_global("installed_apps") or "[]") + + if sort: + installed = [app for app in local.all_apps if app in installed] + + if influxframework_last: + if "influxframework" in installed: + installed.remove("influxframework") + installed.append("influxframework") + + return installed + + +def get_doc_hooks(): + """Returns hooked methods for given doc. It will expand the dict tuple if required.""" + if not hasattr(local, "doc_events_hooks"): + hooks = get_hooks("doc_events", {}) + out = {} + for key, value in hooks.items(): + if isinstance(key, tuple): + for doctype in key: + append_hook(out, doctype, value) + else: + append_hook(out, key, value) + + local.doc_events_hooks = out + + return local.doc_events_hooks + + +@request_cache +def _load_app_hooks(app_name: str | None = None): + hooks = {} + apps = [app_name] if app_name else get_installed_apps(sort=True) + + for app in apps: + try: + app_hooks = get_module(f"{app}.hooks") + except ImportError: + if local.flags.in_install_app: + # if app is not installed while restoring + # ignore it + pass + print(f'Could not find app "{app}"') + if not request: + raise SystemExit + raise + for key in dir(app_hooks): + if not key.startswith("_"): + append_hook(hooks, key, getattr(app_hooks, key)) + return hooks + + +def get_hooks( + hook: str = None, default: Any | None = "_KEEP_DEFAULT_LIST", app_name: str = None +) -> _dict: + """Get hooks via `app/hooks.py` + + :param hook: Name of the hook. Will gather all hooks for this name and return as a list. + :param default: Default if no hook found. + :param app_name: Filter by app.""" + + if app_name: + hooks = _dict(_load_app_hooks(app_name)) + else: + if conf.developer_mode: + hooks = _dict(_load_app_hooks()) + else: + hooks = _dict(cache().get_value("app_hooks", _load_app_hooks)) + + if hook: + return hooks.get(hook, ([] if default == "_KEEP_DEFAULT_LIST" else default)) + return hooks + + +def append_hook(target, key, value): + """appends a hook to the the target dict. + + If the hook key, exists, it will make it a key. + + If the hook value is a dict, like doc_events, it will + listify the values against the key. + """ + if isinstance(value, dict): + # dict? make a list of values against each key + target.setdefault(key, {}) + for inkey in value: + append_hook(target[key], inkey, value[inkey]) + else: + # make a list + target.setdefault(key, []) + if not isinstance(value, list): + value = [value] + target[key].extend(value) + + +def setup_module_map(): + """Rebuild map of all modules (internal).""" + _cache = cache() + + if conf.db_name: + local.app_modules = _cache.get_value("app_modules") + local.module_app = _cache.get_value("module_app") + + if not (local.app_modules and local.module_app): + local.module_app, local.app_modules = {}, {} + for app in get_all_apps(with_internal_apps=True): + local.app_modules.setdefault(app, []) + for module in get_module_list(app): + module = scrub(module) + local.module_app[module] = app + local.app_modules[app].append(module) + + if conf.db_name: + _cache.set_value("app_modules", local.app_modules) + _cache.set_value("module_app", local.module_app) + + +def get_file_items(path, raise_not_found=False, ignore_empty_lines=True): + """Returns items from text file as a list. Ignores empty lines.""" + import influxframework.utils + + content = read_file(path, raise_not_found=raise_not_found) + if content: + content = influxframework.utils.strip(content) + + return [ + p.strip() + for p in content.splitlines() + if (not ignore_empty_lines) or (p.strip() and not p.startswith("#")) + ] + else: + return [] + + +def get_file_json(path): + """Read a file and return parsed JSON object.""" + with open(path) as f: + return json.load(f) + + +def read_file(path, raise_not_found=False): + """Open a file and return its content as Unicode.""" + if isinstance(path, str): + path = path.encode("utf-8") + + if os.path.exists(path): + with open(path) as f: + return as_unicode(f.read()) + elif raise_not_found: + raise OSError(f"{path} Not Found") + else: + return None + + +def get_attr(method_string: str) -> Any: + """Get python method object from its name.""" + app_name = method_string.split(".")[0] + if ( + not local.flags.in_uninstall + and not local.flags.in_install + and app_name not in get_installed_apps() + ): + throw(_("App {0} is not installed").format(app_name), AppNotInstalledError) + + modulename = ".".join(method_string.split(".")[:-1]) + methodname = method_string.split(".")[-1] + return getattr(get_module(modulename), methodname) + + +def call(fn: str | Callable, *args, **kwargs): + """Call a function and match arguments.""" + if isinstance(fn, str): + fn = get_attr(fn) + + newargs = get_newargs(fn, kwargs) + + return fn(*args, **newargs) + + +def get_newargs(fn: Callable, kwargs: dict[str, Any]) -> dict[str, Any]: + """Remove any kwargs that are not supported by the function. + + Example: + >>> def fn(a=1, b=2): pass + + >>> get_newargs(fn, {"a": 2, "c": 1}) + {"a": 2} + """ + + # if function has any **kwargs parameter that capture arbitrary keyword arguments + # Ref: https://docs.python.org/3/library/inspect.html#inspect.Parameter.kind + varkw_exist = False + + if hasattr(fn, "fnargs"): + fnargs = fn.fnargs + else: + signature = inspect.signature(fn) + fnargs = list(signature.parameters) + + for param_name, parameter in signature.parameters.items(): + if parameter.kind == inspect.Parameter.VAR_KEYWORD: + varkw_exist = True + fnargs.remove(param_name) + break + + newargs = {} + for a in kwargs: + if (a in fnargs) or varkw_exist: + newargs[a] = kwargs.get(a) + + newargs.pop("ignore_permissions", None) + newargs.pop("flags", None) + + return newargs + + +def make_property_setter( + args, ignore_validate=False, validate_fields_for_doctype=True, is_system_generated=True +): + """Create a new **Property Setter** (for overriding DocType and DocField properties). + + If doctype is not specified, it will create a property setter for all fields with the + given fieldname""" + args = _dict(args) + if not args.doctype_or_field: + args.doctype_or_field = "DocField" + if not args.property_type: + args.property_type = ( + db.get_value("DocField", {"parent": "DocField", "fieldname": args.property}, "fieldtype") + or "Data" + ) + + if not args.doctype: + DocField_doctype = qb.DocType("DocField") + doctype_list = ( + qb.from_(DocField_doctype) + .select(DocField_doctype.parent) + .where(DocField_doctype.fieldname == args.fieldname) + .distinct() + ).run(pluck=True) + + else: + doctype_list = [args.doctype] + + for doctype in doctype_list: + if not args.property_type: + args.property_type = ( + db.get_value("DocField", {"parent": doctype, "fieldname": args.fieldname}, "fieldtype") + or "Data" + ) + + ps = get_doc( + { + "doctype": "Property Setter", + "doctype_or_field": args.doctype_or_field, + "doc_type": doctype, + "field_name": args.fieldname, + "row_name": args.row_name, + "property": args.property, + "value": args.value, + "property_type": args.property_type or "Data", + "is_system_generated": is_system_generated, + "__islocal": 1, + } + ) + ps.flags.ignore_validate = ignore_validate + ps.flags.validate_fields_for_doctype = validate_fields_for_doctype + ps.validate_fieldtype_change() + ps.insert() + + +def import_doc(path): + """Import a file using Data Import.""" + from influxframework.core.doctype.data_import.data_import import import_doc + + import_doc(path) + + +def copy_doc(doc: "Document", ignore_no_copy: bool = True) -> "Document": + """No_copy fields also get copied.""" + import copy + + def remove_no_copy_fields(d): + for df in d.meta.get("fields", {"no_copy": 1}): + if hasattr(d, df.fieldname): + d.set(df.fieldname, None) + + fields_to_clear = ["name", "owner", "creation", "modified", "modified_by"] + + if not local.flags.in_test: + fields_to_clear.append("docstatus") + + if not isinstance(doc, dict): + d = doc.as_dict() + else: + d = doc + + newdoc = get_doc(copy.deepcopy(d)) + newdoc.set("__islocal", 1) + for fieldname in fields_to_clear + ["amended_from", "amendment_date"]: + newdoc.set(fieldname, None) + + if not ignore_no_copy: + remove_no_copy_fields(newdoc) + + for i, d in enumerate(newdoc.get_all_children()): + d.set("__islocal", 1) + + for fieldname in fields_to_clear: + d.set(fieldname, None) + + if not ignore_no_copy: + remove_no_copy_fields(d) + + return newdoc + + +def compare(val1, condition, val2): + """Compare two values using `influxframework.utils.compare` + + `condition` could be: + - "^" + - "in" + - "not in" + - "=" + - "!=" + - ">" + - "<" + - ">=" + - "<=" + - "not None" + - "None" + """ + import influxframework.utils + + return influxframework.utils.compare(val1, condition, val2) + + +def respond_as_web_page( + title, + html, + success=None, + http_status_code=None, + context=None, + indicator_color=None, + primary_action="/", + primary_label=None, + fullpage=False, + width=None, + template="message", +): + """Send response as a web page with a message rather than JSON. Used to show permission errors etc. + + :param title: Page title and heading. + :param message: Message to be shown. + :param success: Alert message. + :param http_status_code: HTTP status code + :param context: web template context + :param indicator_color: color of indicator in title + :param primary_action: route on primary button (default is `/`) + :param primary_label: label on primary button (default is "Home") + :param fullpage: hide header / footer + :param width: Width of message in pixels + :param template: Optionally pass view template + """ + local.message_title = title + local.message = html + local.response["type"] = "page" + local.response["route"] = template + local.no_cache = 1 + + if http_status_code: + local.response["http_status_code"] = http_status_code + + if not context: + context = {} + + if not indicator_color: + if success: + indicator_color = "green" + elif http_status_code and http_status_code > 300: + indicator_color = "red" + else: + indicator_color = "blue" + + context["indicator_color"] = indicator_color + context["primary_label"] = primary_label + context["primary_action"] = primary_action + context["error_code"] = http_status_code + context["fullpage"] = fullpage + if width: + context["card_width"] = width + + local.response["context"] = context + + +def redirect(url): + """Raise a 301 redirect to url""" + from influxframework.exceptions import Redirect + + flags.redirect_location = url + raise Redirect + + +def redirect_to_message(title, html, http_status_code=None, context=None, indicator_color=None): + """Redirects to /message?id=random + Similar to respond_as_web_page, but used to 'redirect' and show message pages like success, failure, etc. with a detailed message + + :param title: Page title and heading. + :param message: Message to be shown. + :param http_status_code: HTTP status code. + + Example Usage: + influxframework.redirect_to_message(_('Thank you'), "

    You will receive an email at test@example.com

    ") + + """ + + message_id = generate_hash(length=8) + message = {"context": context or {}, "http_status_code": http_status_code or 200} + message["context"].update({"header": title, "title": title, "message": html}) + + if indicator_color: + message["context"].update({"indicator_color": indicator_color}) + + cache().set_value(f"message_id:{message_id}", message, expires_in_sec=60) + location = f"/message?id={message_id}" + + if not getattr(local, "is_ajax", False): + local.response["type"] = "redirect" + local.response["location"] = location + + else: + return location + + +def build_match_conditions(doctype, as_condition=True): + """Return match (User permissions) for given doctype as list or SQL.""" + import influxframework.desk.reportview + + return influxframework.desk.reportview.build_match_conditions(doctype, as_condition=as_condition) + + +def get_list(doctype, *args, **kwargs): + """List database query via `influxframework.model.db_query`. Will also check for permissions. + + :param doctype: DocType on which query is to be made. + :param fields: List of fields or `*`. + :param filters: List of filters (see example). + :param order_by: Order By e.g. `modified desc`. + :param limit_start: Start results at record #. Default 0. + :param limit_page_length: No of records in the page. Default 20. + + Example usage: + + # simple dict filter + influxframework.get_list("ToDo", fields=["name", "description"], filters = {"owner":"test@example.com"}) + + # filter as a list of lists + influxframework.get_list("ToDo", fields="*", filters = [["modified", ">", "2014-01-01"]]) + + # filter as a list of dicts + influxframework.get_list("ToDo", fields="*", filters = {"description": ("like", "test%")}) + """ + import influxframework.model.db_query + + return influxframework.model.db_query.DatabaseQuery(doctype).execute(*args, **kwargs) + + +def get_all(doctype, *args, **kwargs): + """List database query via `influxframework.model.db_query`. Will **not** check for permissions. + Parameters are same as `influxframework.get_list` + + :param doctype: DocType on which query is to be made. + :param fields: List of fields or `*`. Default is: `["name"]`. + :param filters: List of filters (see example). + :param order_by: Order By e.g. `modified desc`. + :param limit_start: Start results at record #. Default 0. + :param limit_page_length: No of records in the page. Default 20. + + Example usage: + + # simple dict filter + influxframework.get_all("ToDo", fields=["name", "description"], filters = {"owner":"test@example.com"}) + + # filter as a list of lists + influxframework.get_all("ToDo", fields=["*"], filters = [["modified", ">", "2014-01-01"]]) + + # filter as a list of dicts + influxframework.get_all("ToDo", fields=["*"], filters = {"description": ("like", "test%")}) + """ + kwargs["ignore_permissions"] = True + if not "limit_page_length" in kwargs: + kwargs["limit_page_length"] = 0 + return get_list(doctype, *args, **kwargs) + + +def get_value(*args, **kwargs): + """Returns a document property or list of properties. + + Alias for `influxframework.db.get_value` + + :param doctype: DocType name. + :param filters: Filters like `{"x":"y"}` or name of the document. `None` if Single DocType. + :param fieldname: Column name. + :param ignore: Don't raise exception if table, column is missing. + :param as_dict: Return values as dict. + :param debug: Print query in error log. + """ + return db.get_value(*args, **kwargs) + + +def as_json(obj: dict | list, indent=1, separators=None) -> str: + from influxframework.utils.response import json_handler + + if separators is None: + separators = (",", ": ") + + try: + return json.dumps( + obj, indent=indent, sort_keys=True, default=json_handler, separators=separators + ) + except TypeError: + # this would break in case the keys are not all os "str" type - as defined in the JSON + # adding this to ensure keys are sorted (expected behaviour) + sorted_obj = dict(sorted(obj.items(), key=lambda kv: str(kv[0]))) + return json.dumps(sorted_obj, indent=indent, default=json_handler, separators=separators) + + +def are_emails_muted(): + from influxframework.utils import cint + + return flags.mute_emails or cint(conf.get("mute_emails") or 0) or False + + +def get_test_records(doctype): + """Returns list of objects from `test_records.json` in the given doctype's folder.""" + from influxframework.modules import get_doctype_module, get_module_path + + path = os.path.join( + get_module_path(get_doctype_module(doctype)), "doctype", scrub(doctype), "test_records.json" + ) + if os.path.exists(path): + with open(path) as f: + return json.loads(f.read()) + else: + return [] + + +def format_value(*args, **kwargs): + """Format value with given field properties. + + :param value: Value to be formatted. + :param df: (Optional) DocField object with properties `fieldtype`, `options` etc.""" + import influxframework.utils.formatters + + return influxframework.utils.formatters.format_value(*args, **kwargs) + + +def format(*args, **kwargs): + """Format value with given field properties. + + :param value: Value to be formatted. + :param df: (Optional) DocField object with properties `fieldtype`, `options` etc.""" + import influxframework.utils.formatters + + return influxframework.utils.formatters.format_value(*args, **kwargs) + + +def get_print( + doctype=None, + name=None, + print_format=None, + style=None, + html=None, + as_pdf=False, + doc=None, + output=None, + no_letterhead=0, + password=None, + pdf_options=None, +): + """Get Print Format for given document. + + :param doctype: DocType of document. + :param name: Name of document. + :param print_format: Print Format name. Default 'Standard', + :param style: Print Format style. + :param as_pdf: Return as PDF. Default False. + :param password: Password to encrypt the pdf with. Default None""" + from influxframework.utils.pdf import get_pdf + from influxframework.website.serve import get_response_content + + local.form_dict.doctype = doctype + local.form_dict.name = name + local.form_dict.format = print_format + local.form_dict.style = style + local.form_dict.doc = doc + local.form_dict.no_letterhead = no_letterhead + + pdf_options = pdf_options or {} + if password: + pdf_options["password"] = password + + if not html: + html = get_response_content("printview") + + if as_pdf: + return get_pdf(html, options=pdf_options, output=output) + else: + return html + + +def attach_print( + doctype, + name, + file_name=None, + print_format=None, + style=None, + html=None, + doc=None, + lang=None, + print_letterhead=True, + password=None, +): + from influxframework.utils import scrub_urls + + if not file_name: + file_name = name + file_name = cstr(file_name).replace(" ", "").replace("/", "-") + + print_settings = db.get_singles_dict("Print Settings") + + _lang = local.lang + + # set lang as specified in print format attachment + if lang: + local.lang = lang + local.flags.ignore_print_permissions = True + + no_letterhead = not print_letterhead + + kwargs = dict( + print_format=print_format, + style=style, + html=html, + doc=doc, + no_letterhead=no_letterhead, + password=password, + ) + + content = "" + if int(print_settings.send_print_as_pdf or 0): + ext = ".pdf" + kwargs["as_pdf"] = True + content = get_print(doctype, name, **kwargs) + else: + ext = ".html" + content = scrub_urls(get_print(doctype, name, **kwargs)).encode("utf-8") + + out = {"fname": file_name + ext, "fcontent": content} + + local.flags.ignore_print_permissions = False + # reset lang to original local lang + local.lang = _lang + + return out + + +def publish_progress(*args, **kwargs): + """Show the user progress for a long request + + :param percent: Percent progress + :param title: Title + :param doctype: Optional, for document type + :param docname: Optional, for document name + :param description: Optional description + """ + import influxframework.realtime + + return influxframework.realtime.publish_progress(*args, **kwargs) + + +def publish_realtime(*args, **kwargs): + """Publish real-time updates + + :param event: Event name, like `task_progress` etc. + :param message: JSON message object. For async must contain `task_id` + :param room: Room in which to publish update (default entire site) + :param user: Transmit to user + :param doctype: Transmit to doctype, docname + :param docname: Transmit to doctype, docname + :param after_commit: (default False) will emit after current transaction is committed + """ + import influxframework.realtime + + return influxframework.realtime.publish_realtime(*args, **kwargs) + + +def local_cache(namespace, key, generator, regenerate_if_none=False): + """A key value store for caching within a request + + :param namespace: influxframework.local.cache[namespace] + :param key: influxframework.local.cache[namespace][key] used to retrieve value + :param generator: method to generate a value if not found in store + + """ + if namespace not in local.cache: + local.cache[namespace] = {} + + if key not in local.cache[namespace]: + local.cache[namespace][key] = generator() + + elif local.cache[namespace][key] is None and regenerate_if_none: + # if key exists but the previous result was None + local.cache[namespace][key] = generator() + + return local.cache[namespace][key] + + +def enqueue(*args, **kwargs): + """ + Enqueue method to be executed using a background worker + + :param method: method string or method object + :param queue: (optional) should be either long, default or short + :param timeout: (optional) should be set according to the functions + :param event: this is passed to enable clearing of jobs from queues + :param is_async: (optional) if is_async=False, the method is executed immediately, else via a worker + :param job_name: (optional) can be used to name an enqueue call, which can be used to prevent duplicate calls + :param kwargs: keyword arguments to be passed to the method + """ + import influxframework.utils.background_jobs + + return influxframework.utils.background_jobs.enqueue(*args, **kwargs) + + +def task(**task_kwargs): + def decorator_task(f): + f.enqueue = lambda **fun_kwargs: enqueue(f, **task_kwargs, **fun_kwargs) + return f + + return decorator_task + + +def enqueue_doc(*args, **kwargs): + """ + Enqueue method to be executed using a background worker + + :param doctype: DocType of the document on which you want to run the event + :param name: Name of the document on which you want to run the event + :param method: method string or method object + :param queue: (optional) should be either long, default or short + :param timeout: (optional) should be set according to the functions + :param kwargs: keyword arguments to be passed to the method + """ + import influxframework.utils.background_jobs + + return influxframework.utils.background_jobs.enqueue_doc(*args, **kwargs) + + +def get_doctype_app(doctype): + def _get_doctype_app(): + doctype_module = local.db.get_value("DocType", doctype, "module") + return local.module_app[scrub(doctype_module)] + + return local_cache("doctype_app", doctype, generator=_get_doctype_app) + + +loggers = {} +log_level = None + + +def logger( + module=None, with_more_info=False, allow_site=True, filter=None, max_size=100_000, file_count=20 +): + """Returns a python logger that uses StreamHandler""" + from influxframework.utils.logger import get_logger + + return get_logger( + module=module, + with_more_info=with_more_info, + allow_site=allow_site, + filter=filter, + max_size=max_size, + file_count=file_count, + ) + + +def log_error(title=None, message=None, reference_doctype=None, reference_name=None): + """Log error to Error Log""" + # Parameter ALERT: + # the title and message may be swapped + # the better API for this is log_error(title, message), and used in many cases this way + # this hack tries to be smart about whats a title (single line ;-)) and fixes it + + traceback = None + if message: + if "\n" in title: # traceback sent as title + traceback, title = title, message + else: + traceback = message + + title = title or "Error" + traceback = as_unicode(traceback or get_traceback(with_context=True)) + + error_log = get_doc( + doctype="Error Log", + error=traceback, + method=title, + reference_doctype=reference_doctype, + reference_name=reference_name, + ) + + if flags.read_only: + error_log.deferred_insert() + else: + return error_log.insert(ignore_permissions=True) + + +def get_desk_link(doctype, name): + html = ( + '
    {doctype_local} {name}' + ) + return html.format(doctype=doctype, name=name, doctype_local=_(doctype)) + + +def bold(text): + return f"{text}" + + +def safe_eval(code, eval_globals=None, eval_locals=None): + """A safer `eval`""" + whitelisted_globals = {"int": int, "float": float, "long": int, "round": round} + + UNSAFE_ATTRIBUTES = { + # Generator Attributes + "gi_frame", + "gi_code", + # Coroutine Attributes + "cr_frame", + "cr_code", + "cr_origin", + # Async Generator Attributes + "ag_code", + "ag_frame", + # Traceback Attributes + "tb_frame", + "tb_next", + # Format Attributes + "format", + "format_map", + } + + for attribute in UNSAFE_ATTRIBUTES: + if attribute in code: + throw(f'Illegal rule {bold(code)}. Cannot use "{attribute}"') + + if "__" in code: + throw(f'Illegal rule {bold(code)}. Cannot use "__"') + + if not eval_globals: + eval_globals = {} + + eval_globals["__builtins__"] = {} + eval_globals.update(whitelisted_globals) + return eval(code, eval_globals, eval_locals) + + +def get_website_settings(key): + if not hasattr(local, "website_settings"): + try: + local.website_settings = get_cached_doc("Website Settings") + except DoesNotExistError: + clear_last_message() + return + + return local.website_settings.get(key) + + +def get_system_settings(key): + if not hasattr(local, "system_settings"): + try: + local.system_settings = get_cached_doc("System Settings") + except DoesNotExistError: # possible during new install + clear_last_message() + return + + return local.system_settings.get(key) + + +def get_active_domains(): + from influxframework.core.doctype.domain_settings.domain_settings import get_active_domains + + return get_active_domains() + + +def get_version(doctype, name, limit=None, head=False, raise_err=True): + """ + Returns a list of version information of a given DocType. + + Note: Applicable only if DocType has changes tracked. + + Example + >>> influxframework.get_version('User', 'foobar@gmail.com') + >>> + [ + { + "version": [version.data], # Refer Version DocType get_diff method and data attribute + "user": "admin@gmail.com", # User that created this version + "creation": # Creation timestamp of that object. + } + ] + """ + meta = get_meta(doctype) + if meta.track_changes: + names = get_all( + "Version", + filters={ + "ref_doctype": doctype, + "docname": name, + "order_by": "creation" if head else None, + "limit": limit, + }, + as_list=1, + ) + + from influxframework.utils import dictify, safe_json_loads, squashify + + versions = [] + + for name in names: + name = squashify(name) + doc = get_doc("Version", name) + + data = doc.data + data = safe_json_loads(data) + data = dictify(dict(version=data, user=doc.owner, creation=doc.creation)) + + versions.append(data) + + return versions + else: + if raise_err: + raise ValueError(_("{0} has no versions tracked.").format(doctype)) + + +@whitelist(allow_guest=True) +def ping(): + return "pong" + + +def safe_encode(param, encoding="utf-8"): + try: + param = param.encode(encoding) + except Exception: + pass + return param + + +def safe_decode(param, encoding="utf-8"): + try: + param = param.decode(encoding) + except Exception: + pass + return param + + +def parse_json(val): + from influxframework.utils import parse_json + + return parse_json(val) + + +def mock(type, size=1, locale="en"): + import faker + + results = [] + fake = faker.Faker(locale) + if type not in dir(fake): + raise ValueError("Not a valid mock type.") + else: + for i in range(size): + data = getattr(fake, type)() + results.append(data) + + from influxframework.utils import squashify + + return squashify(results) + + +from influxframework.desk.search import validate_and_sanitize_search_inputs # noqa diff --git a/influxframework/api.py b/influxframework/api.py new file mode 100644 index 0000000..c6f88c8 --- /dev/null +++ b/influxframework/api.py @@ -0,0 +1,262 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import base64 +import binascii +import json +from urllib.parse import urlencode, urlparse + +import influxframework +import influxframework.client +import influxframework.handler +from influxframework import _ +from influxframework.utils.data import sbool +from influxframework.utils.response import build_response + + +def handle(): + """ + Handler for `/api` methods + + ### Examples: + + `/api/method/{methodname}` will call a whitelisted method + + `/api/resource/{doctype}` will query a table + examples: + - `?fields=["name", "owner"]` + - `?filters=[["Task", "name", "like", "%005"]]` + - `?limit_start=0` + - `?limit_page_length=20` + + `/api/resource/{doctype}/{name}` will point to a resource + `GET` will return doclist + `POST` will insert + `PUT` will update + `DELETE` will delete + + `/api/resource/{doctype}/{name}?run_method={method}` will run a whitelisted controller method + """ + + parts = influxframework.request.path[1:].split("/", 3) + call = doctype = name = None + + if len(parts) > 1: + call = parts[1] + + if len(parts) > 2: + doctype = parts[2] + + if len(parts) > 3: + name = parts[3] + + if call == "method": + influxframework.local.form_dict.cmd = doctype + return influxframework.handler.handle() + + elif call == "resource": + if "run_method" in influxframework.local.form_dict: + method = influxframework.local.form_dict.pop("run_method") + doc = influxframework.get_doc(doctype, name) + doc.is_whitelisted(method) + + if influxframework.local.request.method == "GET": + if not doc.has_permission("read"): + influxframework.throw(_("Not permitted"), influxframework.PermissionError) + influxframework.local.response.update({"data": doc.run_method(method, **influxframework.local.form_dict)}) + + if influxframework.local.request.method == "POST": + if not doc.has_permission("write"): + influxframework.throw(_("Not permitted"), influxframework.PermissionError) + + influxframework.local.response.update({"data": doc.run_method(method, **influxframework.local.form_dict)}) + influxframework.db.commit() + + else: + if name: + if influxframework.local.request.method == "GET": + doc = influxframework.get_doc(doctype, name) + if not doc.has_permission("read"): + raise influxframework.PermissionError + influxframework.local.response.update({"data": doc}) + + if influxframework.local.request.method == "PUT": + data = get_request_form_data() + + doc = influxframework.get_doc(doctype, name, for_update=True) + + if "flags" in data: + del data["flags"] + + # Not checking permissions here because it's checked in doc.save + doc.update(data) + + influxframework.local.response.update({"data": doc.save().as_dict()}) + + # check for child table doctype + if doc.get("parenttype"): + influxframework.get_doc(doc.parenttype, doc.parent).save() + + influxframework.db.commit() + + if influxframework.local.request.method == "DELETE": + # Not checking permissions here because it's checked in delete_doc + influxframework.delete_doc(doctype, name, ignore_missing=False) + influxframework.local.response.http_status_code = 202 + influxframework.local.response.message = "ok" + influxframework.db.commit() + + elif doctype: + if influxframework.local.request.method == "GET": + # set fields for influxframework.get_list + if influxframework.local.form_dict.get("fields"): + influxframework.local.form_dict["fields"] = json.loads(influxframework.local.form_dict["fields"]) + + # set limit of records for influxframework.get_list + influxframework.local.form_dict.setdefault( + "limit_page_length", + influxframework.local.form_dict.limit or influxframework.local.form_dict.limit_page_length or 20, + ) + + # convert strings to native types - only as_dict and debug accept bool + for param in ["as_dict", "debug"]: + param_val = influxframework.local.form_dict.get(param) + if param_val is not None: + influxframework.local.form_dict[param] = sbool(param_val) + + # evaluate influxframework.get_list + data = influxframework.call(influxframework.client.get_list, doctype, **influxframework.local.form_dict) + + # set influxframework.get_list result to response + influxframework.local.response.update({"data": data}) + + if influxframework.local.request.method == "POST": + # fetch data from from dict + data = get_request_form_data() + data.update({"doctype": doctype}) + + # insert document from request data + doc = influxframework.get_doc(data).insert() + + # set response data + influxframework.local.response.update({"data": doc.as_dict()}) + + # commit for POST requests + influxframework.db.commit() + else: + raise influxframework.DoesNotExistError + + else: + raise influxframework.DoesNotExistError + + return build_response("json") + + +def get_request_form_data(): + if influxframework.local.form_dict.data is None: + data = influxframework.safe_decode(influxframework.local.request.get_data()) + else: + data = influxframework.local.form_dict.data + + try: + return influxframework.parse_json(data) + except ValueError: + return influxframework.local.form_dict + + +def validate_auth(): + """ + Authenticate and sets user for the request. + """ + authorization_header = influxframework.get_request_header("Authorization", "").split(" ") + + if len(authorization_header) == 2: + validate_oauth(authorization_header) + validate_auth_via_api_keys(authorization_header) + + validate_auth_via_hooks() + + +def validate_oauth(authorization_header): + """ + Authenticate request using OAuth and set session user + + Args: + authorization_header (list of str): The 'Authorization' header containing the prefix and token + """ + + from influxframework.integrations.oauth2 import get_oauth_server + from influxframework.oauth import get_url_delimiter + + form_dict = influxframework.local.form_dict + token = authorization_header[1] + req = influxframework.request + parsed_url = urlparse(req.url) + access_token = {"access_token": token} + uri = ( + parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path + "?" + urlencode(access_token) + ) + http_method = req.method + headers = req.headers + body = req.get_data() + if req.content_type and "multipart/form-data" in req.content_type: + body = None + + try: + required_scopes = influxframework.db.get_value("OAuth Bearer Token", token, "scopes").split( + get_url_delimiter() + ) + valid, oauthlib_request = get_oauth_server().verify_request( + uri, http_method, body, headers, required_scopes + ) + if valid: + influxframework.set_user(influxframework.db.get_value("OAuth Bearer Token", token, "user")) + influxframework.local.form_dict = form_dict + except AttributeError: + pass + + +def validate_auth_via_api_keys(authorization_header): + """ + Authenticate request using API keys and set session user + + Args: + authorization_header (list of str): The 'Authorization' header containing the prefix and token + """ + + try: + auth_type, auth_token = authorization_header + authorization_source = influxframework.get_request_header("InfluxFramework-Authorization-Source") + if auth_type.lower() == "basic": + api_key, api_secret = influxframework.safe_decode(base64.b64decode(auth_token)).split(":") + validate_api_key_secret(api_key, api_secret, authorization_source) + elif auth_type.lower() == "token": + api_key, api_secret = auth_token.split(":") + validate_api_key_secret(api_key, api_secret, authorization_source) + except binascii.Error: + influxframework.throw( + _("Failed to decode token, please provide a valid base64-encoded token."), + influxframework.InvalidAuthorizationToken, + ) + except (AttributeError, TypeError, ValueError): + pass + + +def validate_api_key_secret(api_key, api_secret, influxframework_authorization_source=None): + """influxframework_authorization_source to provide api key and secret for a doctype apart from User""" + doctype = influxframework_authorization_source or "User" + doc = influxframework.db.get_value(doctype=doctype, filters={"api_key": api_key}, fieldname=["name"]) + form_dict = influxframework.local.form_dict + doc_secret = influxframework.utils.password.get_decrypted_password(doctype, doc, fieldname="api_secret") + if api_secret == doc_secret: + if doctype == "User": + user = influxframework.db.get_value(doctype="User", filters={"api_key": api_key}, fieldname=["name"]) + else: + user = influxframework.db.get_value(doctype, doc, "user") + if influxframework.local.login_manager.user in ("", "Guest"): + influxframework.set_user(user) + influxframework.local.form_dict = form_dict + + +def validate_auth_via_hooks(): + for auth_hook in influxframework.get_hooks("auth_hooks", []): + influxframework.get_attr(auth_hook)() diff --git a/influxframework/app.py b/influxframework/app.py new file mode 100644 index 0000000..056e02a --- /dev/null +++ b/influxframework/app.py @@ -0,0 +1,391 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import logging +import os + +from werkzeug.exceptions import HTTPException, NotFound +from werkzeug.local import LocalManager +from werkzeug.middleware.profiler import ProfilerMiddleware +from werkzeug.middleware.shared_data import SharedDataMiddleware +from werkzeug.wrappers import Request, Response + +import influxframework +import influxframework.api +import influxframework.auth +import influxframework.handler +import influxframework.monitor +import influxframework.rate_limiter +import influxframework.recorder +import influxframework.utils.response +from influxframework import _ +from influxframework.core.doctype.comment.comment import update_comments_in_parent_after_request +from influxframework.middlewares import StaticDataMiddleware +from influxframework.utils import get_site_name, sanitize_html +from influxframework.utils.error import make_error_snapshot +from influxframework.website.serve import get_response + +local_manager = LocalManager(influxframework.local) + +_site = None +_sites_path = os.environ.get("SITES_PATH", ".") +SAFE_HTTP_METHODS = ("GET", "HEAD", "OPTIONS") +UNSAFE_HTTP_METHODS = ("POST", "PUT", "DELETE", "PATCH") + + +class RequestContext: + def __init__(self, environ): + self.request = Request(environ) + + def __enter__(self): + init_request(self.request) + + def __exit__(self, type, value, traceback): + influxframework.destroy() + + +@local_manager.middleware +@Request.application +def application(request: Request): + response = None + + try: + rollback = True + + init_request(request) + + influxframework.recorder.record() + influxframework.monitor.start() + influxframework.rate_limiter.apply() + influxframework.api.validate_auth() + + if request.method == "OPTIONS": + response = Response() + + elif influxframework.form_dict.cmd: + response = influxframework.handler.handle() + + elif request.path.startswith("/api/"): + response = influxframework.api.handle() + + elif request.path.startswith("/backups"): + response = influxframework.utils.response.download_backup(request.path) + + elif request.path.startswith("/private/files/"): + response = influxframework.utils.response.download_private_file(request.path) + + elif request.method in ("GET", "HEAD", "POST"): + response = get_response() + + else: + raise NotFound + + except HTTPException as e: + return e + + except Exception as e: + response = handle_exception(e) + + else: + rollback = after_request(rollback) + + finally: + if request.method in ("POST", "PUT") and influxframework.db and rollback: + influxframework.db.rollback() + + influxframework.rate_limiter.update() + influxframework.monitor.stop(response) + influxframework.recorder.dump() + + log_request(request, response) + process_response(response) + influxframework.destroy() + + return response + + +def init_request(request): + influxframework.local.request = request + influxframework.local.is_ajax = influxframework.get_request_header("X-Requested-With") == "XMLHttpRequest" + + site = _site or request.headers.get("X-InfluxFramework-Site-Name") or get_site_name(request.host) + influxframework.init(site=site, sites_path=_sites_path) + + if not (influxframework.local.conf and influxframework.local.conf.db_name): + # site does not exist + raise NotFound + + if influxframework.local.conf.maintenance_mode: + influxframework.connect() + if influxframework.local.conf.allow_reads_during_maintenance: + setup_read_only_mode() + else: + raise influxframework.SessionStopped("Session Stopped") + else: + influxframework.connect(set_admin_as_user=False) + + request.max_content_length = influxframework.local.conf.get("max_file_size") or 10 * 1024 * 1024 + + make_form_dict(request) + + if request.method != "OPTIONS": + influxframework.local.http_request = influxframework.auth.HTTPRequest() + + +def setup_read_only_mode(): + """During maintenance_mode reads to DB can still be performed to reduce downtime. This + function sets up read only mode + + - Setting global flag so other pages, desk and database can know that we are in read only mode. + - Setup read only database access either by: + - Connecting to read replica if one exists + - Or setting up read only SQL transactions. + """ + influxframework.flags.read_only = True + + # If replica is available then just connect replica, else setup read only transaction. + if influxframework.conf.read_from_replica: + influxframework.connect_replica() + else: + influxframework.db.begin(read_only=True) + + +def log_request(request, response): + if hasattr(influxframework.local, "conf") and influxframework.local.conf.enable_influxframework_logger: + influxframework.logger("influxframework.web", allow_site=influxframework.local.site).info( + { + "site": get_site_name(request.host), + "remote_addr": getattr(request, "remote_addr", "NOTFOUND"), + "base_url": getattr(request, "base_url", "NOTFOUND"), + "full_path": getattr(request, "full_path", "NOTFOUND"), + "method": getattr(request, "method", "NOTFOUND"), + "scheme": getattr(request, "scheme", "NOTFOUND"), + "http_status_code": getattr(response, "status_code", "NOTFOUND"), + } + ) + + +def process_response(response): + if not response: + return + + # set cookies + if hasattr(influxframework.local, "cookie_manager"): + influxframework.local.cookie_manager.flush_cookies(response=response) + + # rate limiter headers + if hasattr(influxframework.local, "rate_limiter"): + response.headers.extend(influxframework.local.rate_limiter.headers()) + + # CORS headers + if hasattr(influxframework.local, "conf"): + set_cors_headers(response) + + +def set_cors_headers(response): + if not ( + (allowed_origins := influxframework.conf.allow_cors) + and (request := influxframework.local.request) + and (origin := request.headers.get("Origin")) + ): + return + + if allowed_origins != "*": + if not isinstance(allowed_origins, list): + allowed_origins = [allowed_origins] + + if origin not in allowed_origins: + return + + cors_headers = { + "Access-Control-Allow-Credentials": "true", + "Access-Control-Allow-Origin": origin, + "Vary": "Origin", + } + + # only required for preflight requests + if request.method == "OPTIONS": + cors_headers["Access-Control-Allow-Methods"] = request.headers.get( + "Access-Control-Request-Method" + ) + + if allowed_headers := request.headers.get("Access-Control-Request-Headers"): + cors_headers["Access-Control-Allow-Headers"] = allowed_headers + + # allow browsers to cache preflight requests for upto a day + if not influxframework.conf.developer_mode: + cors_headers["Access-Control-Max-Age"] = "86400" + + response.headers.extend(cors_headers) + + +def make_form_dict(request): + import json + + request_data = request.get_data(as_text=True) + if "application/json" in (request.content_type or "") and request_data: + args = json.loads(request_data) + else: + args = {} + args.update(request.args or {}) + args.update(request.form or {}) + + if not isinstance(args, dict): + influxframework.throw(_("Invalid request arguments")) + + influxframework.local.form_dict = influxframework._dict(args) + + if "_" in influxframework.local.form_dict: + # _ is passed by $.ajax so that the request is not cached by the browser. So, remove _ from form_dict + influxframework.local.form_dict.pop("_") + + +def handle_exception(e): + response = None + http_status_code = getattr(e, "http_status_code", 500) + return_as_message = False + accept_header = influxframework.get_request_header("Accept") or "" + respond_as_json = ( + influxframework.get_request_header("Accept") + and (influxframework.local.is_ajax or "application/json" in accept_header) + or (influxframework.local.request.path.startswith("/api/") and not accept_header.startswith("text")) + ) + + if not influxframework.session.user: + # If session creation fails then user won't be unset. This causes a lot of code that + # assumes presence of this to fail. Session creation fails => guest or expired login + # usually. + influxframework.session.user = "Guest" + + if respond_as_json: + # handle ajax responses first + # if the request is ajax, send back the trace or error message + response = influxframework.utils.response.report_error(http_status_code) + + elif isinstance(e, influxframework.SessionStopped): + response = influxframework.utils.response.handle_session_stopped() + + elif ( + http_status_code == 500 + and (influxframework.db and isinstance(e, influxframework.db.InternalError)) + and (influxframework.db and (influxframework.db.is_deadlocked(e) or influxframework.db.is_timedout(e))) + ): + http_status_code = 508 + + elif http_status_code == 401: + influxframework.respond_as_web_page( + _("Session Expired"), + _("Your session has expired, please login again to continue."), + http_status_code=http_status_code, + indicator_color="red", + ) + return_as_message = True + + elif http_status_code == 403: + influxframework.respond_as_web_page( + _("Not Permitted"), + _("You do not have enough permissions to complete the action"), + http_status_code=http_status_code, + indicator_color="red", + ) + return_as_message = True + + elif http_status_code == 404: + influxframework.respond_as_web_page( + _("Not Found"), + _("The resource you are looking for is not available"), + http_status_code=http_status_code, + indicator_color="red", + ) + return_as_message = True + + elif http_status_code == 429: + response = influxframework.rate_limiter.respond() + + else: + traceback = "
    " + sanitize_html(influxframework.get_traceback()) + "
    " + # disable traceback in production if flag is set + if influxframework.local.flags.disable_traceback and not influxframework.local.dev_server: + traceback = "" + + influxframework.respond_as_web_page( + "Server Error", traceback, http_status_code=http_status_code, indicator_color="red", width=640 + ) + return_as_message = True + + if e.__class__ == influxframework.AuthenticationError: + if hasattr(influxframework.local, "login_manager"): + influxframework.local.login_manager.clear_cookies() + + if http_status_code >= 500: + make_error_snapshot(e) + + if return_as_message: + response = get_response("message", http_status_code=http_status_code) + + if influxframework.conf.get("developer_mode") and not respond_as_json: + # don't fail silently for non-json response errors + print(influxframework.get_traceback()) + + return response + + +def after_request(rollback): + # if HTTP method would change server state, commit if necessary + if influxframework.db and ( + influxframework.local.flags.commit or influxframework.local.request.method in UNSAFE_HTTP_METHODS + ): + if influxframework.db.transaction_writes: + influxframework.db.commit() + rollback = False + + # update session + if getattr(influxframework.local, "session_obj", None): + updated_in_db = influxframework.local.session_obj.update() + if updated_in_db: + influxframework.db.commit() + rollback = False + + update_comments_in_parent_after_request() + + return rollback + + +def serve( + port=8000, profile=False, no_reload=False, no_threading=False, site=None, sites_path="." +): + global application, _site, _sites_path + _site = site + _sites_path = sites_path + + from werkzeug.serving import run_simple + + if profile or os.environ.get("USE_PROFILER"): + application = ProfilerMiddleware(application, sort_by=("cumtime", "calls")) + + if not os.environ.get("NO_STATICS"): + application = SharedDataMiddleware( + application, {"/assets": str(os.path.join(sites_path, "assets"))} + ) + + application = StaticDataMiddleware(application, {"/files": str(os.path.abspath(sites_path))}) + + application.debug = True + application.config = {"SERVER_NAME": "localhost:8000"} + + log = logging.getLogger("werkzeug") + log.propagate = False + + in_test_env = os.environ.get("CI") + if in_test_env: + log.setLevel(logging.ERROR) + + run_simple( + "0.0.0.0", + int(port), + application, + use_reloader=False if in_test_env else not no_reload, + use_debugger=not in_test_env, + use_evalex=not in_test_env, + threaded=not no_threading, + ) diff --git a/influxframework/auth.py b/influxframework/auth.py new file mode 100644 index 0000000..12241c7 --- /dev/null +++ b/influxframework/auth.py @@ -0,0 +1,554 @@ +# Copyright (c) 2021, InfluxFramework LLC +# MIT License. See LICENSE +from urllib.parse import quote + +import influxframework +import influxframework.database +import influxframework.utils +import influxframework.utils.user +from influxframework import _ +from influxframework.core.doctype.activity_log.activity_log import add_authentication_log +from influxframework.modules.patch_handler import check_session_stopped +from influxframework.sessions import Session, clear_sessions, delete_session +from influxframework.translate import get_language +from influxframework.twofactor import ( + authenticate_for_2factor, + confirm_otp_token, + get_cached_user_pass, + should_run_2fa, +) +from influxframework.utils import cint, date_diff, datetime, get_datetime, today +from influxframework.utils.password import check_password +from influxframework.website.utils import get_home_page + + +class HTTPRequest: + def __init__(self): + # set influxframework.local.request_ip + self.set_request_ip() + + # load cookies + self.set_cookies() + + # login and start/resume user session + self.set_session() + + # set request language + self.set_lang() + + # match csrf token from current session + self.validate_csrf_token() + + # write out latest cookies + influxframework.local.cookie_manager.init_cookies() + + @property + def domain(self): + if not getattr(self, "_domain", None): + self._domain = influxframework.request.host + if self._domain and self._domain.startswith("www."): + self._domain = self._domain[4:] + + return self._domain + + def set_request_ip(self): + if influxframework.get_request_header("X-Forwarded-For"): + influxframework.local.request_ip = (influxframework.get_request_header("X-Forwarded-For").split(",")[0]).strip() + + elif influxframework.get_request_header("REMOTE_ADDR"): + influxframework.local.request_ip = influxframework.get_request_header("REMOTE_ADDR") + + else: + influxframework.local.request_ip = "127.0.0.1" + + def set_cookies(self): + influxframework.local.cookie_manager = CookieManager() + + def set_session(self): + influxframework.local.login_manager = LoginManager() + + def validate_csrf_token(self): + if influxframework.local.request and influxframework.local.request.method in ("POST", "PUT", "DELETE"): + if not influxframework.local.session: + return + if ( + not influxframework.local.session.data.csrf_token + or influxframework.local.session.data.device == "mobile" + or influxframework.conf.get("ignore_csrf", None) + ): + # not via boot + return + + csrf_token = influxframework.get_request_header("X-InfluxFramework-CSRF-Token") + if not csrf_token and "csrf_token" in influxframework.local.form_dict: + csrf_token = influxframework.local.form_dict.csrf_token + del influxframework.local.form_dict["csrf_token"] + + if influxframework.local.session.data.csrf_token != csrf_token: + influxframework.local.flags.disable_traceback = True + influxframework.throw(_("Invalid Request"), influxframework.CSRFTokenError) + + def set_lang(self): + influxframework.local.lang = get_language() + + +class LoginManager: + + __slots__ = ("user", "info", "full_name", "user_type", "resume") + + def __init__(self): + self.user = None + self.info = None + self.full_name = None + self.user_type = None + + if ( + influxframework.local.form_dict.get("cmd") == "login" or influxframework.local.request.path == "/api/method/login" + ): + if self.login() is False: + return + self.resume = False + + # run login triggers + self.run_trigger("on_session_creation") + else: + try: + self.resume = True + self.make_session(resume=True) + self.get_user_info() + self.set_user_info(resume=True) + except AttributeError: + self.user = "Guest" + self.get_user_info() + self.make_session() + self.set_user_info() + + def login(self): + if influxframework.get_system_settings("disable_user_pass_login"): + influxframework.throw(_("Login with username and password is not allowed."), influxframework.AuthenticationError) + + # clear cache + influxframework.clear_cache(user=influxframework.form_dict.get("usr")) + user, pwd = get_cached_user_pass() + self.authenticate(user=user, pwd=pwd) + if self.force_user_to_reset_password(): + doc = influxframework.get_doc("User", self.user) + influxframework.local.response["redirect_to"] = doc.reset_password( + send_email=False, password_expired=True + ) + influxframework.local.response["message"] = "Password Reset" + return False + + if should_run_2fa(self.user): + authenticate_for_2factor(self.user) + if not confirm_otp_token(self): + return False + influxframework.form_dict.pop("pwd", None) + self.post_login() + + def post_login(self): + self.run_trigger("on_login") + validate_ip_address(self.user) + self.validate_hour() + self.get_user_info() + self.make_session() + self.setup_boot_cache() + self.set_user_info() + + def get_user_info(self): + self.info = influxframework.get_cached_value( + "User", self.user, ["user_type", "first_name", "last_name", "user_image"], as_dict=1 + ) + + self.user_type = self.info.user_type + + def setup_boot_cache(self): + influxframework.cache_manager.build_table_count_cache() + influxframework.cache_manager.build_domain_restriced_doctype_cache() + influxframework.cache_manager.build_domain_restriced_page_cache() + + def set_user_info(self, resume=False): + # set sid again + influxframework.local.cookie_manager.init_cookies() + + self.full_name = " ".join(filter(None, [self.info.first_name, self.info.last_name])) + + if self.info.user_type == "Website User": + influxframework.local.cookie_manager.set_cookie("system_user", "no") + if not resume: + influxframework.local.response["message"] = "No App" + influxframework.local.response["home_page"] = "/" + get_home_page() + else: + influxframework.local.cookie_manager.set_cookie("system_user", "yes") + if not resume: + influxframework.local.response["message"] = "Logged In" + influxframework.local.response["home_page"] = "/app" + + if not resume: + influxframework.response["full_name"] = self.full_name + + # redirect information + redirect_to = influxframework.cache().hget("redirect_after_login", self.user) + if redirect_to: + influxframework.local.response["redirect_to"] = redirect_to + influxframework.cache().hdel("redirect_after_login", self.user) + + influxframework.local.cookie_manager.set_cookie("full_name", self.full_name) + influxframework.local.cookie_manager.set_cookie("user_id", self.user) + influxframework.local.cookie_manager.set_cookie("user_image", self.info.user_image or "") + + def clear_preferred_language(self): + influxframework.local.cookie_manager.delete_cookie("preferred_language") + + def make_session(self, resume=False): + # start session + influxframework.local.session_obj = Session( + user=self.user, resume=resume, full_name=self.full_name, user_type=self.user_type + ) + + # reset user if changed to Guest + self.user = influxframework.local.session_obj.user + influxframework.local.session = influxframework.local.session_obj.data + self.clear_active_sessions() + + def clear_active_sessions(self): + """Clear other sessions of the current user if `deny_multiple_sessions` is not set""" + if influxframework.session.user == "Guest": + return + + if not ( + cint(influxframework.conf.get("deny_multiple_sessions")) + or cint(influxframework.db.get_system_setting("deny_multiple_sessions")) + ): + return + + clear_sessions(influxframework.session.user, keep_current=True) + + def authenticate(self, user: str = None, pwd: str = None): + from influxframework.core.doctype.user.user import User + + if not (user and pwd): + user, pwd = influxframework.form_dict.get("usr"), influxframework.form_dict.get("pwd") + if not (user and pwd): + self.fail(_("Incomplete login details"), user=user) + + user = User.find_by_credentials(user, pwd) + + if not user: + self.fail("Invalid login credentials") + + # Current login flow uses cached credentials for authentication while checking OTP. + # Incase of OTP check, tracker for auth needs to be disabled(If not, it can remove tracker history as it is going to succeed anyway) + # Tracker is activated for 2FA incase of OTP. + ignore_tracker = should_run_2fa(user.name) and ("otp" in influxframework.form_dict) + tracker = None if ignore_tracker else get_login_attempt_tracker(user.name) + + if not user.is_authenticated: + tracker and tracker.add_failure_attempt() + self.fail("Invalid login credentials", user=user.name) + elif not (user.name == "Administrator" or user.enabled): + tracker and tracker.add_failure_attempt() + self.fail("User disabled or missing", user=user.name) + else: + tracker and tracker.add_success_attempt() + self.user = user.name + + def force_user_to_reset_password(self): + if not self.user: + return + + if self.user in influxframework.STANDARD_USERS: + return False + + reset_pwd_after_days = cint( + influxframework.db.get_single_value("System Settings", "force_user_to_reset_password") + ) + + if reset_pwd_after_days: + last_password_reset_date = ( + influxframework.db.get_value("User", self.user, "last_password_reset_date") or today() + ) + + last_pwd_reset_days = date_diff(today(), last_password_reset_date) + + if last_pwd_reset_days > reset_pwd_after_days: + return True + + def check_password(self, user, pwd): + """check password""" + try: + # returns user in correct case + return check_password(user, pwd) + except influxframework.AuthenticationError: + self.fail("Incorrect password", user=user) + + def fail(self, message, user=None): + if not user: + user = _("Unknown User") + influxframework.local.response["message"] = message + add_authentication_log(message, user, status="Failed") + influxframework.db.commit() + raise influxframework.AuthenticationError + + def run_trigger(self, event="on_login"): + for method in influxframework.get_hooks().get(event, []): + influxframework.call(influxframework.get_attr(method), login_manager=self) + + def validate_hour(self): + """check if user is logging in during restricted hours""" + login_before = int(influxframework.db.get_value("User", self.user, "login_before", ignore=True) or 0) + login_after = int(influxframework.db.get_value("User", self.user, "login_after", ignore=True) or 0) + + if not (login_before or login_after): + return + + from influxframework.utils import now_datetime + + current_hour = int(now_datetime().strftime("%H")) + + if login_before and current_hour > login_before: + influxframework.throw(_("Login not allowed at this time"), influxframework.AuthenticationError) + + if login_after and current_hour < login_after: + influxframework.throw(_("Login not allowed at this time"), influxframework.AuthenticationError) + + def login_as_guest(self): + """login as guest""" + self.login_as("Guest") + + def login_as(self, user): + self.user = user + self.post_login() + + def logout(self, arg="", user=None): + if not user: + user = influxframework.session.user + self.run_trigger("on_logout") + + if user == influxframework.session.user: + delete_session(influxframework.session.sid, user=user, reason="User Manually Logged Out") + self.clear_cookies() + else: + clear_sessions(user) + + def clear_cookies(self): + clear_cookies() + + +class CookieManager: + def __init__(self): + self.cookies = {} + self.to_delete = [] + + def init_cookies(self): + if not influxframework.local.session.get("sid"): + return + + # sid expires in 3 days + expires = datetime.datetime.now() + datetime.timedelta(days=3) + if influxframework.session.sid: + self.set_cookie("sid", influxframework.session.sid, expires=expires, httponly=True) + if influxframework.session.session_country: + self.set_cookie("country", influxframework.session.session_country) + + def set_cookie(self, key, value, expires=None, secure=False, httponly=False, samesite="Lax"): + if not secure and hasattr(influxframework.local, "request"): + secure = influxframework.local.request.scheme == "https" + + # Cordova does not work with Lax + if influxframework.local.session.data.device == "mobile": + samesite = None + + self.cookies[key] = { + "value": value, + "expires": expires, + "secure": secure, + "httponly": httponly, + "samesite": samesite, + } + + def delete_cookie(self, to_delete): + if not isinstance(to_delete, (list, tuple)): + to_delete = [to_delete] + + self.to_delete.extend(to_delete) + + def flush_cookies(self, response): + for key, opts in self.cookies.items(): + response.set_cookie( + key, + quote((opts.get("value") or "").encode("utf-8")), + expires=opts.get("expires"), + secure=opts.get("secure"), + httponly=opts.get("httponly"), + samesite=opts.get("samesite"), + ) + + # expires yesterday! + expires = datetime.datetime.now() + datetime.timedelta(days=-1) + for key in set(self.to_delete): + response.set_cookie(key, "", expires=expires) + + +@influxframework.whitelist() +def get_logged_user(): + return influxframework.session.user + + +def clear_cookies(): + if hasattr(influxframework.local, "session"): + influxframework.session.sid = "" + influxframework.local.cookie_manager.delete_cookie( + ["full_name", "user_id", "sid", "user_image", "system_user"] + ) + + +def validate_ip_address(user): + """check if IP Address is valid""" + from influxframework.core.doctype.user.user import get_restricted_ip_list + + # Only fetch required fields - for perf + user_fields = ["restrict_ip", "bypass_restrict_ip_check_if_2fa_enabled"] + user_info = ( + influxframework.get_cached_value("User", user, user_fields, as_dict=True) + if not influxframework.flags.in_test + else influxframework.db.get_value("User", user, user_fields, as_dict=True) + ) + ip_list = get_restricted_ip_list(user_info) + if not ip_list: + return + + system_settings = ( + influxframework.get_cached_doc("System Settings") + if not influxframework.flags.in_test + else influxframework.get_single("System Settings") + ) + # check if bypass restrict ip is enabled for all users + bypass_restrict_ip_check = system_settings.bypass_restrict_ip_check_if_2fa_enabled + + # check if two factor auth is enabled + if system_settings.enable_two_factor_auth and not bypass_restrict_ip_check: + # check if bypass restrict ip is enabled for login user + bypass_restrict_ip_check = user_info.bypass_restrict_ip_check_if_2fa_enabled + + for ip in ip_list: + if influxframework.local.request_ip.startswith(ip) or bypass_restrict_ip_check: + return + + influxframework.throw(_("Access not allowed from this IP Address"), influxframework.AuthenticationError) + + +def get_login_attempt_tracker(user_name: str, raise_locked_exception: bool = True): + """Get login attempt tracker instance. + + :param user_name: Name of the loggedin user + :param raise_locked_exception: If set, raises an exception incase of user not allowed to login + """ + sys_settings = influxframework.get_doc("System Settings") + track_login_attempts = sys_settings.allow_consecutive_login_attempts > 0 + tracker_kwargs = {} + + if track_login_attempts: + tracker_kwargs["lock_interval"] = sys_settings.allow_login_after_fail + tracker_kwargs["max_consecutive_login_attempts"] = sys_settings.allow_consecutive_login_attempts + + tracker = LoginAttemptTracker(user_name, **tracker_kwargs) + + if raise_locked_exception and track_login_attempts and not tracker.is_user_allowed(): + influxframework.throw( + _("Your account has been locked and will resume after {0} seconds").format( + sys_settings.allow_login_after_fail + ), + influxframework.SecurityException, + ) + return tracker + + +class LoginAttemptTracker: + """Track login attemts of a user. + + Lock the account for s number of seconds if there have been n consecutive unsuccessful attempts to log in. + """ + + def __init__( + self, user_name: str, max_consecutive_login_attempts: int = 3, lock_interval: int = 5 * 60 + ): + """Initialize the tracker. + + :param user_name: Name of the loggedin user + :param max_consecutive_login_attempts: Maximum allowed consecutive failed login attempts + :param lock_interval: Locking interval incase of maximum failed attempts + """ + self.user_name = user_name + self.lock_interval = datetime.timedelta(seconds=lock_interval) + self.max_failed_logins = max_consecutive_login_attempts + + @property + def login_failed_count(self): + return influxframework.cache().hget("login_failed_count", self.user_name) + + @login_failed_count.setter + def login_failed_count(self, count): + influxframework.cache().hset("login_failed_count", self.user_name, count) + + @login_failed_count.deleter + def login_failed_count(self): + influxframework.cache().hdel("login_failed_count", self.user_name) + + @property + def login_failed_time(self): + """First failed login attempt time within lock interval. + + For every user we track only First failed login attempt time within lock interval of time. + """ + return influxframework.cache().hget("login_failed_time", self.user_name) + + @login_failed_time.setter + def login_failed_time(self, timestamp): + influxframework.cache().hset("login_failed_time", self.user_name, timestamp) + + @login_failed_time.deleter + def login_failed_time(self): + influxframework.cache().hdel("login_failed_time", self.user_name) + + def add_failure_attempt(self): + """Log user failure attempts into the system. + + Increase the failure count if new failure is with in current lock interval time period, if not reset the login failure count. + """ + login_failed_time = self.login_failed_time + login_failed_count = self.login_failed_count # Consecutive login failure count + current_time = get_datetime() + + if not (login_failed_time and login_failed_count): + login_failed_time, login_failed_count = current_time, 0 + + if login_failed_time + self.lock_interval > current_time: + login_failed_count += 1 + else: + login_failed_time, login_failed_count = current_time, 1 + + self.login_failed_time = login_failed_time + self.login_failed_count = login_failed_count + + def add_success_attempt(self): + """Reset login failures.""" + del self.login_failed_count + del self.login_failed_time + + def is_user_allowed(self) -> bool: + """Is user allowed to login + + User is not allowed to login if login failures are greater than threshold within in lock interval from first login failure. + """ + login_failed_time = self.login_failed_time + login_failed_count = self.login_failed_count or 0 + current_time = get_datetime() + + if ( + login_failed_time + and login_failed_time + self.lock_interval > current_time + and login_failed_count > self.max_failed_logins + ): + return False + return True diff --git a/influxframework/automation/__init__.py b/influxframework/automation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/automation/doctype/__init__.py b/influxframework/automation/doctype/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/automation/doctype/assignment_rule/__init__.py b/influxframework/automation/doctype/assignment_rule/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/automation/doctype/assignment_rule/assignment_rule.js b/influxframework/automation/doctype/assignment_rule/assignment_rule.js new file mode 100644 index 0000000..6a4090d --- /dev/null +++ b/influxframework/automation/doctype/assignment_rule/assignment_rule.js @@ -0,0 +1,77 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Assignment Rule", { + refresh: function (frm) { + frm.trigger("setup_assignment_days_buttons"); + frm.trigger("set_options"); + // refresh description + frm.events.rule(frm); + }, + + setup: function (frm) { + frm.set_query("document_type", () => { + return { + filters: { + name: ["!=", "ToDo"], + }, + }; + }); + }, + + document_type: function (frm) { + frm.trigger("set_options"); + }, + + setup_assignment_days_buttons: function (frm) { + const labels = ["Weekends", "Weekdays", "All Days"]; + let get_days = (label) => { + const weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]; + const weekends = ["Saturday", "Sunday"]; + return { + "All Days": weekdays.concat(weekends), + Weekdays: weekdays, + Weekends: weekends, + }[label]; + }; + + let set_days = (e) => { + frm.clear_table("assignment_days"); + const label = $(e.currentTarget).text(); + get_days(label).forEach((day) => frm.add_child("assignment_days", { day: day })); + frm.refresh_field("assignment_days"); + }; + + labels.forEach((label) => + frm.fields_dict["assignment_days"].grid.add_custom_button(label, set_days, "top") + ); + }, + + rule: function (frm) { + const description_map = { + "Round Robin": __("Assign one by one, in sequence"), + "Load Balancing": __("Assign to the one who has the least assignments"), + "Based on Field": __("Assign to the user set in this field"), + }; + frm.get_field("rule").set_description(description_map[frm.doc.rule]); + }, + + set_options(frm) { + const doctype = frm.doc.document_type; + frm.set_fields_as_options( + "field", + doctype, + (df) => + ["Dynamic Link", "Data"].includes(df.fieldtype) || + (df.fieldtype == "Link" && df.options == "User"), + [{ label: "Owner", value: "owner" }] + ); + if (doctype) { + frm.set_fields_as_options("due_date_based_on", doctype, (df) => + ["Date", "Datetime"].includes(df.fieldtype) + ).then((options) => + frm.set_df_property("due_date_based_on", "hidden", !options.length) + ); + } + }, +}); diff --git a/influxframework/automation/doctype/assignment_rule/assignment_rule.json b/influxframework/automation/doctype/assignment_rule/assignment_rule.json new file mode 100644 index 0000000..541d176 --- /dev/null +++ b/influxframework/automation/doctype/assignment_rule/assignment_rule.json @@ -0,0 +1,179 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "Prompt", + "creation": "2019-02-28 17:12:18.815830", + "description": "Automatically Assign Documents to Users", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type", + "due_date_based_on", + "priority", + "disabled", + "column_break_4", + "description", + "assignment_rules_section", + "assign_condition", + "column_break_6", + "unassign_condition", + "section_break_10", + "close_condition", + "sb", + "assignment_days", + "assign_to_users_section", + "rule", + "field", + "users", + "last_user" + ], + "fields": [ + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType", + "reqd": 1 + }, + { + "description": "Higher priority rule will be applied first", + "fieldname": "priority", + "fieldtype": "Int", + "label": "Priority" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "default": "Automatic Assignment", + "description": "Example: {{ subject }}", + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Description", + "reqd": 1 + }, + { + "fieldname": "assignment_rules_section", + "fieldtype": "Section Break", + "label": "Assignment Rules" + }, + { + "description": "Simple Python Expression, Example: status == 'Open' and type == 'Bug'", + "fieldname": "assign_condition", + "fieldtype": "Code", + "in_list_view": 1, + "label": "Assign Condition", + "options": "PythonExpression", + "reqd": 1 + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "description": "Simple Python Expression, Example: Status in (\"Closed\", \"Cancelled\")", + "fieldname": "unassign_condition", + "fieldtype": "Code", + "label": "Unassign Condition", + "options": "PythonExpression" + }, + { + "fieldname": "assign_to_users_section", + "fieldtype": "Section Break", + "label": "Assign To Users" + }, + { + "fieldname": "rule", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Rule", + "options": "Round Robin\nLoad Balancing\nBased on Field", + "reqd": 1 + }, + { + "depends_on": "eval: doc.rule !== 'Based on Field'", + "fieldname": "users", + "fieldtype": "Table MultiSelect", + "label": "Users", + "mandatory_depends_on": "eval: doc.rule !== 'Based on Field'", + "options": "Assignment Rule User" + }, + { + "fieldname": "last_user", + "fieldtype": "Link", + "label": "Last User", + "options": "User", + "read_only": 1 + }, + { + "fieldname": "section_break_10", + "fieldtype": "Section Break" + }, + { + "description": "Simple Python Expression, Example: Status in (\"Invalid\")", + "fieldname": "close_condition", + "fieldtype": "Code", + "label": "Close Condition", + "options": "PythonExpression" + }, + { + "fieldname": "sb", + "fieldtype": "Section Break", + "label": "Assignment Days" + }, + { + "fieldname": "assignment_days", + "fieldtype": "Table", + "label": "Assignment Days", + "options": "Assignment Rule Day", + "reqd": 1 + }, + { + "depends_on": "document_type", + "description": "Value from this field will be set as the due date in the ToDo", + "fieldname": "due_date_based_on", + "fieldtype": "Select", + "label": "Due Date Based On" + }, + { + "depends_on": "eval: doc.rule == 'Based on Field'", + "fieldname": "field", + "fieldtype": "Select", + "label": "Field", + "mandatory_depends_on": "eval: doc.rule == 'Based on Field'" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-07-16 22:51:35.505575", + "modified_by": "Administrator", + "module": "Automation", + "name": "Assignment Rule", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/automation/doctype/assignment_rule/assignment_rule.py b/influxframework/automation/doctype/assignment_rule/assignment_rule.py new file mode 100644 index 0000000..a1ff573 --- /dev/null +++ b/influxframework/automation/doctype/assignment_rule/assignment_rule.py @@ -0,0 +1,373 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE + +from collections.abc import Iterable + +import influxframework +from influxframework import _ +from influxframework.cache_manager import clear_doctype_map, get_doctype_map +from influxframework.desk.form import assign_to +from influxframework.model import log_types +from influxframework.model.document import Document + + +class AssignmentRule(Document): + def validate(self): + self.validate_document_types() + self.validate_assignment_days() + + def clear_cache(self): + super().clear_cache() + clear_doctype_map(self.doctype, self.document_type) + clear_doctype_map(self.doctype, f"due_date_rules_for_{self.document_type}") + + def validate_document_types(self): + if self.document_type == "ToDo": + influxframework.throw( + _("Assignment Rule is not allowed on {0} document type").format(influxframework.bold("ToDo")) + ) + + def validate_assignment_days(self): + assignment_days = self.get_assignment_days() + + if len(set(assignment_days)) != len(assignment_days): + repeated_days = get_repeated(assignment_days) + plural = "s" if len(repeated_days) > 1 else "" + + influxframework.throw( + _("Assignment Day{0} {1} has been repeated.").format( + plural, influxframework.bold(", ".join(repeated_days)) + ) + ) + + def apply_unassign(self, doc, assignments): + if self.unassign_condition and self.name in [d.assignment_rule for d in assignments]: + return self.clear_assignment(doc) + + return False + + def apply_assign(self, doc): + if self.safe_eval("assign_condition", doc): + return self.do_assignment(doc) + + def do_assignment(self, doc): + # clear existing assignment, to reassign + assign_to.clear(doc.get("doctype"), doc.get("name")) + + user = self.get_user(doc) + + if user: + assign_to.add( + dict( + assign_to=[user], + doctype=doc.get("doctype"), + name=doc.get("name"), + description=influxframework.render_template(self.description, doc), + assignment_rule=self.name, + notify=True, + date=doc.get(self.due_date_based_on) if self.due_date_based_on else None, + ) + ) + + # set for reference in round robin + self.db_set("last_user", user) + return True + + return False + + def clear_assignment(self, doc): + """Clear assignments""" + if self.safe_eval("unassign_condition", doc): + return assign_to.clear(doc.get("doctype"), doc.get("name")) + + def close_assignments(self, doc): + """Close assignments""" + if self.safe_eval("close_condition", doc): + return assign_to.close_all_assignments(doc.get("doctype"), doc.get("name")) + + def get_user(self, doc): + """ + Get the next user for assignment + """ + if self.rule == "Round Robin": + return self.get_user_round_robin() + elif self.rule == "Load Balancing": + return self.get_user_load_balancing() + elif self.rule == "Based on Field": + return self.get_user_based_on_field(doc) + + def get_user_round_robin(self): + """ + Get next user based on round robin + """ + + # first time, or last in list, pick the first + if not self.last_user or self.last_user == self.users[-1].user: + return self.users[0].user + + # find out the next user in the list + for i, d in enumerate(self.users): + if self.last_user == d.user: + return self.users[i + 1].user + + # bad last user, assign to the first one + return self.users[0].user + + def get_user_load_balancing(self): + """Assign to the user with least number of open assignments""" + counts = [] + for d in self.users: + counts.append( + dict( + user=d.user, + count=influxframework.db.count( + "ToDo", dict(reference_type=self.document_type, allocated_to=d.user, status="Open") + ), + ) + ) + + # sort by dict value + sorted_counts = sorted(counts, key=lambda k: k["count"]) + + # pick the first user + return sorted_counts[0].get("user") + + def get_user_based_on_field(self, doc): + val = doc.get(self.field) + if influxframework.db.exists("User", val): + return val + + def safe_eval(self, fieldname, doc): + try: + if self.get(fieldname): + return influxframework.safe_eval(self.get(fieldname), None, doc) + except Exception as e: + # when assignment fails, don't block the document as it may be + # a part of the email pulling + influxframework.msgprint(influxframework._("Auto assignment failed: {0}").format(str(e)), indicator="orange") + + return False + + def get_assignment_days(self): + return [d.day for d in self.get("assignment_days", [])] + + def is_rule_not_applicable_today(self): + today = influxframework.flags.assignment_day or influxframework.utils.get_weekday() + assignment_days = self.get_assignment_days() + return assignment_days and today not in assignment_days + + +def get_assignments(doc) -> list[dict]: + return influxframework.get_all( + "ToDo", + fields=["name", "assignment_rule"], + filters=dict( + reference_type=doc.get("doctype"), reference_name=doc.get("name"), status=("!=", "Cancelled") + ), + limit=5, + ) + + +@influxframework.whitelist() +def bulk_apply(doctype, docnames): + docnames = influxframework.parse_json(docnames) + background = len(docnames) > 5 + + for name in docnames: + if background: + influxframework.enqueue( + "influxframework.automation.doctype.assignment_rule.assignment_rule.apply", + doc=None, + doctype=doctype, + name=name, + ) + else: + apply(doctype=doctype, name=name) + + +def reopen_closed_assignment(doc): + todo_list = influxframework.get_all( + "ToDo", + filters={ + "reference_type": doc.doctype, + "reference_name": doc.name, + "status": "Closed", + }, + pluck="name", + ) + + for todo in todo_list: + todo_doc = influxframework.get_doc("ToDo", todo) + todo_doc.status = "Open" + todo_doc.save(ignore_permissions=True) + + return bool(todo_list) + + +def apply(doc=None, method=None, doctype=None, name=None): + doctype = doctype or doc.doctype + + skip_assignment_rules = ( + influxframework.flags.in_patch + or influxframework.flags.in_install + or influxframework.flags.in_setup_wizard + or doctype in log_types + ) + + if skip_assignment_rules: + return + + if not doc and doctype and name: + doc = influxframework.get_doc(doctype, name) + + assignment_rules = get_doctype_map( + "Assignment Rule", + doc.doctype, + filters={"document_type": doc.doctype, "disabled": 0}, + order_by="priority desc", + ) + + # multiple auto assigns + assignment_rule_docs: list[AssignmentRule] = [ + influxframework.get_cached_doc("Assignment Rule", d.get("name")) for d in assignment_rules + ] + + if not assignment_rule_docs: + return + + doc = doc.as_dict() + assignments = get_assignments(doc) + + clear = True # are all assignments cleared + new_apply = False # are new assignments applied + + if assignments: + # first unassign + # use case, there are separate groups to be assigned for say L1 and L2, + # so when the value switches from L1 to L2, L1 team must be unassigned, then L2 can be assigned. + clear = False + for assignment_rule in assignment_rule_docs: + if assignment_rule.is_rule_not_applicable_today(): + continue + + clear = assignment_rule.apply_unassign(doc, assignments) + if clear: + break + + # apply rule only if there are no existing assignments + if clear: + for assignment_rule in assignment_rule_docs: + if assignment_rule.is_rule_not_applicable_today(): + continue + + new_apply = assignment_rule.apply_assign(doc) + if new_apply: + break + + # apply close rule only if assignments exists + assignments = get_assignments(doc) + + if assignments: + for assignment_rule in assignment_rule_docs: + if assignment_rule.is_rule_not_applicable_today(): + continue + + if not new_apply: + # only reopen if close condition is not satisfied + to_close_todos = assignment_rule.safe_eval("close_condition", doc) + + if to_close_todos: + # close todo status + todos_to_close = influxframework.get_all( + "ToDo", + filters={ + "reference_type": doc.doctype, + "reference_name": doc.name, + }, + pluck="name", + ) + + for todo in todos_to_close: + _todo = influxframework.get_doc("ToDo", todo) + _todo.status = "Closed" + _todo.save(ignore_permissions=True) + break + + else: + reopened = reopen_closed_assignment(doc) + if reopened: + break + + assignment_rule.close_assignments(doc) + + +def update_due_date(doc, state=None): + """Run on_update on every Document (via hooks.py)""" + skip_document_update = ( + influxframework.flags.in_migrate + or influxframework.flags.in_patch + or influxframework.flags.in_import + or influxframework.flags.in_setup_wizard + or influxframework.flags.in_install + ) + + if skip_document_update: + return + + assignment_rules = get_doctype_map( + doctype="Assignment Rule", + name=f"due_date_rules_for_{doc.doctype}", + filters={ + "due_date_based_on": ["is", "set"], + "document_type": doc.doctype, + "disabled": 0, + }, + ) + + for rule in assignment_rules: + rule_doc = influxframework.get_cached_doc("Assignment Rule", rule.get("name")) + due_date_field = rule_doc.due_date_based_on + field_updated = ( + doc.meta.has_field(due_date_field) + and doc.has_value_changed(due_date_field) + and rule.get("name") + ) + + if field_updated: + assignment_todos = influxframework.get_all( + "ToDo", + filters={ + "assignment_rule": rule.get("name"), + "reference_type": doc.doctype, + "reference_name": doc.name, + "status": "Open", + }, + pluck="name", + ) + + for todo in assignment_todos: + todo_doc = influxframework.get_doc("ToDo", todo) + todo_doc.date = doc.get(due_date_field) + todo_doc.flags.updater_reference = { + "doctype": "Assignment Rule", + "docname": rule.get("name"), + "label": _("via Assignment Rule"), + } + todo_doc.save(ignore_permissions=True) + + +def get_assignment_rules() -> list[str]: + return influxframework.get_all("Assignment Rule", filters={"disabled": 0}, pluck="document_type") + + +def get_repeated(values: Iterable) -> list: + unique = set() + repeated = set() + + for value in values: + if value in unique: + repeated.add(value) + else: + unique.add(value) + + return [str(x) for x in repeated] diff --git a/influxframework/automation/doctype/assignment_rule/test_assignment_rule.py b/influxframework/automation/doctype/assignment_rule/test_assignment_rule.py new file mode 100644 index 0000000..2aafcef --- /dev/null +++ b/influxframework/automation/doctype/assignment_rule/test_assignment_rule.py @@ -0,0 +1,337 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.test_runner import make_test_records +from influxframework.tests.utils import InfluxFrameworkTestCase +from influxframework.utils import random_string + + +class TestAutoAssign(InfluxFrameworkTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + influxframework.db.delete("Assignment Rule") + + @classmethod + def tearDownClass(cls): + influxframework.db.rollback() + + def setUp(self): + make_test_records("User") + days = [ + dict(day="Sunday"), + dict(day="Monday"), + dict(day="Tuesday"), + dict(day="Wednesday"), + dict(day="Thursday"), + dict(day="Friday"), + dict(day="Saturday"), + ] + self.days = days + self.assignment_rule = get_assignment_rule([days, days]) + clear_assignments() + + def test_round_robin(self): + note = make_note(dict(public=1)) + + # check if auto assigned to first user + self.assertEqual( + influxframework.db.get_value( + "ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to" + ), + "test@example.com", + ) + + note = make_note(dict(public=1)) + + # check if auto assigned to second user + self.assertEqual( + influxframework.db.get_value( + "ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to" + ), + "test1@example.com", + ) + + clear_assignments() + + note = make_note(dict(public=1)) + + # check if auto assigned to third user, even if + # previous assignments where closed + self.assertEqual( + influxframework.db.get_value( + "ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to" + ), + "test2@example.com", + ) + + # check loop back to first user + note = make_note(dict(public=1)) + + self.assertEqual( + influxframework.db.get_value( + "ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to" + ), + "test@example.com", + ) + + def test_load_balancing(self): + self.assignment_rule.rule = "Load Balancing" + self.assignment_rule.save() + + for _ in range(30): + note = make_note(dict(public=1)) + + # check if each user has 10 assignments (?) + for user in ("test@example.com", "test1@example.com", "test2@example.com"): + self.assertEqual( + len(influxframework.get_all("ToDo", dict(allocated_to=user, reference_type="Note"))), 10 + ) + + # clear 5 assignments for first user + # can't do a limit in "delete" since postgres does not support it + for d in influxframework.get_all( + "ToDo", dict(reference_type="Note", allocated_to="test@example.com"), limit=5 + ): + influxframework.db.delete("ToDo", {"name": d.name}) + + # add 5 more assignments + for i in range(5): + make_note(dict(public=1)) + + # check if each user still has 10 assignments + for user in ("test@example.com", "test1@example.com", "test2@example.com"): + self.assertEqual( + len(influxframework.get_all("ToDo", dict(allocated_to=user, reference_type="Note"))), 10 + ) + + def test_based_on_field(self): + self.assignment_rule.rule = "Based on Field" + self.assignment_rule.field = "owner" + self.assignment_rule.save() + + influxframework.set_user("test1@example.com") + note = make_note(dict(public=1)) + # check if auto assigned to doc owner, test1@example.com + self.assertEqual( + influxframework.db.get_value( + "ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "owner" + ), + "test1@example.com", + ) + + influxframework.set_user("test2@example.com") + note = make_note(dict(public=1)) + # check if auto assigned to doc owner, test2@example.com + self.assertEqual( + influxframework.db.get_value( + "ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "owner" + ), + "test2@example.com", + ) + + influxframework.set_user("Administrator") + + def test_assign_condition(self): + # check condition + note = make_note(dict(public=0)) + + self.assertEqual( + influxframework.db.get_value( + "ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to" + ), + None, + ) + + def test_clear_assignment(self): + note = make_note(dict(public=1)) + + # check if auto assigned to first user + todo = influxframework.get_list( + "ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), limit=1 + )[0] + + todo = influxframework.get_doc("ToDo", todo["name"]) + self.assertEqual(todo.allocated_to, "test@example.com") + + # test auto unassign + note.public = 0 + note.save() + + todo.load_from_db() + + # check if todo is cancelled + self.assertEqual(todo.status, "Cancelled") + + def test_close_assignment(self): + note = make_note(dict(public=1, content="valid")) + + # check if auto assigned + todo = influxframework.get_list( + "ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), limit=1 + )[0] + + todo = influxframework.get_doc("ToDo", todo["name"]) + self.assertEqual(todo.allocated_to, "test@example.com") + + note.content = "Closed" + note.save() + + todo.load_from_db() + + # check if todo is closed + self.assertEqual(todo.status, "Closed") + # check if closed todo retained assignment + self.assertEqual(todo.allocated_to, "test@example.com") + + def check_multiple_rules(self): + note = make_note(dict(public=1, notify_on_login=1)) + + # check if auto assigned to test3 (2nd rule is applied, as it has higher priority) + self.assertEqual( + influxframework.db.get_value( + "ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to" + ), + "test@example.com", + ) + + def check_assignment_rule_scheduling(self): + influxframework.db.delete("Assignment Rule") + + days_1 = [dict(day="Sunday"), dict(day="Monday"), dict(day="Tuesday")] + + days_2 = [dict(day="Wednesday"), dict(day="Thursday"), dict(day="Friday"), dict(day="Saturday")] + + get_assignment_rule([days_1, days_2], ["public == 1", "public == 1"]) + + influxframework.flags.assignment_day = "Monday" + note = make_note(dict(public=1)) + + self.assertIn( + influxframework.db.get_value( + "ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to" + ), + ["test@example.com", "test1@example.com", "test2@example.com"], + ) + + influxframework.flags.assignment_day = "Friday" + note = make_note(dict(public=1)) + + self.assertIn( + influxframework.db.get_value( + "ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to" + ), + ["test3@example.com"], + ) + + def test_assignment_rule_condition(self): + influxframework.db.delete("Assignment Rule") + + # Add expiry_date custom field + from influxframework.custom.doctype.custom_field.custom_field import create_custom_field + + df = dict(fieldname="expiry_date", label="Expiry Date", fieldtype="Date") + create_custom_field("Note", df) + + assignment_rule = influxframework.get_doc( + dict( + name="Assignment with Due Date", + doctype="Assignment Rule", + document_type="Note", + assign_condition="public == 0", + due_date_based_on="expiry_date", + assignment_days=self.days, + users=[ + dict(user="test@example.com"), + ], + ) + ).insert() + + expiry_date = influxframework.utils.add_days(influxframework.utils.nowdate(), 2) + note1 = make_note({"expiry_date": expiry_date}) + note2 = make_note({"expiry_date": expiry_date}) + + note1_todo = influxframework.get_all( + "ToDo", filters=dict(reference_type="Note", reference_name=note1.name, status="Open") + )[0] + + note1_todo_doc = influxframework.get_doc("ToDo", note1_todo.name) + self.assertEqual(influxframework.utils.get_date_str(note1_todo_doc.date), expiry_date) + + # due date should be updated if the reference doc's date is updated. + note1.expiry_date = influxframework.utils.add_days(expiry_date, 2) + note1.save() + note1_todo_doc.reload() + self.assertEqual(influxframework.utils.get_date_str(note1_todo_doc.date), note1.expiry_date) + + # saving one note's expiry should not update other note todo's due date + note2_todo = influxframework.get_all( + "ToDo", + filters=dict(reference_type="Note", reference_name=note2.name, status="Open"), + fields=["name", "date"], + )[0] + self.assertNotEqual(influxframework.utils.get_date_str(note2_todo.date), note1.expiry_date) + self.assertEqual(influxframework.utils.get_date_str(note2_todo.date), expiry_date) + assignment_rule.delete() + influxframework.db.commit() # undo changes commited by DDL + + +def clear_assignments(): + influxframework.db.delete("ToDo", {"reference_type": "Note"}) + + +def get_assignment_rule(days, assign=None): + influxframework.delete_doc_if_exists("Assignment Rule", "For Note 1") + + if not assign: + assign = ["public == 1", "notify_on_login == 1"] + + assignment_rule = influxframework.get_doc( + dict( + name="For Note 1", + doctype="Assignment Rule", + priority=0, + document_type="Note", + assign_condition=assign[0], + unassign_condition="public == 0 or notify_on_login == 1", + close_condition='"Closed" in content', + rule="Round Robin", + assignment_days=days[0], + users=[ + dict(user="test@example.com"), + dict(user="test1@example.com"), + dict(user="test2@example.com"), + ], + ) + ).insert() + + influxframework.delete_doc_if_exists("Assignment Rule", "For Note 2") + + # 2nd rule + influxframework.get_doc( + dict( + name="For Note 2", + doctype="Assignment Rule", + priority=1, + document_type="Note", + assign_condition=assign[1], + unassign_condition="notify_on_login == 0", + rule="Round Robin", + assignment_days=days[1], + users=[dict(user="test3@example.com")], + ) + ).insert() + + return assignment_rule + + +def make_note(values=None): + note = influxframework.get_doc(dict(doctype="Note", title=random_string(10), content=random_string(20))) + + if values: + note.update(values) + + note.insert() + + return note diff --git a/influxframework/automation/doctype/assignment_rule_day/__init__.py b/influxframework/automation/doctype/assignment_rule_day/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/automation/doctype/assignment_rule_day/assignment_rule_day.json b/influxframework/automation/doctype/assignment_rule_day/assignment_rule_day.json new file mode 100644 index 0000000..2a41879 --- /dev/null +++ b/influxframework/automation/doctype/assignment_rule_day/assignment_rule_day.json @@ -0,0 +1,28 @@ +{ + "creation": "2019-09-21 16:52:01.705351", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "day" + ], + "fields": [ + { + "fieldname": "day", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Day", + "options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday" + } + ], + "istable": 1, + "modified": "2019-09-21 16:55:09.376291", + "modified_by": "Administrator", + "module": "Automation", + "name": "Assignment Rule Day", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/automation/doctype/assignment_rule_day/assignment_rule_day.py b/influxframework/automation/doctype/assignment_rule_day/assignment_rule_day.py new file mode 100644 index 0000000..1dbc5ca --- /dev/null +++ b/influxframework/automation/doctype/assignment_rule_day/assignment_rule_day.py @@ -0,0 +1,9 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class AssignmentRuleDay(Document): + pass diff --git a/influxframework/automation/doctype/assignment_rule_user/__init__.py b/influxframework/automation/doctype/assignment_rule_user/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/automation/doctype/assignment_rule_user/assignment_rule_user.json b/influxframework/automation/doctype/assignment_rule_user/assignment_rule_user.json new file mode 100644 index 0000000..5a159c8 --- /dev/null +++ b/influxframework/automation/doctype/assignment_rule_user/assignment_rule_user.json @@ -0,0 +1,34 @@ +{ + "actions": [], + "allow_read": 1, + "creation": "2019-02-27 11:41:46.602400", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "in_list_view": 1, + "label": "User", + "options": "User", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-09-29 20:12:14.456785", + "modified_by": "Administrator", + "module": "Automation", + "name": "Assignment Rule User", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/automation/doctype/assignment_rule_user/assignment_rule_user.py b/influxframework/automation/doctype/assignment_rule_user/assignment_rule_user.py new file mode 100644 index 0000000..02b1fb7 --- /dev/null +++ b/influxframework/automation/doctype/assignment_rule_user/assignment_rule_user.py @@ -0,0 +1,9 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class AssignmentRuleUser(Document): + pass diff --git a/influxframework/automation/doctype/auto_repeat/__init__.py b/influxframework/automation/doctype/auto_repeat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/automation/doctype/auto_repeat/auto_repeat.js b/influxframework/automation/doctype/auto_repeat/auto_repeat.js new file mode 100644 index 0000000..3a078af --- /dev/null +++ b/influxframework/automation/doctype/auto_repeat/auto_repeat.js @@ -0,0 +1,122 @@ +// Copyright (c) 2018, InfluxFramework LLC +// For license information, please see license.txt +influxframework.provide("influxframework.auto_repeat"); + +influxframework.ui.form.on("Auto Repeat", { + setup: function (frm) { + frm.fields_dict["reference_doctype"].get_query = function () { + return { + query: "influxframework.automation.doctype.auto_repeat.auto_repeat.get_auto_repeat_doctypes", + }; + }; + + frm.fields_dict["reference_document"].get_query = function () { + return { + filters: { + auto_repeat: "", + }, + }; + }; + + frm.fields_dict["print_format"].get_query = function () { + return { + filters: { + doc_type: frm.doc.reference_doctype, + }, + }; + }; + }, + + refresh: function (frm) { + // auto repeat message + if (frm.is_new()) { + let customize_form_link = `${__("Customize Form")}`; + frm.dashboard.set_headline( + __('To configure Auto Repeat, enable "Allow Auto Repeat" from {0}.', [ + customize_form_link, + ]) + ); + } + + // view document button + if (!frm.is_dirty()) { + let label = __("View {0}", [__(frm.doc.reference_doctype)]); + frm.add_custom_button(label, () => + influxframework.set_route("List", frm.doc.reference_doctype, { auto_repeat: frm.doc.name }) + ); + } + + // auto repeat schedule + influxframework.auto_repeat.render_schedule(frm); + + frm.trigger("toggle_submit_on_creation"); + }, + + reference_doctype: function (frm) { + frm.trigger("toggle_submit_on_creation"); + }, + + toggle_submit_on_creation: function (frm) { + // submit on creation checkbox + if (frm.doc.reference_doctype) { + influxframework.model.with_doctype(frm.doc.reference_doctype, () => { + let meta = influxframework.get_meta(frm.doc.reference_doctype); + frm.toggle_display("submit_on_creation", meta.is_submittable); + }); + } + }, + + template: function (frm) { + if (frm.doc.template) { + influxframework.model.with_doc("Email Template", frm.doc.template, () => { + let email_template = influxframework.get_doc("Email Template", frm.doc.template); + frm.set_value("subject", email_template.subject); + frm.set_value("message", email_template.response); + frm.refresh_field("subject"); + frm.refresh_field("message"); + }); + } + }, + + get_contacts: function (frm) { + frm.call("fetch_linked_contacts"); + }, + + preview_message: function (frm) { + if (frm.doc.message) { + influxframework.call({ + method: "influxframework.automation.doctype.auto_repeat.auto_repeat.generate_message_preview", + args: { + reference_dt: frm.doc.reference_doctype, + reference_doc: frm.doc.reference_document, + subject: frm.doc.subject, + message: frm.doc.message, + }, + callback: function (r) { + if (r.message) { + influxframework.msgprint(r.message.message, r.message.subject); + } + }, + }); + } else { + influxframework.msgprint(__("Please setup a message first"), __("Message not setup")); + } + }, +}); + +influxframework.auto_repeat.render_schedule = function (frm) { + if (!frm.is_dirty() && frm.doc.status !== "Disabled") { + frm.call("get_auto_repeat_schedule").then((r) => { + frm.dashboard.reset(); + frm.dashboard.add_section( + influxframework.render_template("auto_repeat_schedule", { + schedule_details: r.message || [], + }), + __("Auto Repeat Schedule") + ); + frm.dashboard.show(); + }); + } else { + frm.dashboard.hide(); + } +}; diff --git a/influxframework/automation/doctype/auto_repeat/auto_repeat.json b/influxframework/automation/doctype/auto_repeat/auto_repeat.json new file mode 100644 index 0000000..7496534 --- /dev/null +++ b/influxframework/automation/doctype/auto_repeat/auto_repeat.json @@ -0,0 +1,262 @@ +{ + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "format:AUT-AR-{#####}", + "creation": "2018-03-09 11:22:31.192349", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "section_break_1", + "disabled", + "section_break_3", + "reference_doctype", + "reference_document", + "submit_on_creation", + "column_break_5", + "start_date", + "end_date", + "section_break_10", + "frequency", + "repeat_on_day", + "repeat_on_last_day", + "column_break_12", + "next_schedule_date", + "section_break_16", + "repeat_on_days", + "notification", + "notify_by_email", + "recipients", + "get_contacts", + "template", + "subject", + "message", + "preview_message", + "print_format", + "status" + ], + "fields": [ + { + "fieldname": "section_break_1", + "fieldtype": "Section Break" + }, + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Reference Document Type", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "reference_document", + "fieldtype": "Dynamic Link", + "label": "Reference Document", + "no_copy": 1, + "options": "reference_doctype", + "reqd": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "default": "Today", + "fieldname": "start_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Start Date", + "reqd": 1 + }, + { + "fieldname": "end_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "End Date" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled", + "no_copy": 1 + }, + { + "fieldname": "section_break_10", + "fieldtype": "Section Break" + }, + { + "fieldname": "frequency", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Frequency", + "options": "\nDaily\nWeekly\nMonthly\nQuarterly\nHalf-yearly\nYearly", + "reqd": 1 + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval: in_list([\"Monthly\", \"Quarterly\", \"Half-yearly\", \"Yearly\"], doc.frequency) && !doc.repeat_on_last_day\n", + "fieldname": "repeat_on_day", + "fieldtype": "Int", + "label": "Repeat on Day" + }, + { + "fieldname": "next_schedule_date", + "fieldtype": "Date", + "label": "Next Schedule Date", + "no_copy": 1, + "print_hide": 1, + "read_only": 1, + "search_index": 1 + }, + { + "collapsible": 1, + "fieldname": "notification", + "fieldtype": "Section Break", + "label": "Notification" + }, + { + "default": "0", + "fieldname": "notify_by_email", + "fieldtype": "Check", + "label": "Notify by Email" + }, + { + "depends_on": "notify_by_email", + "fieldname": "recipients", + "fieldtype": "Small Text", + "label": "Recipients" + }, + { + "depends_on": "eval: doc.notify_by_email && doc.reference_doctype && doc.reference_document", + "fieldname": "get_contacts", + "fieldtype": "Button", + "label": "Get Contacts" + }, + { + "depends_on": "eval: doc.notify_by_email", + "fieldname": "template", + "fieldtype": "Link", + "label": "Template", + "options": "Email Template" + }, + { + "depends_on": "eval: doc.notify_by_email", + "description": "To add dynamic subject, use jinja tags like\n\n
    New {{ doc.doctype }} #{{ doc.name }}
    ", + "fieldname": "subject", + "fieldtype": "Data", + "label": "Subject" + }, + { + "default": "Please find attached {{ doc.doctype }} #{{ doc.name }}", + "depends_on": "eval: doc.notify_by_email", + "fieldname": "message", + "fieldtype": "Text", + "label": "Message" + }, + { + "depends_on": "eval: doc.notify_by_email && doc.reference_doctype && doc.reference_document", + "fieldname": "preview_message", + "fieldtype": "Button", + "label": "Preview Message" + }, + { + "depends_on": "notify_by_email", + "fieldname": "print_format", + "fieldtype": "Link", + "label": "Print Format", + "options": "Print Format" + }, + { + "fieldname": "status", + "fieldtype": "Select", + "hidden": 1, + "in_list_view": 1, + "label": "Status", + "options": "\nActive\nDisabled\nCompleted", + "read_only": 1 + }, + { + "fieldname": "section_break_3", + "fieldtype": "Section Break" + }, + { + "default": "0", + "depends_on": "eval:doc.frequency === 'Monthly'", + "fieldname": "repeat_on_last_day", + "fieldtype": "Check", + "label": "Repeat on Last Day of the Month" + }, + { + "depends_on": "eval:doc.frequency==='Weekly';", + "fieldname": "repeat_on_days", + "fieldtype": "Table", + "label": "Repeat on Days", + "options": "Auto Repeat Day" + }, + { + "default": "0", + "fieldname": "submit_on_creation", + "fieldtype": "Check", + "label": "Submit on Creation" + }, + { + "depends_on": "eval:doc.frequency==='Weekly';", + "fieldname": "section_break_16", + "fieldtype": "Section Break" + } + ], + "links": [], + "modified": "2021-01-12 09:24:49.719611", + "modified_by": "Administrator", + "module": "Automation", + "name": "Auto Repeat", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "write": 1 + } + ], + "search_fields": "reference_document", + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "reference_document", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/automation/doctype/auto_repeat/auto_repeat.py b/influxframework/automation/doctype/auto_repeat/auto_repeat.py new file mode 100644 index 0000000..1057011 --- /dev/null +++ b/influxframework/automation/doctype/auto_repeat/auto_repeat.py @@ -0,0 +1,551 @@ +# Copyright (c) 2018, InfluxFramework LLC +# License: MIT. See LICENSE + +from datetime import timedelta + +from dateutil.relativedelta import relativedelta + +import influxframework +from influxframework import _ +from influxframework.automation.doctype.assignment_rule.assignment_rule import get_repeated +from influxframework.contacts.doctype.contact.contact import ( + get_contacts_linked_from, + get_contacts_linking_to, +) +from influxframework.core.doctype.communication.email import make +from influxframework.desk.form import assign_to +from influxframework.model.document import Document +from influxframework.utils import ( + add_days, + cstr, + get_first_day, + get_last_day, + getdate, + month_diff, + split_emails, + today, +) +from influxframework.utils.background_jobs import get_jobs +from influxframework.utils.jinja import validate_template +from influxframework.utils.user import get_system_managers + +month_map = {"Monthly": 1, "Quarterly": 3, "Half-yearly": 6, "Yearly": 12} +week_map = { + "Monday": 0, + "Tuesday": 1, + "Wednesday": 2, + "Thursday": 3, + "Friday": 4, + "Saturday": 5, + "Sunday": 6, +} + + +class AutoRepeat(Document): + def validate(self): + self.update_status() + self.validate_reference_doctype() + self.validate_submit_on_creation() + self.validate_dates() + self.validate_email_id() + self.validate_auto_repeat_days() + self.set_dates() + self.update_auto_repeat_id() + self.unlink_if_applicable() + + validate_template(self.subject or "") + validate_template(self.message or "") + + def before_insert(self): + if not influxframework.flags.in_test: + start_date = getdate(self.start_date) + today_date = getdate(today()) + if start_date <= today_date: + self.start_date = today_date + + def after_save(self): + influxframework.get_doc(self.reference_doctype, self.reference_document).notify_update() + + def on_trash(self): + influxframework.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", "") + influxframework.get_doc(self.reference_doctype, self.reference_document).notify_update() + + def set_dates(self): + if self.disabled: + self.next_schedule_date = None + else: + self.next_schedule_date = self.get_next_schedule_date(schedule_date=self.start_date) + + def unlink_if_applicable(self): + if self.status == "Completed" or self.disabled: + influxframework.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", "") + + def validate_reference_doctype(self): + if influxframework.flags.in_test or influxframework.flags.in_patch: + return + if not influxframework.get_meta(self.reference_doctype).allow_auto_repeat: + influxframework.throw( + _("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format( + self.reference_doctype + ) + ) + + def validate_submit_on_creation(self): + if self.submit_on_creation and not influxframework.get_meta(self.reference_doctype).is_submittable: + influxframework.throw( + _("Cannot enable {0} for a non-submittable doctype").format(influxframework.bold("Submit on Creation")) + ) + + def validate_dates(self): + if influxframework.flags.in_patch: + return + + if self.end_date: + self.validate_from_to_dates("start_date", "end_date") + + if self.end_date == self.start_date: + influxframework.throw( + _("{0} should not be same as {1}").format(influxframework.bold("End Date"), influxframework.bold("Start Date")) + ) + + def validate_email_id(self): + if self.notify_by_email: + if self.recipients: + email_list = split_emails(self.recipients.replace("\n", "")) + from influxframework.utils import validate_email_address + + for email in email_list: + if not validate_email_address(email): + influxframework.throw(_("{0} is an invalid email address in 'Recipients'").format(email)) + else: + influxframework.throw(_("'Recipients' not specified")) + + def validate_auto_repeat_days(self): + auto_repeat_days = self.get_auto_repeat_days() + if not len(set(auto_repeat_days)) == len(auto_repeat_days): + repeated_days = get_repeated(auto_repeat_days) + plural = "s" if len(repeated_days) > 1 else "" + + influxframework.throw( + _("Auto Repeat Day{0} {1} has been repeated.").format( + plural, influxframework.bold(", ".join(repeated_days)) + ) + ) + + def update_auto_repeat_id(self): + # check if document is already on auto repeat + auto_repeat = influxframework.db.get_value(self.reference_doctype, self.reference_document, "auto_repeat") + if auto_repeat and auto_repeat != self.name and not influxframework.flags.in_patch: + influxframework.throw( + _("The {0} is already on auto repeat {1}").format(self.reference_document, auto_repeat) + ) + else: + influxframework.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", self.name) + + def update_status(self): + if self.disabled: + self.status = "Disabled" + elif self.is_completed(): + self.status = "Completed" + else: + self.status = "Active" + + def is_completed(self): + return self.end_date and getdate(self.end_date) < getdate(today()) + + @influxframework.whitelist() + def get_auto_repeat_schedule(self): + schedule_details = [] + start_date = getdate(self.start_date) + end_date = getdate(self.end_date) + + if not self.end_date: + next_date = self.get_next_schedule_date(schedule_date=start_date) + row = { + "reference_document": self.reference_document, + "frequency": self.frequency, + "next_scheduled_date": next_date, + } + schedule_details.append(row) + + if self.end_date: + next_date = self.get_next_schedule_date(schedule_date=start_date, for_full_schedule=True) + + while getdate(next_date) < getdate(end_date): + row = { + "reference_document": self.reference_document, + "frequency": self.frequency, + "next_scheduled_date": next_date, + } + schedule_details.append(row) + next_date = self.get_next_schedule_date(schedule_date=next_date, for_full_schedule=True) + + return schedule_details + + def create_documents(self): + try: + new_doc = self.make_new_document() + if self.notify_by_email and self.recipients: + self.send_notification(new_doc) + except Exception: + error_log = self.log_error("Auto repeat failed") + + self.disable_auto_repeat() + + if self.reference_document and not influxframework.flags.in_test: + self.notify_error_to_user(error_log) + + def make_new_document(self): + reference_doc = influxframework.get_doc(self.reference_doctype, self.reference_document) + new_doc = influxframework.copy_doc(reference_doc, ignore_no_copy=False) + self.update_doc(new_doc, reference_doc) + new_doc.insert(ignore_permissions=True) + + if self.submit_on_creation: + new_doc.submit() + + return new_doc + + def update_doc(self, new_doc, reference_doc): + new_doc.docstatus = 0 + if new_doc.meta.get_field("set_posting_time"): + new_doc.set("set_posting_time", 1) + + if new_doc.meta.get_field("auto_repeat"): + new_doc.set("auto_repeat", self.name) + + for fieldname in [ + "naming_series", + "ignore_pricing_rule", + "posting_time", + "select_print_heading", + "user_remark", + "remarks", + "owner", + ]: + if new_doc.meta.get_field(fieldname): + new_doc.set(fieldname, reference_doc.get(fieldname)) + + for data in new_doc.meta.fields: + if data.fieldtype == "Date" and data.reqd: + new_doc.set(data.fieldname, self.next_schedule_date) + + self.set_auto_repeat_period(new_doc) + + auto_repeat_doc = influxframework.get_doc("Auto Repeat", self.name) + + # for any action that needs to take place after the recurring document creation + # on recurring method of that doctype is triggered + new_doc.run_method("on_recurring", reference_doc=reference_doc, auto_repeat_doc=auto_repeat_doc) + + def set_auto_repeat_period(self, new_doc): + mcount = month_map.get(self.frequency) + if mcount and new_doc.meta.get_field("from_date") and new_doc.meta.get_field("to_date"): + last_ref_doc = influxframework.get_all( + doctype=self.reference_doctype, + fields=["name", "from_date", "to_date"], + filters=[ + ["auto_repeat", "=", self.name], + ["docstatus", "<", 2], + ], + order_by="creation desc", + limit=1, + ) + + if not last_ref_doc: + return + + from_date = get_next_date(last_ref_doc[0].from_date, mcount) + + if (cstr(get_first_day(last_ref_doc[0].from_date)) == cstr(last_ref_doc[0].from_date)) and ( + cstr(get_last_day(last_ref_doc[0].to_date)) == cstr(last_ref_doc[0].to_date) + ): + to_date = get_last_day(get_next_date(last_ref_doc[0].to_date, mcount)) + else: + to_date = get_next_date(last_ref_doc[0].to_date, mcount) + + new_doc.set("from_date", from_date) + new_doc.set("to_date", to_date) + + def get_next_schedule_date(self, schedule_date, for_full_schedule=False): + """ + Returns the next schedule date for auto repeat after a recurring document has been created. + Adds required offset to the schedule_date param and returns the next schedule date. + + :param schedule_date: The date when the last recurring document was created. + :param for_full_schedule: If True, returns the immediate next schedule date, else the full schedule. + """ + if month_map.get(self.frequency): + month_count = month_map.get(self.frequency) + month_diff(schedule_date, self.start_date) - 1 + else: + month_count = 0 + + day_count = 0 + if month_count and self.repeat_on_last_day: + day_count = 31 + next_date = get_next_date(self.start_date, month_count, day_count) + elif month_count and self.repeat_on_day: + day_count = self.repeat_on_day + next_date = get_next_date(self.start_date, month_count, day_count) + elif month_count: + next_date = get_next_date(self.start_date, month_count) + else: + days = self.get_days(schedule_date) + next_date = add_days(schedule_date, days) + + # next schedule date should be after or on current date + if not for_full_schedule: + while getdate(next_date) < getdate(today()): + if month_count: + month_count += month_map.get(self.frequency, 0) + next_date = get_next_date(self.start_date, month_count, day_count) + else: + days = self.get_days(next_date) + next_date = add_days(next_date, days) + + return next_date + + def get_days(self, schedule_date): + if self.frequency == "Weekly": + days = self.get_offset_for_weekly_frequency(schedule_date) + else: + # daily frequency + days = 1 + + return days + + def get_offset_for_weekly_frequency(self, schedule_date): + # if weekdays are not set, offset is 7 from current schedule date + if not self.repeat_on_days: + return 7 + + repeat_on_days = self.get_auto_repeat_days() + current_schedule_day = getdate(schedule_date).weekday() + weekdays = list(week_map.keys()) + + # if repeats on more than 1 day or + # start date's weekday is not in repeat days, then get next weekday + # else offset is 7 + if len(repeat_on_days) > 1 or weekdays[current_schedule_day] not in repeat_on_days: + weekday = get_next_weekday(current_schedule_day, repeat_on_days) + next_weekday_number = week_map.get(weekday, 0) + # offset for upcoming weekday + return timedelta((7 + next_weekday_number - current_schedule_day) % 7).days + return 7 + + def get_auto_repeat_days(self): + return [d.day for d in self.get("repeat_on_days", [])] + + def send_notification(self, new_doc): + """Notify concerned people about recurring document generation""" + subject = self.subject or "" + message = self.message or "" + + if not self.subject: + subject = _("New {0}: {1}").format(new_doc.doctype, new_doc.name) + elif "{" in self.subject: + subject = influxframework.render_template(self.subject, {"doc": new_doc}) + + print_format = self.print_format or "Standard" + error_string = None + + try: + attachments = [ + influxframework.attach_print( + new_doc.doctype, new_doc.name, file_name=new_doc.name, print_format=print_format + ) + ] + + except influxframework.PermissionError: + error_string = _("A recurring {0} {1} has been created for you via Auto Repeat {2}.").format( + new_doc.doctype, new_doc.name, self.name + ) + error_string += "

    " + + error_string += _( + "{0}: Failed to attach new recurring document. To enable attaching document in the auto repeat notification email, enable {1} in Print Settings" + ).format(influxframework.bold(_("Note")), influxframework.bold(_("Allow Print for Draft"))) + attachments = "[]" + + if error_string: + message = error_string + elif not self.message: + message = _("Please find attached {0}: {1}").format(new_doc.doctype, new_doc.name) + elif "{" in self.message: + message = influxframework.render_template(self.message, {"doc": new_doc}) + + recipients = self.recipients.split("\n") + + make( + doctype=new_doc.doctype, + name=new_doc.name, + recipients=recipients, + subject=subject, + content=message, + attachments=attachments, + send_email=1, + ) + + @influxframework.whitelist() + def fetch_linked_contacts(self): + if self.reference_doctype and self.reference_document: + res = get_contacts_linking_to( + self.reference_doctype, self.reference_document, fields=["email_id"] + ) + res += get_contacts_linked_from( + self.reference_doctype, self.reference_document, fields=["email_id"] + ) + email_ids = {d.email_id for d in res} + if not email_ids: + influxframework.msgprint(_("No contacts linked to document"), alert=True) + else: + self.recipients = ", ".join(email_ids) + + def disable_auto_repeat(self): + influxframework.db.set_value("Auto Repeat", self.name, "disabled", 1) + + def notify_error_to_user(self, error_log): + recipients = list(get_system_managers(only_name=True)) + recipients.append(self.owner) + subject = _("Auto Repeat Document Creation Failed") + + form_link = influxframework.utils.get_link_to_form(self.reference_doctype, self.reference_document) + auto_repeat_failed_for = _("Auto Repeat failed for {0}").format(form_link) + + error_log_link = influxframework.utils.get_link_to_form("Error Log", error_log.name) + error_log_message = _("Check the Error Log for more information: {0}").format(error_log_link) + + influxframework.sendmail( + recipients=recipients, + subject=subject, + template="auto_repeat_fail", + args={"auto_repeat_failed_for": auto_repeat_failed_for, "error_log_message": error_log_message}, + header=[subject, "red"], + ) + + +def get_next_date(dt, mcount, day=None): + dt = getdate(dt) + dt += relativedelta(months=mcount, day=day) + return dt + + +def get_next_weekday(current_schedule_day, weekdays): + days = list(week_map.keys()) + if current_schedule_day > 0: + days = days[(current_schedule_day + 1) :] + days[:current_schedule_day] + else: + days = days[(current_schedule_day + 1) :] + + for entry in days: + if entry in weekdays: + return entry + + +# called through hooks +def make_auto_repeat_entry(): + enqueued_method = "influxframework.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries" + jobs = get_jobs() + + if not jobs or enqueued_method not in jobs[influxframework.local.site]: + date = getdate(today()) + data = get_auto_repeat_entries(date) + influxframework.enqueue(enqueued_method, data=data) + + +def create_repeated_entries(data): + for d in data: + doc = influxframework.get_doc("Auto Repeat", d.name) + + current_date = getdate(today()) + schedule_date = getdate(doc.next_schedule_date) + + if schedule_date == current_date and not doc.disabled: + doc.create_documents() + schedule_date = doc.get_next_schedule_date(schedule_date=schedule_date) + if schedule_date and not doc.disabled: + influxframework.db.set_value("Auto Repeat", doc.name, "next_schedule_date", schedule_date) + + +def get_auto_repeat_entries(date=None): + if not date: + date = getdate(today()) + return influxframework.get_all( + "Auto Repeat", filters=[["next_schedule_date", "<=", date], ["status", "=", "Active"]] + ) + + +# called through hooks +def set_auto_repeat_as_completed(): + auto_repeat = influxframework.get_all("Auto Repeat", filters={"status": ["!=", "Disabled"]}) + for entry in auto_repeat: + doc = influxframework.get_doc("Auto Repeat", entry.name) + if doc.is_completed(): + doc.status = "Completed" + doc.save() + + +@influxframework.whitelist() +def make_auto_repeat(doctype, docname, frequency="Daily", start_date=None, end_date=None): + if not start_date: + start_date = getdate(today()) + doc = influxframework.new_doc("Auto Repeat") + doc.reference_doctype = doctype + doc.reference_document = docname + doc.frequency = frequency + doc.start_date = start_date + if end_date: + doc.end_date = end_date + doc.save() + return doc + + +# method for reference_doctype filter +@influxframework.whitelist() +@influxframework.validate_and_sanitize_search_inputs +def get_auto_repeat_doctypes(doctype, txt, searchfield, start, page_len, filters): + res = influxframework.get_all( + "Property Setter", + { + "property": "allow_auto_repeat", + "value": "1", + }, + ["doc_type"], + ) + docs = [r.doc_type for r in res] + + res = influxframework.get_all( + "DocType", + { + "allow_auto_repeat": 1, + }, + ["name"], + ) + docs += [r.name for r in res] + docs = set(list(docs)) + + return [[d] for d in docs] + + +@influxframework.whitelist() +def update_reference(docname, reference): + result = "" + try: + influxframework.db.set_value("Auto Repeat", docname, "reference_document", reference) + result = "success" + except Exception as e: + result = "error" + raise e + return result + + +@influxframework.whitelist() +def generate_message_preview(reference_dt, reference_doc, message=None, subject=None): + influxframework.has_permission("Auto Repeat", "write", throw=True) + doc = influxframework.get_doc(reference_dt, reference_doc) + subject_preview = _("Please add a subject to your email") + msg_preview = influxframework.render_template(message, {"doc": doc}) + if subject: + subject_preview = influxframework.render_template(subject, {"doc": doc}) + + return {"message": msg_preview, "subject": subject_preview} diff --git a/influxframework/automation/doctype/auto_repeat/auto_repeat_list.js b/influxframework/automation/doctype/auto_repeat/auto_repeat_list.js new file mode 100644 index 0000000..36dfbaf --- /dev/null +++ b/influxframework/automation/doctype/auto_repeat/auto_repeat_list.js @@ -0,0 +1,11 @@ +influxframework.listview_settings["Auto Repeat"] = { + add_fields: ["next_schedule_date"], + get_indicator: function (doc) { + var colors = { + Active: "green", + Disabled: "red", + Completed: "blue", + }; + return [__(doc.status), colors[doc.status], "status,=," + doc.status]; + }, +}; diff --git a/influxframework/automation/doctype/auto_repeat/auto_repeat_schedule.html b/influxframework/automation/doctype/auto_repeat/auto_repeat_schedule.html new file mode 100644 index 0000000..c6720f0 --- /dev/null +++ b/influxframework/automation/doctype/auto_repeat/auto_repeat_schedule.html @@ -0,0 +1,19 @@ + + + + + + + + + + + {% for(var i=0; i < schedule_details.length; i++) { %} + + + + + + {% } %} + +
    {{ __("Reference Document") }}{{ __("Frequency") }}{{ __("Next Scheduled Date") }}
    {{ schedule_details[i].reference_document }} {{ __(schedule_details[i].frequency) }} {{ influxframework.datetime.str_to_user(schedule_details[i].next_scheduled_date) }}
    diff --git a/influxframework/automation/doctype/auto_repeat/test_auto_repeat.py b/influxframework/automation/doctype/auto_repeat/test_auto_repeat.py new file mode 100644 index 0000000..17f8abb --- /dev/null +++ b/influxframework/automation/doctype/auto_repeat/test_auto_repeat.py @@ -0,0 +1,286 @@ +# Copyright (c) 2018, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.automation.doctype.auto_repeat.auto_repeat import ( + create_repeated_entries, + get_auto_repeat_entries, + week_map, +) +from influxframework.custom.doctype.custom_field.custom_field import create_custom_field +from influxframework.tests.utils import InfluxFrameworkTestCase +from influxframework.utils import add_days, add_months, getdate, today + + +def add_custom_fields(): + df = dict( + fieldname="auto_repeat", + label="Auto Repeat", + fieldtype="Link", + insert_after="sender", + options="Auto Repeat", + hidden=1, + print_hide=1, + read_only=1, + ) + create_custom_field("ToDo", df) + + +class TestAutoRepeat(InfluxFrameworkTestCase): + def setUp(self): + if not influxframework.db.sql( + "SELECT `fieldname` FROM `tabCustom Field` WHERE `fieldname`='auto_repeat' and `dt`=%s", "Todo" + ): + add_custom_fields() + + def test_daily_auto_repeat(self): + todo = influxframework.get_doc( + dict(doctype="ToDo", description="test recurring todo", assigned_by="Administrator") + ).insert() + + doc = make_auto_repeat(reference_document=todo.name) + self.assertEqual(doc.next_schedule_date, today()) + data = get_auto_repeat_entries(getdate(today())) + create_repeated_entries(data) + influxframework.db.commit() + + todo = influxframework.get_doc(doc.reference_doctype, doc.reference_document) + self.assertEqual(todo.auto_repeat, doc.name) + + new_todo = influxframework.db.get_value( + "ToDo", {"auto_repeat": doc.name, "name": ("!=", todo.name)}, "name" + ) + + new_todo = influxframework.get_doc("ToDo", new_todo) + + self.assertEqual(todo.get("description"), new_todo.get("description")) + + def test_weekly_auto_repeat(self): + todo = influxframework.get_doc( + dict(doctype="ToDo", description="test weekly todo", assigned_by="Administrator") + ).insert() + + doc = make_auto_repeat( + reference_doctype="ToDo", + frequency="Weekly", + reference_document=todo.name, + start_date=add_days(today(), -7), + ) + + self.assertEqual(doc.next_schedule_date, today()) + data = get_auto_repeat_entries(getdate(today())) + create_repeated_entries(data) + influxframework.db.commit() + + todo = influxframework.get_doc(doc.reference_doctype, doc.reference_document) + self.assertEqual(todo.auto_repeat, doc.name) + + new_todo = influxframework.db.get_value( + "ToDo", {"auto_repeat": doc.name, "name": ("!=", todo.name)}, "name" + ) + + new_todo = influxframework.get_doc("ToDo", new_todo) + + self.assertEqual(todo.get("description"), new_todo.get("description")) + + def test_weekly_auto_repeat_with_weekdays(self): + todo = influxframework.get_doc( + dict(doctype="ToDo", description="test auto repeat with weekdays", assigned_by="Administrator") + ).insert() + + weekdays = list(week_map.keys()) + current_weekday = getdate().weekday() + days = [{"day": weekdays[current_weekday]}, {"day": weekdays[(current_weekday + 2) % 7]}] + doc = make_auto_repeat( + reference_doctype="ToDo", + frequency="Weekly", + reference_document=todo.name, + start_date=add_days(today(), -7), + days=days, + ) + + self.assertEqual(doc.next_schedule_date, today()) + data = get_auto_repeat_entries(getdate(today())) + create_repeated_entries(data) + influxframework.db.commit() + + todo = influxframework.get_doc(doc.reference_doctype, doc.reference_document) + self.assertEqual(todo.auto_repeat, doc.name) + + doc.reload() + self.assertEqual(doc.next_schedule_date, add_days(getdate(), 2)) + + def test_monthly_auto_repeat(self): + start_date = today() + end_date = add_months(start_date, 12) + + todo = influxframework.get_doc( + dict(doctype="ToDo", description="test recurring todo", assigned_by="Administrator") + ).insert() + + self.monthly_auto_repeat("ToDo", todo.name, start_date, end_date) + # test without end_date + todo = influxframework.get_doc( + dict( + doctype="ToDo", description="test recurring todo without end_date", assigned_by="Administrator" + ) + ).insert() + self.monthly_auto_repeat("ToDo", todo.name, start_date) + + def monthly_auto_repeat(self, doctype, docname, start_date, end_date=None): + def get_months(start, end): + diff = (12 * end.year + end.month) - (12 * start.year + start.month) + return diff + 1 + + doc = make_auto_repeat( + reference_doctype=doctype, + frequency="Monthly", + reference_document=docname, + start_date=start_date, + end_date=end_date, + ) + + doc.disable_auto_repeat() + + data = get_auto_repeat_entries(getdate(today())) + create_repeated_entries(data) + docnames = influxframework.get_all(doc.reference_doctype, {"auto_repeat": doc.name}) + self.assertEqual(len(docnames), 1) + + doc = influxframework.get_doc("Auto Repeat", doc.name) + doc.db_set("disabled", 0) + + months = get_months(getdate(start_date), getdate(today())) + data = get_auto_repeat_entries(getdate(today())) + create_repeated_entries(data) + + docnames = influxframework.get_all(doc.reference_doctype, {"auto_repeat": doc.name}) + self.assertEqual(len(docnames), months) + + def test_notification_is_attached(self): + todo = influxframework.get_doc( + dict( + doctype="ToDo", + description="Test recurring notification attachment", + assigned_by="Administrator", + ) + ).insert() + + doc = make_auto_repeat( + reference_document=todo.name, + notify=1, + recipients="test@domain.com", + subject="New ToDo", + message="A new ToDo has just been created for you", + ) + data = get_auto_repeat_entries(getdate(today())) + create_repeated_entries(data) + influxframework.db.commit() + + new_todo = influxframework.db.get_value( + "ToDo", {"auto_repeat": doc.name, "name": ("!=", todo.name)}, "name" + ) + + linked_comm = influxframework.db.exists( + "Communication", dict(reference_doctype="ToDo", reference_name=new_todo) + ) + self.assertTrue(linked_comm) + + def test_next_schedule_date(self): + current_date = getdate(today()) + todo = influxframework.get_doc( + dict( + doctype="ToDo", description="test next schedule date for monthly", assigned_by="Administrator" + ) + ).insert() + doc = make_auto_repeat( + frequency="Monthly", reference_document=todo.name, start_date=add_months(today(), -2) + ) + + # next_schedule_date is set as on or after current date + # it should not be a previous month's date + self.assertTrue(doc.next_schedule_date >= current_date) + + todo = influxframework.get_doc( + dict( + doctype="ToDo", description="test next schedule date for daily", assigned_by="Administrator" + ) + ).insert() + doc = make_auto_repeat( + frequency="Daily", reference_document=todo.name, start_date=add_days(today(), -2) + ) + self.assertEqual(getdate(doc.next_schedule_date), current_date) + + def test_submit_on_creation(self): + doctype = "Test Submittable DocType" + create_submittable_doctype(doctype) + + current_date = getdate() + submittable_doc = influxframework.get_doc(dict(doctype=doctype, test="test submit on creation")).insert() + submittable_doc.submit() + doc = make_auto_repeat( + frequency="Daily", + reference_doctype=doctype, + reference_document=submittable_doc.name, + start_date=add_days(current_date, -1), + submit_on_creation=1, + ) + + data = get_auto_repeat_entries(current_date) + create_repeated_entries(data) + docnames = influxframework.get_all( + doc.reference_doctype, filters={"auto_repeat": doc.name}, fields=["docstatus"], limit=1 + ) + self.assertEqual(docnames[0].docstatus, 1) + + +def make_auto_repeat(**args): + args = influxframework._dict(args) + doc = influxframework.get_doc( + { + "doctype": "Auto Repeat", + "reference_doctype": args.reference_doctype or "ToDo", + "reference_document": args.reference_document or influxframework.db.get_value("ToDo", "name"), + "submit_on_creation": args.submit_on_creation or 0, + "frequency": args.frequency or "Daily", + "start_date": args.start_date or add_days(today(), -1), + "end_date": args.end_date or "", + "notify_by_email": args.notify or 0, + "recipients": args.recipients or "", + "subject": args.subject or "", + "message": args.message or "", + "repeat_on_days": args.days or [], + } + ).insert(ignore_permissions=True) + + return doc + + +def create_submittable_doctype(doctype, submit_perms=1): + if influxframework.db.exists("DocType", doctype): + return + else: + doc = influxframework.get_doc( + { + "doctype": "DocType", + "__newname": doctype, + "module": "Custom", + "custom": 1, + "is_submittable": 1, + "fields": [{"fieldname": "test", "label": "Test", "fieldtype": "Data"}], + "permissions": [ + { + "role": "System Manager", + "read": 1, + "write": 1, + "create": 1, + "delete": 1, + "submit": submit_perms, + "cancel": submit_perms, + "amend": submit_perms, + } + ], + } + ).insert() + + doc.allow_auto_repeat = 1 + doc.save() diff --git a/influxframework/automation/doctype/auto_repeat_day/__init__.py b/influxframework/automation/doctype/auto_repeat_day/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/automation/doctype/auto_repeat_day/auto_repeat_day.json b/influxframework/automation/doctype/auto_repeat_day/auto_repeat_day.json new file mode 100644 index 0000000..6f5c306 --- /dev/null +++ b/influxframework/automation/doctype/auto_repeat_day/auto_repeat_day.json @@ -0,0 +1,33 @@ +{ + "actions": [], + "creation": "2020-11-10 22:30:53.690228", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "day" + ], + "fields": [ + { + "fieldname": "day", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Day", + "options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-11-10 22:30:53.690228", + "modified_by": "Administrator", + "module": "Automation", + "name": "Auto Repeat Day", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/automation/doctype/auto_repeat_day/auto_repeat_day.py b/influxframework/automation/doctype/auto_repeat_day/auto_repeat_day.py new file mode 100644 index 0000000..506613d --- /dev/null +++ b/influxframework/automation/doctype/auto_repeat_day/auto_repeat_day.py @@ -0,0 +1,9 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class AutoRepeatDay(Document): + pass diff --git a/influxframework/automation/doctype/milestone/__init__.py b/influxframework/automation/doctype/milestone/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/automation/doctype/milestone/milestone.js b/influxframework/automation/doctype/milestone/milestone.js new file mode 100644 index 0000000..4b4d951 --- /dev/null +++ b/influxframework/automation/doctype/milestone/milestone.js @@ -0,0 +1,7 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Milestone", { + // refresh: function(frm) { + // } +}); diff --git a/influxframework/automation/doctype/milestone/milestone.json b/influxframework/automation/doctype/milestone/milestone.json new file mode 100644 index 0000000..8360ce7 --- /dev/null +++ b/influxframework/automation/doctype/milestone/milestone.json @@ -0,0 +1,230 @@ +{ + "allow_copy": 0, + "allow_events_in_timeline": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "", + "beta": 0, + "creation": "2019-04-17 09:39:15.647817", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 0, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, + "fieldname": "reference_type", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Document Type", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 1, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, + "fieldname": "reference_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Document", + "length": 0, + "no_copy": 0, + "options": "", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, + "fieldname": "track_field", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Track Field", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, + "fieldname": "value", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Value", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, + "fieldname": "milestone_tracker", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Milestone Tracker", + "length": 0, + "no_copy": 0, + "options": "Milestone Tracker", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_toolbar": 0, + "idx": 0, + "in_create": 1, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2019-04-17 16:01:21.430344", + "modified_by": "Administrator", + "module": "Automation", + "name": "Milestone", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "ASC", + "title_field": "reference_type", + "track_changes": 1, + "track_seen": 0, + "track_views": 0 +} \ No newline at end of file diff --git a/influxframework/automation/doctype/milestone/milestone.py b/influxframework/automation/doctype/milestone/milestone.py new file mode 100644 index 0000000..6dd0031 --- /dev/null +++ b/influxframework/automation/doctype/milestone/milestone.py @@ -0,0 +1,13 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document + + +class Milestone(Document): + pass + + +def on_doctype_update(): + influxframework.db.add_index("Milestone", ["reference_type", "reference_name"]) diff --git a/influxframework/automation/doctype/milestone/test_milestone.py b/influxframework/automation/doctype/milestone/test_milestone.py new file mode 100644 index 0000000..bb6a752 --- /dev/null +++ b/influxframework/automation/doctype/milestone/test_milestone.py @@ -0,0 +1,8 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestMilestone(InfluxFrameworkTestCase): + pass diff --git a/influxframework/automation/doctype/milestone_tracker/__init__.py b/influxframework/automation/doctype/milestone_tracker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/automation/doctype/milestone_tracker/milestone_tracker.js b/influxframework/automation/doctype/milestone_tracker/milestone_tracker.js new file mode 100644 index 0000000..153365d --- /dev/null +++ b/influxframework/automation/doctype/milestone_tracker/milestone_tracker.js @@ -0,0 +1,31 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Milestone Tracker", { + refresh: function (frm) { + frm.trigger("update_options"); + }, + document_type: function (frm) { + frm.trigger("update_options"); + }, + update_options: function (frm) { + // update select options for `track_field` + let doctype = frm.doc.document_type; + let track_fields = []; + + if (doctype) { + influxframework.model.with_doctype(doctype, () => { + // get all date and datetime fields + influxframework.get_meta(doctype).fields.map((df) => { + if (["Link", "Select"].includes(df.fieldtype)) { + track_fields.push({ label: df.label, value: df.fieldname }); + } + }); + frm.set_df_property("track_field", "options", track_fields); + }); + } else { + // update select options + frm.set_df_property("track_field", "options", []); + } + }, +}); diff --git a/influxframework/automation/doctype/milestone_tracker/milestone_tracker.json b/influxframework/automation/doctype/milestone_tracker/milestone_tracker.json new file mode 100644 index 0000000..8e22e3e --- /dev/null +++ b/influxframework/automation/doctype/milestone_tracker/milestone_tracker.json @@ -0,0 +1,162 @@ +{ + "allow_copy": 0, + "allow_events_in_timeline": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "format:{document_type}-{track_field}", + "beta": 0, + "creation": "2019-04-17 09:36:41.774774", + "custom": 0, + "description": "Track milestones for any document", + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 0, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, + "fieldname": "document_type", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Document Type to Track", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 1 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, + "fieldname": "track_field", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Field to Track", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, + "fieldname": "disabled", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Disabled", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_toolbar": 0, + "idx": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2019-04-22 16:03:32.848937", + "modified_by": "Administrator", + "module": "Automation", + "name": "Milestone Tracker", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "ASC", + "track_changes": 1, + "track_seen": 0, + "track_views": 0 +} \ No newline at end of file diff --git a/influxframework/automation/doctype/milestone_tracker/milestone_tracker.py b/influxframework/automation/doctype/milestone_tracker/milestone_tracker.py new file mode 100644 index 0000000..0471553 --- /dev/null +++ b/influxframework/automation/doctype/milestone_tracker/milestone_tracker.py @@ -0,0 +1,51 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +import influxframework.cache_manager +from influxframework.model import log_types +from influxframework.model.document import Document + + +class MilestoneTracker(Document): + def on_update(self): + influxframework.cache_manager.clear_doctype_map("Milestone Tracker", self.document_type) + + def on_trash(self): + influxframework.cache_manager.clear_doctype_map("Milestone Tracker", self.document_type) + + def apply(self, doc): + before_save = doc.get_doc_before_save() + from_value = before_save and before_save.get(self.track_field) or None + if from_value != doc.get(self.track_field): + influxframework.get_doc( + dict( + doctype="Milestone", + reference_type=doc.doctype, + reference_name=doc.name, + track_field=self.track_field, + from_value=from_value, + value=doc.get(self.track_field), + milestone_tracker=self.name, + ) + ).insert(ignore_permissions=True) + + +def evaluate_milestone(doc, event): + if ( + influxframework.flags.in_install + or influxframework.flags.in_migrate + or influxframework.flags.in_setup_wizard + or doc.doctype in log_types + ): + return + + # track milestones related to this doctype + for d in get_milestone_trackers(doc.doctype): + influxframework.get_doc("Milestone Tracker", d.get("name")).apply(doc) + + +def get_milestone_trackers(doctype): + return influxframework.cache_manager.get_doctype_map( + "Milestone Tracker", doctype, dict(document_type=doctype, disabled=0) + ) diff --git a/influxframework/automation/doctype/milestone_tracker/test_milestone_tracker.py b/influxframework/automation/doctype/milestone_tracker/test_milestone_tracker.py new file mode 100644 index 0000000..35cff1d --- /dev/null +++ b/influxframework/automation/doctype/milestone_tracker/test_milestone_tracker.py @@ -0,0 +1,46 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +import influxframework.cache_manager +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestMilestoneTracker(InfluxFrameworkTestCase): + def test_milestone(self): + influxframework.db.delete("Milestone Tracker") + + influxframework.cache().delete_key("milestone_tracker_map") + + milestone_tracker = influxframework.get_doc( + dict(doctype="Milestone Tracker", document_type="ToDo", track_field="status") + ).insert() + + todo = influxframework.get_doc(dict(doctype="ToDo", description="test milestone", status="Open")).insert() + + milestones = influxframework.get_all( + "Milestone", + fields=["track_field", "value", "milestone_tracker"], + filters=dict(reference_type=todo.doctype, reference_name=todo.name), + ) + + self.assertEqual(len(milestones), 1) + self.assertEqual(milestones[0].track_field, "status") + self.assertEqual(milestones[0].value, "Open") + + todo.status = "Closed" + todo.save() + + milestones = influxframework.get_all( + "Milestone", + fields=["track_field", "value", "milestone_tracker"], + filters=dict(reference_type=todo.doctype, reference_name=todo.name), + order_by="modified desc", + ) + + self.assertEqual(len(milestones), 2) + self.assertEqual(milestones[0].track_field, "status") + self.assertEqual(milestones[0].value, "Closed") + + # cleanup + influxframework.db.delete("Milestone") + milestone_tracker.delete() diff --git a/influxframework/automation/workspace/tools/tools.json b/influxframework/automation/workspace/tools/tools.json new file mode 100644 index 0000000..40b265b --- /dev/null +++ b/influxframework/automation/workspace/tools/tools.json @@ -0,0 +1,249 @@ +{ + "charts": [], + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"ToDo\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Note\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"File\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Assignment Rule\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Auto Repeat\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Email\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Automation\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Event Streaming\",\"col\":4}}]", + "creation": "2020-03-02 14:53:24.980279", + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "tool", + "idx": 0, + "label": "Tools", + "links": [ + { + "hidden": 0, + "is_query_report": 0, + "label": "Tools", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "To Do", + "link_count": 0, + "link_to": "ToDo", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Calendar", + "link_count": 0, + "link_to": "Event", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Note", + "link_count": 0, + "link_to": "Note", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Files", + "link_count": 0, + "link_to": "File", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Activity", + "link_count": 0, + "link_to": "activity", + "link_type": "Page", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Email", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Newsletter", + "link_count": 0, + "link_to": "Newsletter", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Email Group", + "link_count": 0, + "link_to": "Email Group", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Automation", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Assignment Rule", + "link_count": 0, + "link_to": "Assignment Rule", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Milestone", + "link_count": 0, + "link_to": "Milestone", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Auto Repeat", + "link_count": 0, + "link_to": "Auto Repeat", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Event Streaming", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Event Producer", + "link_count": 0, + "link_to": "Event Producer", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Event Consumer", + "link_count": 0, + "link_to": "Event Consumer", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Event Update Log", + "link_count": 0, + "link_to": "Event Update Log", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Event Sync Log", + "link_count": 0, + "link_to": "Event Sync Log", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Document Type Mapping", + "link_count": 0, + "link_to": "Document Type Mapping", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + } + ], + "modified": "2022-01-13 17:48:48.456763", + "modified_by": "Administrator", + "module": "Automation", + "name": "Tools", + "owner": "Administrator", + "parent_page": "", + "public": 1, + "restrict_to_domain": "", + "roles": [], + "sequence_id": 26.0, + "shortcuts": [ + { + "label": "ToDo", + "link_to": "ToDo", + "type": "DocType" + }, + { + "label": "Note", + "link_to": "Note", + "type": "DocType" + }, + { + "label": "File", + "link_to": "File", + "type": "DocType" + }, + { + "label": "Assignment Rule", + "link_to": "Assignment Rule", + "type": "DocType" + }, + { + "label": "Auto Repeat", + "link_to": "Auto Repeat", + "type": "DocType" + } + ], + "title": "Tools" +} \ No newline at end of file diff --git a/influxframework/boot.py b/influxframework/boot.py new file mode 100644 index 0000000..fe4c1ae --- /dev/null +++ b/influxframework/boot.py @@ -0,0 +1,438 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +""" +bootstrap client session +""" + +import influxframework +import influxframework.defaults +import influxframework.desk.desk_page +from influxframework.core.doctype.navbar_settings.navbar_settings import get_app_logo, get_navbar_settings +from influxframework.desk.doctype.route_history.route_history import frequently_visited_links +from influxframework.desk.form.load import get_meta_bundle +from influxframework.email.inbox import get_email_accounts +from influxframework.model.base_document import get_controller +from influxframework.query_builder import DocType +from influxframework.query_builder.functions import Count +from influxframework.query_builder.terms import ParameterizedValueWrapper, SubQuery +from influxframework.social.doctype.energy_point_log.energy_point_log import get_energy_points +from influxframework.social.doctype.energy_point_settings.energy_point_settings import ( + is_energy_point_enabled, +) +from influxframework.translate import get_lang_dict, get_messages_for_boot, get_translated_doctypes +from influxframework.utils import add_user_info, cstr, get_time_zone +from influxframework.utils.change_log import get_versions +from influxframework.website.doctype.web_page_view.web_page_view import is_tracking_enabled + + +def get_bootinfo(): + """build and return boot info""" + influxframework.set_user_lang(influxframework.session.user) + bootinfo = influxframework._dict() + hooks = influxframework.get_hooks() + doclist = [] + + # user + get_user(bootinfo) + + # system info + bootinfo.sitename = influxframework.local.site + bootinfo.sysdefaults = influxframework.defaults.get_defaults() + bootinfo.server_date = influxframework.utils.nowdate() + + if influxframework.session["user"] != "Guest": + bootinfo.user_info = get_user_info() + bootinfo.sid = influxframework.session["sid"] + + bootinfo.modules = {} + bootinfo.module_list = [] + load_desktop_data(bootinfo) + bootinfo.letter_heads = get_letter_heads() + bootinfo.active_domains = influxframework.get_active_domains() + bootinfo.all_domains = [d.get("name") for d in influxframework.get_all("Domain")] + add_layouts(bootinfo) + + bootinfo.module_app = influxframework.local.module_app + bootinfo.single_types = [d.name for d in influxframework.get_all("DocType", {"issingle": 1})] + bootinfo.nested_set_doctypes = [ + d.parent for d in influxframework.get_all("DocField", {"fieldname": "lft"}, ["parent"]) + ] + add_home_page(bootinfo, doclist) + bootinfo.page_info = get_allowed_pages() + load_translations(bootinfo) + add_timezone_info(bootinfo) + load_conf_settings(bootinfo) + load_print(bootinfo, doclist) + doclist.extend(get_meta_bundle("Page")) + bootinfo.home_folder = influxframework.db.get_value("File", {"is_home_folder": 1}) + bootinfo.navbar_settings = get_navbar_settings() + bootinfo.notification_settings = get_notification_settings() + set_time_zone(bootinfo) + + # ipinfo + if influxframework.session.data.get("ipinfo"): + bootinfo.ipinfo = influxframework.session["data"]["ipinfo"] + + # add docs + bootinfo.docs = doclist + load_country_doc(bootinfo) + load_currency_docs(bootinfo) + + for method in hooks.boot_session or []: + influxframework.get_attr(method)(bootinfo) + + if bootinfo.lang: + bootinfo.lang = str(bootinfo.lang) + bootinfo.versions = {k: v["version"] for k, v in get_versions().items()} + + bootinfo.error_report_email = influxframework.conf.error_report_email + bootinfo.calendars = sorted(influxframework.get_hooks("calendars")) + bootinfo.treeviews = influxframework.get_hooks("treeviews") or [] + bootinfo.lang_dict = get_lang_dict() + bootinfo.success_action = get_success_action() + bootinfo.update(get_email_accounts(user=influxframework.session.user)) + bootinfo.energy_points_enabled = is_energy_point_enabled() + bootinfo.website_tracking_enabled = is_tracking_enabled() + bootinfo.points = get_energy_points(influxframework.session.user) + bootinfo.frequently_visited_links = frequently_visited_links() + bootinfo.link_preview_doctypes = get_link_preview_doctypes() + bootinfo.additional_filters_config = get_additional_filters_from_hooks() + bootinfo.desk_settings = get_desk_settings() + bootinfo.app_logo_url = get_app_logo() + bootinfo.link_title_doctypes = get_link_title_doctypes() + bootinfo.translated_doctypes = get_translated_doctypes() + bootinfo.subscription_expiry = add_subscription_expiry() + + return bootinfo + + +def get_letter_heads(): + letter_heads = {} + for letter_head in influxframework.get_all("Letter Head", fields=["name", "content", "footer"]): + letter_heads.setdefault( + letter_head.name, {"header": letter_head.content, "footer": letter_head.footer} + ) + + return letter_heads + + +def load_conf_settings(bootinfo): + from influxframework import conf + + bootinfo.max_file_size = conf.get("max_file_size") or 10485760 + for key in ("developer_mode", "socketio_port", "file_watcher_port"): + if key in conf: + bootinfo[key] = conf.get(key) + + +def load_desktop_data(bootinfo): + from influxframework.desk.desktop import get_workspace_sidebar_items + + bootinfo.allowed_workspaces = get_workspace_sidebar_items().get("pages") + bootinfo.module_page_map = get_controller("Workspace").get_module_page_map() + bootinfo.dashboards = influxframework.get_all("Dashboard") + + +def get_allowed_pages(cache=False): + return get_user_pages_or_reports("Page", cache=cache) + + +def get_allowed_reports(cache=False): + return get_user_pages_or_reports("Report", cache=cache) + + +def get_allowed_report_names(cache=False) -> set[str]: + return {cstr(report) for report in get_allowed_reports(cache).keys() if report} + + +def get_user_pages_or_reports(parent, cache=False): + _cache = influxframework.cache() + + if cache: + has_role = _cache.get_value("has_role:" + parent, user=influxframework.session.user) + if has_role: + return has_role + + roles = influxframework.get_roles() + has_role = {} + + page = DocType("Page") + report = DocType("Report") + + if parent == "Report": + columns = (report.name.as_("title"), report.ref_doctype, report.report_type) + else: + columns = (page.title.as_("title"),) + + customRole = DocType("Custom Role") + hasRole = DocType("Has Role") + parentTable = DocType(parent) + + # get pages or reports set on custom role + pages_with_custom_roles = ( + influxframework.qb.from_(customRole) + .from_(hasRole) + .from_(parentTable) + .select( + customRole[parent.lower()].as_("name"), customRole.modified, customRole.ref_doctype, *columns + ) + .where( + (hasRole.parent == customRole.name) + & (parentTable.name == customRole[parent.lower()]) + & (customRole[parent.lower()].isnotnull()) + & (hasRole.role.isin(roles)) + ) + ).run(as_dict=True) + + for p in pages_with_custom_roles: + has_role[p.name] = {"modified": p.modified, "title": p.title, "ref_doctype": p.ref_doctype} + + subq = ( + influxframework.qb.from_(customRole) + .select(customRole[parent.lower()]) + .where(customRole[parent.lower()].isnotnull()) + ) + + pages_with_standard_roles = ( + influxframework.qb.from_(hasRole) + .from_(parentTable) + .select(parentTable.name.as_("name"), parentTable.modified, *columns) + .where( + (hasRole.role.isin(roles)) + & (hasRole.parent == parentTable.name) + & (parentTable.name.notin(subq)) + ) + .distinct() + ) + + if parent == "Report": + pages_with_standard_roles = pages_with_standard_roles.where(report.disabled == 0) + + pages_with_standard_roles = pages_with_standard_roles.run(as_dict=True) + + for p in pages_with_standard_roles: + if p.name not in has_role: + has_role[p.name] = {"modified": p.modified, "title": p.title} + if parent == "Report": + has_role[p.name].update({"ref_doctype": p.ref_doctype}) + + no_of_roles = SubQuery( + influxframework.qb.from_(hasRole).select(Count("*")).where(hasRole.parent == parentTable.name) + ) + + # pages with no role are allowed + if parent == "Page": + + pages_with_no_roles = ( + influxframework.qb.from_(parentTable) + .select(parentTable.name, parentTable.modified, *columns) + .where(no_of_roles == 0) + ).run(as_dict=True) + + for p in pages_with_no_roles: + if p.name not in has_role: + has_role[p.name] = {"modified": p.modified, "title": p.title} + + elif parent == "Report": + reports = influxframework.get_all( + "Report", + fields=["name", "report_type"], + filters={"name": ("in", has_role.keys())}, + ignore_ifnull=True, + ) + for report in reports: + has_role[report.name]["report_type"] = report.report_type + + # Expire every six hours + _cache.set_value("has_role:" + parent, has_role, influxframework.session.user, 21600) + return has_role + + +def load_translations(bootinfo): + bootinfo["lang"] = influxframework.lang + bootinfo["__messages"] = get_messages_for_boot() + + +def get_user_info(): + # get info for current user + user_info = influxframework._dict() + add_user_info(influxframework.session.user, user_info) + + if influxframework.session.user == "Administrator" and user_info.Administrator.email: + user_info[user_info.Administrator.email] = user_info.Administrator + + return user_info + + +def get_user(bootinfo): + """get user info""" + bootinfo.user = influxframework.get_user().load_user() + + +def add_home_page(bootinfo, docs): + """load home page""" + if influxframework.session.user == "Guest": + return + home_page = influxframework.db.get_default("desktop:home_page") + + if home_page == "setup-wizard": + bootinfo.setup_wizard_requires = influxframework.get_hooks("setup_wizard_requires") + + try: + page = influxframework.desk.desk_page.get(home_page) + docs.append(page) + bootinfo["home_page"] = page.name + except (influxframework.DoesNotExistError, influxframework.PermissionError): + if influxframework.message_log: + influxframework.message_log.pop() + bootinfo["home_page"] = "Workspaces" + + +def add_timezone_info(bootinfo): + system = bootinfo.sysdefaults.get("time_zone") + import influxframework.utils.momentjs + + bootinfo.timezone_info = {"zones": {}, "rules": {}, "links": {}} + influxframework.utils.momentjs.update(system, bootinfo.timezone_info) + + +def load_print(bootinfo, doclist): + print_settings = influxframework.db.get_singles_dict("Print Settings") + print_settings.doctype = ":Print Settings" + doclist.append(print_settings) + load_print_css(bootinfo, print_settings) + + +def load_print_css(bootinfo, print_settings): + import influxframework.www.printview + + bootinfo.print_css = influxframework.www.printview.get_print_style( + print_settings.print_style or "Redesign", for_legacy=True + ) + + +def get_unseen_notes(): + note = DocType("Note") + nsb = DocType("Note Seen By").as_("nsb") + + return ( + influxframework.qb.from_(note) + .select(note.name, note.title, note.content, note.notify_on_every_login) + .where( + (note.notify_on_login == 1) + & (note.expire_notification_on > influxframework.utils.now()) + & ( + ParameterizedValueWrapper(influxframework.session.user).notin( + SubQuery(influxframework.qb.from_(nsb).select(nsb.user).where(nsb.parent == note.name)) + ) + ) + ) + ).run(as_dict=1) + + +def get_success_action(): + return influxframework.get_all("Success Action", fields=["*"]) + + +def get_link_preview_doctypes(): + from influxframework.utils import cint + + link_preview_doctypes = [d.name for d in influxframework.get_all("DocType", {"show_preview_popup": 1})] + customizations = influxframework.get_all( + "Property Setter", fields=["doc_type", "value"], filters={"property": "show_preview_popup"} + ) + + for custom in customizations: + if not cint(custom.value) and custom.doc_type in link_preview_doctypes: + link_preview_doctypes.remove(custom.doc_type) + else: + link_preview_doctypes.append(custom.doc_type) + + return link_preview_doctypes + + +def get_additional_filters_from_hooks(): + filter_config = influxframework._dict() + filter_hooks = influxframework.get_hooks("filters_config") + for hook in filter_hooks: + filter_config.update(influxframework.get_attr(hook)()) + + return filter_config + + +def add_layouts(bootinfo): + # add routes for readable doctypes + bootinfo.doctype_layouts = influxframework.get_all("DocType Layout", ["name", "route", "document_type"]) + + +def get_desk_settings(): + role_list = influxframework.get_all("Role", fields=["*"], filters=dict(name=["in", influxframework.get_roles()])) + desk_settings = {} + + from influxframework.core.doctype.role.role import desk_properties + + for role in role_list: + for key in desk_properties: + desk_settings[key] = desk_settings.get(key) or role.get(key) + + return desk_settings + + +def get_notification_settings(): + return influxframework.get_cached_doc("Notification Settings", influxframework.session.user) + + +def get_link_title_doctypes(): + dts = influxframework.get_all("DocType", {"show_title_field_in_link": 1}) + custom_dts = influxframework.get_all( + "Property Setter", + {"property": "show_title_field_in_link", "value": "1"}, + ["doc_type as name"], + ) + return [d.name for d in dts + custom_dts if d] + + +def set_time_zone(bootinfo): + bootinfo.time_zone = { + "system": get_time_zone(), + "user": bootinfo.get("user_info", {}).get(influxframework.session.user, {}).get("time_zone", None) + or get_time_zone(), + } + + +def load_country_doc(bootinfo): + country = influxframework.db.get_default("country") + if not country: + return + try: + bootinfo.docs.append(influxframework.get_cached_doc("Country", country)) + except Exception: + pass + + +def load_currency_docs(bootinfo): + currency = influxframework.qb.DocType("Currency") + + currency_docs = ( + influxframework.qb.from_(currency) + .select( + currency.name, + currency.fraction, + currency.fraction_units, + currency.number_format, + currency.smallest_currency_fraction_value, + currency.symbol, + currency.symbol_on_right, + ) + .where(currency.enabled == 1) + .run(as_dict=1, update={"doctype": ":Currency"}) + ) + + bootinfo.docs += currency_docs + + +def add_subscription_expiry(): + try: + return influxframework.conf.subscription["expiry"] + except Exception: + return "" diff --git a/influxframework/build.py b/influxframework/build.py new file mode 100644 index 0000000..91ad2dd --- /dev/null +++ b/influxframework/build.py @@ -0,0 +1,423 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE +import os +import re +import shutil +import subprocess +from distutils.spawn import find_executable +from subprocess import getoutput +from tempfile import mkdtemp, mktemp +from urllib.parse import urlparse + +import click +import psutil +from requests import head +from requests.exceptions import HTTPError +from semantic_version import Version + +import influxframework + +timestamps = {} +app_paths = None +sites_path = os.path.abspath(os.getcwd()) +WHITESPACE_PATTERN = re.compile(r"\s+") +HTML_COMMENT_PATTERN = re.compile(r"()") + + +class AssetsNotDownloadedError(Exception): + pass + + +class AssetsDontExistError(HTTPError): + pass + + +def download_file(url, prefix): + from requests import get + + filename = urlparse(url).path.split("/")[-1] + local_filename = os.path.join(prefix, filename) + with get(url, stream=True, allow_redirects=True) as r: + r.raise_for_status() + with open(local_filename, "wb") as f: + for chunk in r.iter_content(chunk_size=8192): + f.write(chunk) + return local_filename + + +def build_missing_files(): + """Check which files dont exist yet from the assets.json and run build for those files""" + + missing_assets = [] + current_asset_files = [] + + for type in ["css", "js"]: + folder = os.path.join(sites_path, "assets", "influxframework", "dist", type) + current_asset_files.extend(os.listdir(folder)) + + development = influxframework.local.conf.developer_mode or influxframework.local.dev_server + build_mode = "development" if development else "production" + + assets_json = influxframework.read_file("assets/assets.json") + if assets_json: + assets_json = influxframework.parse_json(assets_json) + + for bundle_file, output_file in assets_json.items(): + if not output_file.startswith("/assets/influxframework"): + continue + + if os.path.basename(output_file) not in current_asset_files: + missing_assets.append(bundle_file) + + if missing_assets: + click.secho("\nBuilding missing assets...\n", fg="yellow") + files_to_build = ["influxframework/" + name for name in missing_assets] + bundle(build_mode, files=files_to_build) + else: + # no assets.json, run full build + bundle(build_mode, apps="influxframework") + + +def get_assets_link(influxframework_head) -> str: + tag = getoutput( + r"cd ../apps/influxframework && git show-ref --tags -d | grep %s | sed -e 's,.*" + r" refs/tags/,,' -e 's/\^{}//'" % influxframework_head + ) + + if tag: + # if tag exists, download assets from github release + url = f"https://github.com/influxframework/influxframework/releases/download/{tag}/assets.tar.gz" + else: + url = f"http://assets.influxframework.com/{influxframework_head}.tar.gz" + + if not head(url): + reference = f"Release {tag}" if tag else f"Commit {influxframework_head}" + raise AssetsDontExistError(f"Assets for {reference} don't exist") + + return url + + +def fetch_assets(url, influxframework_head): + click.secho("Retrieving assets...", fg="yellow") + + prefix = mkdtemp(prefix="influxframework-assets-", suffix=influxframework_head) + assets_archive = download_file(url, prefix) + + if not assets_archive: + raise AssetsNotDownloadedError(f"Assets could not be retrived from {url}") + + click.echo(click.style("✔", fg="green") + f" Downloaded InfluxFramework assets from {url}") + + return assets_archive + + +def setup_assets(assets_archive): + import tarfile + + directories_created = set() + + click.secho("\nExtracting assets...\n", fg="yellow") + with tarfile.open(assets_archive) as tar: + for file in tar: + if not file.isdir(): + dest = "." + file.name.replace("./influxframework-bench/sites", "") + asset_directory = os.path.dirname(dest) + show = dest.replace("./assets/", "") + + if asset_directory not in directories_created: + if not os.path.exists(asset_directory): + os.makedirs(asset_directory, exist_ok=True) + directories_created.add(asset_directory) + + tar.makefile(file, dest) + click.echo(click.style("✔", fg="green") + f" Restored {show}") + + return directories_created + + +def download_influxframework_assets(verbose=True): + """Downloads and sets up InfluxFramework assets if they exist based on the current + commit HEAD. + Returns True if correctly setup else returns False. + """ + influxframework_head = getoutput("cd ../apps/influxframework && git rev-parse HEAD") + + if not influxframework_head: + return False + + try: + url = get_assets_link(influxframework_head) + assets_archive = fetch_assets(url, influxframework_head) + setup_assets(assets_archive) + build_missing_files() + return True + + except AssetsDontExistError as e: + click.secho(str(e), fg="yellow") + + except Exception as e: + # TODO: log traceback in bench.log + click.secho(str(e), fg="red") + + finally: + try: + shutil.rmtree(os.path.dirname(assets_archive)) + except Exception: + pass + + return False + + +def symlink(target, link_name, overwrite=False): + """ + Create a symbolic link named link_name pointing to target. + If link_name exists then FileExistsError is raised, unless overwrite=True. + When trying to overwrite a directory, IsADirectoryError is raised. + + Source: https://stackoverflow.com/a/55742015/10309266 + """ + + if not overwrite: + return os.symlink(target, link_name) + + # os.replace() may fail if files are on different filesystems + link_dir = os.path.dirname(link_name) + + # Create link to target with temporary filename + while True: + temp_link_name = mktemp(dir=link_dir) + + # os.* functions mimic as closely as possible system functions + # The POSIX symlink() returns EEXIST if link_name already exists + # https://pubs.opengroup.org/onlinepubs/9699919799/functions/symlink.html + try: + os.symlink(target, temp_link_name) + break + except FileExistsError: + pass + + # Replace link_name with temp_link_name + try: + # Pre-empt os.replace on a directory with a nicer message + if os.path.isdir(link_name): + raise IsADirectoryError(f"Cannot symlink over existing directory: '{link_name}'") + try: + os.replace(temp_link_name, link_name) + except AttributeError: + os.renames(temp_link_name, link_name) + except Exception: + if os.path.islink(temp_link_name): + os.remove(temp_link_name) + raise + + +def setup(): + global app_paths, assets_path + + pymodules = [] + for app in influxframework.get_all_apps(True): + try: + pymodules.append(influxframework.get_module(app)) + except ImportError: + pass + app_paths = [os.path.dirname(pymodule.__file__) for pymodule in pymodules] + assets_path = os.path.join(influxframework.local.sites_path, "assets") + + +def bundle( + mode, + apps=None, + hard_link=False, + make_copy=False, + restore=False, + verbose=False, + skip_influxframework=False, + files=None, +): + """concat / minify js files""" + setup() + make_asset_dirs(hard_link=hard_link) + + mode = "production" if mode == "production" else "build" + command = f"yarn run {mode}" + + if apps: + command += f" --apps {apps}" + + if skip_influxframework: + command += " --skip_influxframework" + + if files: + command += " --files {files}".format(files=",".join(files)) + + command += " --run-build-command" + + check_node_executable() + influxframework_app_path = influxframework.get_app_path("influxframework", "..") + influxframework.commands.popen(command, cwd=influxframework_app_path, env=get_node_env(), raise_err=True) + + +def watch(apps=None): + """watch and rebuild if necessary""" + setup() + + command = "yarn run watch" + if apps: + command += f" --apps {apps}" + + live_reload = influxframework.utils.cint(os.environ.get("LIVE_RELOAD", influxframework.conf.live_reload)) + + if live_reload: + command += " --live-reload" + + check_node_executable() + influxframework_app_path = influxframework.get_app_path("influxframework", "..") + influxframework.commands.popen(command, cwd=influxframework_app_path, env=get_node_env()) + + +def check_node_executable(): + node_version = Version(subprocess.getoutput("node -v")[1:]) + warn = "⚠️ " + if node_version.major < 14: + click.echo(f"{warn} Please update your node version to 14") + if not find_executable("yarn"): + click.echo(f"{warn} Please install yarn using below command and try again.\nnpm install -g yarn") + click.echo() + + +def get_node_env(): + node_env = {"NODE_OPTIONS": f"--max_old_space_size={get_safe_max_old_space_size()}"} + return node_env + + +def get_safe_max_old_space_size(): + safe_max_old_space_size = 0 + try: + total_memory = psutil.virtual_memory().total / (1024 * 1024) + # reference for the safe limit assumption + # https://nodejs.org/api/cli.html#cli_max_old_space_size_size_in_megabytes + # set minimum value 1GB + safe_max_old_space_size = max(1024, int(total_memory * 0.75)) + except Exception: + pass + + return safe_max_old_space_size + + +def generate_assets_map(): + symlinks = {} + + for app_name in influxframework.get_all_apps(): + app_doc_path = None + + pymodule = influxframework.get_module(app_name) + app_base_path = os.path.abspath(os.path.dirname(pymodule.__file__)) + app_public_path = os.path.join(app_base_path, "public") + app_node_modules_path = os.path.join(app_base_path, "..", "node_modules") + app_docs_path = os.path.join(app_base_path, "docs") + app_www_docs_path = os.path.join(app_base_path, "www", "docs") + + app_assets = os.path.abspath(app_public_path) + app_node_modules = os.path.abspath(app_node_modules_path) + + # {app}/public > assets/{app} + if os.path.isdir(app_assets): + symlinks[app_assets] = os.path.join(assets_path, app_name) + + # {app}/node_modules > assets/{app}/node_modules + if os.path.isdir(app_node_modules): + symlinks[app_node_modules] = os.path.join(assets_path, app_name, "node_modules") + + # {app}/docs > assets/{app}_docs + if os.path.isdir(app_docs_path): + app_doc_path = os.path.join(app_base_path, "docs") + elif os.path.isdir(app_www_docs_path): + app_doc_path = os.path.join(app_base_path, "www", "docs") + if app_doc_path: + app_docs = os.path.abspath(app_doc_path) + symlinks[app_docs] = os.path.join(assets_path, app_name + "_docs") + + return symlinks + + +def setup_assets_dirs(): + for dir_path in (os.path.join(assets_path, x) for x in ("js", "css")): + os.makedirs(dir_path, exist_ok=True) + + +def clear_broken_symlinks(): + for path in os.listdir(assets_path): + path = os.path.join(assets_path, path) + if os.path.islink(path) and not os.path.exists(path): + os.remove(path) + + +def unstrip(message: str) -> str: + """Pads input string on the right side until the last available column in the terminal""" + _len = len(message) + try: + max_str = os.get_terminal_size().columns + except Exception: + max_str = 80 + + if _len < max_str: + _rem = max_str - _len + else: + _rem = max_str % _len + + return f"{message}{' ' * _rem}" + + +def make_asset_dirs(hard_link=False): + setup_assets_dirs() + clear_broken_symlinks() + symlinks = generate_assets_map() + + for source, target in symlinks.items(): + start_message = unstrip( + f"{'Copying assets from' if hard_link else 'Linking'} {source} to {target}" + ) + fail_message = unstrip(f"Cannot {'copy' if hard_link else 'link'} {source} to {target}") + + # Used '\r' instead of '\x1b[1K\r' to print entire lines in smaller terminal sizes + try: + print(start_message, end="\r") + link_assets_dir(source, target, hard_link=hard_link) + except Exception: + print(fail_message, end="\r") + + click.echo(unstrip(click.style("✔", fg="green") + " Application Assets Linked") + "\n") + + +def link_assets_dir(source, target, hard_link=False): + if not os.path.exists(source): + return + + if os.path.exists(target): + if os.path.islink(target): + os.unlink(target) + else: + shutil.rmtree(target) + + if hard_link: + shutil.copytree(source, target, dirs_exist_ok=True) + else: + symlink(source, target, overwrite=True) + + +def scrub_html_template(content): + """Returns HTML content with removed whitespace and comments""" + # remove whitespace to a single space + content = WHITESPACE_PATTERN.sub(" ", content) + + # strip comments + content = HTML_COMMENT_PATTERN.sub("", content) + + return content.replace("'", "'") + + +def html_to_js_template(path, content): + """returns HTML template content as Javascript code, adding it to `influxframework.templates`""" + return """influxframework.templates["{key}"] = '{content}';\n""".format( + key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content) + ) diff --git a/influxframework/cache_manager.py b/influxframework/cache_manager.py new file mode 100644 index 0000000..29d947d --- /dev/null +++ b/influxframework/cache_manager.py @@ -0,0 +1,245 @@ +# Copyright (c) 2018, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +import influxframework +from influxframework.desk.notifications import clear_notifications, delete_notification_count_for + +common_default_keys = ["__default", "__global"] + +doctype_map_keys = ( + "energy_point_rule_map", + "assignment_rule_map", + "milestone_tracker_map", + "event_consumer_document_type_map", +) + +bench_cache_keys = ("assets_json",) + +global_cache_keys = ( + "app_hooks", + "installed_apps", + "all_apps", + "app_modules", + "module_app", + "system_settings", + "scheduler_events", + "time_zone", + "webhooks", + "active_domains", + "active_modules", + "assignment_rule", + "server_script_map", + "wkhtmltopdf_version", + "domain_restricted_doctypes", + "domain_restricted_pages", + "information_schema:counts", + "sitemap_routes", + "db_tables", + "server_script_autocompletion_items", +) + doctype_map_keys + +user_cache_keys = ( + "bootinfo", + "user_recent", + "roles", + "user_doc", + "lang", + "defaults", + "user_permissions", + "home_page", + "linked_with", + "desktop_icons", + "portal_menu_items", + "user_perm_can_read", + "has_role:Page", + "has_role:Report", + "desk_sidebar_items", +) + +doctype_cache_keys = ( + "doctype_meta", + "doctype_form_meta", + "table_columns", + "last_modified", + "linked_doctypes", + "notifications", + "workflow", + "data_import_column_header_map", +) + doctype_map_keys + + +def clear_user_cache(user=None): + cache = influxframework.cache() + + # this will automatically reload the global cache + # so it is important to clear this first + clear_notifications(user) + + if user: + for name in user_cache_keys: + cache.hdel(name, user) + cache.delete_keys("user:" + user) + clear_defaults_cache(user) + else: + for name in user_cache_keys: + cache.delete_key(name) + clear_defaults_cache() + clear_global_cache() + + +def clear_domain_cache(user=None): + cache = influxframework.cache() + domain_cache_keys = ("domain_restricted_doctypes", "domain_restricted_pages") + cache.delete_value(domain_cache_keys) + + +def clear_global_cache(): + from influxframework.website.utils import clear_website_cache + + clear_doctype_cache() + clear_website_cache() + influxframework.cache().delete_value(global_cache_keys) + influxframework.cache().delete_value(bench_cache_keys) + influxframework.setup_module_map() + + +def clear_defaults_cache(user=None): + if user: + for p in [user] + common_default_keys: + influxframework.cache().hdel("defaults", p) + elif influxframework.flags.in_install != "influxframework": + influxframework.cache().delete_key("defaults") + + +def clear_doctype_cache(doctype=None): + clear_controller_cache(doctype) + cache = influxframework.cache() + + for key in ("is_table", "doctype_modules", "document_cache"): + cache.delete_value(key) + + influxframework.local.document_cache = {} + + def clear_single(dt): + for name in doctype_cache_keys: + cache.hdel(name, dt) + + if doctype: + clear_single(doctype) + + # clear all parent doctypes + for dt in influxframework.get_all( + "DocField", "parent", dict(fieldtype=["in", influxframework.model.table_fields], options=doctype) + ): + clear_single(dt.parent) + + # clear all parent doctypes + if not influxframework.flags.in_install: + for dt in influxframework.get_all( + "Custom Field", "dt", dict(fieldtype=["in", influxframework.model.table_fields], options=doctype) + ): + clear_single(dt.dt) + + # clear all notifications + delete_notification_count_for(doctype) + + else: + # clear all + for name in doctype_cache_keys: + cache.delete_value(name) + + +def clear_controller_cache(doctype=None): + if not doctype: + del influxframework.controllers + influxframework.controllers = {} + return + + for site_controllers in influxframework.controllers.values(): + site_controllers.pop(doctype, None) + + +def get_doctype_map(doctype, name, filters=None, order_by=None): + cache = influxframework.cache() + cache_key = influxframework.scrub(doctype) + "_map" + doctype_map = cache.hget(cache_key, name) + + if doctype_map is not None: + # cached, return + items = json.loads(doctype_map) + else: + # non cached, build cache + try: + items = influxframework.get_all(doctype, filters=filters, order_by=order_by) + cache.hset(cache_key, name, json.dumps(items)) + except influxframework.db.TableMissingError: + # executed from inside patch, ignore + items = [] + + return items + + +def clear_doctype_map(doctype, name): + influxframework.cache().hdel(influxframework.scrub(doctype) + "_map", name) + + +def build_table_count_cache(): + if ( + influxframework.flags.in_patch + or influxframework.flags.in_install + or influxframework.flags.in_migrate + or influxframework.flags.in_import + or influxframework.flags.in_setup_wizard + ): + return + + _cache = influxframework.cache() + table_name = influxframework.qb.Field("table_name").as_("name") + table_rows = influxframework.qb.Field("table_rows").as_("count") + information_schema = influxframework.qb.Schema("information_schema") + + data = (influxframework.qb.from_(information_schema.tables).select(table_name, table_rows)).run( + as_dict=True + ) + counts = {d.get("name").replace("tab", "", 1): d.get("count", None) for d in data} + _cache.set_value("information_schema:counts", counts) + + return counts + + +def build_domain_restriced_doctype_cache(*args, **kwargs): + if ( + influxframework.flags.in_patch + or influxframework.flags.in_install + or influxframework.flags.in_migrate + or influxframework.flags.in_import + or influxframework.flags.in_setup_wizard + ): + return + _cache = influxframework.cache() + active_domains = influxframework.get_active_domains() + doctypes = influxframework.get_all("DocType", filters={"restrict_to_domain": ("IN", active_domains)}) + doctypes = [doc.name for doc in doctypes] + _cache.set_value("domain_restricted_doctypes", doctypes) + + return doctypes + + +def build_domain_restriced_page_cache(*args, **kwargs): + if ( + influxframework.flags.in_patch + or influxframework.flags.in_install + or influxframework.flags.in_migrate + or influxframework.flags.in_import + or influxframework.flags.in_setup_wizard + ): + return + _cache = influxframework.cache() + active_domains = influxframework.get_active_domains() + pages = influxframework.get_all("Page", filters={"restrict_to_domain": ("IN", active_domains)}) + pages = [page.name for page in pages] + _cache.set_value("domain_restricted_pages", pages) + + return pages diff --git a/influxframework/change_log/__init__.py b/influxframework/change_log/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/change_log/current/readme.md b/influxframework/change_log/current/readme.md new file mode 100644 index 0000000..e93bb75 --- /dev/null +++ b/influxframework/change_log/current/readme.md @@ -0,0 +1,3 @@ +Leave change log files in this folder for user release notes. + +(this file is just a place holder, don't delete it) diff --git a/influxframework/change_log/v10/v10_0_0.md b/influxframework/change_log/v10/v10_0_0.md new file mode 100644 index 0000000..6954226 --- /dev/null +++ b/influxframework/change_log/v10/v10_0_0.md @@ -0,0 +1,10 @@ +- Enhanced Data Import Tool + - Data Import Tool is now a normal form, you can maintain records for each Data Import. + - Better error handling + - Background processing for large files + +- InfluxFramework now has a github connector + +- Any doctype can have a calendar view + +- InfluxFramework has a new simple, responsive, modern SVG [charts library](https://github.com/influxframework/charts), developed by us diff --git a/influxframework/change_log/v11/v11_1_0.md b/influxframework/change_log/v11/v11_1_0.md new file mode 100644 index 0000000..f92fdcf --- /dev/null +++ b/influxframework/change_log/v11/v11_1_0.md @@ -0,0 +1,19 @@ +- Dynamic [InfluxFramework Charts](https://github.com/influxframework/charts) with Report Builder (built by @pratu16x7) +- New InfluxFramework Chat for easier internal communication (built by @achillesrasquinha) +- [InfluxFramework DataTable](https://github.com/influxframework/datatable) for better reports (built by @netchampfaris) +- Google Calendar can now be integrated with InfluxFramework +- Files can now be uploaded using drag-and-drop +- Enhanced List View and Tree View +- Bulk Actions from List View +- Quill editor has been introduced in place of Summernote +- HTML Editor has been introduced +- New User Permissions +- Subscriptions in InfluxERP now moved to Auto Repeat in InfluxFramework +- Support for Razorpay and PayPal subscriptions +- Better Social login, Workflow +- Messages for when user goes online/offline +- Logout from all sessions on password change +- Desktop icons can now be selected from a dialog box +- Changes have been made to ensure InfluxFramework is compatible with Python 3 +- Better documentation is now available with support for more languages +- A lot of other fixes have been done to ensure a better overall user experience diff --git a/influxframework/change_log/v12/v12_0_0.md b/influxframework/change_log/v12/v12_0_0.md new file mode 100644 index 0000000..37465dc --- /dev/null +++ b/influxframework/change_log/v12/v12_0_0.md @@ -0,0 +1,29 @@ +# Version 12 Release Notes + +### UI/UX Enhancements +1. [New Desktop](https://influxerp.com/docs/user/manual/en/using-influxerp/desktop) +1. [Keyboard Navigation](https://influxerp.com/docs/user/manual/en/using-influxerp/articles/keyboard-shortcuts) +1. [Link Preview](https://influxerp.com/version-12/release-notes/features#link-preview) +1. [New Upload Dialog](https://influxerp.com/version-12/release-notes/features#new-upload-dialog) +1. [Frequently visited links appear in Awesomebar results](https://influxerp.com/version-12/release-notes/features#frequently-visited-links-appear-in-awesomebar-results) +1. [Full Width Container]((https://influxerp.com/version-12/release-notes/features#full-width-container)) +1. [List View Enhancements](https://influxerp.com/version-12/release-notes/features#list-view-enhancements) + +### New Automation Module +1. [Assignment Rule](https://influxerp.com/docs/user/manual/en/setting-up/automation/assignment-rule) +1. [Milestones](https://influxerp.com/docs/user/manual/en/setting-up/automation/milestone-tracker) +1. [Auto Repeat](https://influxerp.com/docs/user/manual/en/setting-up/automation/auto-repeat) + +### Other Changes & Enhancements +1. [Document Follow](https://influxerp.com/docs/user/manual/en/setting-up/email/document-follow) +1. [Energy Points](https://influxerp.com/docs/user/manual/en/setting-up/energy-point-system) +1. [Dashboards](https://influxerp.com/docs/user/manual/en/customize-influxerp/dashboard) +1. [Disable customization for single doctypes](https://influxerp.com/version-12/release-notes/features#disable-customization-for-single-doctypes) +1. [Email Linking](https://influxerp.com/docs/user/manual/en/setting-up/email/linking-emails-to-document) +1. [Google Contacts](https://influxerp.com/docs/user/manual/en/influxerp_integration/google_contacts) +1. [PDF Encryption](https://influxerp.com/version-12/release-notes/features#pdf-encryption) +1. [Raw Printing](https://influxerp.com/docs/user/manual/en/setting-up/print/raw-printing) +1. [Web Form Refactor](https://influxerp.com/version-12/release-notes/features#web-form-refactor) +1. [Website Refactor](https://influxerp.com/docs/user/manual/en/website) +1. [Added Track Views field to Customize Form](https://influxerp.com/version-12/release-notes/features#added-track-views-field-to-customize-form) +1. [Add custom columns to any report](https://influxerp.com/version-12/release-notes/features#add-custom-columns-to-any-report) diff --git a/influxframework/change_log/v13/v13_0_0.md b/influxframework/change_log/v13/v13_0_0.md new file mode 100644 index 0000000..ac36d10 --- /dev/null +++ b/influxframework/change_log/v13/v13_0_0.md @@ -0,0 +1,54 @@ +# Version 13.0.0 Release Notes + +## Highlights + +- Re-branded UI 💎 ✨🎊 ([#12277](https://github.com/influxframework/influxframework/pull/12277)) +- New Page Builder in Web Page ([#10035](https://github.com/influxframework/influxframework/pull/10035)) +- Customizable desk ([#9617](https://github.com/influxframework/influxframework/pull/9617)) +- Custom Dashboard for DocTypes ([#9872](https://github.com/influxframework/influxframework/pull/9872)) +- Widgets to make dashboards ([#9693](https://github.com/influxframework/influxframework/pull/9693)) +- Events Streaming ([#8567](https://github.com/influxframework/influxframework/pull/8567)) +- Contextual translation and Translation Tool ([#9636](https://github.com/influxframework/influxframework/pull/9636)) + +### Other Features & Enhancements + +- Added permission to grant only `Select` access ([#12063](https://github.com/influxframework/influxframework/pull/12063)) +- Add columns and filters for reports via configuration ([#11287](https://github.com/influxframework/influxframework/pull/11287)) +- Configurable Navbar logo and dropdowns ([#11213](https://github.com/influxframework/influxframework/pull/11213)) +- Rule based naming of documents ([#11439](https://github.com/influxframework/influxframework/pull/11439)) +- New routing style, not using hashes, also /desk -> /app ([#11917](https://github.com/influxframework/influxframework/pull/11917)) +- Web Page tracking ([#9959](https://github.com/influxframework/influxframework/pull/9959)) +- Introduced "Yesterday" and "Tomorrow" options for Timespan filter ([12179](https://github.com/influxframework/influxframework/pull/12179)) +- Child table pagination ([#8786](https://github.com/influxframework/influxframework/pull/8786)) +- Introduced Duration Control ([#10248](https://github.com/influxframework/influxframework/pull/10248)) +- Form Tour feature ([#10287](https://github.com/influxframework/influxframework/pull/10287)) +
    +More + +- Introduced Map View ([#11202](https://github.com/influxframework/influxframework/pull/11202)) +- Custom JS & CSS support in Web Form ([#9121](https://github.com/influxframework/influxframework/pull/9121)) ([#9610](https://github.com/influxframework/influxframework/pull/9610)) +- Ability to attach photo from webcam ([#12160](https://github.com/influxframework/influxframework/pull/12160)) +- Added a System Console to help in debugging ([#11306](https://github.com/influxframework/influxframework/pull/11306)) +- Introduced System Settings to automatically delete old Prepared Reports ([#9751](https://github.com/influxframework/influxframework/pull/9751)) +- "Mandatory Depends On" and "Read Only Depends On" option for document fields ([#8820](https://github.com/influxframework/influxframework/pull/8820)) +- Added 2FA for LDAP users ([#10001](https://github.com/influxframework/influxframework/pull/10001)) +- Introduced Help Article Feedback system ([#10260](https://github.com/influxframework/influxframework/pull/10260)) +- Introduced Razorpay client ([#11418](https://github.com/influxframework/influxframework/pull/11418)) +- Rate Limiting ([#10310](https://github.com/influxframework/influxframework/pull/10310)) +- Introduced Log Settings ([#11699](https://github.com/influxframework/influxframework/pull/11699)) +- Enhancements in notifications ([#11398](https://github.com/influxframework/influxframework/pull/11398)) ([#11409](https://github.com/influxframework/influxframework/pull/11409)) +- Added a field-level permission check for report data ([12163](https://github.com/influxframework/influxframework/pull/12163)) +- Ability to cancel all linked document with a single click ([#8905](https://github.com/influxframework/influxframework/pull/8905)) +- Made checkboxes navigable via tab key ([#11030](https://github.com/influxframework/influxframework/pull/11030)) +- Renamed "Custom Script" to "Client Script" ([#12324](https://github.com/influxframework/influxframework/pull/12324)) + +
    + +### Performance + +- Faster application load ([#12364](https://github.com/influxframework/influxframework/pull/12364)) ([#10229](https://github.com/influxframework/influxframework/pull/10229)) ([#10147](https://github.com/influxframework/influxframework/pull/10147)) ([#9930](https://github.com/influxframework/influxframework/pull/9930)) +- Theme files will now be compressed to make the website load faster ([#11048](https://github.com/influxframework/influxframework/pull/11048)) +- Confirmation emails will be sent instantly ([#10790](https://github.com/influxframework/influxframework/pull/10790)) +- Faster scheduled job processing ([#9928](https://github.com/influxframework/influxframework/pull/9928)) +- Faster data imports ([#12565](https://github.com/influxframework/influxframework/pull/12565)) +- Faster CLI commands ([#12447](https://github.com/influxframework/influxframework/pull/12447)) diff --git a/influxframework/change_log/v13/v13_1_0.md b/influxframework/change_log/v13/v13_1_0.md new file mode 100644 index 0000000..f2f7ca4 --- /dev/null +++ b/influxframework/change_log/v13/v13_1_0.md @@ -0,0 +1,22 @@ +# Version 13.1.0 Release Notes + +### Features & Enhancements + +- Automated mail notifications will be shown in timeline ([#12693](https://github.com/influxframework/influxframework/pull/12693)) +- Introduced Client Script for List views ([#12590](https://github.com/influxframework/influxframework/pull/12590)) +- Introduced language switcher for guest users on website navbar ([#12813](https://github.com/influxframework/influxframework/pull/12813)) +- Option to give submit permission while sharing a document ([#12799](https://github.com/influxframework/influxframework/pull/12799)) +- Added option to set `autoname` in Customize Form ([#12413](https://github.com/influxframework/influxframework/pull/12413)) +- Virtual DocType ([#12121](https://github.com/influxframework/influxframework/pull/12121)) + +### Fixes + +- Workspace fixes ([#12650](https://github.com/influxframework/influxframework/pull/12650)) ([#12655](https://github.com/influxframework/influxframework/pull/12655)) ([#12869](https://github.com/influxframework/influxframework/pull/12869)) +- Fixed an issue where select options were not getting updated in Grid ([#12839](https://github.com/influxframework/influxframework/pull/12839)) +- Webform Fixes ([#12630](https://github.com/influxframework/influxframework/pull/12630)) ([#12756](https://github.com/influxframework/influxframework/pull/12756)) ([#12819](https://github.com/influxframework/influxframework/pull/12819)) +- Fixed timespan filter for next and last timespans ([#12509](https://github.com/influxframework/influxframework/pull/12509)) +- System Notification fixes ([#12719](https://github.com/influxframework/influxframework/pull/12719)) +- Design Fixes ([#12669](https://github.com/influxframework/influxframework/pull/12669)) ([#12591](https://github.com/influxframework/influxframework/pull/12591)) ([#12557](https://github.com/influxframework/influxframework/pull/12557)) ([#12751](https://github.com/influxframework/influxframework/pull/12751)) ([#12864](https://github.com/influxframework/influxframework/pull/12864)) +- Fixed Multi-column paste in grid ([#12861](https://github.com/influxframework/influxframework/pull/12861)) +- Fixed grid validation ([#12744](https://github.com/influxframework/influxframework/pull/12744)) +- Fixed currency value formatting in dashboard chart ([#12613](https://github.com/influxframework/influxframework/pull/12613)) diff --git a/influxframework/change_log/v13/v13_2_0.md b/influxframework/change_log/v13/v13_2_0.md new file mode 100644 index 0000000..897a807 --- /dev/null +++ b/influxframework/change_log/v13/v13_2_0.md @@ -0,0 +1,32 @@ +# Version 13.2.0 Release Notes + +### Features & Enhancements + +- Add option to mention a group of users ([#12844](https://github.com/influxframework/influxframework/pull/12844)) +- Copy DocType / documents across sites ([#12872](https://github.com/influxframework/influxframework/pull/12872)) +- Scheduler log in notifications ([#1135](https://github.com/influxframework/influxframework/pull/1135)) +- Add Enable/Disable Webhook via Check Field ([#12842](https://github.com/influxframework/influxframework/pull/12842)) +- Allow query/custom reports to save custom data in the json field ([#12534](https://github.com/influxframework/influxframework/pull/12534)) + +### Fixes + +- Load server translations in boot (backport #12848) ([#12852](https://github.com/influxframework/influxframework/pull/12852)) +- Allow to override dashboard chart properties type/color ([#12846](https://github.com/influxframework/influxframework/pull/12846)) +- Multi-column paste in grid ([#12861](https://github.com/influxframework/influxframework/pull/12861)) +- Add log_error and InfluxFrameworkClient to restricted python ([#12857](https://github.com/influxframework/influxframework/pull/12857)) +- Redirect Web Form user directly to success URL, if no amount is due ([#12661](https://github.com/influxframework/influxframework/pull/12661)) +- Attachment pill lock icon redirects to File ([#12864](https://github.com/influxframework/influxframework/pull/12864)) +- Redirect Web Form user directly to success URL, if no amount is due (backport #12661) ([#12856](https://github.com/influxframework/influxframework/pull/12856)) +- Remove events to redraw charts ([#12973](https://github.com/influxframework/influxframework/pull/12973)) +- Don't allow user to remove/change data source file in data import ([#12827](https://github.com/influxframework/influxframework/pull/12827)) +- Load server translations in boot ([#12848](https://github.com/influxframework/influxframework/pull/12848)) +- Newly created Workspace not being accessible unless a shortcut u… ([#12866](https://github.com/influxframework/influxframework/pull/12866)) +- Currency labels in grids ([#12974](https://github.com/influxframework/influxframework/pull/12974)) +- Handle error while session start ([#12933](https://github.com/influxframework/influxframework/pull/12933)) +- Add field type check in custom field validation ([#12858](https://github.com/influxframework/influxframework/pull/12858)) +- Make language select optional and fix breakpoint issues ([#12860](https://github.com/influxframework/influxframework/pull/12860)) +- Form Dashboard reference link ([#12945](https://github.com/influxframework/influxframework/pull/12945)) +- Invalid HTML generated by the base template ([#12953](https://github.com/influxframework/influxframework/pull/12953)) +- Default values were not triggering change event ([#12975](https://github.com/influxframework/influxframework/pull/12975)) +- Make strings translatable ([#12877](https://github.com/influxframework/influxframework/pull/12877)) +- Added build-message-files command ([#12950](https://github.com/influxframework/influxframework/pull/12950)) diff --git a/influxframework/change_log/v13/v13_3_0.md b/influxframework/change_log/v13/v13_3_0.md new file mode 100644 index 0000000..785bf78 --- /dev/null +++ b/influxframework/change_log/v13/v13_3_0.md @@ -0,0 +1,49 @@ +# Version 13.3.0 Release Notes + +### Features & Enhancements + +- Deletion Steps in Data Deletion Tool ([#13124](https://github.com/influxframework/influxframework/pull/13124)) +- Format Option for list-apps in bench CLI ([#13125](https://github.com/influxframework/influxframework/pull/13125)) +- Add password fieldtype option for Web Form ([#13093](https://github.com/influxframework/influxframework/pull/13093)) +- Add simple __repr__ for DocTypes ([#13151](https://github.com/influxframework/influxframework/pull/13151)) +- Switch theme with left/right keys ([#13077](https://github.com/influxframework/influxframework/pull/13077)) +- sourceURL for injected javascript ([#13022](https://github.com/influxframework/influxframework/pull/13022)) + +### Fixes + +- Decode uri before importing file via weblink ([#13026](https://github.com/influxframework/influxframework/pull/13026)) +- Respond to /api requests as JSON by default ([#13053](https://github.com/influxframework/influxframework/pull/13053)) +- Disabled checkbox should be disabled ([#13021](https://github.com/influxframework/influxframework/pull/13021)) +- Moving Site folder across different FileSystems failed ([#13038](https://github.com/influxframework/influxframework/pull/13038)) +- Freeze screen till the background request is complete ([#13078](https://github.com/influxframework/influxframework/pull/13078)) +- Added conditional rendering for content field in split section w… ([#13075](https://github.com/influxframework/influxframework/pull/13075)) +- Show delete button on portal if user has permission to delete document ([#13149](https://github.com/influxframework/influxframework/pull/13149)) +- Dont disable dialog scroll on focusing a Link/Autocomplete field ([#13119](https://github.com/influxframework/influxframework/pull/13119)) +- Typo in RecorderDetail.vue ([#13086](https://github.com/influxframework/influxframework/pull/13086)) +- Error for bench drop-site. Added missing import. ([#13064](https://github.com/influxframework/influxframework/pull/13064)) +- Report column context ([#13090](https://github.com/influxframework/influxframework/pull/13090)) +- Different service name for push and pull request events ([#13094](https://github.com/influxframework/influxframework/pull/13094)) +- Moving Site folder across different FileSystems failed ([#13033](https://github.com/influxframework/influxframework/pull/13033)) +- Consistent checkboxes on all browsers ([#13042](https://github.com/influxframework/influxframework/pull/13042)) +- Changed shorcut widgets color picker to dropdown ([#13073](https://github.com/influxframework/influxframework/pull/13073)) +- Error while exporting reports with duration field ([#13118](https://github.com/influxframework/influxframework/pull/13118)) +- Add margin to download backup card ([#13079](https://github.com/influxframework/influxframework/pull/13079)) +- Move mention list generation logic to server-side ([#13074](https://github.com/influxframework/influxframework/pull/13074)) +- Make strings translatable ([#13046](https://github.com/influxframework/influxframework/pull/13046)) +- Don't evaluate dynamic properties to check if conflicts exist ([#13186](https://github.com/influxframework/influxframework/pull/13186)) +- Add __ function in vue global for translation in recorder ([#13089](https://github.com/influxframework/influxframework/pull/13089)) +- Make strings translatable ([#13076](https://github.com/influxframework/influxframework/pull/13076)) +- Show config in bench CLI ([#13128](https://github.com/influxframework/influxframework/pull/13128)) +- Add breadcrumbs for list view ([#13091](https://github.com/influxframework/influxframework/pull/13091)) +- Do not skip data in save while using shortcut ([#13182](https://github.com/influxframework/influxframework/pull/13182)) +- Use docfields from options if no docfields are returned from meta ([#13188](https://github.com/influxframework/influxframework/pull/13188)) +- Disable reloading files in `__pycache__` directory ([#13109](https://github.com/influxframework/influxframework/pull/13109)) +- RTL stylesheet route to load RTL style on demand. ([#13007](https://github.com/influxframework/influxframework/pull/13007)) +- Do not show messsage when exception is handled ([#13111](https://github.com/influxframework/influxframework/pull/13111)) +- Replace parseFloat by Number ([#13082](https://github.com/influxframework/influxframework/pull/13082)) +- Add margin to download backup card ([#13050](https://github.com/influxframework/influxframework/pull/13050)) +- Translate report column labels ([#13083](https://github.com/influxframework/influxframework/pull/13083)) +- Grid row color picker field not working ([#13040](https://github.com/influxframework/influxframework/pull/13040)) +- Improve oauthlib implementation ([#13045](https://github.com/influxframework/influxframework/pull/13045)) +- Replace filter_by like with full text filter ([#13126](https://github.com/influxframework/influxframework/pull/13126)) +- Focus jumps to first field ([#13067](https://github.com/influxframework/influxframework/pull/13067)) diff --git a/influxframework/change_log/v14/v14_0_0.md b/influxframework/change_log/v14/v14_0_0.md new file mode 100644 index 0000000..13c5488 --- /dev/null +++ b/influxframework/change_log/v14/v14_0_0.md @@ -0,0 +1,29 @@ +# Version 14.0.0 Release Notes + +## Version 14 Release Notes + +### Highlights + +- [Tab Break control to organize forms](https://github.com/influxframework/influxframework/pull/13036) +- [New print format builder with simpler drag-n-drop functionality](https://github.com/influxframework/influxframework/pull/14134) +- [Highly customizable workspace with handy new blocks](https://github.com/influxframework/influxframework/pull/13152) + +### Other Features & Enhancements + +- [Add quick lists and other accessible blocks to your workspace](https://github.com/influxframework/influxframework/pull/13152) +- [Stricter validation on phone number using Phone control](https://github.com/influxframework/influxframework/pull/15538) +- [Rating on blogs and configuration option for feedback limits](https://github.com/influxframework/influxframework/pull/14614) +- [New Feedback & Comment Design for Blog Post](https://github.com/influxframework/influxframework/pull/14614) +- [Multistep Web Form](https://github.com/influxframework/influxframework/pull/14978) +- [Configurable splash screen](https://github.com/influxframework/influxframework/pull/17006) +- [Image cropping and optimization](https://github.com/influxframework/influxframework/pull/13835) + +### Performance + +- 50% faster authentication ([#16950](https://github.com/influxframework/influxframework/pull/16950)), ([#17253](https://github.com/influxframework/influxframework/pull/17253)) +- Send newsletter bulk emails ~50x faster ([#17461](https://github.com/influxframework/influxframework/pull/17461)) +- Improved caching ([#16448](https://github.com/influxframework/influxframework/pull/16448)), ([#17107](https://github.com/influxframework/influxframework/pull/17107)) +- Faster processing ([#16949](https://github.com/influxframework/influxframework/pull/16949)), ([#16549](https://github.com/influxframework/influxframework/pull/16549)) +- 90% less memory consumption ([#17061](https://github.com/influxframework/influxframework/pull/17061)) + +> Check out [the complete list](https://github.com/influxframework/influxframework/issues/17532) of enhancements and features in version 14 diff --git a/influxframework/change_log/v5/v5_0_18.md b/influxframework/change_log/v5/v5_0_18.md new file mode 100644 index 0000000..95e07f3 --- /dev/null +++ b/influxframework/change_log/v5/v5_0_18.md @@ -0,0 +1,6 @@ +#### Updates to Web Forms + +- Web Forms list now is a standard portal list and includes paging and other extensions +- Section, Column Breaks in Web Forms +- Consistent User Interface +- Cleanup of Portal Pages diff --git a/influxframework/change_log/v5/v5_0_20.md b/influxframework/change_log/v5/v5_0_20.md new file mode 100644 index 0000000..18e9f43 --- /dev/null +++ b/influxframework/change_log/v5/v5_0_20.md @@ -0,0 +1 @@ +- Ability to send yourself a copy of the outgoing email added back. diff --git a/influxframework/change_log/v5/v5_0_32.md b/influxframework/change_log/v5/v5_0_32.md new file mode 100644 index 0000000..17a7cb3 --- /dev/null +++ b/influxframework/change_log/v5/v5_0_32.md @@ -0,0 +1,5 @@ +- Reports are now searchable from awesome bar +- Show currect label for title in list views +- Datepicker now sets default value as Today +- Map child table as per meta, if not mentioned in table_map via mapper +- Re-enable save button on error \ No newline at end of file diff --git a/influxframework/change_log/v5/v5_1_0.md b/influxframework/change_log/v5/v5_1_0.md new file mode 100644 index 0000000..3036acc --- /dev/null +++ b/influxframework/change_log/v5/v5_1_0.md @@ -0,0 +1,3 @@ +- Change print font from Setup > Print Settings or set it for each Print Format. Font options are "Default", "Arial", "Helvetica", "Verdana", "Monospace". +- Print and full-page print preview in user's language +- Fixed inconsistent visibility of a logged-in user's image in website diff --git a/influxframework/change_log/v5/v5_1_1.md b/influxframework/change_log/v5/v5_1_1.md new file mode 100644 index 0000000..befa5be --- /dev/null +++ b/influxframework/change_log/v5/v5_1_1.md @@ -0,0 +1 @@ +- Ability to **Share with Everyone** (except Guest) using **Share With** diff --git a/influxframework/change_log/v5/v5_3_0.md b/influxframework/change_log/v5/v5_3_0.md new file mode 100644 index 0000000..e547427 --- /dev/null +++ b/influxframework/change_log/v5/v5_3_0.md @@ -0,0 +1,40 @@ +- Added Language Support for Following languages + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    bo*ལྷ་སའི་སྐད་
    fisuomalainen
    kmភាសាខ្មែរ
    mkмакедонски
    myMelayu
    nonorsk
    svSvenska
    sqshqiptar
    + +* Unable to find translations for Tibetian via Google + +- To contribute to translations, please login to [https://translate.influxerp.com](https://translate.influxerp.com) diff --git a/influxframework/change_log/v5/v5_4_0.md b/influxframework/change_log/v5/v5_4_0.md new file mode 100644 index 0000000..5bf04bb --- /dev/null +++ b/influxframework/change_log/v5/v5_4_0.md @@ -0,0 +1 @@ +- Moved Backup Manager and Social Login Keys to the new **Integrations** module diff --git a/influxframework/change_log/v6/v6_0_0.md b/influxframework/change_log/v6/v6_0_0.md new file mode 100644 index 0000000..6f9c8fc --- /dev/null +++ b/influxframework/change_log/v6/v6_0_0.md @@ -0,0 +1,3 @@ +- **Realtime updates** for new comments and list view +- Get warned if someone else modified the document that you are working on +- You can now quickly assign a document to yourself by clicking on "Assign to me" diff --git a/influxframework/change_log/v6/v6_0_8.md b/influxframework/change_log/v6/v6_0_8.md new file mode 100644 index 0000000..6a2fba5 --- /dev/null +++ b/influxframework/change_log/v6/v6_0_8.md @@ -0,0 +1 @@ +- Set HTML in Website Settings. This is usually used for website verification and SEO. diff --git a/influxframework/change_log/v6/v6_12_0.md b/influxframework/change_log/v6/v6_12_0.md new file mode 100644 index 0000000..abcdd16 --- /dev/null +++ b/influxframework/change_log/v6/v6_12_0.md @@ -0,0 +1 @@ +- Extract emails using IMAP. Contributed by Gangadhar Kadam ([New Indictrans](http://indictranstech.com/)) diff --git a/influxframework/change_log/v6/v6_13_0.md b/influxframework/change_log/v6/v6_13_0.md new file mode 100644 index 0000000..661e04f --- /dev/null +++ b/influxframework/change_log/v6/v6_13_0.md @@ -0,0 +1,4 @@ +- Attachments can now be marked as **Private** + - Private files cannot be accessed unless you are logged in + - To access a private file, you need to have read permission on the file or read permission on the document to which the file is attached + - All attachments in a new incoming email are private diff --git a/influxframework/change_log/v6/v6_14_1.md b/influxframework/change_log/v6/v6_14_1.md new file mode 100644 index 0000000..4aed877 --- /dev/null +++ b/influxframework/change_log/v6/v6_14_1.md @@ -0,0 +1 @@ +- Added language support for Malayalam: **ml - മലയാളം** diff --git a/influxframework/change_log/v6/v6_15_0.md b/influxframework/change_log/v6/v6_15_0.md new file mode 100644 index 0000000..35efb41 --- /dev/null +++ b/influxframework/change_log/v6/v6_15_0.md @@ -0,0 +1,4 @@ +- **For Developers:** Automatic logging of request errors and its context in **Error Snapshot** + - Thank you [Maxwell Morais](https://discuss.influxerp.com/users/max_morais_dmm/activity) for this useful feature + - You can access it from *Developer > Logs > Error Snapshot* +- Added language support for [Gujarati](https://translate.influxerp.com/view?lang=gu): **gu - ગુજરાતી** diff --git a/influxframework/change_log/v6/v6_16_1.md b/influxframework/change_log/v6/v6_16_1.md new file mode 100644 index 0000000..b638724 --- /dev/null +++ b/influxframework/change_log/v6/v6_16_1.md @@ -0,0 +1 @@ +- Mention users in comments using `@username`. Mentioned users will receive an email with the comment. \ No newline at end of file diff --git a/influxframework/change_log/v6/v6_16_4.md b/influxframework/change_log/v6/v6_16_4.md new file mode 100644 index 0000000..63ae316 --- /dev/null +++ b/influxframework/change_log/v6/v6_16_4.md @@ -0,0 +1 @@ +- Increased uploaded file size limit upto 10MB \ No newline at end of file diff --git a/influxframework/change_log/v6/v6_17_0.md b/influxframework/change_log/v6/v6_17_0.md new file mode 100644 index 0000000..f660660 --- /dev/null +++ b/influxframework/change_log/v6/v6_17_0.md @@ -0,0 +1,4 @@ +- Ability to **Like** a document, comment or communication + - See notifications about likes that you received + - View it on Activity feed + - *Stars* have been converted to Likes diff --git a/influxframework/change_log/v6/v6_1_0.md b/influxframework/change_log/v6/v6_1_0.md new file mode 100644 index 0000000..cc628ba --- /dev/null +++ b/influxframework/change_log/v6/v6_1_0.md @@ -0,0 +1,7 @@ +- Sections can now be set as **Collapsible**. +- Collapsible sections can be shown as collapsed based on certain rules defined in the **Collapsible Depends On** property of the document field (DocField). +- Title is now editable from the form if the `fieldname` of the title field is **title**. +- Document can now be renamed by clicking the page heading. +- Fields can be set as **Bold** so that they can be easily identified in long forms. +- Fixed mobile views +- See Data Import progress in realtime diff --git a/influxframework/change_log/v6/v6_20_0.md b/influxframework/change_log/v6/v6_20_0.md new file mode 100644 index 0000000..4ca967a --- /dev/null +++ b/influxframework/change_log/v6/v6_20_0.md @@ -0,0 +1,5 @@ +- Fixed **Export** for large reports +- Added language support for: + - [Estonian](https://translate.influxerp.com/view?lang=et): **et - eesti** + - [Telugu](https://translate.influxerp.com/view?lang=te): **te - తెలుగు** + - [Urdu](https://translate.influxerp.com/view?lang=ur): **ur - اردو** diff --git a/influxframework/change_log/v6/v6_21_0.md b/influxframework/change_log/v6/v6_21_0.md new file mode 100644 index 0000000..48e0ef8 --- /dev/null +++ b/influxframework/change_log/v6/v6_21_0.md @@ -0,0 +1 @@ +- Repeating Letter Head and Footer in PDF \ No newline at end of file diff --git a/influxframework/change_log/v6/v6_22_0.md b/influxframework/change_log/v6/v6_22_0.md new file mode 100644 index 0000000..a74cbc1 --- /dev/null +++ b/influxframework/change_log/v6/v6_22_0.md @@ -0,0 +1,2 @@ +- **Comment**, **Feed** and **Communication** are merged into one table, **Communication** +- Ability to turn-off repeating headers and footers in PDF via **Print Settings** diff --git a/influxframework/change_log/v6/v6_23_0.md b/influxframework/change_log/v6/v6_23_0.md new file mode 100644 index 0000000..8566425 --- /dev/null +++ b/influxframework/change_log/v6/v6_23_0.md @@ -0,0 +1,2 @@ +- Autosuggest email address in **CC** when composing emails - contributed by [Robert Schouten](https://github.com/robertschouten) +- Fix: Attach signature with Email Account's auto-reply diff --git a/influxframework/change_log/v6/v6_25_0.md b/influxframework/change_log/v6/v6_25_0.md new file mode 100644 index 0000000..99f694b --- /dev/null +++ b/influxframework/change_log/v6/v6_25_0.md @@ -0,0 +1,4 @@ +- **Custom Translations** via Setup > Customize > Custom Translations +- Print multiple documents from list view + - Check documents and click on Menu > Print +- PDF printing for Query Reports diff --git a/influxframework/change_log/v6/v6_26_0.md b/influxframework/change_log/v6/v6_26_0.md new file mode 100644 index 0000000..8da70c3 --- /dev/null +++ b/influxframework/change_log/v6/v6_26_0.md @@ -0,0 +1,2 @@ +- **Don't allow user to move standard fields.** You can still move any Custom Field to a desired position. +- Landscape orientation for Report PDF diff --git a/influxframework/change_log/v6/v6_26_6.md b/influxframework/change_log/v6/v6_26_6.md new file mode 100644 index 0000000..98bdf8e --- /dev/null +++ b/influxframework/change_log/v6/v6_26_6.md @@ -0,0 +1 @@ +- Check permissions on printing or making pdf of report \ No newline at end of file diff --git a/influxframework/change_log/v6/v6_27_1.md b/influxframework/change_log/v6/v6_27_1.md new file mode 100644 index 0000000..457ac22 --- /dev/null +++ b/influxframework/change_log/v6/v6_27_1.md @@ -0,0 +1,6 @@ +- Configurable Desktop + - Add any Documents, Reports, Modules, Pages to the desktop + - Remove all the unwanted icons +- **Module Page New Design** + - New module design now shows all documents in a module together + - [Read the Details](https://influxframework.io/blog/influxerp-features/configurable-desktop) diff --git a/influxframework/change_log/v6/v6_27_11.md b/influxframework/change_log/v6/v6_27_11.md new file mode 100644 index 0000000..17323a2 --- /dev/null +++ b/influxframework/change_log/v6/v6_27_11.md @@ -0,0 +1,2 @@ +- Get [email sending status](https://discuss.influxerp.com/t/communication-delivery-status-bulk-email-status/11941) in document timeline +- Ability to disable a Role diff --git a/influxframework/change_log/v6/v6_2_0.md b/influxframework/change_log/v6/v6_2_0.md new file mode 100644 index 0000000..0525324 --- /dev/null +++ b/influxframework/change_log/v6/v6_2_0.md @@ -0,0 +1,3 @@ +- **Permissions:** + - If User Permissions are missing for a DocType, don't show non-matching records. + - If **Ignore User Permissions If Missing** is checked in System Settings, show records even if User Permissions are not defined. diff --git a/influxframework/change_log/v6/v6_3_0.md b/influxframework/change_log/v6/v6_3_0.md new file mode 100644 index 0000000..f605c46 --- /dev/null +++ b/influxframework/change_log/v6/v6_3_0.md @@ -0,0 +1,2 @@ +- You can now add **CC** in Email +- Show checkboxes in Print diff --git a/influxframework/change_log/v6/v6_4_0.md b/influxframework/change_log/v6/v6_4_0.md new file mode 100644 index 0000000..ae56802 --- /dev/null +++ b/influxframework/change_log/v6/v6_4_0.md @@ -0,0 +1 @@ +- **File Manager:** A Document Management System for your organisation. Add files, organize them in folders and share it with a few users or everyone in the company. diff --git a/influxframework/change_log/v6/v6_4_8.md b/influxframework/change_log/v6/v6_4_8.md new file mode 100644 index 0000000..afc5e45 --- /dev/null +++ b/influxframework/change_log/v6/v6_4_8.md @@ -0,0 +1,20 @@ +- Added Language Support for Following languages + + + + + + + + + + + + + + + + + + +
    bnবাংলা
    da-DKdansk (Danmark)
    es-PEEspañol (Perú)
    sislovenščina
    diff --git a/influxframework/change_log/v6/v6_5_0.md b/influxframework/change_log/v6/v6_5_0.md new file mode 100644 index 0000000..62afbd3 --- /dev/null +++ b/influxframework/change_log/v6/v6_5_0.md @@ -0,0 +1,2 @@ +- **Linked With** will now show links from Dynamic Links +- **Data** field-type size truncated to 140 characters from 255 (by default). Can be changed by setting the **length** property from **Customize Form View** diff --git a/influxframework/change_log/v6/v6_6_0.md b/influxframework/change_log/v6/v6_6_0.md new file mode 100644 index 0000000..6655897 --- /dev/null +++ b/influxframework/change_log/v6/v6_6_0.md @@ -0,0 +1 @@ +- Added language support for Ukranian: **uk - українська** diff --git a/influxframework/change_log/v6/v6_7_0.md b/influxframework/change_log/v6/v6_7_0.md new file mode 100644 index 0000000..ffb546d --- /dev/null +++ b/influxframework/change_log/v6/v6_7_0.md @@ -0,0 +1,3 @@ +- See who is currently viewing the document +- Sounds for various actions +- Added language support for Slovene: **sl - slovenščina (Slovene)** diff --git a/influxframework/change_log/v6/v6_8_0.md b/influxframework/change_log/v6/v6_8_0.md new file mode 100644 index 0000000..012b406 --- /dev/null +++ b/influxframework/change_log/v6/v6_8_0.md @@ -0,0 +1,2 @@ +- Pre-configured Email Account for Yandex.Mail +- Fixed inline images in received emails diff --git a/influxframework/change_log/v7/v7_0_0.md b/influxframework/change_log/v7/v7_0_0.md new file mode 100644 index 0000000..6f991d3 --- /dev/null +++ b/influxframework/change_log/v7/v7_0_0.md @@ -0,0 +1,36 @@ +#### UI +- Editable Grids +- Image Field in DocType, form and list +- Dashboard, Heatmap, Graphs on Form View +- Document Flow in forms +- List views: remembers user settings + +#### Celery to RQ + +#### Quick Entry + +#### Razorpay Integration + +#### Passwords + +#### Portals +- Statics (`www` folder): directly served from templates +- New Routing +- Web Forms + +#### Limits +- Expiry, space etc +- Usage Info Page + +#### Minor +- **Rename:** Bulk Email is now Email Queue +- `influxframework.require` is async +- `flot.js` replaced by `c3.js` +- Most popular links on the top +- Standard Replies configurable +- Timeline permisions (not based on user permissions) +- "Track Seen" feature in doctypes +- Moved: "Edit Profile" page is now in influxframework (moved from InfluxERP) +- Cleanup UI for chat +- New default user icons (based on initials) +- Multiple assign (add a document to multiple users) diff --git a/influxframework/change_log/v7/v7_0_18.md b/influxframework/change_log/v7/v7_0_18.md new file mode 100644 index 0000000..5e59c02 --- /dev/null +++ b/influxframework/change_log/v7/v7_0_18.md @@ -0,0 +1,2 @@ +- New Feature: Ability to add multiple sessions to users. Edit **User** record and edit "Simultaneous Sessions" +- New Feature: Select columns to export and import in **Data Import Tool** \ No newline at end of file diff --git a/influxframework/change_log/v7/v7_1_0.md b/influxframework/change_log/v7/v7_1_0.md new file mode 100644 index 0000000..7d2187a --- /dev/null +++ b/influxframework/change_log/v7/v7_1_0.md @@ -0,0 +1,24 @@ +#### Gantt View +- New Gantt view for documents where date range is available + +#### In-App Help +- Search for help from within the app. Click on "Help" + +#### Web Form +- Add grids (child tables) +- Add page breaks (for long forms) +- Add payment gateway +- Add attachments + +#### Auto Email Report +- Email reports automatically on daily / weekly / monthly basis + +#### Other Fixes +- Send a popup to all users on login for a new Note by checking on "Notify users with a popup when they log in" +- Portal Users (Customers, Supplier, Students) can now have roles +- Sidebar in portal view will be rendered as per roles and can be configured from Portal Settings +- Restrict the number of backups to be saved in System Settings +- Scheduler log is now error log and as MyISAM +- A better way to export customzations and Email Alert directly from Customize Form +- Option to send email from Data Import Tool where applicable +- Integration Broker \ No newline at end of file diff --git a/influxframework/change_log/v7/v7_2_0.md b/influxframework/change_log/v7/v7_2_0.md new file mode 100644 index 0000000..dfd8250 --- /dev/null +++ b/influxframework/change_log/v7/v7_2_0.md @@ -0,0 +1,18 @@ +- Filters Dashboard + - Dashboard with pre-defined filters in List/Report View +- Tag Category + - Show/Group tags based on category +- Updated Font Awesome version to 4.x.x +- Checkboxes in grid + - Delete selected rows + - Map selected rows from one document to another. +- Show Totals button in report +- OpenID Connect for InfluxFramework +- Threading based on message id in Email Queue +- New control object daterangepicker for filtering +- Orientation selection in PDF +- Expand/Collapse All buttons in tree view reports +- Add attachment from email and copy attachments to Communication Record +- Bulk Upload from zip file +- Tree view decoration +- Custom menu for report view diff --git a/influxframework/change_log/v8/v8_0_0.md b/influxframework/change_log/v8/v8_0_0.md new file mode 100644 index 0000000..4cb1fe7 --- /dev/null +++ b/influxframework/change_log/v8/v8_0_0.md @@ -0,0 +1,41 @@ +#### Global Search +- Now, from awesome bar, you can search all the documents related to a specific keyword. +- For example, you can get all Quotations, Sales Orders and Sales Invoices related to a Customer, by searching with customer's email address / mobile no. + +#### Kanban View +- Kanban Board allows you to identify a field based on which documents will be categorised and viewed together. +- From the board itself, you can update the status of teh document, comment on it or assign it someone. + +#### Document Versioning +- Now the system maintains all the changes of a document with the information of user and timestamp. + +#### Delete and Restore +- In version 8, all the deleted documents are stored in the "Deleted Documents" table, which can be restored later +- To permanently delete a record, you need to delete it from "Deleted Documents". + +#### Email Inbox +- We have introduced an Inbox View for all the communications. +- It has all the basic functionalities of an Email Client like Inbox, Sent Mails, Trash and Spam folders, Contact list, Read/Unread, forwarding of an email etc. +- Issue, Lead and Opportunity can be created directly from the Communication. + +#### Custom Permissions +- Now, all the customised permissions are stored separately in a Custom DocPerm table. + +#### Permissions for Reports and Page +- Report and Page have it's own dedicated permission table now, it's no more dependent on the reference Doctype for permissions. +- There is a page "Role Permission for Page and Report", from where you will be able to customize the permissions for reports and pages. + +#### Newsletter Enhancements +- In the new version, newsletter can be sent to multiple email groups. +- You will also be able to send the attachments with the newsletter. +- Unsubscribe link is optional now + +#### New Calendar and Date Picker +- Get a more crisp view of calendar and events. +- Date picker also has been updated in the version. + +#### Summernote Text Editor +- We have also integrated Summernote text editor in the InfluxERP. It has many tools to create better texts. + +#### Export report in Excel format +- Reports can now also be exported in Excel format. diff --git a/influxframework/change_log/v8/v8_7_0.md b/influxframework/change_log/v8/v8_7_0.md new file mode 100644 index 0000000..883bdb8 --- /dev/null +++ b/influxframework/change_log/v8/v8_7_0.md @@ -0,0 +1,2 @@ +### User Permissions +- User Permission is now a DocType, a new UX for the existing Role and User Permission managers to make it easy to enter permissions. For more details please check User Permissions diff --git a/influxframework/change_log/v8/v8_8_0.md b/influxframework/change_log/v8/v8_8_0.md new file mode 100644 index 0000000..e93c0ad --- /dev/null +++ b/influxframework/change_log/v8/v8_8_0.md @@ -0,0 +1,2 @@ +### Two Factor Authentication +- Now you can authenticate user with two factor authentication. You can enable the Two Factor Authentication from System Settings. \ No newline at end of file diff --git a/influxframework/client.py b/influxframework/client.py new file mode 100644 index 0000000..f644514 --- /dev/null +++ b/influxframework/client.py @@ -0,0 +1,485 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import json +import os +from typing import TYPE_CHECKING + +import influxframework +import influxframework.model +import influxframework.utils +from influxframework import _ +from influxframework.desk.reportview import validate_args +from influxframework.model.db_query import check_parent_permission +from influxframework.utils import get_safe_filters + +if TYPE_CHECKING: + from influxframework.model.document import Document + +""" +Handle RESTful requests that are mapped to the `/api/resource` route. + +Requests via InfluxFrameworkClient are also handled here. +""" + + +@influxframework.whitelist() +def get_list( + doctype, + fields=None, + filters=None, + order_by=None, + limit_start=None, + limit_page_length=20, + parent=None, + debug=False, + as_dict=True, + or_filters=None, +): + """Returns a list of records by filters, fields, ordering and limit + + :param doctype: DocType of the data to be queried + :param fields: fields to be returned. Default is `name` + :param filters: filter list by this dict + :param order_by: Order by this fieldname + :param limit_start: Start at this index + :param limit_page_length: Number of records to be returned (default 20)""" + if influxframework.is_table(doctype): + check_parent_permission(parent, doctype) + + args = influxframework._dict( + doctype=doctype, + parent_doctype=parent, + fields=fields, + filters=filters, + or_filters=or_filters, + order_by=order_by, + limit_start=limit_start, + limit_page_length=limit_page_length, + debug=debug, + as_list=not as_dict, + ) + + validate_args(args) + return influxframework.get_list(**args) + + +@influxframework.whitelist() +def get_count(doctype, filters=None, debug=False, cache=False): + return influxframework.db.count(doctype, get_safe_filters(filters), debug, cache) + + +@influxframework.whitelist() +def get(doctype, name=None, filters=None, parent=None): + """Returns a document by name or filters + + :param doctype: DocType of the document to be returned + :param name: return document of this `name` + :param filters: If name is not set, filter by these values and return the first match""" + if influxframework.is_table(doctype): + check_parent_permission(parent, doctype) + + if name: + doc = influxframework.get_doc(doctype, name) + elif filters or filters == {}: + doc = influxframework.get_doc(doctype, influxframework.parse_json(filters)) + else: + doc = influxframework.get_doc(doctype) # single + + doc.check_permission() + return doc.as_dict() + + +@influxframework.whitelist() +def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, parent=None): + """Returns a value form a document + + :param doctype: DocType to be queried + :param fieldname: Field to be returned (default `name`) + :param filters: dict or string for identifying the record""" + if influxframework.is_table(doctype): + check_parent_permission(parent, doctype) + + if not influxframework.has_permission(doctype, parent_doctype=parent): + influxframework.throw(_("No permission for {0}").format(_(doctype)), influxframework.PermissionError) + + filters = get_safe_filters(filters) + if isinstance(filters, str): + filters = {"name": filters} + + try: + fields = influxframework.parse_json(fieldname) + except (TypeError, ValueError): + # name passed, not json + fields = [fieldname] + + # check whether the used filters were really parseable and usable + # and did not just result in an empty string or dict + if not filters: + filters = None + + if influxframework.get_meta(doctype).issingle: + value = influxframework.db.get_values_from_single(fields, filters, doctype, as_dict=as_dict, debug=debug) + else: + value = get_list( + doctype, + filters=filters, + fields=fields, + debug=debug, + limit_page_length=1, + parent=parent, + as_dict=as_dict, + ) + + if as_dict: + return value[0] if value else {} + + if not value: + return + + return value[0] if len(fields) > 1 else value[0][0] + + +@influxframework.whitelist() +def get_single_value(doctype, field): + if not influxframework.has_permission(doctype): + influxframework.throw(_("No permission for {0}").format(_(doctype)), influxframework.PermissionError) + + return influxframework.db.get_single_value(doctype, field) + + +@influxframework.whitelist(methods=["POST", "PUT"]) +def set_value(doctype, name, fieldname, value=None): + """Set a value using get_doc, group of values + + :param doctype: DocType of the document + :param name: name of the document + :param fieldname: fieldname string or JSON / dict with key value pair + :param value: value if fieldname is JSON / dict""" + + if fieldname in (influxframework.model.default_fields + influxframework.model.child_table_fields): + influxframework.throw(_("Cannot edit standard fields")) + + if not value: + values = fieldname + if isinstance(fieldname, str): + try: + values = json.loads(fieldname) + except ValueError: + values = {fieldname: ""} + else: + values = {fieldname: value} + + # check for child table doctype + if not influxframework.get_meta(doctype).istable: + doc = influxframework.get_doc(doctype, name) + doc.update(values) + else: + doc = influxframework.db.get_value(doctype, name, ["parenttype", "parent"], as_dict=True) + doc = influxframework.get_doc(doc.parenttype, doc.parent) + child = doc.getone({"doctype": doctype, "name": name}) + child.update(values) + + doc.save() + + return doc.as_dict() + + +@influxframework.whitelist(methods=["POST", "PUT"]) +def insert(doc=None): + """Insert a document + + :param doc: JSON or dict object to be inserted""" + if isinstance(doc, str): + doc = json.loads(doc) + + return insert_doc(doc).as_dict() + + +@influxframework.whitelist(methods=["POST", "PUT"]) +def insert_many(docs=None): + """Insert multiple documents + + :param docs: JSON or list of dict objects to be inserted in one request""" + if isinstance(docs, str): + docs = json.loads(docs) + + if len(docs) > 200: + influxframework.throw(_("Only 200 inserts allowed in one request")) + + out = set() + for doc in docs: + out.add(insert_doc(doc).name) + + return out + + +@influxframework.whitelist(methods=["POST", "PUT"]) +def save(doc): + """Update (save) an existing document + + :param doc: JSON or dict object with the properties of the document to be updated""" + if isinstance(doc, str): + doc = json.loads(doc) + + doc = influxframework.get_doc(doc) + doc.save() + + return doc.as_dict() + + +@influxframework.whitelist(methods=["POST", "PUT"]) +def rename_doc(doctype, old_name, new_name, merge=False): + """Rename document + + :param doctype: DocType of the document to be renamed + :param old_name: Current `name` of the document to be renamed + :param new_name: New `name` to be set""" + new_name = influxframework.rename_doc(doctype, old_name, new_name, merge=merge) + return new_name + + +@influxframework.whitelist(methods=["POST", "PUT"]) +def submit(doc): + """Submit a document + + :param doc: JSON or dict object to be submitted remotely""" + if isinstance(doc, str): + doc = json.loads(doc) + + doc = influxframework.get_doc(doc) + doc.submit() + + return doc.as_dict() + + +@influxframework.whitelist(methods=["POST", "PUT"]) +def cancel(doctype, name): + """Cancel a document + + :param doctype: DocType of the document to be cancelled + :param name: name of the document to be cancelled""" + wrapper = influxframework.get_doc(doctype, name) + wrapper.cancel() + + return wrapper.as_dict() + + +@influxframework.whitelist(methods=["DELETE", "POST"]) +def delete(doctype, name): + """Delete a remote document + + :param doctype: DocType of the document to be deleted + :param name: name of the document to be deleted""" + delete_doc(doctype, name) + + +@influxframework.whitelist(methods=["POST", "PUT"]) +def bulk_update(docs): + """Bulk update documents + + :param docs: JSON list of documents to be updated remotely. Each document must have `docname` property""" + docs = json.loads(docs) + failed_docs = [] + for doc in docs: + doc.pop("flags", None) + try: + existing_doc = influxframework.get_doc(doc["doctype"], doc["docname"]) + existing_doc.update(doc) + existing_doc.save() + except Exception: + failed_docs.append({"doc": doc, "exc": influxframework.utils.get_traceback()}) + + return {"failed_docs": failed_docs} + + +@influxframework.whitelist() +def has_permission(doctype, docname, perm_type="read"): + """Returns a JSON with data whether the document has the requested permission + + :param doctype: DocType of the document to be checked + :param docname: `name` of the document to be checked + :param perm_type: one of `read`, `write`, `create`, `submit`, `cancel`, `report`. Default is `read`""" + # perm_type can be one of read, write, create, submit, cancel, report + return {"has_permission": influxframework.has_permission(doctype, perm_type.lower(), docname)} + + +@influxframework.whitelist() +def get_password(doctype, name, fieldname): + """Return a password type property. Only applicable for System Managers + + :param doctype: DocType of the document that holds the password + :param name: `name` of the document that holds the password + :param fieldname: `fieldname` of the password property + """ + influxframework.only_for("System Manager") + return influxframework.get_doc(doctype, name).get_password(fieldname) + + +@influxframework.whitelist() +def get_js(items): + """Load JS code files. Will also append translations + and extend `influxframework._messages` + + :param items: JSON list of paths of the js files to be loaded.""" + items = json.loads(items) + out = [] + for src in items: + src = src.strip("/").split("/") + + if ".." in src or src[0] != "assets": + influxframework.throw(_("Invalid file path: {0}").format("/".join(src))) + + contentpath = os.path.join(influxframework.local.sites_path, *src) + with open(contentpath) as srcfile: + code = influxframework.utils.cstr(srcfile.read()) + + if influxframework.local.lang != "en": + messages = influxframework.get_lang_dict("jsfile", contentpath) + messages = json.dumps(messages) + code += f"\n\n$.extend(influxframework._messages, {messages})" + + out.append(code) + + return out + + +@influxframework.whitelist(allow_guest=True) +def get_time_zone(): + """Returns default time zone""" + return {"time_zone": influxframework.defaults.get_defaults().get("time_zone")} + + +@influxframework.whitelist(methods=["POST", "PUT"]) +def attach_file( + filename=None, + filedata=None, + doctype=None, + docname=None, + folder=None, + decode_base64=False, + is_private=None, + docfield=None, +): + """Attach a file to Document + + :param filename: filename e.g. test-file.txt + :param filedata: base64 encode filedata which must be urlencoded + :param doctype: Reference DocType to attach file to + :param docname: Reference DocName to attach file to + :param folder: Folder to add File into + :param decode_base64: decode filedata from base64 encode, default is False + :param is_private: Attach file as private file (1 or 0) + :param docfield: file to attach to (optional)""" + + doc = influxframework.get_doc(doctype, docname) + doc.check_permission() + + file = influxframework.get_doc( + { + "doctype": "File", + "file_name": filename, + "attached_to_doctype": doctype, + "attached_to_name": docname, + "attached_to_field": docfield, + "folder": folder, + "is_private": is_private, + "content": filedata, + "decode": decode_base64, + } + ).save() + + if docfield and doctype: + doc.set(docfield, file.file_url) + doc.save() + + return file + + +@influxframework.whitelist() +def is_document_amended(doctype, docname): + if influxframework.permissions.has_permission(doctype): + try: + return influxframework.db.exists(doctype, {"amended_from": docname}) + except influxframework.db.InternalError: + pass + + return False + + +@influxframework.whitelist() +def validate_link(doctype: str, docname: str, fields=None): + if not isinstance(doctype, str): + influxframework.throw(_("DocType must be a string")) + + if not isinstance(docname, str): + influxframework.throw(_("Document Name must be a string")) + + if doctype != "DocType" and not ( + influxframework.has_permission(doctype, "select") or influxframework.has_permission(doctype, "read") + ): + influxframework.throw( + _("You do not have Read or Select Permissions for {}").format(influxframework.bold(doctype)), + influxframework.PermissionError, + ) + + values = influxframework._dict() + values.name = influxframework.db.get_value(doctype, docname, cache=True) + + fields = influxframework.parse_json(fields) + if not values.name or not fields: + return values + + try: + values.update(get_value(doctype, fields, docname)) + except influxframework.PermissionError: + influxframework.clear_last_message() + influxframework.msgprint( + _("You need {0} permission to fetch values from {1} {2}").format( + influxframework.bold(_("Read")), influxframework.bold(doctype), influxframework.bold(docname) + ), + title=_("Cannot Fetch Values"), + indicator="orange", + ) + + return values + + +def insert_doc(doc) -> "Document": + """Inserts document and returns parent document object with appended child document + if `doc` is child document else returns the inserted document object + + :param doc: doc to insert (dict)""" + + doc = influxframework._dict(doc) + if influxframework.is_table(doc.doctype): + if not (doc.parenttype and doc.parent and doc.parentfield): + influxframework.throw(_("Parenttype, Parent and Parentfield are required to insert a child record")) + + # inserting a child record + parent = influxframework.get_doc(doc.parenttype, doc.parent) + parent.append(doc.parentfield, doc) + parent.save() + return parent + + return influxframework.get_doc(doc).insert() + + +def delete_doc(doctype, name): + """Deletes document + if doctype is a child table, then deletes the child record using the parent doc + so that the parent doc's `on_update` is called + """ + + if influxframework.is_table(doctype): + values = influxframework.db.get_value(doctype, name, ["parenttype", "parent", "parentfield"]) + if not values: + raise influxframework.DoesNotExistError + parenttype, parent, parentfield = values + parent = influxframework.get_doc(parenttype, parent) + for row in parent.get(parentfield): + if row.name == name: + parent.remove(row) + parent.save() + break + else: + influxframework.delete_doc(doctype, name, ignore_missing=False) diff --git a/influxframework/commands/__init__.py b/influxframework/commands/__init__.py new file mode 100644 index 0000000..13466a0 --- /dev/null +++ b/influxframework/commands/__init__.py @@ -0,0 +1,127 @@ +# Copyright (c) 2015, Web Notes Technologies Pvt. Ltd. and Contributors +# License: MIT. See LICENSE + +import cProfile +import pstats +import subprocess # nosec +import sys +from functools import wraps +from io import StringIO +from os import environ + +import click + +import influxframework +import influxframework.utils + +click.disable_unicode_literals_warning = True + + +def pass_context(f): + @wraps(f) + def _func(ctx, *args, **kwargs): + profile = ctx.obj["profile"] + if profile: + pr = cProfile.Profile() + pr.enable() + + try: + ret = f(influxframework._dict(ctx.obj), *args, **kwargs) + except influxframework.exceptions.SiteNotSpecifiedError as e: + click.secho(str(e), fg="yellow") + sys.exit(1) + except influxframework.exceptions.IncorrectSitePath: + site = ctx.obj.get("sites", "")[0] + click.secho(f"Site {site} does not exist!", fg="yellow") + sys.exit(1) + + if profile: + pr.disable() + s = StringIO() + ps = pstats.Stats(pr, stream=s).sort_stats("cumtime", "tottime", "ncalls") + ps.print_stats() + + # print the top-100 + for line in s.getvalue().splitlines()[:100]: + print(line) + + return ret + + return click.pass_context(_func) + + +def get_site(context, raise_err=True): + try: + site = context.sites[0] + return site + except (IndexError, TypeError): + if raise_err: + raise influxframework.SiteNotSpecifiedError + return None + + +def popen(command, *args, **kwargs): + output = kwargs.get("output", True) + cwd = kwargs.get("cwd") + shell = kwargs.get("shell", True) + raise_err = kwargs.get("raise_err") + env = kwargs.get("env") + if env: + env = dict(environ, **env) + + def set_low_prio(): + import psutil + + if psutil.LINUX: + psutil.Process().nice(19) + psutil.Process().ionice(psutil.IOPRIO_CLASS_IDLE) + elif psutil.WINDOWS: + psutil.Process().nice(psutil.IDLE_PRIORITY_CLASS) + psutil.Process().ionice(psutil.IOPRIO_VERYLOW) + else: + psutil.Process().nice(19) + # ionice not supported + + proc = subprocess.Popen( + command, + stdout=None if output else subprocess.PIPE, + stderr=None if output else subprocess.PIPE, + shell=shell, + cwd=cwd, + preexec_fn=set_low_prio, + env=env, + ) + + return_ = proc.wait() + + if return_ and raise_err: + raise subprocess.CalledProcessError(return_, command) + + return return_ + + +def call_command(cmd, context): + return click.Context(cmd, obj=context).forward(cmd) + + +def get_commands(): + # prevent circular imports + from .redis_utils import commands as redis_commands + from .scheduler import commands as scheduler_commands + from .site import commands as site_commands + from .translate import commands as translate_commands + from .utils import commands as utils_commands + + clickable_link = "\x1b]8;;https://influxframework.com/docs\ainfluxframework.com\x1b]8;;\a" + all_commands = ( + scheduler_commands + site_commands + translate_commands + utils_commands + redis_commands + ) + + for command in all_commands: + if not command.help: + command.help = f"Refer to {clickable_link}" + + return all_commands + + +commands = get_commands() diff --git a/influxframework/commands/redis_utils.py b/influxframework/commands/redis_utils.py new file mode 100644 index 0000000..1296906 --- /dev/null +++ b/influxframework/commands/redis_utils.py @@ -0,0 +1,73 @@ +import os + +import click + +import influxframework +from influxframework.installer import update_site_config +from influxframework.utils.redis_queue import RedisQueue + + +@click.command("create-rq-users") +@click.option( + "--set-admin-password", + is_flag=True, + default=False, + help="Set new Redis admin(default user) password", +) +@click.option( + "--use-rq-auth", is_flag=True, default=False, help="Enable Redis authentication for sites" +) +def create_rq_users(set_admin_password=False, use_rq_auth=False): + """Create Redis Queue users and add to acl and app configs. + + acl config file will be used by redis server while starting the server + and app config is used by app while connecting to redis server. + """ + acl_file_path = os.path.abspath("../config/redis_queue.acl") + + with influxframework.init_site(): + acl_list, user_credentials = RedisQueue.gen_acl_list(set_admin_password=set_admin_password) + + with open(acl_file_path, "w") as f: + f.writelines([acl + "\n" for acl in acl_list]) + + sites_path = os.getcwd() + common_site_config_path = os.path.join(sites_path, "common_site_config.json") + update_site_config( + "rq_username", + user_credentials["bench"][0], + validate=False, + site_config_path=common_site_config_path, + ) + update_site_config( + "rq_password", + user_credentials["bench"][1], + validate=False, + site_config_path=common_site_config_path, + ) + update_site_config( + "use_rq_auth", use_rq_auth, validate=False, site_config_path=common_site_config_path + ) + + click.secho( + "* ACL and site configs are updated with new user credentials. " + "Please restart Redis Queue server to enable namespaces.", + fg="green", + ) + + if set_admin_password: + env_key = "RQ_ADMIN_PASWORD" + click.secho( + "* Redis admin password is successfully set up. " + "Include below line in .bashrc file for system to use", + fg="green", + ) + click.secho(f"`export {env_key}={user_credentials['default'][1]}`") + click.secho( + "NOTE: Please save the admin password as you " + "can not access redis server without the password", + fg="yellow", + ) + + +commands = [create_rq_users] diff --git a/influxframework/commands/scheduler.py b/influxframework/commands/scheduler.py new file mode 100644 index 0000000..e43770c --- /dev/null +++ b/influxframework/commands/scheduler.py @@ -0,0 +1,229 @@ +import sys + +import click + +import influxframework +from influxframework.commands import get_site, pass_context +from influxframework.exceptions import SiteNotSpecifiedError +from influxframework.utils import cint + + +@click.command("trigger-scheduler-event", help="Trigger a scheduler event") +@click.argument("event") +@pass_context +def trigger_scheduler_event(context, event): + import influxframework.utils.scheduler + + exit_code = 0 + + for site in context.sites: + try: + influxframework.init(site=site) + influxframework.connect() + try: + influxframework.get_doc("Scheduled Job Type", {"method": event}).execute() + except influxframework.DoesNotExistError: + click.secho(f"Event {event} does not exist!", fg="red") + exit_code = 1 + finally: + influxframework.destroy() + + if not context.sites: + raise SiteNotSpecifiedError + + sys.exit(exit_code) + + +@click.command("enable-scheduler") +@pass_context +def enable_scheduler(context): + "Enable scheduler" + import influxframework.utils.scheduler + + for site in context.sites: + try: + influxframework.init(site=site) + influxframework.connect() + influxframework.utils.scheduler.enable_scheduler() + influxframework.db.commit() + print("Enabled for", site) + finally: + influxframework.destroy() + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("disable-scheduler") +@pass_context +def disable_scheduler(context): + "Disable scheduler" + import influxframework.utils.scheduler + + for site in context.sites: + try: + influxframework.init(site=site) + influxframework.connect() + influxframework.utils.scheduler.disable_scheduler() + influxframework.db.commit() + print("Disabled for", site) + finally: + influxframework.destroy() + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("scheduler") +@click.option("--site", help="site name") +@click.argument("state", type=click.Choice(["pause", "resume", "disable", "enable"])) +@pass_context +def scheduler(context, state, site=None): + import influxframework.utils.scheduler + from influxframework.installer import update_site_config + + if not site: + site = get_site(context) + + try: + influxframework.init(site=site) + + if state == "pause": + update_site_config("pause_scheduler", 1) + elif state == "resume": + update_site_config("pause_scheduler", 0) + elif state == "disable": + influxframework.connect() + influxframework.utils.scheduler.disable_scheduler() + influxframework.db.commit() + elif state == "enable": + influxframework.connect() + influxframework.utils.scheduler.enable_scheduler() + influxframework.db.commit() + + print(f"Scheduler {state}d for site {site}") + + finally: + influxframework.destroy() + + +@click.command("set-maintenance-mode") +@click.option("--site", help="site name") +@click.argument("state", type=click.Choice(["on", "off"])) +@pass_context +def set_maintenance_mode(context, state, site=None): + from influxframework.installer import update_site_config + + if not site: + site = get_site(context) + + try: + influxframework.init(site=site) + update_site_config("maintenance_mode", 1 if (state == "on") else 0) + + finally: + influxframework.destroy() + + +@click.command( + "doctor" +) # Passing context always gets a site and if there is no use site it breaks +@click.option("--site", help="site name") +@pass_context +def doctor(context, site=None): + "Get diagnostic info about background workers" + from influxframework.utils.doctor import doctor as _doctor + + if not site: + site = get_site(context, raise_err=False) + return _doctor(site=site) + + +@click.command("show-pending-jobs") +@click.option("--site", help="site name") +@pass_context +def show_pending_jobs(context, site=None): + "Get diagnostic info about background jobs" + from influxframework.utils.doctor import pending_jobs as _pending_jobs + + if not site: + site = get_site(context) + + with influxframework.init_site(site): + pending_jobs = _pending_jobs(site=site) + + return pending_jobs + + +@click.command("purge-jobs") +@click.option("--site", help="site name") +@click.option("--queue", default=None, help='one of "low", "default", "high') +@click.option( + "--event", + default=None, + help='one of "all", "weekly", "monthly", "hourly", "daily", "weekly_long", "daily_long"', +) +def purge_jobs(site=None, queue=None, event=None): + "Purge any pending periodic tasks, if event option is not given, it will purge everything for the site" + from influxframework.utils.doctor import purge_pending_jobs + + influxframework.init(site or "") + count = purge_pending_jobs(event=event, site=site, queue=queue) + print(f"Purged {count} jobs") + + +@click.command("schedule") +def start_scheduler(): + from influxframework.utils.scheduler import start_scheduler + + start_scheduler() + + +@click.command("worker") +@click.option("--queue", type=str) +@click.option("--quiet", is_flag=True, default=False, help="Hide Log Outputs") +@click.option("-u", "--rq-username", default=None, help="Redis ACL user") +@click.option("-p", "--rq-password", default=None, help="Redis ACL user password") +def start_worker(queue, quiet=False, rq_username=None, rq_password=None): + """Site is used to find redis credentals.""" + from influxframework.utils.background_jobs import start_worker + + start_worker(queue, quiet=quiet, rq_username=rq_username, rq_password=rq_password) + + +@click.command("ready-for-migration") +@click.option("--site", help="site name") +@pass_context +def ready_for_migration(context, site=None): + from influxframework.utils.doctor import get_pending_jobs + + if not site: + site = get_site(context) + + try: + influxframework.init(site=site) + pending_jobs = get_pending_jobs(site=site) + + if pending_jobs: + print(f"NOT READY for migration: site {site} has pending background jobs") + sys.exit(1) + + else: + print(f"READY for migration: site {site} does not have any background jobs") + return 0 + + finally: + influxframework.destroy() + + +commands = [ + disable_scheduler, + doctor, + enable_scheduler, + purge_jobs, + ready_for_migration, + scheduler, + set_maintenance_mode, + show_pending_jobs, + start_scheduler, + start_worker, + trigger_scheduler_event, +] diff --git a/influxframework/commands/site.py b/influxframework/commands/site.py new file mode 100644 index 0000000..d17a916 --- /dev/null +++ b/influxframework/commands/site.py @@ -0,0 +1,1369 @@ +# imports - standard imports +import os +import shutil +import sys + +# imports - third party imports +import click + +# imports - module imports +import influxframework +from influxframework.commands import get_site, pass_context +from influxframework.core.doctype.log_settings.log_settings import LOG_DOCTYPES +from influxframework.exceptions import SiteNotSpecifiedError + + +@click.command("new-site") +@click.argument("site") +@click.option("--db-name", help="Database name") +@click.option("--db-password", help="Database password") +@click.option( + "--db-type", + default="mariadb", + type=click.Choice(["mariadb", "postgres"]), + help='Optional "postgres" or "mariadb". Default is "mariadb"', +) +@click.option("--db-host", help="Database Host") +@click.option("--db-port", type=int, help="Database Port") +@click.option( + "--db-root-username", + "--mariadb-root-username", + help='Root username for MariaDB or PostgreSQL, Default is "root"', +) +@click.option( + "--db-root-password", "--mariadb-root-password", help="Root password for MariaDB or PostgreSQL" +) +@click.option( + "--no-mariadb-socket", + is_flag=True, + default=False, + help="Set MariaDB host to % and use TCP/IP Socket instead of using the UNIX Socket", +) +@click.option("--admin-password", help="Administrator password for new site", default=None) +@click.option("--verbose", is_flag=True, default=False, help="Verbose") +@click.option( + "--force", help="Force restore if site/database already exists", is_flag=True, default=False +) +@click.option("--source_sql", help="Initiate database with a SQL file") +@click.option("--install-app", multiple=True, help="Install app after installation") +@click.option( + "--set-default", is_flag=True, default=False, help="Set the new site as default site" +) +def new_site( + site, + db_root_username=None, + db_root_password=None, + admin_password=None, + verbose=False, + source_sql=None, + force=None, + no_mariadb_socket=False, + install_app=None, + db_name=None, + db_password=None, + db_type=None, + db_host=None, + db_port=None, + set_default=False, +): + "Create a new site" + from influxframework.installer import _new_site + + influxframework.init(site=site, new_site=True) + + _new_site( + db_name, + site, + db_root_username=db_root_username, + db_root_password=db_root_password, + admin_password=admin_password, + verbose=verbose, + install_apps=install_app, + source_sql=source_sql, + force=force, + no_mariadb_socket=no_mariadb_socket, + db_password=db_password, + db_type=db_type, + db_host=db_host, + db_port=db_port, + ) + + if set_default: + use(site) + + +@click.command("restore") +@click.argument("sql-file-path") +@click.option( + "--db-root-username", + "--mariadb-root-username", + help='Root username for MariaDB or PostgreSQL, Default is "root"', +) +@click.option( + "--db-root-password", "--mariadb-root-password", help="Root password for MariaDB or PostgreSQL" +) +@click.option("--db-name", help="Database name for site in case it is a new one") +@click.option("--admin-password", help="Administrator password for new site") +@click.option("--install-app", multiple=True, help="Install app after installation") +@click.option( + "--with-public-files", help="Restores the public files of the site, given path to its tar file" +) +@click.option( + "--with-private-files", help="Restores the private files of the site, given path to its tar file" +) +@click.option( + "--force", + is_flag=True, + default=False, + help="Ignore the validations and downgrade warnings. This action is not recommended", +) +@click.option("--encryption-key", help="Backup encryption key") +@pass_context +def restore( + context, + sql_file_path, + encryption_key=None, + db_root_username=None, + db_root_password=None, + db_name=None, + verbose=None, + install_app=None, + admin_password=None, + force=None, + with_public_files=None, + with_private_files=None, +): + "Restore site database from an sql file" + from influxframework.installer import ( + _new_site, + extract_files, + extract_sql_from_archive, + is_downgrade, + is_partial, + validate_database_sql, + ) + from influxframework.utils.backups import Backup, get_or_generate_backup_encryption_key + + _backup = Backup(sql_file_path) + + site = get_site(context) + influxframework.init(site=site) + force = context.force or force + + try: + decompressed_file_name = extract_sql_from_archive(sql_file_path) + if is_partial(decompressed_file_name): + click.secho( + "Partial Backup file detected. You cannot use a partial file to restore a InfluxFramework site.", + fg="red", + ) + click.secho( + "Use `bench partial-restore` to restore a partial backup to an existing site.", fg="yellow" + ) + _backup.decryption_rollback() + sys.exit(1) + + except UnicodeDecodeError: + _backup.decryption_rollback() + if encryption_key: + click.secho("Encrypted backup file detected. Decrypting using provided key.", fg="yellow") + _backup.backup_decryption(encryption_key) + + else: + click.secho("Encrypted backup file detected. Decrypting using site config.", fg="yellow") + encryption_key = get_or_generate_backup_encryption_key() + _backup.backup_decryption(encryption_key) + + # Rollback on unsuccessful decryrption + if not os.path.exists(sql_file_path): + click.secho("Decryption failed. Please provide a valid key and try again.", fg="red") + + _backup.decryption_rollback() + sys.exit(1) + + decompressed_file_name = extract_sql_from_archive(sql_file_path) + + if is_partial(decompressed_file_name): + click.secho( + "Partial Backup file detected. You cannot use a partial file to restore a InfluxFramework site.", + fg="red", + ) + click.secho( + "Use `bench partial-restore` to restore a partial backup to an existing site.", fg="yellow" + ) + _backup.decryption_rollback() + sys.exit(1) + + validate_database_sql(decompressed_file_name, _raise=not force) + + # dont allow downgrading to older versions of influxframework without force + if not force and is_downgrade(decompressed_file_name, verbose=True): + warn_message = ( + "This is not recommended and may lead to unexpected behaviour. " + "Do you want to continue anyway?" + ) + click.confirm(warn_message, abort=True) + + try: + _new_site( + influxframework.conf.db_name, + site, + db_root_username=db_root_username, + db_root_password=db_root_password, + admin_password=admin_password, + verbose=context.verbose, + install_apps=install_app, + source_sql=decompressed_file_name, + force=True, + db_type=influxframework.conf.db_type, + ) + + except Exception as err: + print(err.args[1]) + _backup.decryption_rollback() + sys.exit(1) + + # Removing temporarily created file + if decompressed_file_name != sql_file_path: + os.remove(decompressed_file_name) + _backup.decryption_rollback() + + # Extract public and/or private files to the restored site, if user has given the path + if with_public_files: + # Decrypt data if there is a Key + if encryption_key: + _backup = Backup(with_public_files) + _backup.backup_decryption(encryption_key) + if not os.path.exists(with_public_files): + _backup.decryption_rollback() + public = extract_files(site, with_public_files) + + # Removing temporarily created file + os.remove(public) + _backup.decryption_rollback() + + if with_private_files: + # Decrypt data if there is a Key + if encryption_key: + _backup = Backup(with_private_files) + _backup.backup_decryption(encryption_key) + if not os.path.exists(with_private_files): + _backup.decryption_rollback() + private = extract_files(site, with_private_files) + + # Removing temporarily created file + os.remove(private) + _backup.decryption_rollback() + + success_message = "Site {} has been restored{}".format( + site, " with files" if (with_public_files or with_private_files) else "" + ) + click.secho(success_message, fg="green") + + +@click.command("partial-restore") +@click.argument("sql-file-path") +@click.option("--verbose", "-v", is_flag=True) +@click.option("--encryption-key", help="Backup encryption key") +@pass_context +def partial_restore(context, sql_file_path, verbose, encryption_key=None): + from influxframework.installer import extract_sql_from_archive, partial_restore + from influxframework.utils.backups import Backup, get_or_generate_backup_encryption_key + + if not os.path.exists(sql_file_path): + print("Invalid path", sql_file_path) + sys.exit(1) + + site = get_site(context) + influxframework.init(site=site) + + _backup = Backup(sql_file_path) + + verbose = context.verbose or verbose + + influxframework.connect(site=site) + try: + decompressed_file_name = extract_sql_from_archive(sql_file_path) + + with open(decompressed_file_name) as f: + header = " ".join(f.readline() for _ in range(5)) + + # Check for full backup file + if "Partial Backup" not in header: + click.secho( + "Full backup file detected.Use `bench restore` to restore a InfluxFramework Site.", fg="red" + ) + _backup.decryption_rollback() + sys.exit(1) + + except UnicodeDecodeError: + _backup.decryption_rollback() + if encryption_key: + click.secho("Encrypted backup file detected. Decrypting using provided key.", fg="yellow") + key = encryption_key + + else: + click.secho("Encrypted backup file detected. Decrypting using site config.", fg="yellow") + key = get_or_generate_backup_encryption_key() + + _backup.backup_decryption(key) + + # Rollback on unsuccessful decryrption + if not os.path.exists(sql_file_path): + click.secho("Decryption failed. Please provide a valid key and try again.", fg="red") + _backup.decryption_rollback() + sys.exit(1) + + decompressed_file_name = extract_sql_from_archive(sql_file_path) + + with open(decompressed_file_name) as f: + header = " ".join(f.readline() for _ in range(5)) + + # Check for Full backup file. + if "Partial Backup" not in header: + click.secho( + "Full Backup file detected.Use `bench restore` to restore a InfluxFramework Site.", fg="red" + ) + _backup.decryption_rollback() + sys.exit(1) + + partial_restore(sql_file_path, verbose) + + # Removing temporarily created file + _backup.decryption_rollback() + if os.path.exists(sql_file_path.rstrip(".gz")): + os.remove(sql_file_path.rstrip(".gz")) + + influxframework.destroy() + + +@click.command("reinstall") +@click.option("--admin-password", help="Administrator Password for reinstalled site") +@click.option( + "--db-root-username", + "--mariadb-root-username", + help='Root username for MariaDB or PostgreSQL, Default is "root"', +) +@click.option( + "--db-root-password", "--mariadb-root-password", help="Root password for MariaDB or PostgreSQL" +) +@click.option("--yes", is_flag=True, default=False, help="Pass --yes to skip confirmation") +@pass_context +def reinstall( + context, admin_password=None, db_root_username=None, db_root_password=None, yes=False +): + "Reinstall site ie. wipe all data and start over" + site = get_site(context) + _reinstall(site, admin_password, db_root_username, db_root_password, yes, verbose=context.verbose) + + +def _reinstall( + site, admin_password=None, db_root_username=None, db_root_password=None, yes=False, verbose=False +): + from influxframework.installer import _new_site + + if not yes: + click.confirm("This will wipe your database. Are you sure you want to reinstall?", abort=True) + try: + influxframework.init(site=site) + influxframework.connect() + influxframework.clear_cache() + installed = influxframework.get_installed_apps() + influxframework.clear_cache() + except Exception: + installed = [] + finally: + if influxframework.db: + influxframework.db.close() + influxframework.destroy() + + influxframework.init(site=site) + _new_site( + influxframework.conf.db_name, + site, + verbose=verbose, + force=True, + reinstall=True, + install_apps=installed, + db_root_username=db_root_username, + db_root_password=db_root_password, + admin_password=admin_password, + ) + + +@click.command("install-app") +@click.argument("apps", nargs=-1) +@click.option("--force", is_flag=True, default=False) +@pass_context +def install_app(context, apps, force=False): + "Install a new app to site, supports multiple apps" + from influxframework.installer import install_app as _install_app + + exit_code = 0 + + if not context.sites: + raise SiteNotSpecifiedError + + for site in context.sites: + influxframework.init(site=site) + influxframework.connect() + + for app in apps: + try: + _install_app(app, verbose=context.verbose, force=force) + except influxframework.IncompatibleApp as err: + err_msg = f":\n{err}" if str(err) else "" + print(f"App {app} is Incompatible with Site {site}{err_msg}") + exit_code = 1 + except Exception as err: + err_msg = f": {str(err)}\n{influxframework.get_traceback()}" + print(f"An error occurred while installing {app}{err_msg}") + exit_code = 1 + + if not exit_code: + influxframework.db.commit() + + influxframework.destroy() + + sys.exit(exit_code) + + +@click.command("list-apps") +@click.option("--format", "-f", type=click.Choice(["text", "json"]), default="text") +@pass_context +def list_apps(context, format): + "List apps in site" + + summary_dict = {} + + def fix_whitespaces(text): + if site == context.sites[-1]: + text = text.rstrip() + if len(context.sites) == 1: + text = text.lstrip() + return text + + for site in context.sites: + influxframework.init(site=site) + influxframework.connect() + site_title = click.style(f"{site}", fg="green") if len(context.sites) > 1 else "" + apps = influxframework.get_single("Installed Applications").installed_applications + + if apps: + name_len, ver_len = (max(len(x.get(y)) for x in apps) for y in ["app_name", "app_version"]) + template = f"{{0:{name_len}}} {{1:{ver_len}}} {{2}}" + + installed_applications = [ + template.format(app.app_name, app.app_version, app.git_branch) for app in apps + ] + applications_summary = "\n".join(installed_applications) + summary = f"{site_title}\n{applications_summary}\n" + summary_dict[site] = [app.app_name for app in apps] + + else: + installed_applications = influxframework.get_installed_apps() + applications_summary = "\n".join(installed_applications) + summary = f"{site_title}\n{applications_summary}\n" + summary_dict[site] = installed_applications + + summary = fix_whitespaces(summary) + + if format == "text" and applications_summary and summary: + print(summary) + + influxframework.destroy() + + if format == "json": + click.echo(influxframework.as_json(summary_dict)) + + +@click.command("add-system-manager") +@click.argument("email") +@click.option("--first-name") +@click.option("--last-name") +@click.option("--password") +@click.option("--send-welcome-email", default=False, is_flag=True) +@pass_context +def add_system_manager(context, email, first_name, last_name, send_welcome_email, password): + "Add a new system manager to a site" + import influxframework.utils.user + + for site in context.sites: + influxframework.connect(site=site) + try: + influxframework.utils.user.add_system_manager(email, first_name, last_name, send_welcome_email, password) + influxframework.db.commit() + finally: + influxframework.destroy() + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("add-user") +@click.argument("email") +@click.option("--first-name") +@click.option("--last-name") +@click.option("--password") +@click.option("--user-type") +@click.option("--add-role", multiple=True) +@click.option("--send-welcome-email", default=False, is_flag=True) +@pass_context +def add_user_for_sites( + context, email, first_name, last_name, user_type, send_welcome_email, password, add_role +): + "Add user to a site" + import influxframework.utils.user + + for site in context.sites: + influxframework.connect(site=site) + try: + add_new_user(email, first_name, last_name, user_type, send_welcome_email, password, add_role) + influxframework.db.commit() + finally: + influxframework.destroy() + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("disable-user") +@click.argument("email") +@pass_context +def disable_user(context, email): + site = get_site(context) + with influxframework.init_site(site): + influxframework.connect() + user = influxframework.get_doc("User", email) + user.enabled = 0 + user.save(ignore_permissions=True) + influxframework.db.commit() + + +@click.command("migrate") +@click.option("--skip-failing", is_flag=True, help="Skip patches that fail to run") +@click.option("--skip-search-index", is_flag=True, help="Skip search indexing for web documents") +@pass_context +def migrate(context, skip_failing=False, skip_search_index=False): + "Run patches, sync schema and rebuild files/translations" + from influxframework.migrate import SiteMigration + + for site in context.sites: + click.secho(f"Migrating {site}", fg="green") + try: + SiteMigration( + skip_failing=skip_failing, + skip_search_index=skip_search_index, + ).run(site=site) + finally: + print() + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("migrate-to") +@click.argument("influxframework_provider") +@pass_context +def migrate_to(context, influxframework_provider): + "Migrates site to the specified provider" + from influxframework.integrations.influxframework_providers import migrate_to + + for site in context.sites: + influxframework.init(site=site) + influxframework.connect() + migrate_to(site, influxframework_provider) + influxframework.destroy() + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("run-patch") +@click.argument("module") +@click.option("--force", is_flag=True) +@pass_context +def run_patch(context, module, force): + "Run a particular patch" + import influxframework.modules.patch_handler + + for site in context.sites: + influxframework.init(site=site) + try: + influxframework.connect() + influxframework.modules.patch_handler.run_single(module, force=force or context.force) + finally: + influxframework.destroy() + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("reload-doc") +@click.argument("module") +@click.argument("doctype") +@click.argument("docname") +@pass_context +def reload_doc(context, module, doctype, docname): + "Reload schema for a DocType" + for site in context.sites: + try: + influxframework.init(site=site) + influxframework.connect() + influxframework.reload_doc(module, doctype, docname, force=context.force) + influxframework.db.commit() + finally: + influxframework.destroy() + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("reload-doctype") +@click.argument("doctype") +@pass_context +def reload_doctype(context, doctype): + "Reload schema for a DocType" + for site in context.sites: + try: + influxframework.init(site=site) + influxframework.connect() + influxframework.reload_doctype(doctype, force=context.force) + influxframework.db.commit() + finally: + influxframework.destroy() + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("add-to-hosts") +@pass_context +def add_to_hosts(context): + "Add site to hosts" + for site in context.sites: + influxframework.commands.popen(f"echo 127.0.0.1\t{site} | sudo tee -a /etc/hosts") + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("use") +@click.argument("site") +def _use(site, sites_path="."): + "Set a default site" + use(site, sites_path=sites_path) + + +def use(site, sites_path="."): + if os.path.exists(os.path.join(sites_path, site)): + with open(os.path.join(sites_path, "currentsite.txt"), "w") as sitefile: + sitefile.write(site) + print(f"Current Site set to {site}") + else: + print(f"Site {site} does not exist") + + +@click.command("backup") +@click.option("--with-files", default=False, is_flag=True, help="Take backup with files") +@click.option( + "--include", + "--only", + "-i", + default="", + type=str, + help="Specify the DocTypes to backup seperated by commas", +) +@click.option( + "--exclude", + "-e", + default="", + type=str, + help="Specify the DocTypes to not backup seperated by commas", +) +@click.option( + "--backup-path", default=None, help="Set path for saving all the files in this operation" +) +@click.option("--backup-path-db", default=None, help="Set path for saving database file") +@click.option("--backup-path-files", default=None, help="Set path for saving public file") +@click.option("--backup-path-private-files", default=None, help="Set path for saving private file") +@click.option("--backup-path-conf", default=None, help="Set path for saving config file") +@click.option( + "--ignore-backup-conf", default=False, is_flag=True, help="Ignore excludes/includes set in config" +) +@click.option("--verbose", default=False, is_flag=True, help="Add verbosity") +@click.option("--compress", default=False, is_flag=True, help="Compress private and public files") +@pass_context +def backup( + context, + with_files=False, + backup_path=None, + backup_path_db=None, + backup_path_files=None, + backup_path_private_files=None, + backup_path_conf=None, + ignore_backup_conf=False, + verbose=False, + compress=False, + include="", + exclude="", +): + "Backup" + + from influxframework.utils.backups import scheduled_backup + + verbose = verbose or context.verbose + exit_code = 0 + + for site in context.sites: + try: + influxframework.init(site=site) + influxframework.connect() + odb = scheduled_backup( + ignore_files=not with_files, + backup_path=backup_path, + backup_path_db=backup_path_db, + backup_path_files=backup_path_files, + backup_path_private_files=backup_path_private_files, + backup_path_conf=backup_path_conf, + ignore_conf=ignore_backup_conf, + include_doctypes=include, + exclude_doctypes=exclude, + compress=compress, + verbose=verbose, + force=True, + ) + except Exception: + click.secho( + f"Backup failed for Site {site}. Database or site_config.json may be corrupted", + fg="red", + ) + if verbose: + print(influxframework.get_traceback()) + exit_code = 1 + continue + if influxframework.get_system_settings("encrypt_backup") and influxframework.get_site_config().encryption_key: + click.secho( + "Backup encryption is turned on. Please note the backup encryption key.", fg="yellow" + ) + + odb.print_summary() + click.secho( + "Backup for Site {} has been successfully completed{}".format( + site, " with files" if with_files else "" + ), + fg="green", + ) + influxframework.destroy() + + if not context.sites: + raise SiteNotSpecifiedError + + sys.exit(exit_code) + + +@click.command("remove-from-installed-apps") +@click.argument("app") +@pass_context +def remove_from_installed_apps(context, app): + "Remove app from site's installed-apps list" + from influxframework.installer import remove_from_installed_apps + + for site in context.sites: + try: + influxframework.init(site=site) + influxframework.connect() + remove_from_installed_apps(app) + finally: + influxframework.destroy() + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("uninstall-app") +@click.argument("app") +@click.option( + "--yes", + "-y", + help="To bypass confirmation prompt for uninstalling the app", + is_flag=True, + default=False, +) +@click.option( + "--dry-run", help="List all doctypes that will be deleted", is_flag=True, default=False +) +@click.option("--no-backup", help="Do not backup the site", is_flag=True, default=False) +@click.option("--force", help="Force remove app from site", is_flag=True, default=False) +@pass_context +def uninstall(context, app, dry_run, yes, no_backup, force): + "Remove app and linked modules from site" + from influxframework.installer import remove_app + + for site in context.sites: + try: + influxframework.init(site=site) + influxframework.connect() + remove_app(app_name=app, dry_run=dry_run, yes=yes, no_backup=no_backup, force=force) + finally: + influxframework.destroy() + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("drop-site") +@click.argument("site") +@click.option( + "--db-root-username", + "--mariadb-root-username", + "--root-login", + help='Root username for MariaDB or PostgreSQL, Default is "root"', +) +@click.option( + "--db-root-password", + "--mariadb-root-password", + "--root-password", + help="Root password for MariaDB or PostgreSQL", +) +@click.option("--archived-sites-path") +@click.option("--no-backup", is_flag=True, default=False) +@click.option( + "--force", help="Force drop-site even if an error is encountered", is_flag=True, default=False +) +def drop_site( + site, + db_root_username="root", + db_root_password=None, + archived_sites_path=None, + force=False, + no_backup=False, +): + _drop_site(site, db_root_username, db_root_password, archived_sites_path, force, no_backup) + + +def _drop_site( + site, + db_root_username=None, + db_root_password=None, + archived_sites_path=None, + force=False, + no_backup=False, +): + "Remove site from database and filesystem" + from influxframework.database import drop_user_and_database + from influxframework.utils.backups import scheduled_backup + + influxframework.init(site=site) + influxframework.connect() + + try: + if not no_backup: + click.secho(f"Taking backup of {site}", fg="green") + odb = scheduled_backup(ignore_files=False, ignore_conf=True, force=True, verbose=True) + odb.print_summary() + except Exception as err: + if force: + pass + else: + messages = [ + "=" * 80, + f"Error: The operation has stopped because backup of {site}'s database failed.", + f"Reason: {str(err)}\n", + "Fix the issue and try again.", + "Hint: Use 'bench drop-site {0} --force' to force the removal of {0}".format(site), + ] + click.echo("\n".join(messages)) + sys.exit(1) + + click.secho("Dropping site database and user", fg="green") + drop_user_and_database(influxframework.conf.db_name, db_root_username, db_root_password) + + archived_sites_path = archived_sites_path or os.path.join( + influxframework.get_app_path("influxframework"), "..", "..", "..", "archived", "sites" + ) + archived_sites_path = os.path.realpath(archived_sites_path) + + click.secho(f"Moving site to archive under {archived_sites_path}", fg="green") + os.makedirs(archived_sites_path, exist_ok=True) + move(archived_sites_path, site) + + +def move(dest_dir, site): + if not os.path.isdir(dest_dir): + raise Exception("destination is not a directory or does not exist") + + influxframework.init(site) + old_path = influxframework.utils.get_site_path() + new_path = os.path.join(dest_dir, site) + + # check if site dump of same name already exists + site_dump_exists = True + count = 0 + while site_dump_exists: + final_new_path = new_path + (count and str(count) or "") + site_dump_exists = os.path.exists(final_new_path) + count = int(count or 0) + 1 + + shutil.move(old_path, final_new_path) + influxframework.destroy() + return final_new_path + + +@click.command("set-password") +@click.argument("user") +@click.argument("password", required=False) +@click.option( + "--logout-all-sessions", help="Log out from all sessions", is_flag=True, default=False +) +@pass_context +def set_password(context, user, password=None, logout_all_sessions=False): + "Set password for a user on a site" + if not context.sites: + raise SiteNotSpecifiedError + + for site in context.sites: + set_user_password(site, user, password, logout_all_sessions) + + +@click.command("set-admin-password") +@click.argument("admin-password", required=False) +@click.option( + "--logout-all-sessions", help="Log out from all sessions", is_flag=True, default=False +) +@pass_context +def set_admin_password(context, admin_password=None, logout_all_sessions=False): + "Set Administrator password for a site" + if not context.sites: + raise SiteNotSpecifiedError + + for site in context.sites: + set_user_password(site, "Administrator", admin_password, logout_all_sessions) + + +def set_user_password(site, user, password, logout_all_sessions=False): + import getpass + + from influxframework.utils.password import update_password + + try: + influxframework.init(site=site) + + while not password: + password = getpass.getpass(f"{user}'s password for {site}: ") + + influxframework.connect() + if not influxframework.db.exists("User", user): + print(f"User {user} does not exist") + sys.exit(1) + + update_password(user=user, pwd=password, logout_all_sessions=logout_all_sessions) + influxframework.db.commit() + finally: + influxframework.destroy() + + +@click.command("set-last-active-for-user") +@click.option("--user", help="Setup last active date for user") +@pass_context +def set_last_active_for_user(context, user=None): + "Set users last active date to current datetime" + from influxframework.core.doctype.user.user import get_system_users + from influxframework.utils import now_datetime + + site = get_site(context) + + with influxframework.init_site(site): + influxframework.connect() + if not user: + user = get_system_users(limit=1) + if len(user) > 0: + user = user[0] + else: + return + + influxframework.db.set_value("User", user, "last_active", now_datetime()) + influxframework.db.commit() + + +@click.command("publish-realtime") +@click.argument("event") +@click.option("--message") +@click.option("--room") +@click.option("--user") +@click.option("--doctype") +@click.option("--docname") +@click.option("--after-commit") +@pass_context +def publish_realtime(context, event, message, room, user, doctype, docname, after_commit): + "Publish realtime event from bench" + from influxframework import publish_realtime + + for site in context.sites: + try: + influxframework.init(site=site) + influxframework.connect() + publish_realtime( + event, + message=message, + room=room, + user=user, + doctype=doctype, + docname=docname, + after_commit=after_commit, + ) + influxframework.db.commit() + finally: + influxframework.destroy() + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("browse") +@click.argument("site", required=False) +@click.option("--user", required=False, help="Login as user") +@pass_context +def browse(context, site, user=None): + """Opens the site on web browser""" + from influxframework.auth import CookieManager, LoginManager + + site = get_site(context, raise_err=False) or site + + if not site: + raise SiteNotSpecifiedError + + if site not in influxframework.utils.get_sites(): + click.echo(f"\nSite named {click.style(site, bold=True)} doesn't exist\n", err=True) + sys.exit(1) + + influxframework.init(site=site) + influxframework.connect() + + sid = "" + if user: + if influxframework.conf.developer_mode or user == "Administrator": + influxframework.utils.set_request(path="/") + influxframework.local.cookie_manager = CookieManager() + influxframework.local.login_manager = LoginManager() + influxframework.local.login_manager.login_as(user) + sid = f"/app?sid={influxframework.session.sid}" + else: + click.echo("Please enable developer mode to login as a user") + + url = f"{influxframework.utils.get_site_url(site)}{sid}" + + if user == "Administrator": + click.echo(f"Login URL: {url}") + + click.launch(url) + + +@click.command("start-recording") +@pass_context +def start_recording(context): + import influxframework.recorder + + for site in context.sites: + influxframework.init(site=site) + influxframework.set_user("Administrator") + influxframework.recorder.start() + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("stop-recording") +@pass_context +def stop_recording(context): + import influxframework.recorder + + for site in context.sites: + influxframework.init(site=site) + influxframework.set_user("Administrator") + influxframework.recorder.stop() + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("ngrok") +@click.option( + "--bind-tls", is_flag=True, default=False, help="Returns a reference to the https tunnel." +) +@pass_context +def start_ngrok(context, bind_tls): + from pyngrok import ngrok + + site = get_site(context) + influxframework.init(site=site) + + port = influxframework.conf.http_port or influxframework.conf.webserver_port + tunnel = ngrok.connect(addr=str(port), host_header=site, bind_tls=bind_tls) + print(f"Public URL: {tunnel.public_url}") + print("Inspect logs at http://localhost:4040") + + ngrok_process = ngrok.get_ngrok_process() + try: + # Block until CTRL-C or some other terminating event + ngrok_process.proc.wait() + except KeyboardInterrupt: + print("Shutting down server...") + influxframework.destroy() + ngrok.kill() + + +@click.command("build-search-index") +@pass_context +def build_search_index(context): + from influxframework.search.website_search import build_index_for_all_routes + + site = get_site(context) + if not site: + raise SiteNotSpecifiedError + + print(f"Building search index for {site}") + influxframework.init(site=site) + influxframework.connect() + try: + build_index_for_all_routes() + finally: + influxframework.destroy() + + +@click.command("clear-log-table") +@click.option("--doctype", default="text", type=click.Choice(LOG_DOCTYPES), help="Log DocType") +@click.option("--days", type=int, help="Keep records for days") +@click.option("--no-backup", is_flag=True, default=False, help="Do not backup the table") +@pass_context +def clear_log_table(context, doctype, days, no_backup): + """If any logtype table grows too large then clearing it with DELETE query + is not feasible in reasonable time. This command copies recent data to new + table and replaces current table with new smaller table. + + + ref: https://mariadb.com/kb/en/big-deletes/#deleting-more-than-half-a-table + """ + from influxframework.core.doctype.log_settings.log_settings import clear_log_table as clear_logs + from influxframework.utils.backups import scheduled_backup + + if not context.sites: + raise SiteNotSpecifiedError + + if doctype not in LOG_DOCTYPES: + raise influxframework.ValidationError(f"Unsupported logging DocType: {doctype}") + + for site in context.sites: + influxframework.init(site=site) + influxframework.connect() + + if not no_backup: + scheduled_backup( + ignore_conf=False, + include_doctypes=doctype, + ignore_files=True, + force=True, + ) + click.echo(f"Backed up {doctype}") + + try: + click.echo(f"Copying {doctype} records from last {days} days to temporary table.") + clear_logs(doctype, days=days) + except Exception as e: + click.echo(f"Log cleanup for {doctype} failed:\n{e}") + sys.exit(1) + else: + click.secho(f"Cleared {doctype} records older than {days} days", fg="green") + + +@click.command("trim-database") +@click.option("--dry-run", is_flag=True, default=False, help="Show what would be deleted") +@click.option( + "--format", "-f", default="text", type=click.Choice(["json", "text"]), help="Output format" +) +@click.option("--no-backup", is_flag=True, default=False, help="Do not backup the site") +@pass_context +def trim_database(context, dry_run, format, no_backup): + if not context.sites: + raise SiteNotSpecifiedError + + from influxframework.utils.backups import scheduled_backup + + ALL_DATA = {} + + for site in context.sites: + influxframework.init(site=site) + influxframework.connect() + + TABLES_TO_DROP = [] + STANDARD_TABLES = get_standard_tables() + information_schema = influxframework.qb.Schema("information_schema") + table_name = influxframework.qb.Field("table_name").as_("name") + + queried_result = ( + influxframework.qb.from_(information_schema.tables) + .select(table_name) + .where(information_schema.tables.table_schema == influxframework.conf.db_name) + .run() + ) + + database_tables = [x[0] for x in queried_result] + doctype_tables = influxframework.get_all("DocType", pluck="name") + + for x in database_tables: + doctype = x.replace("tab", "", 1) + if not (doctype in doctype_tables or x.startswith("__") or x in STANDARD_TABLES): + TABLES_TO_DROP.append(x) + + if not TABLES_TO_DROP: + if format == "text": + click.secho(f"No ghost tables found in {influxframework.local.site}...Great!", fg="green") + else: + if not (no_backup or dry_run): + if format == "text": + print(f"Backing Up Tables: {', '.join(TABLES_TO_DROP)}") + + odb = scheduled_backup( + ignore_conf=False, + include_doctypes=",".join(x.replace("tab", "", 1) for x in TABLES_TO_DROP), + ignore_files=True, + force=True, + ) + if format == "text": + odb.print_summary() + print("\nTrimming Database") + + for table in TABLES_TO_DROP: + if format == "text": + print(f"* Dropping Table '{table}'...") + if not dry_run: + influxframework.db.sql_ddl(f"drop table `{table}`") + + ALL_DATA[influxframework.local.site] = TABLES_TO_DROP + influxframework.destroy() + + if format == "json": + import json + + print(json.dumps(ALL_DATA, indent=1)) + + +def get_standard_tables(): + import re + + tables = [] + sql_file = os.path.join( + "..", + "apps", + "influxframework", + "influxframework", + "database", + influxframework.conf.db_type, + f"framework_{influxframework.conf.db_type}.sql", + ) + content = open(sql_file).read().splitlines() + + for line in content: + table_found = re.search(r"""CREATE TABLE ("|`)(.*)?("|`) \(""", line) + if table_found: + tables.append(table_found.group(2)) + + return tables + + +@click.command("trim-tables") +@click.option("--dry-run", is_flag=True, default=False, help="Show what would be deleted") +@click.option( + "--format", "-f", default="table", type=click.Choice(["json", "table"]), help="Output format" +) +@click.option("--no-backup", is_flag=True, default=False, help="Do not backup the site") +@pass_context +def trim_tables(context, dry_run, format, no_backup): + if not context.sites: + raise SiteNotSpecifiedError + + from influxframework.model.meta import trim_tables + from influxframework.utils.backups import scheduled_backup + + for site in context.sites: + influxframework.init(site=site) + influxframework.connect() + + if not (no_backup or dry_run): + click.secho(f"Taking backup for {influxframework.local.site}", fg="green") + odb = scheduled_backup(ignore_files=False, force=True) + odb.print_summary() + + try: + trimmed_data = trim_tables(dry_run=dry_run, quiet=format == "json") + + if format == "table" and not dry_run: + click.secho(f"The following data have been removed from {influxframework.local.site}", fg="green") + + handle_data(trimmed_data, format=format) + finally: + influxframework.destroy() + + +def handle_data(data: dict, format="json"): + if format == "json": + import json + + print(json.dumps({influxframework.local.site: data}, indent=1, sort_keys=True)) + else: + from influxframework.utils.commands import render_table + + data = [["DocType", "Fields"]] + [[table, ", ".join(columns)] for table, columns in data.items()] + render_table(data) + + +def add_new_user( + email, + first_name=None, + last_name=None, + user_type="System User", + send_welcome_email=False, + password=None, + role=None, +): + user = influxframework.new_doc("User") + user.update( + { + "name": email, + "email": email, + "enabled": 1, + "first_name": first_name or email, + "last_name": last_name, + "user_type": user_type, + "send_welcome_email": 1 if send_welcome_email else 0, + } + ) + user.insert() + user.add_roles(*role) + if password: + from influxframework.utils.password import update_password + + update_password(user=user.name, pwd=password) + + +commands = [ + add_system_manager, + add_user_for_sites, + backup, + drop_site, + install_app, + list_apps, + migrate, + migrate_to, + new_site, + reinstall, + reload_doc, + reload_doctype, + remove_from_installed_apps, + restore, + run_patch, + set_password, + set_admin_password, + uninstall, + disable_user, + _use, + set_last_active_for_user, + publish_realtime, + browse, + start_recording, + stop_recording, + add_to_hosts, + start_ngrok, + build_search_index, + partial_restore, + trim_tables, + trim_database, + clear_log_table, +] diff --git a/influxframework/commands/translate.py b/influxframework/commands/translate.py new file mode 100644 index 0000000..31838c0 --- /dev/null +++ b/influxframework/commands/translate.py @@ -0,0 +1,111 @@ +import click + +from influxframework.commands import get_site, pass_context +from influxframework.exceptions import SiteNotSpecifiedError + + +# translation +@click.command("build-message-files") +@pass_context +def build_message_files(context): + "Build message files for translation" + import influxframework.translate + + for site in context.sites: + try: + influxframework.init(site=site) + influxframework.connect() + influxframework.translate.rebuild_all_translation_files() + finally: + influxframework.destroy() + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("new-language") # , help="Create lang-code.csv for given app") +@pass_context +@click.argument("lang_code") # , help="Language code eg. en") +@click.argument("app") # , help="App name eg. influxframework") +def new_language(context, lang_code, app): + """Create lang-code.csv for given app""" + import influxframework.translate + + if not context["sites"]: + raise Exception("--site is required") + + # init site + influxframework.connect(site=context["sites"][0]) + influxframework.translate.write_translations_file(app, lang_code) + + print( + "File created at ./apps/{app}/{app}/translations/{lang_code}.csv".format( + app=app, lang_code=lang_code + ) + ) + print( + "You will need to add the language in influxframework/geo/languages.json, if you haven't done it already." + ) + + +@click.command("get-untranslated") +@click.option("--app", default="_ALL_APPS") +@click.argument("lang") +@click.argument("untranslated_file") +@click.option("--all", default=False, is_flag=True, help="Get all message strings") +@pass_context +def get_untranslated(context, lang, untranslated_file, app="_ALL_APPS", all=None): + "Get untranslated strings for language" + import influxframework.translate + + site = get_site(context) + try: + influxframework.init(site=site) + influxframework.connect() + influxframework.translate.get_untranslated(lang, untranslated_file, get_all=all, app=app) + finally: + influxframework.destroy() + + +@click.command("update-translations") +@click.option("--app", default="_ALL_APPS") +@click.argument("lang") +@click.argument("untranslated_file") +@click.argument("translated-file") +@pass_context +def update_translations(context, lang, untranslated_file, translated_file, app="_ALL_APPS"): + "Update translated strings" + import influxframework.translate + + site = get_site(context) + try: + influxframework.init(site=site) + influxframework.connect() + influxframework.translate.update_translations(lang, untranslated_file, translated_file, app=app) + finally: + influxframework.destroy() + + +@click.command("import-translations") +@click.argument("lang") +@click.argument("path") +@pass_context +def import_translations(context, lang, path): + "Update translated strings" + import influxframework.translate + + site = get_site(context) + try: + influxframework.init(site=site) + influxframework.connect() + influxframework.translate.import_translations(lang, path) + finally: + influxframework.destroy() + + +commands = [ + build_message_files, + get_untranslated, + import_translations, + new_language, + update_translations, +] diff --git a/influxframework/commands/utils.py b/influxframework/commands/utils.py new file mode 100644 index 0000000..b9e511b --- /dev/null +++ b/influxframework/commands/utils.py @@ -0,0 +1,1180 @@ +import json +import os +import subprocess +import sys +from distutils.spawn import find_executable + +import click + +import influxframework +from influxframework.commands import get_site, pass_context +from influxframework.coverage import CodeCoverage +from influxframework.exceptions import SiteNotSpecifiedError +from influxframework.utils import cint, update_progress_bar + +DATA_IMPORT_DEPRECATION = ( + "[DEPRECATED] The `import-csv` command used 'Data Import Legacy' which has been deprecated.\n" + "Use `data-import` command instead to import data via 'Data Import'." +) + + +@click.command("build") +@click.option("--app", help="Build assets for app") +@click.option("--apps", help="Build assets for specific apps") +@click.option( + "--hard-link", is_flag=True, default=False, help="Copy the files instead of symlinking" +) +@click.option( + "--make-copy", + is_flag=True, + default=False, + help="[DEPRECATED] Copy the files instead of symlinking", +) +@click.option( + "--restore", + is_flag=True, + default=False, + help="[DEPRECATED] Copy the files instead of symlinking with force", +) +@click.option("--production", is_flag=True, default=False, help="Build assets in production mode") +@click.option("--verbose", is_flag=True, default=False, help="Verbose") +@click.option( + "--force", is_flag=True, default=False, help="Force build assets instead of downloading available" +) +def build( + app=None, + apps=None, + hard_link=False, + make_copy=False, + restore=False, + production=False, + verbose=False, + force=False, +): + "Compile JS and CSS source files" + from influxframework.build import bundle, download_influxframework_assets + + influxframework.init("") + + if not apps and app: + apps = app + + # dont try downloading assets if force used, app specified or running via CI + if not (force or apps or os.environ.get("CI")): + # skip building influxframework if assets exist remotely + skip_influxframework = download_influxframework_assets(verbose=verbose) + else: + skip_influxframework = False + + # don't minify in developer_mode for faster builds + development = influxframework.local.conf.developer_mode or influxframework.local.dev_server + mode = "development" if development else "production" + if production: + mode = "production" + + if make_copy or restore: + hard_link = make_copy or restore + click.secho( + "bench build: --make-copy and --restore options are deprecated in favour of --hard-link", + fg="yellow", + ) + + bundle(mode, apps=apps, hard_link=hard_link, verbose=verbose, skip_influxframework=skip_influxframework) + + +@click.command("watch") +@click.option("--apps", help="Watch assets for specific apps") +def watch(apps=None): + "Watch and compile JS and CSS files as and when they change" + from influxframework.build import watch + + influxframework.init("") + watch(apps) + + +@click.command("clear-cache") +@pass_context +def clear_cache(context): + "Clear cache, doctype cache and defaults" + import influxframework.sessions + from influxframework.desk.notifications import clear_notifications + from influxframework.website.utils import clear_website_cache + + for site in context.sites: + try: + influxframework.connect(site) + influxframework.clear_cache() + clear_notifications() + clear_website_cache() + finally: + influxframework.destroy() + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("clear-website-cache") +@pass_context +def clear_website_cache(context): + "Clear website cache" + from influxframework.website.utils import clear_website_cache + + for site in context.sites: + try: + influxframework.init(site=site) + influxframework.connect() + clear_website_cache() + finally: + influxframework.destroy() + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("destroy-all-sessions") +@click.option("--reason") +@pass_context +def destroy_all_sessions(context, reason=None): + "Clear sessions of all users (logs them out)" + import influxframework.sessions + + for site in context.sites: + try: + influxframework.init(site=site) + influxframework.connect() + influxframework.sessions.clear_all_sessions(reason) + influxframework.db.commit() + finally: + influxframework.destroy() + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("show-config") +@click.option("--format", "-f", type=click.Choice(["text", "json"]), default="text") +@pass_context +def show_config(context, format): + "Print configuration file to STDOUT in speified format" + + if not context.sites: + raise SiteNotSpecifiedError + + sites_config = {} + sites_path = os.getcwd() + + from influxframework.utils.commands import render_table + + def transform_config(config, prefix=None): + prefix = f"{prefix}." if prefix else "" + site_config = [] + + for conf, value in config.items(): + if isinstance(value, dict): + site_config += transform_config(value, prefix=f"{prefix}{conf}") + else: + log_value = json.dumps(value) if isinstance(value, list) else value + site_config += [[f"{prefix}{conf}", log_value]] + + return site_config + + for site in context.sites: + influxframework.init(site) + + if len(context.sites) != 1 and format == "text": + if context.sites.index(site) != 0: + click.echo() + click.secho(f"Site {site}", fg="yellow") + + configuration = influxframework.get_site_config(sites_path=sites_path, site_path=site) + + if format == "text": + data = transform_config(configuration) + data.insert(0, ["Config", "Value"]) + render_table(data) + + if format == "json": + sites_config[site] = configuration + + influxframework.destroy() + + if format == "json": + click.echo(influxframework.as_json(sites_config)) + + +@click.command("reset-perms") +@pass_context +def reset_perms(context): + "Reset permissions for all doctypes" + from influxframework.permissions import reset_perms + + for site in context.sites: + try: + influxframework.init(site=site) + influxframework.connect() + for d in influxframework.db.sql_list( + """select name from `tabDocType` + where istable=0 and custom=0""" + ): + influxframework.clear_cache(doctype=d) + reset_perms(d) + finally: + influxframework.destroy() + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("execute") +@click.argument("method") +@click.option("--args") +@click.option("--kwargs") +@click.option("--profile", is_flag=True, default=False) +@pass_context +def execute(context, method, args=None, kwargs=None, profile=False): + "Execute a function" + for site in context.sites: + ret = "" + try: + influxframework.init(site=site) + influxframework.connect() + + if args: + try: + args = eval(args) + except NameError: + args = [args] + else: + args = () + + if kwargs: + kwargs = eval(kwargs) + else: + kwargs = {} + + if profile: + import cProfile + + pr = cProfile.Profile() + pr.enable() + + try: + ret = influxframework.get_attr(method)(*args, **kwargs) + except Exception: + ret = influxframework.safe_eval( + method + "(*args, **kwargs)", eval_globals=globals(), eval_locals=locals() + ) + + if profile: + import pstats + from io import StringIO + + pr.disable() + s = StringIO() + pstats.Stats(pr, stream=s).sort_stats("cumulative").print_stats(0.5) + print(s.getvalue()) + + if influxframework.db: + influxframework.db.commit() + finally: + influxframework.destroy() + if ret: + from influxframework.utils.response import json_handler + + print(json.dumps(ret, default=json_handler)) + + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("add-to-email-queue") +@click.argument("email-path") +@pass_context +def add_to_email_queue(context, email_path): + "Add an email to the Email Queue" + site = get_site(context) + + if os.path.isdir(email_path): + with influxframework.init_site(site): + influxframework.connect() + for email in os.listdir(email_path): + with open(os.path.join(email_path, email)) as email_data: + kwargs = json.load(email_data) + kwargs["delayed"] = True + influxframework.sendmail(**kwargs) + influxframework.db.commit() + + +@click.command("export-doc") +@click.argument("doctype") +@click.argument("docname") +@pass_context +def export_doc(context, doctype, docname): + "Export a single document to csv" + import influxframework.modules + + for site in context.sites: + try: + influxframework.init(site=site) + influxframework.connect() + influxframework.modules.export_doc(doctype, docname) + finally: + influxframework.destroy() + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("export-json") +@click.argument("doctype") +@click.argument("path") +@click.option("--name", help="Export only one document") +@pass_context +def export_json(context, doctype, path, name=None): + "Export doclist as json to the given path, use '-' as name for Singles." + from influxframework.core.doctype.data_import.data_import import export_json + + for site in context.sites: + try: + influxframework.init(site=site) + influxframework.connect() + export_json(doctype, path, name=name) + finally: + influxframework.destroy() + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("export-csv") +@click.argument("doctype") +@click.argument("path") +@pass_context +def export_csv(context, doctype, path): + "Export data import template with data for DocType" + from influxframework.core.doctype.data_import.data_import import export_csv + + for site in context.sites: + try: + influxframework.init(site=site) + influxframework.connect() + export_csv(doctype, path) + finally: + influxframework.destroy() + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("export-fixtures") +@click.option("--app", default=None, help="Export fixtures of a specific app") +@pass_context +def export_fixtures(context, app=None): + "Export fixtures" + from influxframework.utils.fixtures import export_fixtures + + for site in context.sites: + try: + influxframework.init(site=site) + influxframework.connect() + export_fixtures(app=app) + finally: + influxframework.destroy() + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("import-doc") +@click.argument("path") +@pass_context +def import_doc(context, path, force=False): + "Import (insert/update) doclist. If the argument is a directory, all files ending with .json are imported" + from influxframework.core.doctype.data_import.data_import import import_doc + + if not os.path.exists(path): + path = os.path.join("..", path) + if not os.path.exists(path): + print(f"Invalid path {path}") + sys.exit(1) + + for site in context.sites: + try: + influxframework.init(site=site) + influxframework.connect() + import_doc(path) + finally: + influxframework.destroy() + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("import-csv", help=DATA_IMPORT_DEPRECATION) +@click.argument("path") +@click.option( + "--only-insert", default=False, is_flag=True, help="Do not overwrite existing records" +) +@click.option( + "--submit-after-import", default=False, is_flag=True, help="Submit document after importing it" +) +@click.option( + "--ignore-encoding-errors", + default=False, + is_flag=True, + help="Ignore encoding errors while coverting to unicode", +) +@click.option("--no-email", default=True, is_flag=True, help="Send email if applicable") +@pass_context +def import_csv( + context, + path, + only_insert=False, + submit_after_import=False, + ignore_encoding_errors=False, + no_email=True, +): + click.secho(DATA_IMPORT_DEPRECATION, fg="yellow") + sys.exit(1) + + +@click.command("data-import") +@click.option( + "--file", "file_path", type=click.Path(), required=True, help="Path to import file (.csv, .xlsx)" +) +@click.option("--doctype", type=str, required=True) +@click.option( + "--type", + "import_type", + type=click.Choice(["Insert", "Update"], case_sensitive=False), + default="Insert", + help="Insert New Records or Update Existing Records", +) +@click.option( + "--submit-after-import", default=False, is_flag=True, help="Submit document after importing it" +) +@click.option("--mute-emails", default=True, is_flag=True, help="Mute emails during import") +@pass_context +def data_import( + context, file_path, doctype, import_type=None, submit_after_import=False, mute_emails=True +): + "Import documents in bulk from CSV or XLSX using data import" + from influxframework.core.doctype.data_import.data_import import import_file + + site = get_site(context) + + influxframework.init(site=site) + influxframework.connect() + import_file(doctype, file_path, import_type, submit_after_import, console=True) + influxframework.destroy() + + +@click.command("bulk-rename") +@click.argument("doctype") +@click.argument("path") +@pass_context +def bulk_rename(context, doctype, path): + "Rename multiple records via CSV file" + from influxframework.model.rename_doc import bulk_rename + from influxframework.utils.csvutils import read_csv_content + + site = get_site(context) + + with open(path) as csvfile: + rows = read_csv_content(csvfile.read()) + + influxframework.init(site=site) + influxframework.connect() + + bulk_rename(doctype, rows, via_console=True) + + influxframework.destroy() + + +@click.command("db-console") +@pass_context +def database(context): + """ + Enter into the Database console for given site. + """ + site = get_site(context) + if not site: + raise SiteNotSpecifiedError + influxframework.init(site=site) + if not influxframework.conf.db_type or influxframework.conf.db_type == "mariadb": + _mariadb() + elif influxframework.conf.db_type == "postgres": + _psql() + + +@click.command("mariadb") +@pass_context +def mariadb(context): + """ + Enter into mariadb console for a given site. + """ + site = get_site(context) + if not site: + raise SiteNotSpecifiedError + influxframework.init(site=site) + _mariadb() + + +@click.command("postgres") +@pass_context +def postgres(context): + """ + Enter into postgres console for a given site. + """ + site = get_site(context) + influxframework.init(site=site) + _psql() + + +def _mariadb(): + from influxframework.database.mariadb.database import MariaDBDatabase + + mysql = find_executable("mysql") + command = [ + mysql, + "--port", + influxframework.conf.db_port or MariaDBDatabase.default_port, + "-u", + influxframework.conf.db_name, + f"-p{influxframework.conf.db_password}", + influxframework.conf.db_name, + "-h", + influxframework.conf.db_host or "localhost", + "--pager=less -SFX", + "--safe-updates", + "-A", + ] + os.execv(mysql, command) + + +def _psql(): + psql = find_executable("psql") + subprocess.run([psql, "-d", influxframework.conf.db_name]) + + +@click.command("jupyter") +@pass_context +def jupyter(context): + installed_packages = ( + r.split("==")[0] + for r in subprocess.check_output([sys.executable, "-m", "pip", "freeze"], encoding="utf8") + ) + + if "jupyter" not in installed_packages: + subprocess.check_output([sys.executable, "-m", "pip", "install", "jupyter"]) + + site = get_site(context) + influxframework.init(site=site) + + jupyter_notebooks_path = os.path.abspath(influxframework.get_site_path("jupyter_notebooks")) + sites_path = os.path.abspath(influxframework.get_site_path("..")) + + try: + os.stat(jupyter_notebooks_path) + except OSError: + print(f"Creating folder to keep jupyter notebooks at {jupyter_notebooks_path}") + os.mkdir(jupyter_notebooks_path) + bin_path = os.path.abspath("../env/bin") + print( + """ +Starting Jupyter notebook +Run the following in your first cell to connect notebook to influxframework +``` +import influxframework +influxframework.init(site='{site}', sites_path='{sites_path}') +influxframework.connect() +influxframework.local.lang = influxframework.db.get_default('lang') +influxframework.db.connect() +``` + """.format( + site=site, sites_path=sites_path + ) + ) + os.execv( + f"{bin_path}/jupyter", + [ + f"{bin_path}/jupyter", + "notebook", + jupyter_notebooks_path, + ], + ) + + +def _console_cleanup(): + # Execute rollback_observers on console close + influxframework.db.rollback() + influxframework.destroy() + + +@click.command("console") +@click.option("--autoreload", is_flag=True, help="Reload changes to code automatically") +@pass_context +def console(context, autoreload=False): + "Start ipython console for a site" + site = get_site(context) + influxframework.init(site=site) + influxframework.connect() + influxframework.local.lang = influxframework.db.get_default("lang") + + from atexit import register + + from IPython.terminal.embed import InteractiveShellEmbed + + register(_console_cleanup) + + terminal = InteractiveShellEmbed() + if autoreload: + terminal.extension_manager.load_extension("autoreload") + terminal.run_line_magic("autoreload", "2") + + all_apps = influxframework.get_installed_apps() + failed_to_import = [] + + for app in all_apps: + try: + locals()[app] = __import__(app) + except ModuleNotFoundError: + failed_to_import.append(app) + all_apps.remove(app) + + print("Apps in this namespace:\n{}".format(", ".join(all_apps))) + if failed_to_import: + print("\nFailed to import:\n{}".format(", ".join(failed_to_import))) + + terminal.colors = "neutral" + terminal.display_banner = False + terminal() + + +@click.command( + "transform-database", help="Change tables' internal settings changing engine and row formats" +) +@click.option( + "--table", + required=True, + help="Comma separated name of tables to convert. To convert all tables, pass 'all'", +) +@click.option( + "--engine", + default=None, + type=click.Choice(["InnoDB", "MyISAM"]), + help="Choice of storage engine for said table(s)", +) +@click.option( + "--row_format", + default=None, + type=click.Choice(["DYNAMIC", "COMPACT", "REDUNDANT", "COMPRESSED"]), + help="Set ROW_FORMAT parameter for said table(s)", +) +@click.option("--failfast", is_flag=True, default=False, help="Exit on first failure occurred") +@pass_context +def transform_database(context, table, engine, row_format, failfast): + "Transform site database through given parameters" + site = get_site(context) + check_table = [] + add_line = False + skipped = 0 + influxframework.init(site=site) + + if influxframework.conf.db_type and influxframework.conf.db_type != "mariadb": + click.secho("This command only has support for MariaDB databases at this point", fg="yellow") + sys.exit(1) + + if not (engine or row_format): + click.secho("Values for `--engine` or `--row_format` must be set") + sys.exit(1) + + influxframework.connect() + + if table == "all": + information_schema = influxframework.qb.Schema("information_schema") + queried_tables = ( + influxframework.qb.from_(information_schema.tables) + .select("table_name") + .where( + (information_schema.tables.row_format != row_format) + & (information_schema.tables.table_schema == influxframework.conf.db_name) + ) + .run() + ) + tables = [x[0] for x in queried_tables] + else: + tables = [x.strip() for x in table.split(",")] + + total = len(tables) + + for current, table in enumerate(tables): + values_to_set = "" + if engine: + values_to_set += f" ENGINE={engine}" + if row_format: + values_to_set += f" ROW_FORMAT={row_format}" + + try: + influxframework.db.sql(f"ALTER TABLE `{table}`{values_to_set}") + update_progress_bar("Updating table schema", current - skipped, total) + add_line = True + + except Exception as e: + check_table.append([table, e.args]) + skipped += 1 + + if failfast: + break + + if add_line: + print() + + for errored_table in check_table: + table, err = errored_table + err_msg = f"{table}: ERROR {err[0]}: {err[1]}" + click.secho(err_msg, fg="yellow") + + influxframework.destroy() + + +@click.command("run-tests") +@click.option("--app", help="For App") +@click.option("--doctype", help="For DocType") +@click.option("--module-def", help="For all Doctypes in Module Def") +@click.option("--case", help="Select particular TestCase") +@click.option( + "--doctype-list-path", + help="Path to .txt file for list of doctypes. Example influxerp/tests/server/agriculture.txt", +) +@click.option("--test", multiple=True, help="Specific test") +@click.option("--ui-tests", is_flag=True, default=False, help="Run UI Tests") +@click.option("--module", help="Run tests in a module") +@click.option("--profile", is_flag=True, default=False) +@click.option("--coverage", is_flag=True, default=False) +@click.option("--skip-test-records", is_flag=True, default=False, help="Don't create test records") +@click.option( + "--skip-before-tests", is_flag=True, default=False, help="Don't run before tests hook" +) +@click.option("--junit-xml-output", help="Destination file path for junit xml report") +@click.option( + "--failfast", is_flag=True, default=False, help="Stop the test run on the first error or failure" +) +@pass_context +def run_tests( + context, + app=None, + module=None, + doctype=None, + module_def=None, + test=(), + profile=False, + coverage=False, + junit_xml_output=False, + ui_tests=False, + doctype_list_path=None, + skip_test_records=False, + skip_before_tests=False, + failfast=False, + case=None, +): + + with CodeCoverage(coverage, app): + import influxframework + import influxframework.test_runner + + tests = test + site = get_site(context) + + allow_tests = influxframework.get_conf(site).allow_tests + + if not (allow_tests or os.environ.get("CI")): + click.secho("Testing is disabled for the site!", bold=True) + click.secho("You can enable tests by entering following command:") + click.secho(f"bench --site {site} set-config allow_tests true", fg="green") + return + + influxframework.init(site=site) + + influxframework.flags.skip_before_tests = skip_before_tests + influxframework.flags.skip_test_records = skip_test_records + + ret = influxframework.test_runner.main( + app, + module, + doctype, + module_def, + context.verbose, + tests=tests, + force=context.force, + profile=profile, + junit_xml_output=junit_xml_output, + ui_tests=ui_tests, + doctype_list_path=doctype_list_path, + failfast=failfast, + case=case, + ) + + if len(ret.failures) == 0 and len(ret.errors) == 0: + ret = 0 + + if os.environ.get("CI"): + sys.exit(ret) + + +@click.command("run-parallel-tests") +@click.option("--app", help="For App", default="influxframework") +@click.option("--build-number", help="Build number", default=1) +@click.option("--total-builds", help="Total number of builds", default=1) +@click.option("--with-coverage", is_flag=True, help="Build coverage file") +@click.option("--use-orchestrator", is_flag=True, help="Use orchestrator to run parallel tests") +@click.option("--dry-run", is_flag=True, default=False, help="Dont actually run tests") +@pass_context +def run_parallel_tests( + context, + app, + build_number, + total_builds, + with_coverage=False, + use_orchestrator=False, + dry_run=False, +): + with CodeCoverage(with_coverage, app): + site = get_site(context) + if use_orchestrator: + from influxframework.parallel_test_runner import ParallelTestWithOrchestrator + + ParallelTestWithOrchestrator(app, site=site) + else: + from influxframework.parallel_test_runner import ParallelTestRunner + + ParallelTestRunner( + app, + site=site, + build_number=build_number, + total_builds=total_builds, + dry_run=dry_run, + ) + + +@click.command( + "run-ui-tests", + context_settings=dict( + ignore_unknown_options=True, + ), +) +@click.argument("app") +@click.argument("cypressargs", nargs=-1, type=click.UNPROCESSED) +@click.option("--headless", is_flag=True, help="Run UI Test in headless mode") +@click.option("--parallel", is_flag=True, help="Run UI Test in parallel mode") +@click.option("--with-coverage", is_flag=True, help="Generate coverage report") +@click.option("--ci-build-id") +@pass_context +def run_ui_tests( + context, + app, + headless=False, + parallel=True, + with_coverage=False, + ci_build_id=None, + cypressargs=None, +): + "Run UI tests" + site = get_site(context) + app_base_path = os.path.abspath(os.path.join(influxframework.get_app_path(app), "..")) + site_url = influxframework.utils.get_site_url(site) + admin_password = influxframework.get_conf(site).admin_password + + # override baseUrl using env variable + site_env = f"CYPRESS_baseUrl={site_url}" + password_env = f"CYPRESS_adminPassword={admin_password}" if admin_password else "" + coverage_env = f"CYPRESS_coverage={str(with_coverage).lower()}" + + os.chdir(app_base_path) + + node_bin = subprocess.getoutput("npm bin") + cypress_path = f"{node_bin}/cypress" + drag_drop_plugin_path = f"{node_bin}/../@4tw/cypress-drag-drop" + real_events_plugin_path = f"{node_bin}/../cypress-real-events" + testing_library_path = f"{node_bin}/../@testing-library" + coverage_plugin_path = f"{node_bin}/../@cypress/code-coverage" + + # check if cypress in path...if not, install it. + if not ( + os.path.exists(cypress_path) + and os.path.exists(drag_drop_plugin_path) + and os.path.exists(real_events_plugin_path) + and os.path.exists(testing_library_path) + and os.path.exists(coverage_plugin_path) + ): + # install cypress & dependent plugins + click.secho("Installing Cypress...", fg="yellow") + packages = " ".join( + [ + "cypress@^10", + "@4tw/cypress-drag-drop@^2", + "cypress-real-events", + "@testing-library/cypress@^8", + "@testing-library/dom@8.17.1", + "@cypress/code-coverage@^3", + ] + ) + influxframework.commands.popen(f"yarn add {packages} --no-lockfile") + + # run for headless mode + run_or_open = "run --browser chrome" if headless else "open" + formatted_command = f"{site_env} {password_env} {coverage_env} {cypress_path} {run_or_open}" + + if parallel: + formatted_command += " --parallel" + + if ci_build_id: + formatted_command += f" --ci-build-id {ci_build_id}" + + if cypressargs: + formatted_command += " " + " ".join(cypressargs) + + click.secho("Running Cypress...", fg="yellow") + influxframework.commands.popen(formatted_command, cwd=app_base_path, raise_err=True) + + +@click.command("serve") +@click.option("--port", default=8000) +@click.option("--profile", is_flag=True, default=False) +@click.option("--noreload", "no_reload", is_flag=True, default=False) +@click.option("--nothreading", "no_threading", is_flag=True, default=False) +@click.option("--with-coverage", is_flag=True, default=False) +@pass_context +def serve( + context, + port=None, + profile=False, + no_reload=False, + no_threading=False, + sites_path=".", + site=None, + with_coverage=False, +): + "Start development web server" + import influxframework.app + + if not context.sites: + site = None + else: + site = context.sites[0] + with CodeCoverage(with_coverage, "influxframework"): + if with_coverage: + # unable to track coverage with threading enabled + no_threading = True + no_reload = True + influxframework.app.serve( + port=port, + profile=profile, + no_reload=no_reload, + no_threading=no_threading, + site=site, + sites_path=".", + ) + + +@click.command("request") +@click.option("--args", help="arguments like `?cmd=test&key=value` or `/api/request/method?..`") +@click.option("--path", help="path to request JSON") +@pass_context +def request(context, args=None, path=None): + "Run a request as an admin" + import influxframework.api + import influxframework.handler + + for site in context.sites: + try: + influxframework.init(site=site) + influxframework.connect() + if args: + if "?" in args: + influxframework.local.form_dict = influxframework._dict([a.split("=") for a in args.split("?")[-1].split("&")]) + else: + influxframework.local.form_dict = influxframework._dict() + + if args.startswith("/api/method"): + influxframework.local.form_dict.cmd = args.split("?")[0].split("/")[-1] + elif path: + with open(os.path.join("..", path)) as f: + args = json.loads(f.read()) + + influxframework.local.form_dict = influxframework._dict(args) + + influxframework.handler.execute_cmd(influxframework.form_dict.cmd) + + print(influxframework.response) + finally: + influxframework.destroy() + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("make-app") +@click.argument("destination") +@click.argument("app_name") +@click.option( + "--no-git", is_flag=True, default=False, help="Do not initialize git repository for the app" +) +def make_app(destination, app_name, no_git=False): + "Creates a boilerplate app" + from influxframework.utils.boilerplate import make_boilerplate + + make_boilerplate(destination, app_name, no_git=no_git) + + +@click.command("set-config") +@click.argument("key") +@click.argument("value") +@click.option( + "-g", "--global", "global_", is_flag=True, default=False, help="Set value in bench config" +) +@click.option("-p", "--parse", is_flag=True, default=False, help="Evaluate as Python Object") +@click.option("--as-dict", is_flag=True, default=False, help="Legacy: Evaluate as Python Object") +@pass_context +def set_config(context, key, value, global_=False, parse=False, as_dict=False): + "Insert/Update a value in site_config.json" + from influxframework.installer import update_site_config + + if as_dict: + from influxframework.utils.commands import warn + + warn( + "--as-dict will be deprecated in v14. Use --parse instead", category=PendingDeprecationWarning + ) + parse = as_dict + + if parse: + import ast + + value = ast.literal_eval(value) + + if global_: + sites_path = os.getcwd() + common_site_config_path = os.path.join(sites_path, "common_site_config.json") + update_site_config(key, value, validate=False, site_config_path=common_site_config_path) + else: + for site in context.sites: + influxframework.init(site=site) + update_site_config(key, value, validate=False) + influxframework.destroy() + + +@click.command("version") +@click.option( + "-f", + "--format", + "output", + type=click.Choice(["plain", "table", "json", "legacy"]), + help="Output format", + default="legacy", +) +def get_version(output): + """Show the versions of all the installed apps.""" + from git import Repo + from git.exc import InvalidGitRepositoryError + + from influxframework.utils.change_log import get_app_branch + from influxframework.utils.commands import render_table + + influxframework.init("") + data = [] + + for app in sorted(influxframework.get_all_apps()): + module = influxframework.get_module(app) + app_hooks = influxframework.get_module(app + ".hooks") + + app_info = influxframework._dict() + + try: + app_info.commit = Repo(influxframework.get_app_path(app, "..")).head.object.hexsha[:7] + except InvalidGitRepositoryError: + app_info.commit = "" + + app_info.app = app + app_info.branch = get_app_branch(app) + app_info.version = getattr(app_hooks, f"{app_info.branch}_version", None) or module.__version__ + + data.append(app_info) + + { + "legacy": lambda: [click.echo(f"{app_info.app} {app_info.version}") for app_info in data], + "plain": lambda: [ + click.echo(f"{app_info.app} {app_info.version} {app_info.branch} ({app_info.commit})") + for app_info in data + ], + "table": lambda: render_table( + [["App", "Version", "Branch", "Commit"]] + + [[app_info.app, app_info.version, app_info.branch, app_info.commit] for app_info in data] + ), + "json": lambda: click.echo(json.dumps(data, indent=4)), + }[output]() + + +@click.command("rebuild-global-search") +@click.option( + "--static-pages", is_flag=True, default=False, help="Rebuild global search for static pages" +) +@pass_context +def rebuild_global_search(context, static_pages=False): + """Setup help table in the current site (called after migrate)""" + from influxframework.utils.global_search import ( + add_route_to_global_search, + get_doctypes_with_global_search, + get_routes_to_index, + rebuild_for_doctype, + sync_global_search, + ) + + for site in context.sites: + try: + influxframework.init(site) + influxframework.connect() + + if static_pages: + routes = get_routes_to_index() + for i, route in enumerate(routes): + add_route_to_global_search(route) + influxframework.local.request = None + update_progress_bar("Rebuilding Global Search", i, len(routes)) + sync_global_search() + else: + doctypes = get_doctypes_with_global_search() + for i, doctype in enumerate(doctypes): + rebuild_for_doctype(doctype) + update_progress_bar("Rebuilding Global Search", i, len(doctypes)) + + finally: + influxframework.destroy() + if not context.sites: + raise SiteNotSpecifiedError + + +commands = [ + build, + clear_cache, + clear_website_cache, + database, + transform_database, + jupyter, + console, + destroy_all_sessions, + execute, + export_csv, + export_doc, + export_fixtures, + export_json, + get_version, + import_csv, + data_import, + import_doc, + make_app, + mariadb, + postgres, + request, + reset_perms, + run_tests, + run_ui_tests, + serve, + set_config, + show_config, + watch, + bulk_rename, + add_to_email_queue, + rebuild_global_search, + run_parallel_tests, +] diff --git a/influxframework/config/__init__.py b/influxframework/config/__init__.py new file mode 100644 index 0000000..b87d5ca --- /dev/null +++ b/influxframework/config/__init__.py @@ -0,0 +1,77 @@ +import influxframework +from influxframework import _ +from influxframework.desk.moduleview import ( + config_exists, + get_data, + get_module_link_items_from_list, + get_onboard_items, +) + + +def get_modules_from_all_apps_for_user(user=None): + if not user: + user = influxframework.session.user + + all_modules = get_modules_from_all_apps() + global_blocked_modules = influxframework.get_doc("User", "Administrator").get_blocked_modules() + user_blocked_modules = influxframework.get_doc("User", user).get_blocked_modules() + blocked_modules = global_blocked_modules + user_blocked_modules + allowed_modules_list = [m for m in all_modules if m.get("module_name") not in blocked_modules] + + empty_tables_by_module = get_all_empty_tables_by_module() + + for module in allowed_modules_list: + module_name = module.get("module_name") + + # Apply onboarding status + if module_name in empty_tables_by_module: + module["onboard_present"] = 1 + + # Set defaults links + module["links"] = get_onboard_items(module["app"], influxframework.scrub(module_name))[:5] + + return allowed_modules_list + + +def get_modules_from_all_apps(): + modules_list = [] + for app in influxframework.get_installed_apps(): + modules_list += get_modules_from_app(app) + return modules_list + + +def get_modules_from_app(app): + return influxframework.get_all( + "Module Def", filters={"app_name": app}, fields=["module_name", "app_name as app"] + ) + + +def get_all_empty_tables_by_module(): + table_rows = influxframework.qb.Field("table_rows") + table_name = influxframework.qb.Field("table_name") + information_schema = influxframework.qb.Schema("information_schema") + + empty_tables = ( + influxframework.qb.from_(information_schema.tables).select(table_name).where(table_rows == 0) + ).run() + + empty_tables = {r[0] for r in empty_tables} + + results = influxframework.get_all("DocType", fields=["name", "module"]) + empty_tables_by_module = {} + + for doctype, module in results: + if "tab" + doctype in empty_tables: + if module in empty_tables_by_module: + empty_tables_by_module[module].append(doctype) + else: + empty_tables_by_module[module] = [doctype] + return empty_tables_by_module + + +def is_domain(module): + return module.get("category") == "Domains" + + +def is_module(module): + return module.get("type") == "module" diff --git a/influxframework/contacts/__init__.py b/influxframework/contacts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/contacts/address_and_contact.py b/influxframework/contacts/address_and_contact.py new file mode 100644 index 0000000..3c78756 --- /dev/null +++ b/influxframework/contacts/address_and_contact.py @@ -0,0 +1,205 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +import functools +import re + +import influxframework +from influxframework import _ + + +def load_address_and_contact(doc, key=None): + """Loads address list and contact list in `__onload`""" + from influxframework.contacts.doctype.address.address import get_address_display, get_condensed_address + + filters = [ + ["Dynamic Link", "link_doctype", "=", doc.doctype], + ["Dynamic Link", "link_name", "=", doc.name], + ["Dynamic Link", "parenttype", "=", "Address"], + ] + address_list = influxframework.get_list("Address", filters=filters, fields=["*"], order_by="creation asc") + + address_list = [a.update({"display": get_address_display(a)}) for a in address_list] + + address_list = sorted( + address_list, + key=functools.cmp_to_key( + lambda a, b: (int(a.is_primary_address - b.is_primary_address)) + or (1 if a.modified - b.modified else 0) + ), + reverse=True, + ) + + doc.set_onload("addr_list", address_list) + + contact_list = [] + filters = [ + ["Dynamic Link", "link_doctype", "=", doc.doctype], + ["Dynamic Link", "link_name", "=", doc.name], + ["Dynamic Link", "parenttype", "=", "Contact"], + ] + contact_list = influxframework.get_list("Contact", filters=filters, fields=["*"]) + + for contact in contact_list: + contact["email_ids"] = influxframework.get_all( + "Contact Email", + filters={"parenttype": "Contact", "parent": contact.name, "is_primary": 0}, + fields=["email_id"], + ) + + contact["phone_nos"] = influxframework.get_all( + "Contact Phone", + filters={ + "parenttype": "Contact", + "parent": contact.name, + "is_primary_phone": 0, + "is_primary_mobile_no": 0, + }, + fields=["phone"], + ) + + if contact.address: + address = influxframework.get_doc("Address", contact.address) + contact["address"] = get_condensed_address(address) + + contact_list = sorted( + contact_list, + key=functools.cmp_to_key( + lambda a, b: (int(a.is_primary_contact - b.is_primary_contact)) + or (1 if a.modified - b.modified else 0) + ), + reverse=True, + ) + + doc.set_onload("contact_list", contact_list) + + +def has_permission(doc, ptype, user): + links = get_permitted_and_not_permitted_links(doc.doctype) + if not links.get("not_permitted_links"): + # optimization: don't determine permissions based on link fields + return True + + # True if any one is True or all are empty + names = [] + for df in links.get("permitted_links") + links.get("not_permitted_links"): + doctype = df.options + name = doc.get(df.fieldname) + names.append(name) + + if name and influxframework.has_permission(doctype, ptype, doc=name): + return True + + if not any(names): + return True + return False + + +def get_permission_query_conditions_for_contact(user): + return get_permission_query_conditions("Contact") + + +def get_permission_query_conditions_for_address(user): + return get_permission_query_conditions("Address") + + +def get_permission_query_conditions(doctype): + links = get_permitted_and_not_permitted_links(doctype) + + if not links.get("not_permitted_links"): + # when everything is permitted, don't add additional condition + return "" + + elif not links.get("permitted_links"): + conditions = [] + + # when everything is not permitted + for df in links.get("not_permitted_links"): + # like ifnull(customer, '')='' and ifnull(supplier, '')='' + conditions.append(f"ifnull(`tab{doctype}`.`{df.fieldname}`, '')=''") + + return "( " + " and ".join(conditions) + " )" + + else: + conditions = [] + + for df in links.get("permitted_links"): + # like ifnull(customer, '')!='' or ifnull(supplier, '')!='' + conditions.append(f"ifnull(`tab{doctype}`.`{df.fieldname}`, '')!=''") + + return "( " + " or ".join(conditions) + " )" + + +def get_permitted_and_not_permitted_links(doctype): + permitted_links = [] + not_permitted_links = [] + + meta = influxframework.get_meta(doctype) + allowed_doctypes = influxframework.permissions.get_doctypes_with_read() + + for df in meta.get_link_fields(): + if df.options not in ("Customer", "Supplier", "Company", "Sales Partner"): + continue + + if df.options in allowed_doctypes: + permitted_links.append(df) + else: + not_permitted_links.append(df) + + return {"permitted_links": permitted_links, "not_permitted_links": not_permitted_links} + + +def delete_contact_and_address(doctype, docname): + for parenttype in ("Contact", "Address"): + items = influxframework.db.sql_list( + """select parent from `tabDynamic Link` + where parenttype=%s and link_doctype=%s and link_name=%s""", + (parenttype, doctype, docname), + ) + + for name in items: + doc = influxframework.get_doc(parenttype, name) + if len(doc.links) == 1: + doc.delete() + + +@influxframework.whitelist() +@influxframework.validate_and_sanitize_search_inputs +def filter_dynamic_link_doctypes( + doctype, txt: str, searchfield, start, page_len, filters: dict +) -> list[list[str]]: + from influxframework.permissions import get_doctypes_with_read + + txt = txt or "" + filters = filters or {} + + _doctypes_from_df = influxframework.get_all( + "DocField", + filters=filters, + pluck="parent", + distinct=True, + order_by=None, + ) + doctypes_from_df = {d for d in _doctypes_from_df if txt.lower() in _(d).lower()} + + filters.update({"dt": ("not in", doctypes_from_df)}) + _doctypes_from_cdf = influxframework.get_all( + "Custom Field", filters=filters, pluck="dt", distinct=True, order_by=None + ) + doctypes_from_cdf = {d for d in _doctypes_from_cdf if txt.lower() in _(d).lower()} + + all_doctypes = doctypes_from_df.union(doctypes_from_cdf) + allowed_doctypes = set(get_doctypes_with_read()) + + valid_doctypes = sorted(all_doctypes.intersection(allowed_doctypes)) + + return [[doctype] for doctype in valid_doctypes] + + +def set_link_title(doc): + if not doc.links: + return + for link in doc.links: + if not link.link_title: + linked_doc = influxframework.get_doc(link.link_doctype, link.link_name) + link.link_title = linked_doc.get_title() or link.link_name diff --git a/influxframework/contacts/doctype/__init__.py b/influxframework/contacts/doctype/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/contacts/doctype/address/__init__.py b/influxframework/contacts/doctype/address/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/contacts/doctype/address/address.js b/influxframework/contacts/doctype/address/address.js new file mode 100644 index 0000000..a124342 --- /dev/null +++ b/influxframework/contacts/doctype/address/address.js @@ -0,0 +1,75 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Address", { + refresh: function (frm) { + if (frm.doc.__islocal) { + const last_doc = influxframework.contacts.get_last_doc(frm); + if ( + influxframework.dynamic_link && + influxframework.dynamic_link.doc && + influxframework.dynamic_link.doc.name == last_doc.docname + ) { + frm.set_value("links", ""); + frm.add_child("links", { + link_doctype: influxframework.dynamic_link.doctype, + link_name: influxframework.dynamic_link.doc[influxframework.dynamic_link.fieldname], + }); + } + } + frm.set_query("link_doctype", "links", function () { + return { + query: "influxframework.contacts.address_and_contact.filter_dynamic_link_doctypes", + filters: { + fieldtype: "HTML", + fieldname: "address_html", + }, + }; + }); + frm.refresh_field("links"); + + if (frm.doc.links) { + for (let i in frm.doc.links) { + let link = frm.doc.links[i]; + frm.add_custom_button( + __("{0}: {1}", [__(link.link_doctype), __(link.link_name)]), + function () { + influxframework.set_route("Form", link.link_doctype, link.link_name); + }, + __("Links") + ); + } + } + }, + validate: function (frm) { + // clear linked customer / supplier / sales partner on saving... + if (frm.doc.links) { + frm.doc.links.forEach(function (d) { + influxframework.model.remove_from_locals(d.link_doctype, d.link_name); + }); + } + }, + after_save: function (frm) { + influxframework.run_serially([ + () => influxframework.timeout(1), + () => { + const last_doc = influxframework.contacts.get_last_doc(frm); + if ( + influxframework.dynamic_link && + influxframework.dynamic_link.doc && + influxframework.dynamic_link.doc.name == last_doc.docname + ) { + for (let i in frm.doc.links) { + let link = frm.doc.links[i]; + if ( + last_doc.doctype == link.link_doctype && + last_doc.docname == link.link_name + ) { + influxframework.set_route("Form", last_doc.doctype, last_doc.docname); + } + } + } + }, + ]); + }, +}); diff --git a/influxframework/contacts/doctype/address/address.json b/influxframework/contacts/doctype/address/address.json new file mode 100644 index 0000000..e85a89f --- /dev/null +++ b/influxframework/contacts/doctype/address/address.json @@ -0,0 +1,216 @@ +{ + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "creation": "2013-01-10 16:34:32", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "address_details", + "address_title", + "address_type", + "address_line1", + "address_line2", + "city", + "county", + "state", + "country", + "pincode", + "column_break0", + "email_id", + "phone", + "fax", + "is_primary_address", + "is_shipping_address", + "disabled", + "linked_with", + "links" + ], + "fields": [ + { + "fieldname": "address_details", + "fieldtype": "Section Break", + "options": "fa fa-map-marker" + }, + { + "fieldname": "address_title", + "fieldtype": "Data", + "label": "Address Title" + }, + { + "fieldname": "address_type", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Address Type", + "options": "Billing\nShipping\nOffice\nPersonal\nPlant\nPostal\nShop\nSubsidiary\nWarehouse\nCurrent\nPermanent\nOther", + "reqd": 1 + }, + { + "fieldname": "address_line1", + "fieldtype": "Data", + "label": "Address Line 1", + "reqd": 1 + }, + { + "fieldname": "address_line2", + "fieldtype": "Data", + "label": "Address Line 2" + }, + { + "fieldname": "city", + "fieldtype": "Data", + "in_global_search": 1, + "in_list_view": 1, + "label": "City/Town", + "reqd": 1, + "search_index": 1 + }, + { + "fieldname": "county", + "fieldtype": "Data", + "label": "County" + }, + { + "fieldname": "state", + "fieldtype": "Data", + "label": "State/Province" + }, + { + "fieldname": "country", + "fieldtype": "Link", + "in_global_search": 1, + "in_standard_filter": 1, + "label": "Country", + "options": "Country", + "reqd": 1, + "search_index": 1 + }, + { + "fieldname": "pincode", + "fieldtype": "Data", + "label": "Postal Code", + "search_index": 1 + }, + { + "fieldname": "column_break0", + "fieldtype": "Column Break", + "width": "50%" + }, + { + "fieldname": "email_id", + "fieldtype": "Data", + "label": "Email Address", + "options": "Email" + }, + { + "fieldname": "phone", + "fieldtype": "Data", + "label": "Phone" + }, + { + "fieldname": "fax", + "fieldtype": "Data", + "label": "Fax" + }, + { + "default": "0", + "fieldname": "is_primary_address", + "fieldtype": "Check", + "label": "Preferred Billing Address" + }, + { + "default": "0", + "fieldname": "is_shipping_address", + "fieldtype": "Check", + "label": "Preferred Shipping Address" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fieldname": "linked_with", + "fieldtype": "Section Break", + "label": "Reference", + "options": "fa fa-pushpin" + }, + { + "fieldname": "links", + "fieldtype": "Table", + "label": "Links", + "options": "Dynamic Link" + } + ], + "icon": "fa fa-map-marker", + "idx": 5, + "links": [], + "modified": "2020-10-21 16:14:37.284830", + "modified_by": "Administrator", + "module": "Contacts", + "name": "Address", + "name_case": "Title Case", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase User", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Maintenance User", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 1, + "share": 1, + "write": 1 + } + ], + "search_fields": "country, state", + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/influxframework/contacts/doctype/address/address.py b/influxframework/contacts/doctype/address/address.py new file mode 100644 index 0000000..ee99626 --- /dev/null +++ b/influxframework/contacts/doctype/address/address.py @@ -0,0 +1,291 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +from jinja2 import TemplateSyntaxError + +import influxframework +from influxframework import _, throw +from influxframework.contacts.address_and_contact import set_link_title +from influxframework.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links +from influxframework.model.document import Document +from influxframework.model.naming import make_autoname +from influxframework.utils import cstr + + +class Address(Document): + def __setup__(self): + self.flags.linked = False + + def autoname(self): + if not self.address_title: + if self.links: + self.address_title = self.links[0].link_name + + if self.address_title: + self.name = cstr(self.address_title).strip() + "-" + cstr(_(self.address_type)).strip() + if influxframework.db.exists("Address", self.name): + self.name = make_autoname( + cstr(self.address_title).strip() + "-" + cstr(self.address_type).strip() + "-.#" + ) + else: + throw(_("Address Title is mandatory.")) + + def validate(self): + self.link_address() + self.validate_preferred_address() + set_link_title(self) + deduplicate_dynamic_links(self) + + def link_address(self): + """Link address based on owner""" + if not self.links: + contact_name = influxframework.db.get_value("Contact", {"email_id": self.owner}) + if contact_name: + contact = influxframework.get_cached_doc("Contact", contact_name) + for link in contact.links: + self.append("links", dict(link_doctype=link.link_doctype, link_name=link.link_name)) + return True + + return False + + def validate_preferred_address(self): + preferred_fields = ["is_primary_address", "is_shipping_address"] + + for field in preferred_fields: + if self.get(field): + for link in self.links: + address = get_preferred_address(link.link_doctype, link.link_name, field) + + if address: + update_preferred_address(address, field) + + def get_display(self): + return get_address_display(self.as_dict()) + + def has_link(self, doctype, name): + for link in self.links: + if link.link_doctype == doctype and link.link_name == name: + return True + + def has_common_link(self, doc): + reference_links = [(link.link_doctype, link.link_name) for link in doc.links] + for link in self.links: + if (link.link_doctype, link.link_name) in reference_links: + return True + + return False + + +def get_preferred_address(doctype, name, preferred_key="is_primary_address"): + if preferred_key in ["is_shipping_address", "is_primary_address"]: + address = influxframework.db.sql( + """ SELECT + addr.name + FROM + `tabAddress` addr, `tabDynamic Link` dl + WHERE + dl.parent = addr.name and dl.link_doctype = %s and + dl.link_name = %s and ifnull(addr.disabled, 0) = 0 and + %s = %s + """ + % ("%s", "%s", preferred_key, "%s"), + (doctype, name, 1), + as_dict=1, + ) + + if address: + return address[0].name + + return + + +@influxframework.whitelist() +def get_default_address(doctype, name, sort_key="is_primary_address"): + """Returns default Address name for the given doctype, name""" + if sort_key not in ["is_shipping_address", "is_primary_address"]: + return None + + out = influxframework.db.sql( + """ SELECT + addr.name, addr.%s + FROM + `tabAddress` addr, `tabDynamic Link` dl + WHERE + dl.parent = addr.name and dl.link_doctype = %s and + dl.link_name = %s and ifnull(addr.disabled, 0) = 0 + """ + % (sort_key, "%s", "%s"), + (doctype, name), + as_dict=True, + ) + + if out: + for contact in out: + if contact.get(sort_key): + return contact.name + return out[0].name + else: + return None + + +@influxframework.whitelist() +def get_address_display(address_dict): + if not address_dict: + return + + if not isinstance(address_dict, dict): + address_dict = influxframework.db.get_value("Address", address_dict, "*", as_dict=True, cache=True) or {} + + name, template = get_address_templates(address_dict) + + try: + return influxframework.render_template(template, address_dict) + except TemplateSyntaxError: + influxframework.throw(_("There is an error in your Address Template {0}").format(name)) + + +def get_territory_from_address(address): + """Tries to match city, state and country of address to existing territory""" + if not address: + return + + if isinstance(address, str): + address = influxframework.get_cached_doc("Address", address) + + territory = None + for fieldname in ("city", "state", "country"): + if address.get(fieldname): + territory = influxframework.db.get_value("Territory", address.get(fieldname)) + if territory: + break + + return territory + + +def get_list_context(context=None): + return { + "title": _("Addresses"), + "get_list": get_address_list, + "row_template": "templates/includes/address_row.html", + "no_breadcrumbs": True, + } + + +def get_address_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by=None): + from influxframework.www.list import get_list + + user = influxframework.session.user + ignore_permissions = True + + if not filters: + filters = [] + filters.append(("Address", "owner", "=", user)) + + return get_list( + doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions + ) + + +def has_website_permission(doc, ptype, user, verbose=False): + """Returns true if there is a related lead or contact related to this document""" + contact_name = influxframework.db.get_value("Contact", {"email_id": influxframework.session.user}) + + if contact_name: + contact = influxframework.get_doc("Contact", contact_name) + return contact.has_common_link(doc) + + return False + + +def get_address_templates(address): + result = influxframework.db.get_value( + "Address Template", {"country": address.get("country")}, ["name", "template"] + ) + + if not result: + result = influxframework.db.get_value("Address Template", {"is_default": 1}, ["name", "template"]) + + if not result: + influxframework.throw( + _( + "No default Address Template found. Please create a new one from Setup > Printing and Branding > Address Template." + ) + ) + else: + return result + + +def get_company_address(company): + ret = influxframework._dict() + ret.company_address = get_default_address("Company", company) + ret.company_address_display = get_address_display(ret.company_address) + + return ret + + +@influxframework.whitelist() +@influxframework.validate_and_sanitize_search_inputs +def address_query(doctype, txt, searchfield, start, page_len, filters): + from influxframework.desk.reportview import get_match_cond + + doctype = "Address" + link_doctype = filters.pop("link_doctype") + link_name = filters.pop("link_name") + + condition = "" + meta = influxframework.get_meta(doctype) + for fieldname, value in filters.items(): + if meta.get_field(fieldname) or fieldname in influxframework.db.DEFAULT_COLUMNS: + condition += f" and {fieldname}={influxframework.db.escape(value)}" + + searchfields = meta.get_search_fields() + + if searchfield and (meta.get_field(searchfield) or searchfield in influxframework.db.DEFAULT_COLUMNS): + searchfields.append(searchfield) + + search_condition = "" + for field in searchfields: + if search_condition == "": + search_condition += f"`tabAddress`.`{field}` like %(txt)s" + else: + search_condition += f" or `tabAddress`.`{field}` like %(txt)s" + + return influxframework.db.sql( + """select + `tabAddress`.name, `tabAddress`.city, `tabAddress`.country + from + `tabAddress`, `tabDynamic Link` + where + `tabDynamic Link`.parent = `tabAddress`.name and + `tabDynamic Link`.parenttype = 'Address' and + `tabDynamic Link`.link_doctype = %(link_doctype)s and + `tabDynamic Link`.link_name = %(link_name)s and + ifnull(`tabAddress`.disabled, 0) = 0 and + ({search_condition}) + {mcond} {condition} + order by + if(locate(%(_txt)s, `tabAddress`.name), locate(%(_txt)s, `tabAddress`.name), 99999), + `tabAddress`.idx desc, `tabAddress`.name + limit %(start)s, %(page_len)s """.format( + mcond=get_match_cond(doctype), + search_condition=search_condition, + condition=condition or "", + ), + { + "txt": "%" + txt + "%", + "_txt": txt.replace("%", ""), + "start": start, + "page_len": page_len, + "link_name": link_name, + "link_doctype": link_doctype, + }, + ) + + +def get_condensed_address(doc): + fields = ["address_title", "address_line1", "address_line2", "city", "county", "state", "country"] + return ", ".join(doc.get(d) for d in fields if doc.get(d)) + + +def update_preferred_address(address, field): + influxframework.db.set_value("Address", address, field, 0) diff --git a/influxframework/contacts/doctype/address/test_address.py b/influxframework/contacts/doctype/address/test_address.py new file mode 100644 index 0000000..34c2125 --- /dev/null +++ b/influxframework/contacts/doctype/address/test_address.py @@ -0,0 +1,30 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.contacts.doctype.address.address import get_address_display +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestAddress(InfluxFrameworkTestCase): + def test_template_works(self): + if not influxframework.db.exists("Address Template", "India"): + influxframework.get_doc({"doctype": "Address Template", "country": "India", "is_default": 1}).insert() + + if not influxframework.db.exists("Address", "_Test Address-Office"): + influxframework.get_doc( + { + "address_line1": "_Test Address Line 1", + "address_title": "_Test Address", + "address_type": "Office", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 1, + "phone": "+91 0000000000", + } + ).insert() + + address = influxframework.get_list("Address")[0].name + display = get_address_display(influxframework.get_doc("Address", address).as_dict()) + self.assertTrue(display) diff --git a/influxframework/contacts/doctype/address_template/__init__.py b/influxframework/contacts/doctype/address_template/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/contacts/doctype/address_template/address_template.js b/influxframework/contacts/doctype/address_template/address_template.js new file mode 100644 index 0000000..7813f09 --- /dev/null +++ b/influxframework/contacts/doctype/address_template/address_template.js @@ -0,0 +1,16 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Address Template", { + refresh: function (frm) { + if (frm.is_new() && !frm.doc.template) { + // set default template via js so that it is translated + influxframework.call({ + method: "influxframework.contacts.doctype.address_template.address_template.get_default_address_template", + callback: function (r) { + frm.set_value("template", r.message); + }, + }); + } + }, +}); diff --git a/influxframework/contacts/doctype/address_template/address_template.json b/influxframework/contacts/doctype/address_template/address_template.json new file mode 100644 index 0000000..e27d97d --- /dev/null +++ b/influxframework/contacts/doctype/address_template/address_template.json @@ -0,0 +1,152 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 1, + "autoname": "field:country", + "beta": 0, + "creation": "2014-06-05 02:22:36.029850", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 0, + "engine": "InnoDB", + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "country", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Country", + "length": 0, + "no_copy": 0, + "options": "Country", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 1, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "This format is used if country specific format is not found", + "fieldname": "is_default", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Is Default", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "", + "description": "

    Default Template

    \n

    Uses Jinja Templating and all the fields of Address (including Custom Fields if any) will be available

    \n
    {{ address_line1 }}<br>\n{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}\n{{ city }}<br>\n{% if state %}{{ state }}<br>{% endif -%}\n{% if pincode %} PIN:  {{ pincode }}<br>{% endif -%}\n{{ country }}<br>\n{% if phone %}Phone: {{ phone }}<br>{% endif -%}\n{% if fax %}Fax: {{ fax }}<br>{% endif -%}\n{% if email_id %}Email: {{ email_id }}<br>{% endif -%}\n
    ", + "fieldname": "template", + "fieldtype": "Code", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Template", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "icon": "fa fa-map-marker", + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2017-04-10 13:09:53.761009", + "modified_by": "Administrator", + "module": "Contacts", + "name": "Address Template", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 0, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 0, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 1, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 0, + "track_seen": 0 +} \ No newline at end of file diff --git a/influxframework/contacts/doctype/address_template/address_template.py b/influxframework/contacts/doctype/address_template/address_template.py new file mode 100644 index 0000000..fb9c850 --- /dev/null +++ b/influxframework/contacts/doctype/address_template/address_template.py @@ -0,0 +1,55 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework import _ +from influxframework.model.document import Document +from influxframework.utils import cint +from influxframework.utils.jinja import validate_template + + +class AddressTemplate(Document): + def validate(self): + if not self.template: + self.template = get_default_address_template() + + self.defaults = influxframework.db.get_values( + "Address Template", {"is_default": 1, "name": ("!=", self.name)} + ) + if not self.is_default: + if not self.defaults: + self.is_default = 1 + if cint(influxframework.db.get_single_value("System Settings", "setup_complete")): + influxframework.msgprint(_("Setting this Address Template as default as there is no other default")) + + validate_template(self.template) + + def on_update(self): + if self.is_default and self.defaults: + for d in self.defaults: + influxframework.db.set_value("Address Template", d[0], "is_default", 0) + + def on_trash(self): + if self.is_default: + influxframework.throw(_("Default Address Template cannot be deleted")) + + +@influxframework.whitelist() +def get_default_address_template(): + """Get default address template (translated)""" + return ( + """{{ address_line1 }}
    {% if address_line2 %}{{ address_line2 }}
    {% endif -%}\ +{{ city }}
    +{% if state %}{{ state }}
    {% endif -%} +{% if pincode %}{{ pincode }}
    {% endif -%} +{{ country }}
    +{% if phone %}""" + + _("Phone") + + """: {{ phone }}
    {% endif -%} +{% if fax %}""" + + _("Fax") + + """: {{ fax }}
    {% endif -%} +{% if email_id %}""" + + _("Email") + + """: {{ email_id }}
    {% endif -%}""" + ) diff --git a/influxframework/contacts/doctype/address_template/test_address_template.py b/influxframework/contacts/doctype/address_template/test_address_template.py new file mode 100644 index 0000000..50c1169 --- /dev/null +++ b/influxframework/contacts/doctype/address_template/test_address_template.py @@ -0,0 +1,39 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestAddressTemplate(InfluxFrameworkTestCase): + def setUp(self): + self.make_default_address_template() + + def test_default_is_unset(self): + a = influxframework.get_doc("Address Template", "India") + a.is_default = 1 + a.save() + + b = influxframework.get_doc("Address Template", "Brazil") + b.is_default = 1 + b.save() + + self.assertEqual(influxframework.db.get_value("Address Template", "India", "is_default"), 0) + + def tearDown(self): + a = influxframework.get_doc("Address Template", "India") + a.is_default = 1 + a.save() + + @classmethod + def make_default_address_template(self): + template = """{{ address_line1 }}
    {% if address_line2 %}{{ address_line2 }}
    {% endif -%}{{ city }}
    {% if state %}{{ state }}
    {% endif -%}{% if pincode %}{{ pincode }}
    {% endif -%}{{ country }}
    {% if phone %}Phone: {{ phone }}
    {% endif -%}{% if fax %}Fax: {{ fax }}
    {% endif -%}{% if email_id %}Email: {{ email_id }}
    {% endif -%}""" + + if not influxframework.db.exists("Address Template", "India"): + influxframework.get_doc( + {"doctype": "Address Template", "country": "India", "is_default": 1, "template": template} + ).insert() + + if not influxframework.db.exists("Address Template", "Brazil"): + influxframework.get_doc( + {"doctype": "Address Template", "country": "Brazil", "template": template} + ).insert() diff --git a/influxframework/contacts/doctype/contact/__init__.py b/influxframework/contacts/doctype/contact/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/contacts/doctype/contact/contact.js b/influxframework/contacts/doctype/contact/contact.js new file mode 100644 index 0000000..3f1ffb4 --- /dev/null +++ b/influxframework/contacts/doctype/contact/contact.js @@ -0,0 +1,151 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Contact", { + onload(frm) { + frm.email_field = "email_id"; + }, + refresh: function (frm) { + if (frm.doc.__islocal) { + const last_doc = influxframework.contacts.get_last_doc(frm); + if ( + influxframework.dynamic_link && + influxframework.dynamic_link.doc && + influxframework.dynamic_link.doc.name == last_doc.docname + ) { + frm.set_value("links", ""); + frm.add_child("links", { + link_doctype: influxframework.dynamic_link.doctype, + link_name: influxframework.dynamic_link.doc[influxframework.dynamic_link.fieldname], + }); + } + } + + if (!frm.doc.user && !frm.is_new() && frm.perm[0].write) { + frm.add_custom_button(__("Invite as User"), function () { + return influxframework.call({ + method: "influxframework.contacts.doctype.contact.contact.invite_user", + args: { + contact: frm.doc.name, + }, + callback: function (r) { + frm.set_value("user", r.message); + }, + }); + }); + } + frm.set_query("link_doctype", "links", function () { + return { + query: "influxframework.contacts.address_and_contact.filter_dynamic_link_doctypes", + filters: { + fieldtype: "HTML", + fieldname: "contact_html", + }, + }; + }); + frm.refresh_field("links"); + + let numbers = frm.doc.phone_nos; + if (numbers && numbers.length && influxframework.phone_call.handler) { + frm.add_custom_button(__("Call"), () => { + numbers = frm.doc.phone_nos + .sort((prev, next) => next.is_primary_mobile_no - prev.is_primary_mobile_no) + .map((d) => d.phone); + influxframework.phone_call.handler(numbers); + }); + } + + if (frm.doc.links) { + influxframework.call({ + method: "influxframework.contacts.doctype.contact.contact.address_query", + args: { links: frm.doc.links }, + callback: function (r) { + if (r && r.message) { + frm.set_query("address", function () { + return { + filters: { + name: ["in", r.message], + }, + }; + }); + } + }, + }); + + for (let i in frm.doc.links) { + let link = frm.doc.links[i]; + frm.add_custom_button( + __("{0}: {1}", [__(link.link_doctype), __(link.link_name)]), + function () { + influxframework.set_route("Form", link.link_doctype, link.link_name); + }, + __("Links") + ); + } + } + }, + validate: function (frm) { + // clear linked customer / supplier / sales partner on saving... + if (frm.doc.links) { + frm.doc.links.forEach(function (d) { + influxframework.model.remove_from_locals(d.link_doctype, d.link_name); + }); + } + }, + after_save: function (frm) { + influxframework.run_serially([ + () => influxframework.timeout(1), + () => { + const last_doc = influxframework.contacts.get_last_doc(frm); + if ( + influxframework.dynamic_link && + influxframework.dynamic_link.doc && + influxframework.dynamic_link.doc.name == last_doc.docname + ) { + for (let i in frm.doc.links) { + let link = frm.doc.links[i]; + if ( + last_doc.doctype == link.link_doctype && + last_doc.docname == link.link_name + ) { + influxframework.set_route("Form", last_doc.doctype, last_doc.docname); + } + } + } + }, + ]); + }, + sync_with_google_contacts: function (frm) { + if (frm.doc.sync_with_google_contacts) { + influxframework.db.get_value( + "Google Contacts", + { email_id: influxframework.session.user }, + "name", + (r) => { + if (r && r.name) { + frm.set_value("google_contacts", r.name); + } + } + ); + } + }, +}); + +influxframework.ui.form.on("Dynamic Link", { + link_name: function (frm, cdt, cdn) { + var child = locals[cdt][cdn]; + if (child.link_name) { + influxframework.model.with_doctype(child.link_doctype, function () { + var title_field = influxframework.get_meta(child.link_doctype).title_field || "name"; + influxframework.model.get_value( + child.link_doctype, + child.link_name, + title_field, + function (r) { + influxframework.model.set_value(cdt, cdn, "link_title", r[title_field]); + } + ); + }); + } + }, +}); diff --git a/influxframework/contacts/doctype/contact/contact.json b/influxframework/contacts/doctype/contact/contact.json new file mode 100644 index 0000000..696cd61 --- /dev/null +++ b/influxframework/contacts/doctype/contact/contact.json @@ -0,0 +1,385 @@ +{ + "actions": [], + "allow_events_in_timeline": 1, + "allow_import": 1, + "allow_rename": 1, + "creation": "2013-01-10 16:34:32", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "contact_section", + "first_name", + "middle_name", + "last_name", + "email_id", + "user", + "address", + "sync_with_google_contacts", + "cb00", + "status", + "salutation", + "designation", + "gender", + "phone", + "mobile_no", + "company_name", + "image", + "sb_00", + "google_contacts", + "google_contacts_id", + "cb_00", + "pulled_from_google_contacts", + "sb_01", + "email_ids", + "phone_nos", + "contact_details", + "links", + "is_primary_contact", + "more_info", + "department", + "unsubscribed" + ], + "fields": [ + { + "fieldname": "contact_section", + "fieldtype": "Section Break", + "options": "fa fa-user" + }, + { + "fieldname": "first_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "First Name", + "oldfieldname": "first_name", + "oldfieldtype": "Data", + "reqd": 1 + }, + { + "bold": 1, + "fieldname": "last_name", + "fieldtype": "Data", + "label": "Last Name", + "oldfieldname": "last_name", + "oldfieldtype": "Data" + }, + { + "bold": 1, + "fieldname": "email_id", + "fieldtype": "Data", + "in_global_search": 1, + "in_list_view": 1, + "label": "Email Address", + "oldfieldname": "email_id", + "oldfieldtype": "Data", + "options": "Email", + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "user", + "fieldtype": "Link", + "in_global_search": 1, + "label": "User Id", + "options": "User" + }, + { + "fieldname": "cb00", + "fieldtype": "Column Break" + }, + { + "default": "Passive", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "Passive\nOpen\nReplied" + }, + { + "fieldname": "salutation", + "fieldtype": "Link", + "label": "Salutation", + "options": "Salutation" + }, + { + "fieldname": "gender", + "fieldtype": "Link", + "label": "Gender", + "options": "Gender" + }, + { + "bold": 1, + "fieldname": "phone", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Phone", + "oldfieldname": "contact_no", + "oldfieldtype": "Data", + "options": "Phone", + "read_only": 1 + }, + { + "fieldname": "image", + "fieldtype": "Attach Image", + "hidden": 1, + "label": "Image", + "print_hide": 1 + }, + { + "fieldname": "contact_details", + "fieldtype": "Section Break", + "label": "Reference", + "options": "fa fa-pushpin" + }, + { + "default": "0", + "fieldname": "is_primary_contact", + "fieldtype": "Check", + "label": "Is Primary Contact", + "oldfieldname": "is_primary_contact", + "oldfieldtype": "Select" + }, + { + "fieldname": "links", + "fieldtype": "Table", + "label": "Links", + "options": "Dynamic Link" + }, + { + "fieldname": "more_info", + "fieldtype": "Section Break", + "label": "More Information", + "options": "fa fa-file-text" + }, + { + "fieldname": "department", + "fieldtype": "Data", + "label": "Department" + }, + { + "fieldname": "designation", + "fieldtype": "Data", + "label": "Designation" + }, + { + "default": "0", + "fieldname": "unsubscribed", + "fieldtype": "Check", + "label": "Unsubscribed" + }, + { + "fieldname": "middle_name", + "fieldtype": "Data", + "label": "Middle Name" + }, + { + "collapsible": 1, + "depends_on": "eval:doc.sync_with_google_contacts || doc.pulled_from_google_contacts", + "fieldname": "sb_00", + "fieldtype": "Section Break", + "label": "Google Contacts" + }, + { + "fieldname": "email_ids", + "fieldtype": "Table", + "label": "Email IDs", + "options": "Contact Email" + }, + { + "fieldname": "address", + "fieldtype": "Link", + "label": "Address", + "options": "Address" + }, + { + "fieldname": "phone_nos", + "fieldtype": "Table", + "label": "Contact Numbers", + "options": "Contact Phone" + }, + { + "fieldname": "mobile_no", + "fieldtype": "Data", + "label": "Mobile No", + "options": "Phone", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "pulled_from_google_contacts", + "fieldtype": "Check", + "label": "Pulled from Google Contacts", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "sync_with_google_contacts", + "fieldtype": "Check", + "label": "Sync with Google Contacts" + }, + { + "fieldname": "google_contacts", + "fieldtype": "Link", + "label": "Google Contacts", + "options": "Google Contacts" + }, + { + "fieldname": "cb_00", + "fieldtype": "Column Break" + }, + { + "fieldname": "sb_01", + "fieldtype": "Section Break", + "label": "Contact Details" + }, + { + "fieldname": "google_contacts_id", + "fieldtype": "Data", + "label": "Google Contacts Id", + "read_only": 1 + }, + { + "fieldname": "company_name", + "fieldtype": "Data", + "label": "Company Name" + } + ], + "icon": "fa fa-user", + "idx": 1, + "image_field": "image", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-08-27 14:12:09.906719", + "modified_by": "Administrator", + "module": "Contacts", + "name": "Contact", + "name_case": "Title Case", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 1, + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Master Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase Master Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Maintenance Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase User", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Maintenance User", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "write": 1 + }, + { + "permlevel": 1, + "read": 1, + "report": 1, + "role": "All" + } + ], + "sort_field": "modified", + "sort_order": "ASC" +} \ No newline at end of file diff --git a/influxframework/contacts/doctype/contact/contact.py b/influxframework/contacts/doctype/contact/contact.py new file mode 100644 index 0000000..94c5d4b --- /dev/null +++ b/influxframework/contacts/doctype/contact/contact.py @@ -0,0 +1,329 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework import _ +from influxframework.contacts.address_and_contact import set_link_title +from influxframework.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links +from influxframework.model.document import Document +from influxframework.model.naming import append_number_if_name_exists +from influxframework.utils import cstr, has_gravatar + + +class Contact(Document): + def autoname(self): + # concat first and last name + self.name = " ".join( + filter(None, [cstr(self.get(f)).strip() for f in ["first_name", "last_name"]]) + ) + + if influxframework.db.exists("Contact", self.name): + self.name = append_number_if_name_exists("Contact", self.name) + + # concat party name if reqd + for link in self.links: + self.name = self.name + "-" + link.link_name.strip() + break + + def validate(self): + self.set_primary_email() + self.set_primary("phone") + self.set_primary("mobile_no") + + self.set_user() + + set_link_title(self) + + if self.email_id and not self.image: + self.image = has_gravatar(self.email_id) + + if self.get("sync_with_google_contacts") and not self.get("google_contacts"): + influxframework.throw(_("Select Google Contacts to which contact should be synced.")) + + deduplicate_dynamic_links(self) + + def set_user(self): + if not self.user and self.email_id: + self.user = influxframework.db.get_value("User", {"email": self.email_id}) + + def get_link_for(self, link_doctype): + """Return the link name, if exists for the given link DocType""" + for link in self.links: + if link.link_doctype == link_doctype: + return link.link_name + + return None + + def has_link(self, doctype, name): + for link in self.links: + if link.link_doctype == doctype and link.link_name == name: + return True + + def has_common_link(self, doc): + reference_links = [(link.link_doctype, link.link_name) for link in doc.links] + for link in self.links: + if (link.link_doctype, link.link_name) in reference_links: + return True + + def add_email(self, email_id, is_primary=0, autosave=False): + if not influxframework.db.exists("Contact Email", {"email_id": email_id, "parent": self.name}): + self.append("email_ids", {"email_id": email_id, "is_primary": is_primary}) + + if autosave: + self.save(ignore_permissions=True) + + def add_phone(self, phone, is_primary_phone=0, is_primary_mobile_no=0, autosave=False): + if not influxframework.db.exists("Contact Phone", {"phone": phone, "parent": self.name}): + self.append( + "phone_nos", + { + "phone": phone, + "is_primary_phone": is_primary_phone, + "is_primary_mobile_no": is_primary_mobile_no, + }, + ) + + if autosave: + self.save(ignore_permissions=True) + + def set_primary_email(self): + if not self.email_ids: + self.email_id = "" + return + + if len([email.email_id for email in self.email_ids if email.is_primary]) > 1: + influxframework.throw(_("Only one {0} can be set as primary.").format(influxframework.bold("Email ID"))) + + primary_email_exists = False + for d in self.email_ids: + if d.is_primary == 1: + primary_email_exists = True + self.email_id = d.email_id.strip() + break + + if not primary_email_exists: + self.email_id = "" + + def set_primary(self, fieldname): + # Used to set primary mobile and phone no. + if len(self.phone_nos) == 0: + setattr(self, fieldname, "") + return + + field_name = "is_primary_" + fieldname + + is_primary = [phone.phone for phone in self.phone_nos if phone.get(field_name)] + + if len(is_primary) > 1: + influxframework.throw( + _("Only one {0} can be set as primary.").format(influxframework.bold(influxframework.unscrub(fieldname))) + ) + + primary_number_exists = False + for d in self.phone_nos: + if d.get(field_name) == 1: + primary_number_exists = True + setattr(self, fieldname, d.phone) + break + + if not primary_number_exists: + setattr(self, fieldname, "") + + +def get_default_contact(doctype, name): + """Returns default contact for the given doctype, name""" + out = influxframework.db.sql( + '''select parent, + IFNULL((select is_primary_contact from tabContact c where c.name = dl.parent), 0) + as is_primary_contact + from + `tabDynamic Link` dl + where + dl.link_doctype=%s and + dl.link_name=%s and + dl.parenttype = "Contact"''', + (doctype, name), + as_dict=True, + ) + + if out: + for contact in out: + if contact.is_primary_contact: + return contact.parent + return out[0].parent + else: + return None + + +@influxframework.whitelist() +def invite_user(contact): + contact = influxframework.get_doc("Contact", contact) + + if not contact.email_id: + influxframework.throw(_("Please set Email Address")) + + if contact.has_permission("write"): + user = influxframework.get_doc( + { + "doctype": "User", + "first_name": contact.first_name, + "last_name": contact.last_name, + "email": contact.email_id, + "user_type": "Website User", + "send_welcome_email": 1, + } + ).insert(ignore_permissions=True) + + return user.name + + +@influxframework.whitelist() +def get_contact_details(contact): + contact = influxframework.get_doc("Contact", contact) + out = { + "contact_person": contact.get("name"), + "contact_display": " ".join( + filter(None, [contact.get("salutation"), contact.get("first_name"), contact.get("last_name")]) + ), + "contact_email": contact.get("email_id"), + "contact_mobile": contact.get("mobile_no"), + "contact_phone": contact.get("phone"), + "contact_designation": contact.get("designation"), + "contact_department": contact.get("department"), + } + return out + + +def update_contact(doc, method): + """Update contact when user is updated, if contact is found. Called via hooks""" + contact_name = influxframework.db.get_value("Contact", {"email_id": doc.name}) + if contact_name: + contact = influxframework.get_doc("Contact", contact_name) + for key in ("first_name", "last_name", "phone"): + if doc.get(key): + contact.set(key, doc.get(key)) + contact.flags.ignore_mandatory = True + contact.save(ignore_permissions=True) + + +@influxframework.whitelist() +@influxframework.validate_and_sanitize_search_inputs +def contact_query(doctype, txt, searchfield, start, page_len, filters): + from influxframework.desk.reportview import get_match_cond + + doctype = "Contact" + if ( + not influxframework.get_meta(doctype).get_field(searchfield) + and searchfield not in influxframework.db.DEFAULT_COLUMNS + ): + return [] + + link_doctype = filters.pop("link_doctype") + link_name = filters.pop("link_name") + + return influxframework.db.sql( + """select + `tabContact`.name, `tabContact`.first_name, `tabContact`.last_name + from + `tabContact`, `tabDynamic Link` + where + `tabDynamic Link`.parent = `tabContact`.name and + `tabDynamic Link`.parenttype = 'Contact' and + `tabDynamic Link`.link_doctype = %(link_doctype)s and + `tabDynamic Link`.link_name = %(link_name)s and + `tabContact`.`{key}` like %(txt)s + {mcond} + order by + if(locate(%(_txt)s, `tabContact`.name), locate(%(_txt)s, `tabContact`.name), 99999), + `tabContact`.idx desc, `tabContact`.name + limit %(start)s, %(page_len)s """.format( + mcond=get_match_cond(doctype), key=searchfield + ), + { + "txt": "%" + txt + "%", + "_txt": txt.replace("%", ""), + "start": start, + "page_len": page_len, + "link_name": link_name, + "link_doctype": link_doctype, + }, + ) + + +@influxframework.whitelist() +def address_query(links): + import json + + links = [ + {"link_doctype": d.get("link_doctype"), "link_name": d.get("link_name")} + for d in json.loads(links) + ] + result = [] + + for link in links: + if not influxframework.has_permission( + doctype=link.get("link_doctype"), ptype="read", doc=link.get("link_name") + ): + continue + + res = influxframework.db.sql( + """ + SELECT `tabAddress`.name + FROM `tabAddress`, `tabDynamic Link` + WHERE `tabDynamic Link`.parenttype='Address' + AND `tabDynamic Link`.parent=`tabAddress`.name + AND `tabDynamic Link`.link_doctype = %(link_doctype)s + AND `tabDynamic Link`.link_name = %(link_name)s + """, + { + "link_doctype": link.get("link_doctype"), + "link_name": link.get("link_name"), + }, + as_dict=True, + ) + + result.extend([l.name for l in res]) + + return result + + +def get_contact_with_phone_number(number): + if not number: + return + + contacts = influxframework.get_all( + "Contact Phone", filters=[["phone", "like", f"%{number}"]], fields=["parent"], limit=1 + ) + + return contacts[0].parent if contacts else None + + +def get_contact_name(email_id): + contact = influxframework.get_all( + "Contact Email", filters={"email_id": email_id}, fields=["parent"], limit=1 + ) + return contact[0].parent if contact else None + + +def get_contacts_linking_to(doctype, docname, fields=None): + """Return a list of contacts containing a link to the given document.""" + return influxframework.get_list( + "Contact", + fields=fields, + filters=[ + ["Dynamic Link", "link_doctype", "=", doctype], + ["Dynamic Link", "link_name", "=", docname], + ], + ) + + +def get_contacts_linked_from(doctype, docname, fields=None): + """Return a list of contacts that are contained in (linked from) the given document.""" + link_fields = influxframework.get_meta(doctype).get("fields", {"fieldtype": "Link", "options": "Contact"}) + if not link_fields: + return [] + + contact_names = influxframework.get_value(doctype, docname, fieldname=[f.fieldname for f in link_fields]) + if not contact_names: + return [] + + return influxframework.get_list("Contact", fields=fields, filters={"name": ("in", contact_names)}) diff --git a/influxframework/contacts/doctype/contact/contact_list.js b/influxframework/contacts/doctype/contact/contact_list.js new file mode 100644 index 0000000..b780c55 --- /dev/null +++ b/influxframework/contacts/doctype/contact/contact_list.js @@ -0,0 +1,3 @@ +influxframework.listview_settings["Contact"] = { + add_fields: ["image"], +}; diff --git a/influxframework/contacts/doctype/contact/test_contact.py b/influxframework/contacts/doctype/contact/test_contact.py new file mode 100644 index 0000000..69d4d4b --- /dev/null +++ b/influxframework/contacts/doctype/contact/test_contact.py @@ -0,0 +1,51 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + +test_dependencies = ["Contact", "Salutation"] + + +class TestContact(InfluxFrameworkTestCase): + def test_check_default_email(self): + emails = [ + {"email": "test1@example.com", "is_primary": 0}, + {"email": "test2@example.com", "is_primary": 0}, + {"email": "test3@example.com", "is_primary": 0}, + {"email": "test4@example.com", "is_primary": 1}, + {"email": "test5@example.com", "is_primary": 0}, + ] + contact = create_contact("Email", "Mr", emails=emails) + + self.assertEqual(contact.email_id, "test4@example.com") + + def test_check_default_phone_and_mobile(self): + phones = [ + {"phone": "+91 0000000000", "is_primary_phone": 0, "is_primary_mobile_no": 0}, + {"phone": "+91 0000000001", "is_primary_phone": 0, "is_primary_mobile_no": 0}, + {"phone": "+91 0000000002", "is_primary_phone": 1, "is_primary_mobile_no": 0}, + {"phone": "+91 0000000003", "is_primary_phone": 0, "is_primary_mobile_no": 1}, + ] + contact = create_contact("Phone", "Mr", phones=phones) + + self.assertEqual(contact.phone, "+91 0000000002") + self.assertEqual(contact.mobile_no, "+91 0000000003") + + +def create_contact(name, salutation, emails=None, phones=None, save=True): + doc = influxframework.get_doc( + {"doctype": "Contact", "first_name": name, "status": "Open", "salutation": salutation} + ) + + if emails: + for d in emails: + doc.add_email(d.get("email"), d.get("is_primary")) + + if phones: + for d in phones: + doc.add_phone(d.get("phone"), d.get("is_primary_phone"), d.get("is_primary_mobile_no")) + + if save: + doc.insert() + + return doc diff --git a/influxframework/contacts/doctype/contact/test_records.json b/influxframework/contacts/doctype/contact/test_records.json new file mode 100644 index 0000000..11c5329 --- /dev/null +++ b/influxframework/contacts/doctype/contact/test_records.json @@ -0,0 +1,39 @@ +[ + { + "doctype": "Contact", + "salutation": "Mr", + "first_name": "_Test Contact For _Test Customer", + "is_primary_contact": 1, + "status": "Open", + "email_ids": [ + { + "email_id": "test_contact@example.com", + "is_primary": 1 + } + ], + "phone_nos": [ + { + "phone": "+91 0000000000", + "is_primary_phone": 1 + } + ] + }, + { + "doctype": "Contact", + "first_name": "_Test Contact For _Test Supplier", + "is_primary_contact": 1, + "status": "Open", + "email_ids": [ + { + "email_id": "test_contact@example.com", + "is_primary": 1 + } + ], + "phone_nos": [ + { + "phone": "+91 0000000000", + "is_primary_phone": 1 + } + ] + } +] \ No newline at end of file diff --git a/influxframework/contacts/doctype/contact_email/__init__.py b/influxframework/contacts/doctype/contact_email/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/contacts/doctype/contact_email/contact_email.json b/influxframework/contacts/doctype/contact_email/contact_email.json new file mode 100644 index 0000000..f36e155 --- /dev/null +++ b/influxframework/contacts/doctype/contact_email/contact_email.json @@ -0,0 +1,39 @@ +{ + "creation": "2019-08-02 13:08:59.291097", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "email_id", + "is_primary" + ], + "fields": [ + { + "fieldname": "email_id", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Email ID", + "options": "Email", + "reqd": 1 + }, + { + "columns": 2, + "default": "0", + "fieldname": "is_primary", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Is Primary" + } + ], + "istable": 1, + "modified": "2019-09-24 17:47:30.565805", + "modified_by": "Administrator", + "module": "Contacts", + "name": "Contact Email", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/contacts/doctype/contact_email/contact_email.py b/influxframework/contacts/doctype/contact_email/contact_email.py new file mode 100644 index 0000000..ffb3136 --- /dev/null +++ b/influxframework/contacts/doctype/contact_email/contact_email.py @@ -0,0 +1,9 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class ContactEmail(Document): + pass diff --git a/influxframework/contacts/doctype/contact_phone/__init__.py b/influxframework/contacts/doctype/contact_phone/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/contacts/doctype/contact_phone/contact_phone.json b/influxframework/contacts/doctype/contact_phone/contact_phone.json new file mode 100644 index 0000000..5412e4a --- /dev/null +++ b/influxframework/contacts/doctype/contact_phone/contact_phone.json @@ -0,0 +1,50 @@ +{ + "actions": [], + "creation": "2019-08-02 13:10:37.890214", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "phone", + "is_primary_phone", + "is_primary_mobile_no" + ], + "fields": [ + { + "fieldname": "phone", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Number", + "options": "Phone", + "reqd": 1 + }, + { + "columns": 2, + "default": "0", + "fieldname": "is_primary_phone", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Is Primary Phone" + }, + { + "columns": 2, + "default": "0", + "fieldname": "is_primary_mobile_no", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Is Primary Mobile" + } + ], + "istable": 1, + "links": [], + "modified": "2020-04-06 18:28:10.486220", + "modified_by": "Administrator", + "module": "Contacts", + "name": "Contact Phone", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/contacts/doctype/contact_phone/contact_phone.py b/influxframework/contacts/doctype/contact_phone/contact_phone.py new file mode 100644 index 0000000..331608e --- /dev/null +++ b/influxframework/contacts/doctype/contact_phone/contact_phone.py @@ -0,0 +1,9 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class ContactPhone(Document): + pass diff --git a/influxframework/contacts/doctype/gender/__init__.py b/influxframework/contacts/doctype/gender/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/contacts/doctype/gender/gender.js b/influxframework/contacts/doctype/gender/gender.js new file mode 100644 index 0000000..c676596 --- /dev/null +++ b/influxframework/contacts/doctype/gender/gender.js @@ -0,0 +1,6 @@ +// Copyright (c) 2017, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Gender", { + refresh: function () {}, +}); diff --git a/influxframework/contacts/doctype/gender/gender.json b/influxframework/contacts/doctype/gender/gender.json new file mode 100644 index 0000000..34e1dda --- /dev/null +++ b/influxframework/contacts/doctype/gender/gender.json @@ -0,0 +1,48 @@ +{ + "actions": [], + "autoname": "field:gender", + "creation": "2017-04-10 12:11:36.526508", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "gender" + ], + "fields": [ + { + "fieldname": "gender", + "fieldtype": "Data", + "label": "Gender", + "unique": 1 + } + ], + "links": [], + "modified": "2022-08-05 18:33:28.043370", + "modified_by": "Administrator", + "module": "Contacts", + "name": "Gender", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "read": 1, + "role": "All" + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1, + "translated_doctype": 1 +} \ No newline at end of file diff --git a/influxframework/contacts/doctype/gender/gender.py b/influxframework/contacts/doctype/gender/gender.py new file mode 100644 index 0000000..2989409 --- /dev/null +++ b/influxframework/contacts/doctype/gender/gender.py @@ -0,0 +1,8 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE + +from influxframework.model.document import Document + + +class Gender(Document): + pass diff --git a/influxframework/contacts/doctype/gender/test_gender.py b/influxframework/contacts/doctype/gender/test_gender.py new file mode 100644 index 0000000..3937984 --- /dev/null +++ b/influxframework/contacts/doctype/gender/test_gender.py @@ -0,0 +1,7 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestGender(InfluxFrameworkTestCase): + pass diff --git a/influxframework/contacts/doctype/salutation/__init__.py b/influxframework/contacts/doctype/salutation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/contacts/doctype/salutation/salutation.js b/influxframework/contacts/doctype/salutation/salutation.js new file mode 100644 index 0000000..c5ad3e6 --- /dev/null +++ b/influxframework/contacts/doctype/salutation/salutation.js @@ -0,0 +1,6 @@ +// Copyright (c) 2017, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Salutation", { + refresh: function () {}, +}); diff --git a/influxframework/contacts/doctype/salutation/salutation.json b/influxframework/contacts/doctype/salutation/salutation.json new file mode 100644 index 0000000..98ed082 --- /dev/null +++ b/influxframework/contacts/doctype/salutation/salutation.json @@ -0,0 +1,61 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:salutation", + "creation": "2017-04-10 12:17:58.071915", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "salutation" + ], + "fields": [ + { + "fieldname": "salutation", + "fieldtype": "Data", + "label": "Salutation", + "unique": 1 + } + ], + "links": [], + "modified": "2022-08-05 18:33:28.196387", + "modified_by": "Administrator", + "module": "Contacts", + "name": "Salutation", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "read": 1, + "role": "All" + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1, + "translated_doctype": 1 +} \ No newline at end of file diff --git a/influxframework/contacts/doctype/salutation/salutation.py b/influxframework/contacts/doctype/salutation/salutation.py new file mode 100644 index 0000000..a0261ec --- /dev/null +++ b/influxframework/contacts/doctype/salutation/salutation.py @@ -0,0 +1,8 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE + +from influxframework.model.document import Document + + +class Salutation(Document): + pass diff --git a/influxframework/contacts/doctype/salutation/test_records.json b/influxframework/contacts/doctype/salutation/test_records.json new file mode 100644 index 0000000..3a87fff --- /dev/null +++ b/influxframework/contacts/doctype/salutation/test_records.json @@ -0,0 +1,8 @@ +[ + { + "salutation": "Mr" + }, + { + "salutation": "Mrs" + } +] \ No newline at end of file diff --git a/influxframework/contacts/doctype/salutation/test_salutation.py b/influxframework/contacts/doctype/salutation/test_salutation.py new file mode 100644 index 0000000..3583e32 --- /dev/null +++ b/influxframework/contacts/doctype/salutation/test_salutation.py @@ -0,0 +1,7 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestSalutation(InfluxFrameworkTestCase): + pass diff --git a/influxframework/contacts/report/__init__.py b/influxframework/contacts/report/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/contacts/report/addresses_and_contacts/__init__.py b/influxframework/contacts/report/addresses_and_contacts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/contacts/report/addresses_and_contacts/addresses_and_contacts.js b/influxframework/contacts/report/addresses_and_contacts/addresses_and_contacts.js new file mode 100644 index 0000000..497970a --- /dev/null +++ b/influxframework/contacts/report/addresses_and_contacts/addresses_and_contacts.js @@ -0,0 +1,33 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.query_reports["Addresses And Contacts"] = { + filters: [ + { + reqd: 1, + fieldname: "reference_doctype", + label: __("Entity Type"), + fieldtype: "Link", + options: "DocType", + get_query: function () { + return { + filters: { + name: ["in", "Contact, Address"], + }, + }; + }, + }, + { + fieldname: "reference_name", + label: __("Entity Name"), + fieldtype: "Dynamic Link", + get_options: function () { + let reference_doctype = influxframework.query_report.get_filter_value("reference_doctype"); + if (!reference_doctype) { + influxframework.throw(__("Please select Entity Type first")); + } + return reference_doctype; + }, + }, + ], +}; diff --git a/influxframework/contacts/report/addresses_and_contacts/addresses_and_contacts.json b/influxframework/contacts/report/addresses_and_contacts/addresses_and_contacts.json new file mode 100644 index 0000000..2d62444 --- /dev/null +++ b/influxframework/contacts/report/addresses_and_contacts/addresses_and_contacts.json @@ -0,0 +1,32 @@ +{ + "add_total_row": 0, + "apply_user_permissions": 1, + "creation": "2017-01-19 12:57:22.881566", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 2, + "is_standard": "Yes", + "modified": "2017-04-10 15:04:12.498920", + "modified_by": "Administrator", + "module": "Contacts", + "name": "Addresses And Contacts", + "owner": "Administrator", + "ref_doctype": "Address", + "report_name": "Addresses And Contacts", + "report_type": "Script Report", + "roles": [ + { + "role": "Sales User" + }, + { + "role": "Purchase User" + }, + { + "role": "Maintenance User" + }, + { + "role": "Accounts User" + } + ] +} \ No newline at end of file diff --git a/influxframework/contacts/report/addresses_and_contacts/addresses_and_contacts.py b/influxframework/contacts/report/addresses_and_contacts/addresses_and_contacts.py new file mode 100644 index 0000000..9385f5f --- /dev/null +++ b/influxframework/contacts/report/addresses_and_contacts/addresses_and_contacts.py @@ -0,0 +1,131 @@ +# Copyright (c) 2013, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework import _ + +field_map = { + "Contact": [ + "first_name", + "last_name", + "address", + "phone", + "mobile_no", + "email_id", + "is_primary_contact", + ], + "Address": [ + "address_line1", + "address_line2", + "city", + "state", + "pincode", + "country", + "is_primary_address", + ], +} + + +def execute(filters=None): + columns, data = get_columns(filters), get_data(filters) + return columns, data + + +def get_columns(filters): + return [ + "{reference_doctype}:Link/{reference_doctype}".format( + reference_doctype=filters.get("reference_doctype") + ), + "Address Line 1", + "Address Line 2", + "City", + "State", + "Postal Code", + "Country", + "Is Primary Address:Check", + "First Name", + "Last Name", + "Address", + "Phone", + "Email Id", + "Is Primary Contact:Check", + ] + + +def get_data(filters): + data = [] + reference_doctype = filters.get("reference_doctype") + reference_name = filters.get("reference_name") + + return get_reference_addresses_and_contact(reference_doctype, reference_name) + + +def get_reference_addresses_and_contact(reference_doctype, reference_name): + data = [] + filters = None + reference_details = influxframework._dict() + + if not reference_doctype: + return [] + + if reference_name: + filters = {"name": reference_name} + + reference_list = [ + d[0] for d in influxframework.get_list(reference_doctype, filters=filters, fields=["name"], as_list=True) + ] + + for d in reference_list: + reference_details.setdefault(d, influxframework._dict()) + reference_details = get_reference_details( + reference_doctype, "Address", reference_list, reference_details + ) + reference_details = get_reference_details( + reference_doctype, "Contact", reference_list, reference_details + ) + + for reference_name, details in reference_details.items(): + addresses = details.get("address", []) + contacts = details.get("contact", []) + if not any([addresses, contacts]): + result = [reference_name] + result.extend(add_blank_columns_for("Address")) + result.extend(add_blank_columns_for("Contact")) + data.append(result) + else: + addresses = list(map(list, addresses)) + contacts = list(map(list, contacts)) + + max_length = max(len(addresses), len(contacts)) + for idx in range(0, max_length): + result = [reference_name] + + result.extend(addresses[idx] if idx < len(addresses) else add_blank_columns_for("Address")) + result.extend(contacts[idx] if idx < len(contacts) else add_blank_columns_for("Contact")) + + data.append(result) + + return data + + +def get_reference_details(reference_doctype, doctype, reference_list, reference_details): + filters = [ + ["Dynamic Link", "link_doctype", "=", reference_doctype], + ["Dynamic Link", "link_name", "in", reference_list], + ] + fields = ["`tabDynamic Link`.link_name"] + field_map.get(doctype, []) + + records = influxframework.get_list(doctype, filters=filters, fields=fields, as_list=True) + temp_records = list() + + for d in records: + temp_records.append(d[1:]) + + if not reference_list: + influxframework.throw(_("No records present in {0}").format(reference_doctype)) + + reference_details[reference_list[0]][influxframework.scrub(doctype)] = temp_records + return reference_details + + +def add_blank_columns_for(doctype): + return ["" for field in field_map.get(doctype, [])] diff --git a/influxframework/contacts/report/addresses_and_contacts/test_addresses_and_contacts.py b/influxframework/contacts/report/addresses_and_contacts/test_addresses_and_contacts.py new file mode 100644 index 0000000..70c9710 --- /dev/null +++ b/influxframework/contacts/report/addresses_and_contacts/test_addresses_and_contacts.py @@ -0,0 +1,117 @@ +import influxframework +import influxframework.defaults +from influxframework.contacts.report.addresses_and_contacts.addresses_and_contacts import get_data +from influxframework.tests.utils import InfluxFrameworkTestCase + + +def get_custom_linked_doctype(): + if bool(influxframework.get_all("DocType", filters={"name": "Test Custom Doctype"})): + return + + doc = influxframework.get_doc( + { + "doctype": "DocType", + "module": "Core", + "custom": 1, + "fields": [ + {"label": "Test Field", "fieldname": "test_field", "fieldtype": "Data"}, + {"label": "Contact HTML", "fieldname": "contact_html", "fieldtype": "HTML"}, + {"label": "Address HTML", "fieldname": "address_html", "fieldtype": "HTML"}, + ], + "permissions": [{"role": "System Manager", "read": 1}], + "name": "Test Custom Doctype", + } + ) + doc.insert() + + +def get_custom_doc_for_address_and_contacts(): + get_custom_linked_doctype() + linked_doc = influxframework.get_doc( + { + "doctype": "Test Custom Doctype", + "test_field": "Hello", + } + ).insert() + return linked_doc + + +def create_linked_address(link_list): + if influxframework.flags.test_address_created: + return + + address = influxframework.get_doc( + { + "doctype": "Address", + "address_title": "_Test Address", + "address_type": "Billing", + "address_line1": "test address line 1", + "address_line2": "test address line 2", + "city": "Milan", + "country": "Italy", + } + ) + + for name in link_list: + address.append("links", {"link_doctype": "Test Custom Doctype", "link_name": name}) + + address.insert() + influxframework.flags.test_address_created = True + + return address.name + + +def create_linked_contact(link_list, address): + if influxframework.flags.test_contact_created: + return + + contact = influxframework.get_doc( + { + "doctype": "Contact", + "salutation": "Mr", + "first_name": "_Test First Name", + "last_name": "_Test Last Name", + "is_primary_contact": 1, + "address": address, + "status": "Open", + } + ) + contact.add_email("test_contact@example.com", is_primary=True) + contact.add_phone("+91 0000000000", is_primary_phone=True) + + for name in link_list: + contact.append("links", {"link_doctype": "Test Custom Doctype", "link_name": name}) + + contact.insert(ignore_permissions=True) + influxframework.flags.test_contact_created = True + + +class TestAddressesAndContacts(InfluxFrameworkTestCase): + def test_get_data(self): + linked_docs = [get_custom_doc_for_address_and_contacts()] + links_list = [item.name for item in linked_docs] + d = create_linked_address(links_list) + create_linked_contact(links_list, d) + report_data = get_data({"reference_doctype": "Test Custom Doctype"}) + for idx, link in enumerate(links_list): + test_item = [ + link, + "test address line 1", + "test address line 2", + "Milan", + None, + None, + "Italy", + 0, + "_Test First Name", + "_Test Last Name", + "_Test Address-Billing", + "+91 0000000000", + "", + "test_contact@example.com", + 1, + ] + self.assertListEqual(test_item, report_data[idx]) + + def tearDown(self): + influxframework.db.rollback() diff --git a/influxframework/core/README.md b/influxframework/core/README.md new file mode 100644 index 0000000..ce5469b --- /dev/null +++ b/influxframework/core/README.md @@ -0,0 +1 @@ +Core module contains the models required for the basic functioning of influxframework including DocType, User (user), Role and others. diff --git a/influxframework/core/__init__.py b/influxframework/core/__init__.py new file mode 100644 index 0000000..2e11a12 --- /dev/null +++ b/influxframework/core/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE diff --git a/influxframework/core/api/__init__.py b/influxframework/core/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/api/file.py b/influxframework/core/api/file.py new file mode 100644 index 0000000..7363eeb --- /dev/null +++ b/influxframework/core/api/file.py @@ -0,0 +1,121 @@ +import json + +import influxframework +from influxframework.core.doctype.file.file import File, setup_folder_path +from influxframework.utils import cint, cstr + + +@influxframework.whitelist() +def unzip_file(name: str): + """Unzip the given file and make file records for each of the extracted files""" + file: File = influxframework.get_doc("File", name) + return file.unzip() + + +@influxframework.whitelist() +def get_attached_images(doctype: str, names: list[str]) -> influxframework._dict: + """get list of image urls attached in form + returns {name: ['image.jpg', 'image.png']}""" + + if isinstance(names, str): + names = json.loads(names) + + img_urls = influxframework.db.get_list( + "File", + filters={ + "attached_to_doctype": doctype, + "attached_to_name": ("in", names), + "is_folder": 0, + }, + fields=["file_url", "attached_to_name as docname"], + ) + + out = influxframework._dict() + for i in img_urls: + out[i.docname] = out.get(i.docname, []) + out[i.docname].append(i.file_url) + + return out + + +@influxframework.whitelist() +def get_files_in_folder(folder: str, start: int = 0, page_length: int = 20) -> dict: + start = cint(start) + page_length = cint(page_length) + + attachment_folder = influxframework.db.get_value( + "File", + "Home/Attachments", + ["name", "file_name", "file_url", "is_folder", "modified"], + as_dict=1, + ) + + files = influxframework.get_list( + "File", + {"folder": folder}, + ["name", "file_name", "file_url", "is_folder", "modified"], + start=start, + page_length=page_length + 1, + ) + + if folder == "Home" and attachment_folder not in files: + files.insert(0, attachment_folder) + + return {"files": files[:page_length], "has_more": len(files) > page_length} + + +@influxframework.whitelist() +def get_files_by_search_text(text: str) -> list[dict]: + if not text: + return [] + + text = "%" + cstr(text).lower() + "%" + return influxframework.get_list( + "File", + fields=["name", "file_name", "file_url", "is_folder", "modified"], + filters={"is_folder": False}, + or_filters={ + "file_name": ("like", text), + "file_url": text, + "name": ("like", text), + }, + order_by="modified desc", + limit=20, + ) + + +@influxframework.whitelist(allow_guest=True) +def get_max_file_size() -> int: + return cint(influxframework.conf.get("max_file_size")) or 10485760 + + +@influxframework.whitelist() +def create_new_folder(file_name: str, folder: str) -> File: + """create new folder under current parent folder""" + file = influxframework.new_doc("File") + file.file_name = file_name + file.is_folder = 1 + file.folder = folder + file.insert(ignore_if_duplicate=True) + return file + + +@influxframework.whitelist() +def move_file(file_list: list[File], new_parent: str, old_parent: str) -> None: + if isinstance(file_list, str): + file_list = json.loads(file_list) + + for file_obj in file_list: + setup_folder_path(file_obj.get("name"), new_parent) + + # recalculate sizes + influxframework.get_doc("File", old_parent).save() + influxframework.get_doc("File", new_parent).save() + + +@influxframework.whitelist() +def zip_files(files: str): + files = influxframework.parse_json(files) + influxframework.response["filename"] = "files.zip" + influxframework.response["filecontent"] = File.zip_files(files) + influxframework.response["type"] = "download" diff --git a/influxframework/core/doctype/__init__.py b/influxframework/core/doctype/__init__.py new file mode 100644 index 0000000..2e11a12 --- /dev/null +++ b/influxframework/core/doctype/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE diff --git a/influxframework/core/doctype/access_log/__init__.py b/influxframework/core/doctype/access_log/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/access_log/access_log.js b/influxframework/core/doctype/access_log/access_log.js new file mode 100644 index 0000000..cc54c84 --- /dev/null +++ b/influxframework/core/doctype/access_log/access_log.js @@ -0,0 +1,17 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Access Log", { + show_document: function (frm) { + influxframework.set_route("Form", frm.doc.export_from, frm.doc.reference_document); + }, + + show_report: function (frm) { + if (frm.doc.report_name.includes("/")) { + influxframework.set_route(frm.doc.report_name); + } else { + let filters = frm.doc.filters ? JSON.parse(frm.doc.filters) : {}; + influxframework.set_route("query-report", frm.doc.report_name, filters); + } + }, +}); diff --git a/influxframework/core/doctype/access_log/access_log.json b/influxframework/core/doctype/access_log/access_log.json new file mode 100644 index 0000000..69803ef --- /dev/null +++ b/influxframework/core/doctype/access_log/access_log.json @@ -0,0 +1,156 @@ +{ + "actions": [], + "autoname": "hash", + "creation": "2019-07-25 15:44:44.955496", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "log_data_section", + "export_from", + "user", + "show_document", + "column_break_3", + "reference_document", + "timestamp", + "private_file_section", + "file_type", + "method", + "report_information_section", + "report_name", + "filters", + "show_report", + "raw_information_log_section", + "page", + "columns" + ], + "fields": [ + { + "fieldname": "export_from", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Export From", + "read_only": 1 + }, + { + "fieldname": "user", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "User ", + "options": "User", + "read_only": 1 + }, + { + "default": "Now", + "fieldname": "timestamp", + "fieldtype": "Datetime", + "label": "Timestamp", + "read_only": 1 + }, + { + "fieldname": "reference_document", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Reference Document", + "read_only": 1 + }, + { + "fieldname": "file_type", + "fieldtype": "Data", + "label": "File Type", + "read_only": 1 + }, + { + "fieldname": "report_name", + "fieldtype": "Data", + "label": "Report Name", + "read_only": 1 + }, + { + "fieldname": "page", + "fieldtype": "HTML Editor", + "label": "HTML Page", + "read_only": 1 + }, + { + "fieldname": "log_data_section", + "fieldtype": "Section Break", + "label": "Log Data" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "private_file_section", + "fieldtype": "Section Break", + "label": "File Information" + }, + { + "fieldname": "report_information_section", + "fieldtype": "Section Break", + "label": "Report Information" + }, + { + "fieldname": "raw_information_log_section", + "fieldtype": "Section Break", + "label": "RAW Information Log" + }, + { + "fieldname": "method", + "fieldtype": "Data", + "label": "Method", + "read_only": 1 + }, + { + "depends_on": "report_name", + "fieldname": "show_report", + "fieldtype": "Button", + "label": "Show Report" + }, + { + "depends_on": "reference_document", + "fieldname": "show_document", + "fieldtype": "Button", + "label": "Show Document" + }, + { + "depends_on": "eval: doc.filters != null", + "fieldname": "filters", + "fieldtype": "Code", + "label": "Filters", + "read_only": 1 + }, + { + "fieldname": "columns", + "fieldtype": "HTML Editor", + "label": "Columns / Fields", + "read_only": 1 + } + ], + "links": [], + "modified": "2022-06-13 05:59:26.866004", + "modified_by": "Administrator", + "module": "Core", + "name": "Access Log", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [ + { + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_seen": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/access_log/access_log.py b/influxframework/core/doctype/access_log/access_log.py new file mode 100644 index 0000000..5fafa8f --- /dev/null +++ b/influxframework/core/doctype/access_log/access_log.py @@ -0,0 +1,71 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE +from tenacity import retry, retry_if_exception_type, stop_after_attempt + +import influxframework +from influxframework.model.document import Document +from influxframework.utils import cstr + + +class AccessLog(Document): + pass + + +@influxframework.whitelist() +def make_access_log( + doctype=None, + document=None, + method=None, + file_type=None, + report_name=None, + filters=None, + page=None, + columns=None, +): + _make_access_log( + doctype, + document, + method, + file_type, + report_name, + filters, + page, + columns, + ) + + +@influxframework.write_only() +@retry(stop=stop_after_attempt(3), retry=retry_if_exception_type(influxframework.DuplicateEntryError)) +def _make_access_log( + doctype=None, + document=None, + method=None, + file_type=None, + report_name=None, + filters=None, + page=None, + columns=None, +): + user = influxframework.session.user + in_request = influxframework.request and influxframework.request.method == "GET" + + influxframework.get_doc( + { + "doctype": "Access Log", + "user": user, + "export_from": doctype, + "reference_document": document, + "file_type": file_type, + "report_name": report_name, + "page": page, + "method": method, + "filters": cstr(filters) or None, + "columns": columns, + } + ).db_insert() + + # `influxframework.db.commit` added because insert doesnt `commit` when called in GET requests like `printview` + # dont commit in test mode. It must be tempting to put this block along with the in_request in the + # whitelisted method...yeah, don't do it. That part would be executed possibly on a read only DB conn + if not influxframework.flags.in_test or in_request: + influxframework.db.commit() diff --git a/influxframework/core/doctype/access_log/test_access_log.py b/influxframework/core/doctype/access_log/test_access_log.py new file mode 100644 index 0000000..7ad749f --- /dev/null +++ b/influxframework/core/doctype/access_log/test_access_log.py @@ -0,0 +1,175 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +import base64 +import os + +# imports - third party imports +import requests + +# imports - module imports +import influxframework +from influxframework.core.doctype.access_log.access_log import make_access_log +from influxframework.core.doctype.data_import.data_import import export_csv +from influxframework.core.doctype.user.user import generate_keys + +# imports - standard imports +from influxframework.tests.utils import InfluxFrameworkTestCase +from influxframework.utils import cstr, get_site_url + + +class TestAccessLog(InfluxFrameworkTestCase): + def setUp(self): + # generate keys for current user to send requests for the following tests + generate_keys(influxframework.session.user) + influxframework.db.commit() + generated_secret = influxframework.utils.password.get_decrypted_password( + "User", influxframework.session.user, fieldname="api_secret" + ) + api_key = influxframework.db.get_value("User", "Administrator", "api_key") + self.header = {"Authorization": f"token {api_key}:{generated_secret}"} + + self.test_html_template = """ + + + + + + + +

    HTML Table

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    CompanyContactCountry
    Alfreds FutterkisteMaria AndersGermany
    Centro comercial MoctezumaFrancisco ChangMexico
    Ernst HandelRoland MendelAustria
    Island TradingHelen BennettUK
    Laughing Bacchus WinecellarsYoshi TannamuriCanada
    Magazzini Alimentari RiunitiGiovanni RovelliItaly
    + + + + """ + self.test_filters = { + "from_date": "2019-06-30", + "to_date": "2019-07-31", + "party": [], + "group_by": "Group by Voucher (Consolidated)", + "cost_center": [], + "project": [], + } + + self.test_doctype = "File" + self.test_document = "Test Document" + self.test_report_name = "General Ledger" + self.test_file_type = "CSV" + self.test_method = "Test Method" + self.file_name = influxframework.utils.random_string(10) + ".txt" + self.test_content = influxframework.utils.random_string(1024) + + def test_make_full_access_log(self): + self.maxDiff = None + + # test if all fields maintain data: html page and filters are converted? + make_access_log( + doctype=self.test_doctype, + document=self.test_document, + report_name=self.test_report_name, + page=self.test_html_template, + file_type=self.test_file_type, + method=self.test_method, + filters=self.test_filters, + ) + + last_doc = influxframework.get_last_doc("Access Log") + self.assertEqual(last_doc.filters, cstr(self.test_filters)) + self.assertEqual(self.test_doctype, last_doc.export_from) + self.assertEqual(self.test_document, last_doc.reference_document) + + def test_make_export_log(self): + # export data and delete temp file generated on disk + export_csv(self.test_doctype, self.file_name) + os.remove(self.file_name) + + # test if the exported data is logged + last_doc = influxframework.get_last_doc("Access Log") + self.assertEqual(self.test_doctype, last_doc.export_from) + + def test_private_file_download(self): + # create new private file + new_private_file = influxframework.get_doc( + { + "doctype": self.test_doctype, + "file_name": self.file_name, + "content": base64.b64encode(self.test_content.encode("utf-8")), + "is_private": 1, + } + ) + new_private_file.insert() + + # access the created file + private_file_link = get_site_url(influxframework.local.site) + new_private_file.file_url + + try: + request = requests.post(private_file_link, headers=self.header) + last_doc = influxframework.get_last_doc("Access Log") + + if request.ok: + # check for the access log of downloaded file + self.assertEqual(new_private_file.doctype, last_doc.export_from) + self.assertEqual(new_private_file.name, last_doc.reference_document) + + except requests.ConnectionError: + pass + + # cleanup + new_private_file.delete() + + def tearDown(self): + pass diff --git a/influxframework/core/doctype/activity_log/__init__.py b/influxframework/core/doctype/activity_log/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/activity_log/activity_log.js b/influxframework/core/doctype/activity_log/activity_log.js new file mode 100644 index 0000000..a0ab475 --- /dev/null +++ b/influxframework/core/doctype/activity_log/activity_log.js @@ -0,0 +1,6 @@ +// Copyright (c) 2017, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Activity Log", { + refresh: function () {}, +}); diff --git a/influxframework/core/doctype/activity_log/activity_log.json b/influxframework/core/doctype/activity_log/activity_log.json new file mode 100644 index 0000000..910bace --- /dev/null +++ b/influxframework/core/doctype/activity_log/activity_log.json @@ -0,0 +1,186 @@ +{ + "actions": [], + "allow_import": 1, + "creation": "2017-10-05 11:10:38.780133", + "description": "Keep track of all update feeds", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "subject", + "section_break_8", + "content", + "column_break_5", + "additional_info", + "communication_date", + "column_break_7", + "operation", + "status", + "reference_section", + "reference_doctype", + "reference_name", + "reference_owner", + "column_break_14", + "timeline_doctype", + "timeline_name", + "link_doctype", + "link_name", + "user", + "full_name" + ], + "fields": [ + { + "fieldname": "subject", + "fieldtype": "Small Text", + "in_global_search": 1, + "in_list_view": 1, + "label": "Subject", + "reqd": 1 + }, + { + "fieldname": "section_break_8", + "fieldtype": "Section Break" + }, + { + "fieldname": "content", + "fieldtype": "Text Editor", + "label": "Message", + "width": "400" + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "collapsible": 1, + "fieldname": "additional_info", + "fieldtype": "Section Break", + "label": "More Information" + }, + { + "default": "Now", + "fieldname": "communication_date", + "fieldtype": "Datetime", + "label": "Date" + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "fieldname": "operation", + "fieldtype": "Select", + "label": "Operation", + "options": "\nLogin\nLogout" + }, + { + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "options": "\nSuccess\nFailed\nLinked\nClosed" + }, + { + "collapsible": 1, + "fieldname": "reference_section", + "fieldtype": "Section Break", + "label": "Reference" + }, + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "label": "Reference Document Type", + "options": "DocType" + }, + { + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "label": "Reference Name", + "options": "reference_doctype" + }, + { + "fetch_from": "reference_name.owner", + "fieldname": "reference_owner", + "fieldtype": "Read Only", + "label": "Reference Owner" + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break" + }, + { + "fieldname": "timeline_doctype", + "fieldtype": "Link", + "label": "Timeline DocType", + "options": "DocType" + }, + { + "fieldname": "timeline_name", + "fieldtype": "Dynamic Link", + "label": "Timeline Name", + "options": "timeline_doctype" + }, + { + "fieldname": "link_doctype", + "fieldtype": "Link", + "label": "Link DocType", + "options": "DocType", + "read_only": 1 + }, + { + "fieldname": "link_name", + "fieldtype": "Dynamic Link", + "label": "Link Name", + "options": "link_doctype", + "read_only": 1 + }, + { + "default": "__user", + "fieldname": "user", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "User", + "options": "User", + "read_only": 1 + }, + { + "fieldname": "full_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Full Name" + } + ], + "icon": "fa fa-comment", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2022-09-13 15:19:42.474114", + "modified_by": "Administrator", + "module": "Core", + "name": "Activity Log", + "owner": "Administrator", + "permissions": [ + { + "email": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1 + }, + { + "email": 1, + "export": 1, + "if_owner": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 + } + ], + "search_fields": "subject", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "subject", + "track_seen": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/activity_log/activity_log.py b/influxframework/core/doctype/activity_log/activity_log.py new file mode 100644 index 0000000..4bcdf75 --- /dev/null +++ b/influxframework/core/doctype/activity_log/activity_log.py @@ -0,0 +1,51 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.core.utils import set_timeline_doc +from influxframework.model.document import Document +from influxframework.query_builder import DocType, Interval +from influxframework.query_builder.functions import Now +from influxframework.utils import get_fullname, now + + +class ActivityLog(Document): + def before_insert(self): + self.full_name = get_fullname(self.user) + self.date = now() + + def validate(self): + self.set_status() + set_timeline_doc(self) + + def set_status(self): + if not self.is_new(): + return + + if self.reference_doctype and self.reference_name: + self.status = "Linked" + + @staticmethod + def clear_old_logs(days=None): + if not days: + days = 90 + doctype = DocType("Activity Log") + influxframework.db.delete(doctype, filters=(doctype.modified < (Now() - Interval(days=days)))) + + +def on_doctype_update(): + """Add indexes in `tabActivity Log`""" + influxframework.db.add_index("Activity Log", ["reference_doctype", "reference_name"]) + influxframework.db.add_index("Activity Log", ["timeline_doctype", "timeline_name"]) + + +def add_authentication_log(subject, user, operation="Login", status="Success"): + influxframework.get_doc( + { + "doctype": "Activity Log", + "user": user, + "status": status, + "subject": subject, + "operation": operation, + } + ).insert(ignore_permissions=True, ignore_links=True) diff --git a/influxframework/core/doctype/activity_log/activity_log_list.js b/influxframework/core/doctype/activity_log/activity_log_list.js new file mode 100644 index 0000000..a93e901 --- /dev/null +++ b/influxframework/core/doctype/activity_log/activity_log_list.js @@ -0,0 +1,12 @@ +influxframework.listview_settings["Activity Log"] = { + get_indicator: function (doc) { + if (doc.operation == "Login" && doc.status == "Success") return [__(doc.status), "green"]; + else if (doc.operation == "Login" && doc.status == "Failed") + return [__(doc.status), "red"]; + }, + onload: function (listview) { + influxframework.require("logtypes.bundle.js", () => { + influxframework.utils.logtypes.show_log_retention_message(cur_list.doctype); + }); + }, +}; diff --git a/influxframework/core/doctype/activity_log/feed.py b/influxframework/core/doctype/activity_log/feed.py new file mode 100644 index 0000000..595409a --- /dev/null +++ b/influxframework/core/doctype/activity_log/feed.py @@ -0,0 +1,102 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +import influxframework.permissions +from influxframework import _ +from influxframework.core.doctype.activity_log.activity_log import add_authentication_log +from influxframework.utils import get_fullname + + +def update_feed(doc, method=None): + if influxframework.flags.in_patch or influxframework.flags.in_install or influxframework.flags.in_import: + return + + if doc._action != "save" or doc.flags.ignore_feed: + return + + if doc.doctype == "Activity Log" or doc.meta.issingle: + return + + if hasattr(doc, "get_feed"): + feed = doc.get_feed() + + if feed: + if isinstance(feed, str): + feed = {"subject": feed} + + feed = influxframework._dict(feed) + doctype = feed.doctype or doc.doctype + name = feed.name or doc.name + + # delete earlier feed + influxframework.db.delete( + "Activity Log", + {"reference_doctype": doctype, "reference_name": name, "link_doctype": feed.link_doctype}, + ) + + influxframework.get_doc( + { + "doctype": "Activity Log", + "reference_doctype": doctype, + "reference_name": name, + "subject": feed.subject, + "full_name": get_fullname(doc.owner), + "reference_owner": influxframework.db.get_value(doctype, name, "owner"), + "link_doctype": feed.link_doctype, + "link_name": feed.link_name, + } + ).insert(ignore_permissions=True) + + +def login_feed(login_manager): + if login_manager.user != "Guest": + subject = _("{0} logged in").format(get_fullname(login_manager.user)) + add_authentication_log(subject, login_manager.user) + + +def logout_feed(user, reason): + if user and user != "Guest": + subject = _("{0} logged out: {1}").format(get_fullname(user), influxframework.bold(reason)) + add_authentication_log(subject, user, operation="Logout") + + +def get_feed_match_conditions(user=None, doctype="Comment"): + if not user: + user = influxframework.session.user + + conditions = [ + "`tab{doctype}`.owner={user} or `tab{doctype}`.reference_owner={user}".format( + user=influxframework.db.escape(user), doctype=doctype + ) + ] + + user_permissions = influxframework.permissions.get_user_permissions(user) + can_read = influxframework.get_user().get_can_read() + + can_read_doctypes = [f"'{dt}'" for dt in list(set(can_read) - set(list(user_permissions)))] + + if can_read_doctypes: + conditions += [ + """(`tab{doctype}`.reference_doctype is null + or `tab{doctype}`.reference_doctype = '' + or `tab{doctype}`.reference_doctype + in ({values}))""".format( + doctype=doctype, values=", ".join(can_read_doctypes) + ) + ] + + if user_permissions: + can_read_docs = [] + for dt, obj in user_permissions.items(): + for n in obj: + can_read_docs.append("{}|{}".format(influxframework.db.escape(dt), influxframework.db.escape(n.get("doc", "")))) + + if can_read_docs: + conditions.append( + "concat_ws('|', `tab{doctype}`.reference_doctype, `tab{doctype}`.reference_name) in ({values})".format( + doctype=doctype, values=", ".join(can_read_docs) + ) + ) + + return "(" + " or ".join(conditions) + ")" diff --git a/influxframework/core/doctype/activity_log/test_activity_log.py b/influxframework/core/doctype/activity_log/test_activity_log.py new file mode 100644 index 0000000..6b67acf --- /dev/null +++ b/influxframework/core/doctype/activity_log/test_activity_log.py @@ -0,0 +1,95 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import time + +import influxframework +from influxframework.auth import CookieManager, LoginManager +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestActivityLog(InfluxFrameworkTestCase): + def test_activity_log(self): + + # test user login log + influxframework.local.form_dict = influxframework._dict( + { + "cmd": "login", + "sid": "Guest", + "pwd": influxframework.conf.admin_password or "admin", + "usr": "Administrator", + } + ) + + influxframework.local.cookie_manager = CookieManager() + influxframework.local.login_manager = LoginManager() + + auth_log = self.get_auth_log() + self.assertFalse(influxframework.form_dict.pwd) + self.assertEqual(auth_log.status, "Success") + + # test user logout log + influxframework.local.login_manager.logout() + auth_log = self.get_auth_log(operation="Logout") + self.assertEqual(auth_log.status, "Success") + + # test invalid login + influxframework.form_dict.update({"pwd": "password"}) + self.assertRaises(influxframework.AuthenticationError, LoginManager) + auth_log = self.get_auth_log() + self.assertEqual(auth_log.status, "Failed") + + influxframework.local.form_dict = influxframework._dict() + + def get_auth_log(self, operation="Login"): + names = influxframework.get_all( + "Activity Log", + filters={ + "user": "Administrator", + "operation": operation, + }, + order_by="`creation` DESC", + ) + + name = names[0] + auth_log = influxframework.get_doc("Activity Log", name) + return auth_log + + def test_brute_security(self): + update_system_settings({"allow_consecutive_login_attempts": 3, "allow_login_after_fail": 5}) + + influxframework.local.form_dict = influxframework._dict( + {"cmd": "login", "sid": "Guest", "pwd": "admin", "usr": "Administrator"} + ) + + influxframework.local.cookie_manager = CookieManager() + influxframework.local.login_manager = LoginManager() + + auth_log = self.get_auth_log() + self.assertEqual(auth_log.status, "Success") + + # test user logout log + influxframework.local.login_manager.logout() + auth_log = self.get_auth_log(operation="Logout") + self.assertEqual(auth_log.status, "Success") + + # test invalid login + influxframework.form_dict.update({"pwd": "password"}) + self.assertRaises(influxframework.AuthenticationError, LoginManager) + self.assertRaises(influxframework.AuthenticationError, LoginManager) + self.assertRaises(influxframework.AuthenticationError, LoginManager) + + # REMOVE ME: current logic allows allow_consecutive_login_attempts+1 attempts + # before raising security exception, remove below line when that is fixed. + self.assertRaises(influxframework.AuthenticationError, LoginManager) + self.assertRaises(influxframework.SecurityException, LoginManager) + time.sleep(5) + self.assertRaises(influxframework.AuthenticationError, LoginManager) + + influxframework.local.form_dict = influxframework._dict() + + +def update_system_settings(args): + doc = influxframework.get_doc("System Settings") + doc.update(args) + doc.flags.ignore_mandatory = 1 + doc.save() diff --git a/influxframework/core/doctype/block_module/__init__.py b/influxframework/core/doctype/block_module/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/block_module/block_module.json b/influxframework/core/doctype/block_module/block_module.json new file mode 100644 index 0000000..64deff6 --- /dev/null +++ b/influxframework/core/doctype/block_module/block_module.json @@ -0,0 +1,71 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2015-03-24 14:28:15.882903", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Other", + "editable_grid": 1, + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "module", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Module", + "length": 0, + "no_copy": 0, + "options": "", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "max_attachments": 0, + "modified": "2017-10-31 19:36:18.586834", + "modified_by": "Administrator", + "module": "Core", + "name": "Block Module", + "name_case": "", + "owner": "Administrator", + "permissions": [], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 0, + "track_seen": 0 +} \ No newline at end of file diff --git a/influxframework/core/doctype/block_module/block_module.py b/influxframework/core/doctype/block_module/block_module.py new file mode 100644 index 0000000..0538c45 --- /dev/null +++ b/influxframework/core/doctype/block_module/block_module.py @@ -0,0 +1,8 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +from influxframework.model.document import Document + + +class BlockModule(Document): + pass diff --git a/influxframework/core/doctype/comment/__init__.py b/influxframework/core/doctype/comment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/comment/comment.js b/influxframework/core/doctype/comment/comment.js new file mode 100644 index 0000000..7ffe34e --- /dev/null +++ b/influxframework/core/doctype/comment/comment.js @@ -0,0 +1,7 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Comment", { + // refresh: function(frm) { + // } +}); diff --git a/influxframework/core/doctype/comment/comment.json b/influxframework/core/doctype/comment/comment.json new file mode 100644 index 0000000..9f27e7e --- /dev/null +++ b/influxframework/core/doctype/comment/comment.json @@ -0,0 +1,153 @@ +{ + "actions": [], + "creation": "2019-02-07 10:10:46.845678", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "comment_type", + "comment_email", + "subject", + "comment_by", + "published", + "seen", + "column_break_5", + "reference_doctype", + "reference_name", + "link_doctype", + "link_name", + "reference_owner", + "section_break_10", + "content", + "ip_address" + ], + "fields": [ + { + "default": "Comment", + "fieldname": "comment_type", + "fieldtype": "Select", + "label": "Comment Type", + "options": "Comment\nLike\nInfo\nLabel\nWorkflow\nCreated\nSubmitted\nCancelled\nUpdated\nDeleted\nAssigned\nAssignment Completed\nAttachment\nAttachment Removed\nShared\nUnshared\nBot\nRelinked\nEdit", + "reqd": 1 + }, + { + "fieldname": "comment_email", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Comment Email" + }, + { + "fieldname": "subject", + "fieldtype": "Text", + "label": "Subject" + }, + { + "fieldname": "comment_by", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Comment By" + }, + { + "default": "0", + "fieldname": "published", + "fieldtype": "Check", + "label": "Published" + }, + { + "default": "0", + "fieldname": "seen", + "fieldtype": "Check", + "label": "Seen" + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Reference Document Type", + "options": "DocType" + }, + { + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "label": "Reference Name", + "options": "reference_doctype" + }, + { + "fieldname": "link_doctype", + "fieldtype": "Link", + "label": "Link DocType", + "options": "DocType" + }, + { + "fieldname": "link_name", + "fieldtype": "Dynamic Link", + "label": "Link Name", + "options": "link_doctype" + }, + { + "fieldname": "reference_owner", + "fieldtype": "Data", + "label": "Reference Owner", + "read_only": 1 + }, + { + "fieldname": "section_break_10", + "fieldtype": "Section Break" + }, + { + "fieldname": "content", + "fieldtype": "HTML Editor", + "ignore_xss_filter": 1, + "in_list_view": 1, + "label": "Content" + }, + { + "fieldname": "ip_address", + "fieldtype": "Data", + "hidden": 1, + "label": "IP Address" + } + ], + "links": [], + "modified": "2022-07-12 17:35:31.774137", + "modified_by": "Administrator", + "module": "Core", + "name": "Comment", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Website Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "comment_type", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/comment/comment.py b/influxframework/core/doctype/comment/comment.py new file mode 100644 index 0000000..48f2c7c --- /dev/null +++ b/influxframework/core/doctype/comment/comment.py @@ -0,0 +1,179 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +import json + +import influxframework +from influxframework.database.schema import add_column +from influxframework.desk.notifications import notify_mentions +from influxframework.exceptions import ImplicitCommitError +from influxframework.model.document import Document +from influxframework.model.utils import is_virtual_doctype +from influxframework.website.utils import clear_cache + + +class Comment(Document): + def after_insert(self): + notify_mentions(self.reference_doctype, self.reference_name, self.content) + self.notify_change("add") + + def validate(self): + if not self.comment_email: + self.comment_email = influxframework.session.user + self.content = influxframework.utils.sanitize_html(self.content) + + def on_update(self): + update_comment_in_doc(self) + if self.is_new(): + self.notify_change("update") + + def on_trash(self): + self.remove_comment_from_cache() + self.notify_change("delete") + + def notify_change(self, action): + key_map = { + "Like": "like_logs", + "Assigned": "assignment_logs", + "Assignment Completed": "assignment_logs", + "Comment": "comments", + "Attachment": "attachment_logs", + "Attachment Removed": "attachment_logs", + } + key = key_map.get(self.comment_type) + if not key: + return + + influxframework.publish_realtime( + f"update_docinfo_for_{self.reference_doctype}_{self.reference_name}", + {"doc": self.as_dict(), "key": key, "action": action}, + after_commit=True, + ) + + def remove_comment_from_cache(self): + _comments = get_comments_from_parent(self) + for c in _comments: + if c.get("name") == self.name: + _comments.remove(c) + + update_comments_in_parent(self.reference_doctype, self.reference_name, _comments) + + +def on_doctype_update(): + influxframework.db.add_index("Comment", ["reference_doctype", "reference_name"]) + + +def update_comment_in_doc(doc): + """Updates `_comments` (JSON) property in parent Document. + Creates a column `_comments` if property does not exist. + + Only user created Communication or Comment of type Comment are saved. + + `_comments` format + + { + "comment": [String], + "by": [user], + "name": [Comment Document name] + }""" + + # only comments get updates, not likes, assignments etc. + if doc.doctype == "Comment" and doc.comment_type != "Comment": + return + + def get_truncated(content): + return (content[:97] + "...") if len(content) > 100 else content + + if doc.reference_doctype and doc.reference_name and doc.content: + _comments = get_comments_from_parent(doc) + + updated = False + for c in _comments: + if c.get("name") == doc.name: + c["comment"] = get_truncated(doc.content) + updated = True + + if not updated: + _comments.append( + { + "comment": get_truncated(doc.content), + # "comment_email" for Comment and "sender" for Communication + "by": getattr(doc, "comment_email", None) or getattr(doc, "sender", None) or doc.owner, + "name": doc.name, + } + ) + + update_comments_in_parent(doc.reference_doctype, doc.reference_name, _comments) + + +def get_comments_from_parent(doc): + """ + get the list of comments cached in the document record in the column + `_comments` + """ + try: + if is_virtual_doctype(doc.reference_doctype): + _comments = "[]" + else: + _comments = influxframework.db.get_value(doc.reference_doctype, doc.reference_name, "_comments") or "[]" + + except Exception as e: + if influxframework.db.is_missing_table_or_column(e): + _comments = "[]" + + else: + raise + + try: + return json.loads(_comments) + except ValueError: + return [] + + +def update_comments_in_parent(reference_doctype, reference_name, _comments): + """Updates `_comments` property in parent Document with given dict. + + :param _comments: Dict of comments.""" + if ( + not reference_doctype + or not reference_name + or influxframework.db.get_value("DocType", reference_doctype, "issingle") + or is_virtual_doctype(reference_doctype) + ): + return + + try: + # use sql, so that we do not mess with the timestamp + influxframework.db.sql( + f"""update `tab{reference_doctype}` set `_comments`=%s where name=%s""", # nosec + (json.dumps(_comments[-100:]), reference_name), + ) + + except Exception as e: + if influxframework.db.is_column_missing(e) and getattr(influxframework.local, "request", None): + # missing column and in request, add column and update after commit + influxframework.local._comments = getattr(influxframework.local, "_comments", []) + [ + (reference_doctype, reference_name, _comments) + ] + + elif influxframework.db.is_data_too_long(e): + raise influxframework.DataTooLongException + + else: + raise ImplicitCommitError + else: + if influxframework.flags.in_patch: + return + + # Clear route cache + if route := influxframework.get_cached_value(reference_doctype, reference_name, "route"): + clear_cache(route) + + +def update_comments_in_parent_after_request(): + """update _comments in parent if _comments column is missing""" + if hasattr(influxframework.local, "_comments"): + for (reference_doctype, reference_name, _comments) in influxframework.local._comments: + add_column(reference_doctype, "_comments", "Text") + update_comments_in_parent(reference_doctype, reference_name, _comments) + + influxframework.db.commit() diff --git a/influxframework/core/doctype/comment/test_comment.py b/influxframework/core/doctype/comment/test_comment.py new file mode 100644 index 0000000..057f78c --- /dev/null +++ b/influxframework/core/doctype/comment/test_comment.py @@ -0,0 +1,104 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +import json + +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestComment(InfluxFrameworkTestCase): + def tearDown(self): + influxframework.form_dict.comment = None + influxframework.form_dict.comment_email = None + influxframework.form_dict.comment_by = None + influxframework.form_dict.reference_doctype = None + influxframework.form_dict.reference_name = None + influxframework.form_dict.route = None + influxframework.local.request_ip = None + + def test_comment_creation(self): + test_doc = influxframework.get_doc(dict(doctype="ToDo", description="test")) + test_doc.insert() + comment = test_doc.add_comment("Comment", "test comment") + + test_doc.reload() + + # check if updated in _comments cache + comments = json.loads(test_doc.get("_comments")) + self.assertEqual(comments[0].get("name"), comment.name) + self.assertEqual(comments[0].get("comment"), comment.content) + + # check document creation + comment_1 = influxframework.get_all( + "Comment", + fields=["*"], + filters=dict(reference_doctype=test_doc.doctype, reference_name=test_doc.name), + )[0] + + self.assertEqual(comment_1.content, "test comment") + + # test via blog + def test_public_comment(self): + from influxframework.website.doctype.blog_post.test_blog_post import make_test_blog + + test_blog = make_test_blog() + + influxframework.db.delete("Comment", {"reference_doctype": "Blog Post"}) + + from influxframework.templates.includes.comments.comments import add_comment + + influxframework.form_dict.comment = "Good comment with 10 chars" + influxframework.form_dict.comment_email = "test@test.com" + influxframework.form_dict.comment_by = "Good Tester" + influxframework.form_dict.reference_doctype = "Blog Post" + influxframework.form_dict.reference_name = test_blog.name + influxframework.form_dict.route = test_blog.route + influxframework.local.request_ip = "127.0.0.1" + + add_comment() + + self.assertEqual( + influxframework.get_all( + "Comment", + fields=["*"], + filters=dict(reference_doctype=test_blog.doctype, reference_name=test_blog.name), + )[0].published, + 1, + ) + + influxframework.db.delete("Comment", {"reference_doctype": "Blog Post"}) + + influxframework.form_dict.comment = "pleez vizits my site http://mysite.com" + influxframework.form_dict.comment_by = "bad commentor" + + add_comment() + + self.assertEqual( + len( + influxframework.get_all( + "Comment", + fields=["*"], + filters=dict(reference_doctype=test_blog.doctype, reference_name=test_blog.name), + ) + ), + 0, + ) + + # test for filtering html and css injection elements + influxframework.db.delete("Comment", {"reference_doctype": "Blog Post"}) + + influxframework.form_dict.comment = "Comment" + influxframework.form_dict.comment_by = "hacker" + + add_comment() + + self.assertEqual( + influxframework.get_all( + "Comment", + fields=["content"], + filters=dict(reference_doctype=test_blog.doctype, reference_name=test_blog.name), + )[0]["content"], + "Comment", + ) + + test_blog.delete() diff --git a/influxframework/core/doctype/communication/README.md b/influxframework/core/doctype/communication/README.md new file mode 100644 index 0000000..8ae1d4a --- /dev/null +++ b/influxframework/core/doctype/communication/README.md @@ -0,0 +1 @@ +Email or message sent or received from a contact and other DocTypes. `receive.py` will create a communication when a mail is received in the mailbox. \ No newline at end of file diff --git a/influxframework/core/doctype/communication/__init__.py b/influxframework/core/doctype/communication/__init__.py new file mode 100644 index 0000000..2e11a12 --- /dev/null +++ b/influxframework/core/doctype/communication/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE diff --git a/influxframework/core/doctype/communication/communication.js b/influxframework/core/doctype/communication/communication.js new file mode 100644 index 0000000..3a9ae63 --- /dev/null +++ b/influxframework/core/doctype/communication/communication.js @@ -0,0 +1,356 @@ +influxframework.ui.form.on("Communication", { + onload: function (frm) { + if (frm.doc.content) { + frm.doc.content = influxframework.dom.remove_script_and_style(frm.doc.content); + } + frm.set_query("reference_doctype", function () { + return { + filters: { + issingle: 0, + istable: 0, + }, + }; + }); + }, + refresh: function (frm) { + if (frm.is_new()) return; + + frm.convert_to_click && frm.set_convert_button(); + frm.subject_field = "subject"; + + // content field contains weird table html that does not render well in Quill + // this field is not to be edited directly anyway, so setting it as read only + frm.set_df_property("content", "read_only", 1); + + if (frm.doc.reference_doctype && frm.doc.reference_name) { + frm.add_custom_button(__(frm.doc.reference_name), function () { + influxframework.set_route("Form", frm.doc.reference_doctype, frm.doc.reference_name); + }); + } else { + // if an unlinked communication, set email field + if (frm.doc.sent_or_received === "Received") { + frm.email_field = "sender"; + } else { + frm.email_field = "recipients"; + } + } + + if (frm.doc.status === "Open") { + frm.add_custom_button(__("Close"), function () { + frm.trigger("mark_as_closed_open"); + }); + } else if (frm.doc.status !== "Linked") { + frm.add_custom_button(__("Reopen"), function () { + frm.trigger("mark_as_closed_open"); + }); + } + + frm.add_custom_button(__("Relink"), function () { + frm.trigger("show_relink_dialog"); + }); + + if ( + frm.doc.communication_type == "Communication" && + frm.doc.communication_medium == "Email" && + frm.doc.sent_or_received == "Received" + ) { + frm.add_custom_button(__("Reply"), function () { + frm.trigger("reply"); + }); + + frm.add_custom_button( + __("Reply All"), + function () { + frm.trigger("reply_all"); + }, + __("Actions") + ); + + frm.add_custom_button( + __("Forward"), + function () { + frm.trigger("forward_mail"); + }, + __("Actions") + ); + + frm.add_custom_button( + frm.doc.seen ? __("Mark as Unread") : __("Mark as Read"), + function () { + frm.trigger("mark_as_read_unread"); + }, + __("Actions") + ); + + frm.add_custom_button( + __("Move"), + function () { + frm.trigger("show_move_dialog"); + }, + __("Actions") + ); + + if (frm.doc.email_status != "Spam") + frm.add_custom_button( + __("Mark as Spam"), + function () { + frm.trigger("mark_as_spam"); + }, + __("Actions") + ); + + if (frm.doc.email_status != "Trash") { + frm.add_custom_button( + __("Move To Trash"), + function () { + frm.trigger("move_to_trash"); + }, + __("Actions") + ); + } + + frm.add_custom_button( + __("Contact"), + function () { + frm.trigger("add_to_contact"); + }, + __("Create") + ); + } + + if ( + frm.doc.communication_type == "Communication" && + frm.doc.communication_medium == "Phone" && + frm.doc.sent_or_received == "Received" + ) { + frm.add_custom_button( + __("Add Contact"), + function () { + frm.trigger("add_to_contact"); + }, + __("Actions") + ); + } + }, + + show_relink_dialog: function (frm) { + var d = new influxframework.ui.Dialog({ + title: __("Relink Communication"), + fields: [ + { + fieldtype: "Link", + options: "DocType", + label: __("Reference Doctype"), + fieldname: "reference_doctype", + get_query: function () { + return { query: "influxframework.email.get_communication_doctype" }; + }, + }, + { + fieldtype: "Dynamic Link", + options: "reference_doctype", + label: __("Reference Name"), + fieldname: "reference_name", + }, + ], + }); + d.set_value("reference_doctype", frm.doc.reference_doctype); + d.set_value("reference_name", frm.doc.reference_name); + d.set_primary_action(__("Relink"), function () { + var values = d.get_values(); + if (values) { + influxframework.confirm( + __("Are you sure you want to relink this communication to {0}?", [ + values["reference_name"], + ]), + function () { + d.hide(); + influxframework.call({ + method: "influxframework.email.relink", + args: { + name: frm.doc.name, + reference_doctype: values["reference_doctype"], + reference_name: values["reference_name"], + }, + callback: function () { + frm.refresh(); + }, + }); + }, + function () { + influxframework.show_alert({ + message: __("Document not Relinked"), + indicator: "info", + }); + } + ); + } + }); + d.show(); + }, + + show_move_dialog: function (frm) { + var d = new influxframework.ui.Dialog({ + title: __("Move"), + fields: [ + { + fieldtype: "Link", + options: "Email Account", + label: __("Email Account"), + fieldname: "email_account", + reqd: 1, + get_query: function () { + return { + filters: { + name: ["!=", frm.doc.email_account], + enable_incoming: ["=", 1], + }, + }; + }, + }, + ], + primary_action_label: __("Move"), + primary_action(values) { + d.hide(); + influxframework.call({ + method: "influxframework.email.inbox.move_email", + args: { + communication: frm.doc.name, + email_account: values.email_account, + }, + freeze: true, + callback: function () { + window.history.back(); + }, + }); + }, + }); + d.show(); + }, + + mark_as_read_unread: function (frm) { + var action = frm.doc.seen ? "Unread" : "Read"; + var flag = "(\\SEEN)"; + + return influxframework.call({ + method: "influxframework.email.inbox.create_email_flag_queue", + args: { + names: [frm.doc.name], + action: action, + flag: flag, + }, + freeze: true, + callback: function () { + frm.reload_doc(); + }, + }); + }, + + mark_as_closed_open: function (frm) { + var status = frm.doc.status == "Open" ? "Closed" : "Open"; + + return influxframework.call({ + method: "influxframework.email.inbox.mark_as_closed_open", + args: { + communication: frm.doc.name, + status: status, + }, + freeze: true, + callback: function () { + frm.reload_doc(); + }, + }); + }, + + reply: function (frm) { + var args = frm.events.get_mail_args(frm); + $.extend(args, { + subject: __("Re: {0}", [frm.doc.subject]), + recipients: frm.doc.sender, + }); + + new influxframework.views.CommunicationComposer(args); + }, + + reply_all: function (frm) { + var args = frm.events.get_mail_args(frm); + $.extend(args, { + subject: __("Res: {0}", [frm.doc.subject]), + recipients: frm.doc.sender, + cc: frm.doc.cc, + }); + new influxframework.views.CommunicationComposer(args); + }, + + forward_mail: function (frm) { + var args = frm.events.get_mail_args(frm); + $.extend(args, { + forward: true, + subject: __("Fw: {0}", [frm.doc.subject]), + }); + + new influxframework.views.CommunicationComposer(args); + }, + + get_mail_args: function (frm) { + var sender_email_id = ""; + $.each(influxframework.boot.email_accounts, function (idx, account) { + if (account.email_account == frm.doc.email_account) { + sender_email_id = account.email_id; + return; + } + }); + + return { + frm: frm, + doc: frm.doc, + last_email: frm.doc, + sender: sender_email_id, + attachments: frm.doc.attachments, + }; + }, + + add_to_contact: function (frm) { + var me = this; + var fullname = frm.doc.sender_full_name || ""; + + var names = fullname.split(" "); + var first_name = names[0]; + var last_name = names.length >= 2 ? names[names.length - 1] : ""; + + influxframework.route_options = { + email_id: frm.doc.sender || "", + first_name: first_name, + last_name: last_name, + mobile_no: frm.doc.phone_no || "", + }; + influxframework.new_doc("Contact"); + }, + + mark_as_spam: function (frm) { + influxframework.call({ + method: "influxframework.email.inbox.mark_as_spam", + args: { + communication: frm.doc.name, + sender: frm.doc.sender, + }, + freeze: true, + callback: function (r) { + influxframework.msgprint(__("Email has been marked as spam")); + }, + }); + }, + + move_to_trash: function (frm) { + influxframework.call({ + method: "influxframework.email.inbox.mark_as_trash", + args: { + communication: frm.doc.name, + }, + freeze: true, + callback: function (r) { + influxframework.msgprint(__("Email has been moved to trash")); + }, + }); + }, +}); diff --git a/influxframework/core/doctype/communication/communication.json b/influxframework/core/doctype/communication/communication.json new file mode 100644 index 0000000..293a6b2 --- /dev/null +++ b/influxframework/core/doctype/communication/communication.json @@ -0,0 +1,462 @@ +{ + "actions": [], + "allow_import": 1, + "creation": "2013-01-29 10:47:14", + "default_view": "Inbox", + "description": "Keeps track of all communications", + "doctype": "DocType", + "document_type": "Setup", + "email_append_to": 1, + "engine": "InnoDB", + "field_order": [ + "subject", + "section_break_10", + "communication_medium", + "sender", + "column_break_4", + "recipients", + "cc", + "bcc", + "phone_no", + "delivery_status", + "section_break_8", + "content", + "status_section", + "text_content", + "communication_type", + "comment_type", + "column_break_5", + "status", + "sent_or_received", + "additional_info", + "communication_date", + "read_receipt", + "column_break_14", + "sender_full_name", + "read_by_recipient", + "read_by_recipient_on", + "reference_section", + "reference_doctype", + "reference_name", + "reference_owner", + "email_account", + "in_reply_to", + "user", + "column_break_27", + "email_template", + "unread_notification_sent", + "seen", + "_user_tags", + "timeline_links_sections", + "timeline_links", + "email_inbox", + "message_id", + "uid", + "imap_folder", + "email_status", + "has_attachment", + "feedback_section", + "rating", + "feedback_request" + ], + "fields": [ + { + "fieldname": "subject", + "fieldtype": "Small Text", + "in_global_search": 1, + "label": "Subject", + "reqd": 1 + }, + { + "collapsible": 1, + "fieldname": "section_break_10", + "fieldtype": "Section Break", + "label": "To and CC" + }, + { + "depends_on": "eval:doc.communication_type===\"Communication\"", + "fieldname": "communication_medium", + "fieldtype": "Select", + "label": "Type", + "options": "\nEmail\nChat\nPhone\nSMS\nEvent\nMeeting\nVisit\nOther" + }, + { + "depends_on": "eval:doc.communication_medium===\"Email\"", + "fieldname": "sender", + "fieldtype": "Data", + "in_global_search": 1, + "label": "From", + "length": 255, + "options": "Email" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "recipients", + "fieldtype": "Code", + "label": "To", + "options": "Email" + }, + { + "depends_on": "eval:doc.communication_medium===\"Email\"", + "fieldname": "cc", + "fieldtype": "Code", + "label": "CC", + "options": "Email" + }, + { + "depends_on": "eval:doc.communication_medium===\"Email\"", + "fieldname": "bcc", + "fieldtype": "Code", + "label": "BCC", + "options": "Email" + }, + { + "depends_on": "eval:in_list([\"Phone\",\"SMS\"],doc.communication_medium)", + "fieldname": "phone_no", + "fieldtype": "Data", + "label": "Phone No." + }, + { + "description": "Integrations can use this field to set email delivery status", + "fieldname": "delivery_status", + "fieldtype": "Select", + "hidden": 1, + "label": "Delivery Status", + "options": "\nSent\nBounced\nOpened\nMarked As Spam\nRejected\nDelayed\nSoft-Bounced\nClicked\nRecipient Unsubscribed\nError\nExpired\nSending\nRead" + }, + { + "fieldname": "section_break_8", + "fieldtype": "Section Break" + }, + { + "fieldname": "content", + "fieldtype": "Text Editor", + "label": "Message", + "width": "400" + }, + { + "collapsible": 1, + "fieldname": "status_section", + "fieldtype": "Section Break", + "label": "Status" + }, + { + "fieldname": "text_content", + "fieldtype": "Code", + "hidden": 1, + "label": "Text Content" + }, + { + "default": "Communication", + "fieldname": "communication_type", + "fieldtype": "Select", + "label": "Communication Type", + "options": "Communication\nComment\nChat\nNotification\nFeedback\nAutomated Message", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "comment_type", + "fieldtype": "Select", + "hidden": 1, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Comment Type", + "options": "\nComment\nLike\nInfo\nLabel\nWorkflow\nCreated\nSubmitted\nCancelled\nUpdated\nDeleted\nAssigned\nAssignment Completed\nAttachment\nAttachment Removed\nShared\nUnshared\nRelinked", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.communication_type===\"Communication\"", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "Open\nReplied\nClosed\nLinked", + "reqd": 1 + }, + { + "depends_on": "eval:doc.communication_type===\"Communication\"", + "fieldname": "sent_or_received", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Sent or Received", + "options": "Sent\nReceived", + "reqd": 1 + }, + { + "collapsible": 1, + "fieldname": "additional_info", + "fieldtype": "Section Break", + "label": "More Information" + }, + { + "default": "Now", + "fieldname": "communication_date", + "fieldtype": "Datetime", + "label": "Date" + }, + { + "default": "0", + "fieldname": "read_receipt", + "fieldtype": "Check", + "label": "Sent Read Receipt", + "read_only": 1 + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break" + }, + { + "fieldname": "sender_full_name", + "fieldtype": "Data", + "label": "From Full Name", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "read_by_recipient", + "fieldtype": "Check", + "label": "Read by Recipient", + "read_only": 1 + }, + { + "fieldname": "read_by_recipient_on", + "fieldtype": "Datetime", + "label": "Read by Recipient On", + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "reference_section", + "fieldtype": "Section Break", + "label": "Reference" + }, + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "label": "Reference Document Type", + "options": "DocType" + }, + { + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "label": "Reference Name", + "options": "reference_doctype" + }, + { + "fetch_from": "reference_name.owner", + "fieldname": "reference_owner", + "fieldtype": "Read Only", + "label": "Reference Owner", + "search_index": 1 + }, + { + "depends_on": "eval:doc.communication_medium===\"Email\"", + "fieldname": "email_account", + "fieldtype": "Link", + "label": "Email Account", + "options": "Email Account", + "read_only": 1 + }, + { + "fieldname": "in_reply_to", + "fieldtype": "Link", + "label": "In Reply To", + "options": "Communication", + "read_only": 1 + }, + { + "default": "__user", + "fieldname": "user", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "User", + "options": "User", + "read_only": 1 + }, + { + "fieldname": "column_break_27", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "unread_notification_sent", + "fieldtype": "Check", + "label": "Unread Notification Sent", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "seen", + "fieldtype": "Check", + "label": "Seen", + "read_only": 1 + }, + { + "fieldname": "_user_tags", + "fieldtype": "Data", + "hidden": 1, + "label": "User Tags", + "no_copy": 1, + "print_hide": 1 + }, + { + "collapsible": 1, + "fieldname": "email_inbox", + "fieldtype": "Section Break", + "label": "Email Inbox", + "permlevel": 1 + }, + { + "fieldname": "message_id", + "fieldtype": "Data", + "ignore_xss_filter": 1, + "label": "Message ID", + "length": 995, + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "uid", + "fieldtype": "Int", + "hidden": 1, + "label": "UID", + "no_copy": 1 + }, + { + "fieldname": "email_status", + "fieldtype": "Select", + "label": "Email Status", + "options": "Open\nSpam\nTrash" + }, + { + "default": "0", + "fieldname": "has_attachment", + "fieldtype": "Check", + "hidden": 1, + "label": "Has Attachment" + }, + { + "collapsible": 1, + "depends_on": "eval: doc.rating > 0", + "fieldname": "feedback_section", + "fieldtype": "Section Break", + "label": "Feedback" + }, + { + "fieldname": "rating", + "fieldtype": "Int", + "label": "Rating", + "read_only": 1 + }, + { + "fieldname": "feedback_request", + "fieldtype": "Data", + "label": "Feedback Request", + "read_only": 1 + }, + { + "fieldname": "email_template", + "fieldtype": "Link", + "label": "Email Template", + "options": "Email Template", + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "timeline_links_sections", + "fieldtype": "Section Break", + "label": "Timeline Links" + }, + { + "fieldname": "timeline_links", + "fieldtype": "Table", + "label": "Timeline Links", + "options": "Communication Link", + "permlevel": 2 + }, + { + "fieldname": "imap_folder", + "fieldtype": "Data", + "hidden": 1, + "label": "IMAP Folder", + "read_only": 1 + } + ], + "icon": "fa fa-comment", + "idx": 1, + "links": [], + "modified": "2022-05-09 00:13:45.310564", + "modified_by": "Administrator", + "module": "Core", + "name": "Communication", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1 + }, + { + "delete": 1, + "email": 1, + "export": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1 + }, + { + "delete": 1, + "email": 1, + "export": 1, + "permlevel": 2, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "role": "Inbox User" + }, + { + "delete": 1, + "email": 1, + "if_owner": 1, + "read": 1, + "role": "All" + } + ], + "search_fields": "subject", + "sender_field": "sender", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "subject_field": "subject", + "title_field": "subject", + "track_changes": 1, + "track_seen": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/communication/communication.py b/influxframework/core/doctype/communication/communication.py new file mode 100644 index 0000000..42e384b --- /dev/null +++ b/influxframework/core/doctype/communication/communication.py @@ -0,0 +1,596 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +from collections import Counter +from email.utils import getaddresses +from urllib.parse import unquote + +from bs4 import BeautifulSoup + +import influxframework +from influxframework import _ +from influxframework.automation.doctype.assignment_rule.assignment_rule import ( + apply as apply_assignment_rule, +) +from influxframework.contacts.doctype.contact.contact import get_contact_name +from influxframework.core.doctype.comment.comment import update_comment_in_doc +from influxframework.core.doctype.communication.email import validate_email +from influxframework.core.doctype.communication.mixins import CommunicationEmailMixin +from influxframework.core.utils import get_parent_doc +from influxframework.model.document import Document +from influxframework.utils import ( + cstr, + parse_addr, + split_emails, + strip_html, + time_diff_in_seconds, + validate_email_address, +) +from influxframework.utils.user import is_system_user + +exclude_from_linked_with = True + + +class Communication(Document, CommunicationEmailMixin): + """Communication represents an external communication like Email.""" + + no_feed_on_delete = True + DOCTYPE = "Communication" + + def onload(self): + """create email flag queue""" + if ( + self.communication_type == "Communication" + and self.communication_medium == "Email" + and self.sent_or_received == "Received" + and self.uid + and self.uid != -1 + ): + + email_flag_queue = influxframework.db.get_value( + "Email Flag Queue", {"communication": self.name, "is_completed": 0} + ) + if email_flag_queue: + return + + influxframework.get_doc( + { + "doctype": "Email Flag Queue", + "action": "Read", + "communication": self.name, + "uid": self.uid, + "email_account": self.email_account, + } + ).insert(ignore_permissions=True) + influxframework.db.commit() + + def validate(self): + self.validate_reference() + + if not self.user: + self.user = influxframework.session.user + + if not self.subject: + self.subject = strip_html((self.content or "")[:141]) + + if not self.sent_or_received: + self.seen = 1 + self.sent_or_received = "Sent" + + self.set_status() + + validate_email(self) + + if self.communication_medium == "Email": + self.parse_email_for_timeline_links() + self.set_timeline_links() + self.deduplicate_timeline_links() + + self.set_sender_full_name() + + def validate_reference(self): + if self.reference_doctype and self.reference_name: + if not self.reference_owner: + self.reference_owner = influxframework.db.get_value( + self.reference_doctype, self.reference_name, "owner" + ) + + # prevent communication against a child table + if influxframework.get_meta(self.reference_doctype).istable: + influxframework.throw( + _("Cannot create a {0} against a child document: {1}").format( + _(self.communication_type), _(self.reference_doctype) + ) + ) + + # Prevent circular linking of Communication DocTypes + if self.reference_doctype == "Communication": + circular_linking = False + doc = get_parent_doc(self) + while doc.reference_doctype == "Communication": + if get_parent_doc(doc).name == self.name: + circular_linking = True + break + doc = get_parent_doc(doc) + + if circular_linking: + influxframework.throw( + _("Please make sure the Reference Communication Docs are not circularly linked."), + influxframework.CircularLinkingError, + ) + + def after_insert(self): + if not (self.reference_doctype and self.reference_name): + return + + if self.reference_doctype == "Communication" and self.sent_or_received == "Sent": + influxframework.db.set_value("Communication", self.reference_name, "status", "Replied") + + if self.communication_type == "Communication": + self.notify_change("add") + + elif self.communication_type in ("Chat", "Notification"): + if self.reference_name == influxframework.session.user: + message = self.as_dict() + message["broadcast"] = True + influxframework.publish_realtime("new_message", message, after_commit=True) + else: + # reference_name contains the user who is addressed in the messages' page comment + influxframework.publish_realtime( + "new_message", self.as_dict(), user=self.reference_name, after_commit=True + ) + + def set_signature_in_email_content(self): + """Set sender's User.email_signature or default outgoing's EmailAccount.signature to the email""" + if not self.content: + return + + soup = BeautifulSoup(self.content, "html.parser") + email_body = soup.find("div", {"class": "ql-editor read-mode"}) + + if not email_body: + return + + user_email_signature = ( + influxframework.db.get_value( + "User", + self.sender, + "email_signature", + ) + if self.sender + else None + ) + + signature = user_email_signature or influxframework.db.get_value( + "Email Account", + {"default_outgoing": 1, "add_signature": 1}, + "signature", + ) + + if not signature: + return + + soup = BeautifulSoup(signature, "html.parser") + html_signature = soup.find("div", {"class": "ql-editor read-mode"}) + _signature = None + if html_signature: + _signature = html_signature.renderContents() + + if (cstr(_signature) or signature) not in self.content: + self.content = f'{self.content}


    {signature}' + + def before_save(self): + if not self.flags.skip_add_signature: + self.set_signature_in_email_content() + + def on_update(self): + # add to _comment property of the doctype, so it shows up in + # comments count for the list view + update_comment_in_doc(self) + + if self.comment_type != "Updated": + update_parent_document_on_communication(self) + + def on_trash(self): + if self.communication_type == "Communication": + self.notify_change("delete") + + @property + def sender_mailid(self): + return parse_addr(self.sender)[1] if self.sender else "" + + @staticmethod + def _get_emails_list(emails=None, exclude_displayname=False): + """Returns list of emails from given email string. + + * Removes duplicate mailids + * Removes display name from email address if exclude_displayname is True + """ + emails = split_emails(emails) if isinstance(emails, str) else (emails or []) + if exclude_displayname: + return [email.lower() for email in {parse_addr(email)[1] for email in emails} if email] + return [email.lower() for email in set(emails) if email] + + def to_list(self, exclude_displayname=True): + """Returns to list.""" + return self._get_emails_list(self.recipients, exclude_displayname=exclude_displayname) + + def cc_list(self, exclude_displayname=True): + """Returns cc list.""" + return self._get_emails_list(self.cc, exclude_displayname=exclude_displayname) + + def bcc_list(self, exclude_displayname=True): + """Returns bcc list.""" + return self._get_emails_list(self.bcc, exclude_displayname=exclude_displayname) + + def get_attachments(self): + attachments = influxframework.get_all( + "File", + fields=["name", "file_name", "file_url", "is_private"], + filters={"attached_to_name": self.name, "attached_to_doctype": self.DOCTYPE}, + ) + return attachments + + def notify_change(self, action): + influxframework.publish_realtime( + f"update_docinfo_for_{self.reference_doctype}_{self.reference_name}", + {"doc": self.as_dict(), "key": "communications", "action": action}, + after_commit=True, + ) + + def set_status(self): + if not self.is_new(): + return + + if self.reference_doctype and self.reference_name: + self.status = "Linked" + elif self.communication_type == "Communication": + self.status = "Open" + else: + self.status = "Closed" + + # set email status to spam + email_rule = influxframework.db.get_value("Email Rule", {"email_id": self.sender, "is_spam": 1}) + if ( + self.communication_type == "Communication" + and self.communication_medium == "Email" + and self.sent_or_received == "Sent" + and email_rule + ): + + self.email_status = "Spam" + + @classmethod + def find(cls, name, ignore_error=False): + try: + return influxframework.get_doc(cls.DOCTYPE, name) + except influxframework.DoesNotExistError: + if ignore_error: + return + raise + + @classmethod + def find_one_by_filters(cls, *, order_by=None, **kwargs): + name = influxframework.db.get_value(cls.DOCTYPE, kwargs, order_by=order_by) + return cls.find(name) if name else None + + def update_db(self, **kwargs): + influxframework.db.set_value(self.DOCTYPE, self.name, kwargs) + + def set_sender_full_name(self): + if not self.sender_full_name and self.sender: + if self.sender == "Administrator": + self.sender_full_name = influxframework.db.get_value("User", "Administrator", "full_name") + self.sender = influxframework.db.get_value("User", "Administrator", "email") + elif self.sender == "Guest": + self.sender_full_name = self.sender + self.sender = None + else: + if self.sent_or_received == "Sent": + validate_email_address(self.sender, throw=True) + sender_name, sender_email = parse_addr(self.sender) + if sender_name == sender_email: + sender_name = None + + self.sender = sender_email + self.sender_full_name = sender_name + + if not self.sender_full_name: + self.sender_full_name = influxframework.db.get_value("User", self.sender, "full_name") + + if not self.sender_full_name: + first_name, last_name = influxframework.db.get_value( + "Contact", filters={"email_id": sender_email}, fieldname=["first_name", "last_name"] + ) or [None, None] + self.sender_full_name = (first_name or "") + (last_name or "") + + if not self.sender_full_name: + self.sender_full_name = sender_email + + def set_delivery_status(self, commit=False): + """Look into the status of Email Queue linked to this Communication and set the Delivery Status of this Communication""" + delivery_status = None + status_counts = Counter( + influxframework.get_all("Email Queue", pluck="status", filters={"communication": self.name}) + ) + if self.sent_or_received == "Received": + return + + if status_counts.get("Not Sent") or status_counts.get("Sending"): + delivery_status = "Sending" + + elif status_counts.get("Error"): + delivery_status = "Error" + + elif status_counts.get("Expired"): + delivery_status = "Expired" + + elif status_counts.get("Sent"): + delivery_status = "Sent" + + if delivery_status: + self.db_set("delivery_status", delivery_status) + self.notify_change("update") + + # for list views and forms + self.notify_update() + + if commit: + influxframework.db.commit() + + def parse_email_for_timeline_links(self): + parse_email(self, [self.recipients, self.cc, self.bcc]) + + # Timeline Links + def set_timeline_links(self): + contacts = [] + create_contact_enabled = self.email_account and influxframework.db.get_value( + "Email Account", self.email_account, "create_contact" + ) + contacts = get_contacts( + [self.sender, self.recipients, self.cc, self.bcc], auto_create_contact=create_contact_enabled + ) + + for contact_name in contacts: + self.add_link("Contact", contact_name) + + # link contact's dynamic links to communication + add_contact_links_to_communication(self, contact_name) + + def deduplicate_timeline_links(self): + if self.timeline_links: + links, duplicate = [], False + + for l in self.timeline_links: + t = (l.link_doctype, l.link_name) + if not t in links: + links.append(t) + else: + duplicate = True + + if duplicate: + del self.timeline_links[:] # make it python 2 compatible as list.clear() is python 3 only + for l in links: + self.add_link(link_doctype=l[0], link_name=l[1]) + + def add_link(self, link_doctype, link_name, autosave=False): + self.append("timeline_links", {"link_doctype": link_doctype, "link_name": link_name}) + + if autosave: + self.save(ignore_permissions=True) + + def get_links(self): + return self.timeline_links + + def remove_link(self, link_doctype, link_name, autosave=False, ignore_permissions=True): + for l in self.timeline_links: + if l.link_doctype == link_doctype and l.link_name == link_name: + self.timeline_links.remove(l) + + if autosave: + self.save(ignore_permissions=ignore_permissions) + + +def on_doctype_update(): + """Add indexes in `tabCommunication`""" + influxframework.db.add_index("Communication", ["reference_doctype", "reference_name"]) + influxframework.db.add_index("Communication", ["status", "communication_type"]) + + +def has_permission(doc, ptype, user): + if ptype == "read": + if doc.reference_doctype == "Communication" and doc.reference_name == doc.name: + return + + if doc.reference_doctype and doc.reference_name: + if influxframework.has_permission(doc.reference_doctype, ptype="read", doc=doc.reference_name): + return True + + +def get_permission_query_conditions_for_communication(user): + if not user: + user = influxframework.session.user + + roles = influxframework.get_roles(user) + + if "Super Email User" in roles or "System Manager" in roles: + return None + else: + accounts = influxframework.get_all( + "User Email", filters={"parent": user}, fields=["email_account"], distinct=True, order_by="idx" + ) + + if not accounts: + return """`tabCommunication`.communication_medium!='Email'""" + + email_accounts = ['"%s"' % account.get("email_account") for account in accounts] + return """`tabCommunication`.email_account in ({email_accounts})""".format( + email_accounts=",".join(email_accounts) + ) + + +def get_contacts(email_strings: list[str], auto_create_contact=False) -> list[str]: + email_addrs = get_emails(email_strings) + contacts = [] + for email in email_addrs: + email = get_email_without_link(email) + contact_name = get_contact_name(email) + + if not contact_name and email and auto_create_contact: + email_parts = email.split("@") + first_name = influxframework.unscrub(email_parts[0]) + + try: + contact_name = f"{first_name}-{email_parts[1]}" if first_name == "Contact" else first_name + contact = influxframework.get_doc( + {"doctype": "Contact", "first_name": contact_name, "name": contact_name} + ) + contact.add_email(email_id=email, is_primary=True) + contact.insert(ignore_permissions=True) + contact_name = contact.name + except Exception: + contact.log_error("Unable to add contact") + + if contact_name: + contacts.append(contact_name) + + return contacts + + +def get_emails(email_strings: list[str]) -> list[str]: + email_addrs = [] + + for email_string in email_strings: + if email_string: + result = getaddresses([email_string]) + for email in result: + email_addrs.append(email[1]) + + return email_addrs + + +def add_contact_links_to_communication(communication, contact_name): + contact_links = influxframework.get_all( + "Dynamic Link", + filters={"parenttype": "Contact", "parent": contact_name}, + fields=["link_doctype", "link_name"], + ) + + if contact_links: + for contact_link in contact_links: + communication.add_link(contact_link.link_doctype, contact_link.link_name) + + +def parse_email(communication, email_strings): + """ + Parse email to add timeline links. + When automatic email linking is enabled, an email from email_strings can contain + a doctype and docname ie in the format `admin+doctype+docname@example.com`, + the email is parsed and doctype and docname is extracted and timeline link is added. + """ + if not influxframework.get_all("Email Account", filters={"enable_automatic_linking": 1}): + return + + delimiter = "+" + + for email_string in email_strings: + if email_string: + for email in email_string.split(","): + if delimiter in email: + email = email.split("@")[0] + email_local_parts = email.split(delimiter) + if not len(email_local_parts) == 3: + continue + + doctype = unquote(email_local_parts[1]) + docname = unquote(email_local_parts[2]) + + if doctype and docname and influxframework.db.exists(doctype, docname): + communication.add_link(doctype, docname) + + +def get_email_without_link(email): + """ + returns email address without doctype links + returns admin@example.com for email admin+doctype+docname@example.com + """ + if not influxframework.get_all("Email Account", filters={"enable_automatic_linking": 1}): + return email + + try: + _email = email.split("@") + email_id = _email[0].split("+")[0] + email_host = _email[1] + except IndexError: + return email + + return f"{email_id}@{email_host}" + + +def update_parent_document_on_communication(doc): + """Update mins_to_first_communication of parent document based on who is replying.""" + + parent = get_parent_doc(doc) + if not parent: + return + + # update parent mins_to_first_communication only if we create the Email communication + # ignore in case of only Comment is added + if doc.communication_type == "Comment": + return + + status_field = parent.meta.get_field("status") + if status_field: + options = (status_field.options or "").splitlines() + + # if status has a "Replied" option, then update the status for received communication + if ("Replied" in options) and doc.sent_or_received == "Received": + parent.db_set("status", "Open") + parent.run_method("handle_hold_time", "Replied") + apply_assignment_rule(parent) + else: + # update the modified date for document + parent.update_modified() + + update_first_response_time(parent, doc) + set_avg_response_time(parent, doc) + parent.run_method("notify_communication", doc) + parent.notify_update() + + +def update_first_response_time(parent, communication): + if parent.meta.has_field("first_response_time") and not parent.get("first_response_time"): + if is_system_user(communication.sender): + if communication.sent_or_received == "Sent": + first_responded_on = communication.creation + if parent.meta.has_field("first_responded_on"): + parent.db_set("first_responded_on", first_responded_on) + first_response_time = round(time_diff_in_seconds(first_responded_on, parent.creation), 2) + parent.db_set("first_response_time", first_response_time) + + +def set_avg_response_time(parent, communication): + if parent.meta.has_field("avg_response_time") and communication.sent_or_received == "Sent": + # avg response time for all the responses + communications = influxframework.get_list( + "Communication", + filters={"reference_doctype": parent.doctype, "reference_name": parent.name}, + fields=["sent_or_received", "name", "creation"], + order_by="creation", + ) + + if len(communications): + response_times = [] + for i in range(len(communications)): + if ( + communications[i].sent_or_received == "Sent" + and communications[i - 1].sent_or_received == "Received" + ): + response_time = round( + time_diff_in_seconds(communications[i].creation, communications[i - 1].creation), 2 + ) + if response_time > 0: + response_times.append(response_time) + if response_times: + avg_response_time = sum(response_times) / len(response_times) + parent.db_set("avg_response_time", avg_response_time) diff --git a/influxframework/core/doctype/communication/communication_list.js b/influxframework/core/doctype/communication/communication_list.js new file mode 100644 index 0000000..cf0fc17 --- /dev/null +++ b/influxframework/core/doctype/communication/communication_list.js @@ -0,0 +1,32 @@ +influxframework.listview_settings["Communication"] = { + add_fields: [ + "sent_or_received", + "recipients", + "subject", + "communication_medium", + "communication_type", + "sender", + "seen", + "reference_doctype", + "reference_name", + "has_attachment", + "communication_date", + ], + + filters: [["status", "=", "Open"]], + + onload: function (list_view) { + let method = "influxframework.email.inbox.create_email_flag_queue"; + + list_view.page.add_menu_item(__("Mark as Read"), function () { + list_view.call_for_selected_items(method, { action: "Read" }); + }); + list_view.page.add_menu_item(__("Mark as Unread"), function () { + list_view.call_for_selected_items(method, { action: "Unread" }); + }); + }, + + primary_action: function () { + new influxframework.views.CommunicationComposer(); + }, +}; diff --git a/influxframework/core/doctype/communication/email.py b/influxframework/core/doctype/communication/email.py new file mode 100644 index 0000000..fa07f58 --- /dev/null +++ b/influxframework/core/doctype/communication/email.py @@ -0,0 +1,279 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE + +import json +from typing import TYPE_CHECKING + +import influxframework +import influxframework.email.smtp +from influxframework import _ +from influxframework.email.email_body import get_message_id +from influxframework.utils import ( + cint, + get_datetime, + get_formatted_email, + get_string_between, + list_to_str, + split_emails, + validate_email_address, +) + +if TYPE_CHECKING: + from influxframework.core.doctype.communication.communication import Communication + + +@influxframework.whitelist() +def make( + doctype=None, + name=None, + content=None, + subject=None, + sent_or_received="Sent", + sender=None, + sender_full_name=None, + recipients=None, + communication_medium="Email", + send_email=False, + print_html=None, + print_format=None, + attachments="[]", + send_me_a_copy=False, + cc=None, + bcc=None, + read_receipt=None, + print_letterhead=True, + email_template=None, + communication_type=None, + **kwargs, +) -> dict[str, str]: + """Make a new communication. Checks for email permissions for specified Document. + + :param doctype: Reference DocType. + :param name: Reference Document name. + :param content: Communication body. + :param subject: Communication subject. + :param sent_or_received: Sent or Received (default **Sent**). + :param sender: Communcation sender (default current user). + :param recipients: Communication recipients as list. + :param communication_medium: Medium of communication (default **Email**). + :param send_email: Send via email (default **False**). + :param print_html: HTML Print format to be sent as attachment. + :param print_format: Print Format name of parent document to be sent as attachment. + :param attachments: List of attachments as list of files or JSON string. + :param send_me_a_copy: Send a copy to the sender (default **False**). + :param email_template: Template which is used to compose mail . + """ + if kwargs: + from influxframework.utils.commands import warn + + warn( + f"Options {kwargs} used in influxframework.core.doctype.communication.email.make " + "are deprecated or unsupported", + category=DeprecationWarning, + ) + + if doctype and name and not influxframework.has_permission(doctype=doctype, ptype="email", doc=name): + raise influxframework.PermissionError(f"You are not allowed to send emails related to: {doctype} {name}") + + return _make( + doctype=doctype, + name=name, + content=content, + subject=subject, + sent_or_received=sent_or_received, + sender=sender, + sender_full_name=sender_full_name, + recipients=recipients, + communication_medium=communication_medium, + send_email=send_email, + print_html=print_html, + print_format=print_format, + attachments=attachments, + send_me_a_copy=cint(send_me_a_copy), + cc=cc, + bcc=bcc, + read_receipt=read_receipt, + print_letterhead=print_letterhead, + email_template=email_template, + communication_type=communication_type, + add_signature=False, + ) + + +def _make( + doctype=None, + name=None, + content=None, + subject=None, + sent_or_received="Sent", + sender=None, + sender_full_name=None, + recipients=None, + communication_medium="Email", + send_email=False, + print_html=None, + print_format=None, + attachments="[]", + send_me_a_copy=False, + cc=None, + bcc=None, + read_receipt=None, + print_letterhead=True, + email_template=None, + communication_type=None, + add_signature=True, +) -> dict[str, str]: + """Internal method to make a new communication that ignores Permission checks.""" + + sender = sender or get_formatted_email(influxframework.session.user) + recipients = list_to_str(recipients) if isinstance(recipients, list) else recipients + cc = list_to_str(cc) if isinstance(cc, list) else cc + bcc = list_to_str(bcc) if isinstance(bcc, list) else bcc + + comm: "Communication" = influxframework.get_doc( + { + "doctype": "Communication", + "subject": subject, + "content": content, + "sender": sender, + "sender_full_name": sender_full_name, + "recipients": recipients, + "cc": cc or None, + "bcc": bcc or None, + "communication_medium": communication_medium, + "sent_or_received": sent_or_received, + "reference_doctype": doctype, + "reference_name": name, + "email_template": email_template, + "message_id": get_string_between("<", get_message_id(), ">"), + "read_receipt": read_receipt, + "has_attachment": 1 if attachments else 0, + "communication_type": communication_type, + } + ) + comm.flags.skip_add_signature = not add_signature + comm.insert(ignore_permissions=True) + + # if not committed, delayed task doesn't find the communication + if attachments: + if isinstance(attachments, str): + attachments = json.loads(attachments) + add_attachments(comm.name, attachments) + + if cint(send_email): + if not comm.get_outgoing_email_account(): + influxframework.throw( + _( + "Unable to send mail because of a missing email account. Please setup default Email Account from Setup > Email > Email Account" + ), + exc=influxframework.OutgoingEmailError, + ) + + comm.send_email( + print_html=print_html, + print_format=print_format, + send_me_a_copy=send_me_a_copy, + print_letterhead=print_letterhead, + ) + + emails_not_sent_to = comm.exclude_emails_list(include_sender=send_me_a_copy) + + return {"name": comm.name, "emails_not_sent_to": ", ".join(emails_not_sent_to)} + + +def validate_email(doc: "Communication") -> None: + """Validate Email Addresses of Recipients and CC""" + if ( + not (doc.communication_type == "Communication" and doc.communication_medium == "Email") + or doc.flags.in_receive + ): + return + + # validate recipients + for email in split_emails(doc.recipients): + validate_email_address(email, throw=True) + + # validate CC + for email in split_emails(doc.cc): + validate_email_address(email, throw=True) + + for email in split_emails(doc.bcc): + validate_email_address(email, throw=True) + + +def set_incoming_outgoing_accounts(doc): + from influxframework.email.doctype.email_account.email_account import EmailAccount + + incoming_email_account = EmailAccount.find_incoming( + match_by_email=doc.sender, match_by_doctype=doc.reference_doctype + ) + doc.incoming_email_account = incoming_email_account.email_id if incoming_email_account else None + + doc.outgoing_email_account = EmailAccount.find_outgoing( + match_by_email=doc.sender, match_by_doctype=doc.reference_doctype + ) + + if doc.sent_or_received == "Sent": + doc.db_set("email_account", doc.outgoing_email_account.name) + + +def add_attachments(name, attachments): + """Add attachments to the given Communication""" + # loop through attachments + for a in attachments: + if isinstance(a, str): + attach = influxframework.db.get_value( + "File", {"name": a}, ["file_name", "file_url", "is_private"], as_dict=1 + ) + # save attachments to new doc + _file = influxframework.get_doc( + { + "doctype": "File", + "file_url": attach.file_url, + "attached_to_doctype": "Communication", + "attached_to_name": name, + "folder": "Home/Attachments", + "is_private": attach.is_private, + } + ) + _file.save(ignore_permissions=True) + + +@influxframework.whitelist(allow_guest=True, methods=("GET",)) +def mark_email_as_seen(name: str = None): + try: + update_communication_as_read(name) + influxframework.db.commit() # nosemgrep: this will be called in a GET request + + except Exception: + influxframework.log_error("Unable to mark as seen", None, "Communication", name) + + finally: + influxframework.response.update( + { + "type": "binary", + "filename": "imaginary_pixel.png", + "filecontent": ( + b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00" + b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\r" + b"IDATx\x9cc\xf8\xff\xff?\x03\x00\x08\xfc\x02\xfe\xa7\x9a\xa0" + b"\xa0\x00\x00\x00\x00IEND\xaeB`\x82" + ), + } + ) + + +def update_communication_as_read(name): + if not name or not isinstance(name, str): + return + + communication = influxframework.db.get_value("Communication", name, "read_by_recipient", as_dict=True) + + if not communication or communication.read_by_recipient: + return + + influxframework.db.set_value( + "Communication", + name, + {"read_by_recipient": 1, "delivery_status": "Read", "read_by_recipient_on": get_datetime()}, + ) diff --git a/influxframework/core/doctype/communication/mixins.py b/influxframework/core/doctype/communication/mixins.py new file mode 100644 index 0000000..d573584 --- /dev/null +++ b/influxframework/core/doctype/communication/mixins.py @@ -0,0 +1,309 @@ +import influxframework +from influxframework import _ +from influxframework.core.utils import get_parent_doc +from influxframework.desk.doctype.todo.todo import ToDo +from influxframework.email.doctype.email_account.email_account import EmailAccount +from influxframework.utils import get_formatted_email, get_url, parse_addr + + +class CommunicationEmailMixin: + """Mixin class to handle communication mails.""" + + def is_email_communication(self): + return self.communication_type == "Communication" and self.communication_medium == "Email" + + def get_owner(self): + """Get owner of the communication docs parent.""" + parent_doc = get_parent_doc(self) + return parent_doc.owner if parent_doc else None + + def get_all_email_addresses(self, exclude_displayname=False): + """Get all Email addresses mentioned in the doc along with display name.""" + return ( + self.to_list(exclude_displayname=exclude_displayname) + + self.cc_list(exclude_displayname=exclude_displayname) + + self.bcc_list(exclude_displayname=exclude_displayname) + ) + + def get_email_with_displayname(self, email_address): + """Returns email address after adding displayname.""" + display_name, email = parse_addr(email_address) + if display_name and display_name != email: + return email_address + + # emailid to emailid with display name map. + email_map = {parse_addr(email)[1]: email for email in self.get_all_email_addresses()} + return email_map.get(email, email) + + def mail_recipients(self, is_inbound_mail_communcation=False): + """Build to(recipient) list to send an email.""" + # Incase of inbound mail, recipients already received the mail, no need to send again. + if is_inbound_mail_communcation: + return [] + + if hasattr(self, "_final_recipients"): + return self._final_recipients + + to = self.to_list() + self._final_recipients = list(filter(lambda id: id != "Administrator", to)) + return self._final_recipients + + def get_mail_recipients_with_displayname(self, is_inbound_mail_communcation=False): + """Build to(recipient) list to send an email including displayname in email.""" + to_list = self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation) + return [self.get_email_with_displayname(email) for email in to_list] + + def mail_cc(self, is_inbound_mail_communcation=False, include_sender=False): + """Build cc list to send an email. + + * if email copy is requested by sender, then add sender to CC. + * If this doc is created through inbound mail, then add doc owner to cc list + * remove all the thread_notify disabled users. + * Make sure that all users enabled in the system + * Remove admin from email list + + * FixMe: Removed adding TODO owners to cc list. Check if that is needed. + """ + if hasattr(self, "_final_cc"): + return self._final_cc + + cc = self.cc_list() + + # Need to inform parent document owner incase communication is created through inbound mail + if include_sender: + cc.append(self.sender_mailid) + if is_inbound_mail_communcation: + if (doc_owner := self.get_owner()) not in influxframework.STANDARD_USERS: + cc.append(doc_owner) + cc = set(cc) - {self.sender_mailid} + cc.update(self.get_assignees()) + + cc = set(cc) - set(self.filter_thread_notification_disbled_users(cc)) + cc = cc - set(self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation)) + cc = cc - set(self.filter_disabled_users(cc)) + + # # Incase of inbound mail, to and cc already received the mail, no need to send again. + if is_inbound_mail_communcation: + cc = cc - set(self.cc_list() + self.to_list()) + + self._final_cc = list(filter(lambda id: id != "Administrator", cc)) + return self._final_cc + + def get_mail_cc_with_displayname(self, is_inbound_mail_communcation=False, include_sender=False): + cc_list = self.mail_cc( + is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=include_sender + ) + return [self.get_email_with_displayname(email) for email in cc_list if email] + + def mail_bcc(self, is_inbound_mail_communcation=False): + """ + * Thread_notify check + * Email unsubscribe list + * User must be enabled in the system + * remove_administrator_from_email_list + """ + if hasattr(self, "_final_bcc"): + return self._final_bcc + + bcc = set(self.bcc_list()) + if is_inbound_mail_communcation: + bcc = bcc - {self.sender_mailid} + bcc = bcc - set(self.filter_thread_notification_disbled_users(bcc)) + bcc = bcc - set(self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation)) + bcc = bcc - set(self.filter_disabled_users(bcc)) + + # Incase of inbound mail, to and cc & bcc already received the mail, no need to send again. + if is_inbound_mail_communcation: + bcc = bcc - set(self.bcc_list() + self.to_list()) + + self._final_bcc = list(filter(lambda id: id != "Administrator", bcc)) + return self._final_bcc + + def get_mail_bcc_with_displayname(self, is_inbound_mail_communcation=False): + bcc_list = self.mail_bcc(is_inbound_mail_communcation=is_inbound_mail_communcation) + return [self.get_email_with_displayname(email) for email in bcc_list if email] + + def mail_sender(self): + email_account = self.get_outgoing_email_account() + if not self.sender_mailid and email_account: + return email_account.email_id + return self.sender_mailid + + def mail_sender_fullname(self): + email_account = self.get_outgoing_email_account() + if not self.sender_full_name: + return (email_account and email_account.name) or _("Notification") + return self.sender_full_name + + def get_mail_sender_with_displayname(self): + return get_formatted_email(self.mail_sender_fullname(), mail=self.mail_sender()) + + def get_content(self, print_format=None): + if print_format: + return self.content + self.get_attach_link(print_format) + return self.content + + def get_attach_link(self, print_format): + """Returns public link for the attachment via `templates/emails/print_link.html`.""" + return influxframework.get_template("templates/emails/print_link.html").render( + { + "url": get_url(), + "doctype": self.reference_doctype, + "name": self.reference_name, + "print_format": print_format, + "key": get_parent_doc(self).get_document_share_key(), + } + ) + + def get_outgoing_email_account(self): + if not hasattr(self, "_outgoing_email_account"): + if self.email_account: + self._outgoing_email_account = EmailAccount.find(self.email_account) + else: + self._outgoing_email_account = EmailAccount.find_outgoing( + match_by_email=self.sender_mailid, match_by_doctype=self.reference_doctype + ) + + if self.sent_or_received == "Sent" and self._outgoing_email_account: + if influxframework.db.exists("Email Account", self._outgoing_email_account.name): + self.db_set("email_account", self._outgoing_email_account.name) + + return self._outgoing_email_account + + def get_incoming_email_account(self): + if not hasattr(self, "_incoming_email_account"): + self._incoming_email_account = EmailAccount.find_incoming( + match_by_email=self.sender_mailid, match_by_doctype=self.reference_doctype + ) + return self._incoming_email_account + + def mail_attachments(self, print_format=None, print_html=None): + final_attachments = [] + + if print_format or print_html: + d = { + "print_format": print_format, + "html": print_html, + "print_format_attachment": 1, + "doctype": self.reference_doctype, + "name": self.reference_name, + } + final_attachments.append(d) + + for a in self.get_attachments() or []: + final_attachments.append({"fid": a["name"]}) + + return final_attachments + + def get_unsubscribe_message(self): + email_account = self.get_outgoing_email_account() + if email_account and email_account.send_unsubscribe_message: + return _("Leave this conversation") + return "" + + def exclude_emails_list(self, is_inbound_mail_communcation=False, include_sender=False) -> list: + """List of mail id's excluded while sending mail.""" + all_ids = self.get_all_email_addresses(exclude_displayname=True) + + final_ids = ( + self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation) + + self.mail_bcc(is_inbound_mail_communcation=is_inbound_mail_communcation) + + self.mail_cc( + is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=include_sender + ) + ) + + return list(set(all_ids) - set(final_ids)) + + def get_assignees(self): + """Get owners of the reference document.""" + filters = { + "status": "Open", + "reference_name": self.reference_name, + "reference_type": self.reference_doctype, + } + return ToDo.get_owners(filters) + + @staticmethod + def filter_thread_notification_disbled_users(emails): + """Filter users based on notifications for email threads setting is disabled.""" + if not emails: + return [] + + return influxframework.get_all( + "User", pluck="email", filters={"email": ["in", emails], "thread_notify": 0} + ) + + @staticmethod + def filter_disabled_users(emails): + """ """ + if not emails: + return [] + + return influxframework.get_all("User", pluck="email", filters={"email": ["in", emails], "enabled": 0}) + + def sendmail_input_dict( + self, + print_html=None, + print_format=None, + send_me_a_copy=None, + print_letterhead=None, + is_inbound_mail_communcation=None, + ) -> dict: + + outgoing_email_account = self.get_outgoing_email_account() + if not outgoing_email_account: + return {} + + recipients = self.get_mail_recipients_with_displayname( + is_inbound_mail_communcation=is_inbound_mail_communcation + ) + cc = self.get_mail_cc_with_displayname( + is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=send_me_a_copy + ) + bcc = self.get_mail_bcc_with_displayname( + is_inbound_mail_communcation=is_inbound_mail_communcation + ) + + if not (recipients or cc): + return {} + + final_attachments = self.mail_attachments(print_format=print_format, print_html=print_html) + incoming_email_account = self.get_incoming_email_account() + return { + "recipients": recipients, + "cc": cc, + "bcc": bcc, + "expose_recipients": "header", + "sender": self.get_mail_sender_with_displayname(), + "reply_to": incoming_email_account and incoming_email_account.email_id, + "subject": self.subject, + "content": self.get_content(print_format=print_format), + "reference_doctype": self.reference_doctype, + "reference_name": self.reference_name, + "attachments": final_attachments, + "message_id": self.message_id, + "unsubscribe_message": self.get_unsubscribe_message(), + "delayed": True, + "communication": self.name, + "read_receipt": self.read_receipt, + "is_notification": (self.sent_or_received == "Received" and True) or False, + "print_letterhead": print_letterhead, + } + + def send_email( + self, + print_html=None, + print_format=None, + send_me_a_copy=None, + print_letterhead=None, + is_inbound_mail_communcation=None, + ): + if input_dict := self.sendmail_input_dict( + print_html=print_html, + print_format=print_format, + send_me_a_copy=send_me_a_copy, + print_letterhead=print_letterhead, + is_inbound_mail_communcation=is_inbound_mail_communcation, + ): + influxframework.sendmail(**input_dict) diff --git a/influxframework/core/doctype/communication/test_communication.py b/influxframework/core/doctype/communication/test_communication.py new file mode 100644 index 0000000..1f43584 --- /dev/null +++ b/influxframework/core/doctype/communication/test_communication.py @@ -0,0 +1,395 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE +from typing import TYPE_CHECKING +from urllib.parse import quote + +import influxframework +from influxframework.core.doctype.communication.communication import Communication, get_emails +from influxframework.email.doctype.email_queue.email_queue import EmailQueue +from influxframework.tests.utils import InfluxFrameworkTestCase + +if TYPE_CHECKING: + from influxframework.contacts.doctype.contact.contact import Contact + from influxframework.email.doctype.email_account.email_account import EmailAccount + +test_records = influxframework.get_test_records("Communication") + + +class TestCommunication(InfluxFrameworkTestCase): + def test_email(self): + valid_email_list = [ + "Full Name ", + '"Full Name with quotes and " ', + "Surname, Name ", + "Purchase@ABC ", + "xyz@abc2.com ", + "Name [something else] ", + ] + + invalid_email_list = [ + "[invalid!email]", + "invalid-email", + "tes2", + "e", + "rrrrrrrr", + "manas", + "[[[sample]]]", + "[invalid!email].com", + ] + + for i, x in enumerate(valid_email_list): + with self.subTest(i=i, x=x): + self.assertTrue(influxframework.utils.parse_addr(x)[1]) + + for i, x in enumerate(invalid_email_list): + with self.subTest(i=i, x=x): + self.assertFalse(influxframework.utils.parse_addr(x)[0]) + + def test_name(self): + valid_email_list = [ + "Full Name ", + '"Full Name with quotes and " ', + "Surname, Name ", + "Purchase@ABC ", + "xyz@abc2.com ", + "Name [something else] ", + ] + + invalid_email_list = [ + "[invalid!email]", + "invalid-email", + "tes2", + "e", + "rrrrrrrr", + "manas", + "[[[sample]]]", + "[invalid!email].com", + ] + + for x in valid_email_list: + self.assertTrue(influxframework.utils.parse_addr(x)[0]) + + for x in invalid_email_list: + self.assertFalse(influxframework.utils.parse_addr(x)[0]) + + def test_circular_linking(self): + a = influxframework.get_doc( + { + "doctype": "Communication", + "communication_type": "Communication", + "content": "This was created to test circular linking: Communication A", + } + ).insert(ignore_permissions=True) + + b = influxframework.get_doc( + { + "doctype": "Communication", + "communication_type": "Communication", + "content": "This was created to test circular linking: Communication B", + "reference_doctype": "Communication", + "reference_name": a.name, + } + ).insert(ignore_permissions=True) + + c = influxframework.get_doc( + { + "doctype": "Communication", + "communication_type": "Communication", + "content": "This was created to test circular linking: Communication C", + "reference_doctype": "Communication", + "reference_name": b.name, + } + ).insert(ignore_permissions=True) + + a = influxframework.get_doc("Communication", a.name) + a.reference_doctype = "Communication" + a.reference_name = c.name + + self.assertRaises(influxframework.CircularLinkingError, a.save) + + def test_deduplication_timeline_links(self): + influxframework.delete_doc_if_exists("Note", "deduplication timeline links") + + note = influxframework.get_doc( + { + "doctype": "Note", + "title": "deduplication timeline links", + "content": "deduplication timeline links", + } + ).insert(ignore_permissions=True) + + comm = influxframework.get_doc( + { + "doctype": "Communication", + "communication_type": "Communication", + "content": "Deduplication of Links", + "communication_medium": "Email", + } + ).insert(ignore_permissions=True) + + # adding same link twice + comm.add_link(link_doctype="Note", link_name=note.name, autosave=True) + comm.add_link(link_doctype="Note", link_name=note.name, autosave=True) + + comm = influxframework.get_doc("Communication", comm.name) + + self.assertNotEqual(2, len(comm.timeline_links)) + + def test_contacts_attached(self): + contact_sender: "Contact" = influxframework.get_doc( + { + "doctype": "Contact", + "first_name": "contact_sender", + } + ) + contact_sender.add_email("comm_sender@example.com") + contact_sender.insert(ignore_permissions=True) + + contact_recipient: "Contact" = influxframework.get_doc( + { + "doctype": "Contact", + "first_name": "contact_recipient", + } + ) + contact_recipient.add_email("comm_recipient@example.com") + contact_recipient.insert(ignore_permissions=True) + + contact_cc: "Contact" = influxframework.get_doc( + { + "doctype": "Contact", + "first_name": "contact_cc", + } + ) + contact_cc.add_email("comm_cc@example.com") + contact_cc.insert(ignore_permissions=True) + + comm: Communication = influxframework.get_doc( + { + "doctype": "Communication", + "communication_medium": "Email", + "subject": "Contacts Attached Test", + "sender": "comm_sender@example.com", + "recipients": "comm_recipient@example.com", + "cc": "comm_cc@example.com", + } + ).insert(ignore_permissions=True) + + comm = influxframework.get_doc("Communication", comm.name) + contact_links = [x.link_name for x in comm.timeline_links] + + self.assertIn(contact_sender.name, contact_links) + self.assertIn(contact_recipient.name, contact_links) + self.assertIn(contact_cc.name, contact_links) + + def test_get_communication_data(self): + from influxframework.desk.form.load import get_communication_data + + influxframework.delete_doc_if_exists("Note", "get communication data") + + note = influxframework.get_doc( + {"doctype": "Note", "title": "get communication data", "content": "get communication data"} + ).insert(ignore_permissions=True) + + comm_note_1 = influxframework.get_doc( + { + "doctype": "Communication", + "communication_type": "Communication", + "content": "Test Get Communication Data 1", + "communication_medium": "Email", + } + ).insert(ignore_permissions=True) + + comm_note_1.add_link(link_doctype="Note", link_name=note.name, autosave=True) + + comm_note_2 = influxframework.get_doc( + { + "doctype": "Communication", + "communication_type": "Communication", + "content": "Test Get Communication Data 2", + "communication_medium": "Email", + } + ).insert(ignore_permissions=True) + + comm_note_2.add_link(link_doctype="Note", link_name=note.name, autosave=True) + + comms = get_communication_data("Note", note.name, as_dict=True) + + data = [comm.name for comm in comms] + self.assertIn(comm_note_1.name, data) + self.assertIn(comm_note_2.name, data) + + def test_link_in_email(self): + influxframework.delete_doc_if_exists("Note", "test document link in email") + + create_email_account() + + note = influxframework.get_doc( + { + "doctype": "Note", + "title": "test document link in email", + "content": "test document link in email", + } + ).insert(ignore_permissions=True) + + comm = influxframework.get_doc( + { + "doctype": "Communication", + "communication_medium": "Email", + "subject": "Document Link in Email", + "sender": "comm_sender@example.com", + "recipients": f'comm_recipient+{quote("Note")}+{quote(note.name)}@example.com', + } + ).insert(ignore_permissions=True) + + doc_links = [ + (timeline_link.link_doctype, timeline_link.link_name) for timeline_link in comm.timeline_links + ] + self.assertIn(("Note", note.name), doc_links) + + def test_parse_emails(self): + emails = get_emails( + [ + "comm_recipient+DocType+DocName@example.com", + '"First, LastName" ', + "test@user.com", + ] + ) + + self.assertEqual(emails[0], "comm_recipient+DocType+DocName@example.com") + self.assertEqual(emails[1], "first.lastname@email.com") + self.assertEqual(emails[2], "test@user.com") + + def test_signature_in_email_content(self): + email_account = create_email_account() + signature = email_account.signature + base_communication = { + "doctype": "Communication", + "communication_medium": "Email", + "subject": "Document Link in Email", + "sender": "comm_sender@example.com", + } + comm_with_signature = influxframework.get_doc( + base_communication + | { + "content": f"""

    + Hi, + How are you? +


    {signature}

    """, + } + ).insert(ignore_permissions=True) + comm_without_signature = influxframework.get_doc( + base_communication + | { + "content": """
    + Hi, + How are you? +
    """ + } + ).insert(ignore_permissions=True) + + self.assertEqual(comm_with_signature.content, comm_without_signature.content) + self.assertEqual(comm_with_signature.content.count(signature), 1) + self.assertEqual(comm_without_signature.content.count(signature), 1) + + +class TestCommunicationEmailMixin(InfluxFrameworkTestCase): + def new_communication(self, recipients=None, cc=None, bcc=None) -> Communication: + recipients = ", ".join(recipients or []) + cc = ", ".join(cc or []) + bcc = ", ".join(bcc or []) + + return influxframework.get_doc( + { + "doctype": "Communication", + "communication_type": "Communication", + "communication_medium": "Email", + "content": "Test content", + "recipients": recipients, + "cc": cc, + "bcc": bcc, + } + ).insert(ignore_permissions=True) + + def new_user(self, email, **user_data): + user_data.setdefault("first_name", "first_name") + user = influxframework.new_doc("User") + user.email = email + user.update(user_data) + user.insert(ignore_permissions=True, ignore_if_duplicate=True) + return user + + def test_recipients(self): + to_list = ["to@test.com", "receiver ", "to@test.com"] + comm = self.new_communication(recipients=to_list) + res = comm.get_mail_recipients_with_displayname() + self.assertCountEqual(res, ["to@test.com", "receiver "]) + comm.delete() + + def test_cc(self): + to_list = ["to@test.com"] + cc_list = ["cc+1@test.com", "cc ", "to@test.com"] + user = self.new_user(email="cc+1@test.com", thread_notify=0) + comm = self.new_communication(recipients=to_list, cc=cc_list) + res = comm.get_mail_cc_with_displayname() + self.assertCountEqual(res, ["cc "]) + user.delete() + comm.delete() + + def test_bcc(self): + bcc_list = [ + "bcc+1@test.com", + "cc ", + ] + user = self.new_user(email="bcc+2@test.com", enabled=0) + comm = self.new_communication(bcc=bcc_list) + res = comm.get_mail_bcc_with_displayname() + self.assertCountEqual(res, ["bcc+1@test.com"]) + user.delete() + comm.delete() + + def test_sendmail(self): + to_list = ["to "] + cc_list = ["cc ", "cc "] + + comm = self.new_communication(recipients=to_list, cc=cc_list) + comm.send_email() + doc = EmailQueue.find_one_by_filters(communication=comm.name) + mail_receivers = [each.recipient for each in doc.recipients] + self.assertIsNotNone(doc) + self.assertCountEqual(to_list + cc_list, mail_receivers) + doc.delete() + comm.delete() + + +def create_email_account() -> "EmailAccount": + influxframework.delete_doc_if_exists("Email Account", "_Test Comm Account 1") + + influxframework.flags.mute_emails = False + influxframework.flags.sent_mail = None + + return influxframework.get_doc( + { + "is_default": 1, + "is_global": 1, + "doctype": "Email Account", + "domain": "example.com", + "append_to": "ToDo", + "email_account_name": "_Test Comm Account 1", + "enable_outgoing": 1, + "default_outgoing": 1, + "smtp_server": "test.example.com", + "email_id": "test_comm@example.com", + "password": "password", + "add_signature": 1, + "signature": "\nBest Wishes\nTest Signature", + "enable_auto_reply": 1, + "auto_reply_message": "", + "enable_incoming": 1, + "notify_if_unreplied": 1, + "unreplied_for_mins": 20, + "send_notification_to": "test_comm@example.com", + "pop3_server": "pop.test.example.com", + "imap_folder": [{"folder_name": "INBOX", "append_to": "ToDo"}], + "enable_automatic_linking": 1, + } + ).insert(ignore_permissions=True) diff --git a/influxframework/core/doctype/communication/test_records.json b/influxframework/core/doctype/communication/test_records.json new file mode 100644 index 0000000..a69d3e9 --- /dev/null +++ b/influxframework/core/doctype/communication/test_records.json @@ -0,0 +1,10 @@ +[ + { + "doctype": "Communication", + "name": "_Test Communication 1", + "subject": "Test Subject", + "sent_or_received": "Received", + "parenttype": "User", + "parent": "Administrator" + } +] diff --git a/influxframework/core/doctype/communication_link/__init__.py b/influxframework/core/doctype/communication_link/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/communication_link/communication_link.json b/influxframework/core/doctype/communication_link/communication_link.json new file mode 100644 index 0000000..1dd051b --- /dev/null +++ b/influxframework/core/doctype/communication_link/communication_link.json @@ -0,0 +1,47 @@ +{ + "creation": "2019-05-21 09:47:23.043960", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "link_doctype", + "link_name", + "link_title" + ], + "fields": [ + { + "fieldname": "link_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Link DocType", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "link_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Link Name", + "options": "link_doctype", + "reqd": 1 + }, + { + "fieldname": "link_title", + "fieldtype": "Read Only", + "in_list_view": 1, + "label": "Link Title", + "read_only": 1 + } + ], + "istable": 1, + "modified": "2019-05-21 09:47:23.043960", + "modified_by": "Administrator", + "module": "Core", + "name": "Communication Link", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/communication_link/communication_link.py b/influxframework/core/doctype/communication_link/communication_link.py new file mode 100644 index 0000000..1a89179 --- /dev/null +++ b/influxframework/core/doctype/communication_link/communication_link.py @@ -0,0 +1,13 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document + + +class CommunicationLink(Document): + pass + + +def on_doctype_update(): + influxframework.db.add_index("Communication Link", ["link_doctype", "link_name"]) diff --git a/influxframework/core/doctype/custom_docperm/__init__.py b/influxframework/core/doctype/custom_docperm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/custom_docperm/custom_docperm.js b/influxframework/core/doctype/custom_docperm/custom_docperm.js new file mode 100644 index 0000000..f6d204a --- /dev/null +++ b/influxframework/core/doctype/custom_docperm/custom_docperm.js @@ -0,0 +1,6 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Custom DocPerm", { + refresh: function (frm) {}, +}); diff --git a/influxframework/core/doctype/custom_docperm/custom_docperm.json b/influxframework/core/doctype/custom_docperm/custom_docperm.json new file mode 100644 index 0000000..2c594f5 --- /dev/null +++ b/influxframework/core/doctype/custom_docperm/custom_docperm.json @@ -0,0 +1,249 @@ +{ + "actions": [], + "allow_import": 1, + "autoname": "hash", + "creation": "2017-01-11 04:21:35.217943", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "parent", + "role_and_level", + "role", + "if_owner", + "column_break_2", + "permlevel", + "section_break_4", + "select", + "read", + "write", + "create", + "delete", + "column_break_8", + "submit", + "cancel", + "amend", + "additional_permissions", + "report", + "export", + "import", + "set_user_permissions", + "column_break_19", + "share", + "print", + "email" + ], + "fields": [ + { + "fieldname": "role_and_level", + "fieldtype": "Section Break", + "label": "Role and Level" + }, + { + "fieldname": "role", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Role", + "oldfieldname": "role", + "oldfieldtype": "Link", + "options": "Role", + "print_width": "150px", + "reqd": 1, + "width": "150px" + }, + { + "default": "0", + "description": "Apply this rule if the User is the Owner", + "fieldname": "if_owner", + "fieldtype": "Check", + "label": "If user is the owner" + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "permlevel", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Level", + "oldfieldname": "permlevel", + "oldfieldtype": "Int", + "print_width": "40px", + "width": "40px" + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break", + "label": "Permissions" + }, + { + "default": "1", + "fieldname": "read", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Read", + "oldfieldname": "read", + "oldfieldtype": "Check", + "print_width": "32px", + "width": "32px" + }, + { + "default": "0", + "fieldname": "write", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Write", + "oldfieldname": "write", + "oldfieldtype": "Check", + "print_width": "32px", + "width": "32px" + }, + { + "default": "0", + "fieldname": "create", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Create", + "oldfieldname": "create", + "oldfieldtype": "Check", + "print_width": "32px", + "width": "32px" + }, + { + "default": "0", + "fieldname": "delete", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Delete" + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "submit", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Submit", + "oldfieldname": "submit", + "oldfieldtype": "Check", + "print_width": "32px", + "width": "32px" + }, + { + "default": "0", + "fieldname": "cancel", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Cancel", + "oldfieldname": "cancel", + "oldfieldtype": "Check", + "print_width": "32px", + "width": "32px" + }, + { + "default": "0", + "fieldname": "amend", + "fieldtype": "Check", + "label": "Amend", + "oldfieldname": "amend", + "oldfieldtype": "Check", + "print_width": "32px", + "width": "32px" + }, + { + "fieldname": "additional_permissions", + "fieldtype": "Section Break", + "label": "Additional Permissions" + }, + { + "default": "0", + "fieldname": "report", + "fieldtype": "Check", + "label": "Report", + "print_width": "32px", + "width": "32px" + }, + { + "default": "1", + "fieldname": "export", + "fieldtype": "Check", + "label": "Export" + }, + { + "default": "0", + "fieldname": "import", + "fieldtype": "Check", + "label": "Import" + }, + { + "default": "0", + "description": "This role update User Permissions for a user", + "fieldname": "set_user_permissions", + "fieldtype": "Check", + "label": "Set User Permissions" + }, + { + "fieldname": "column_break_19", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "share", + "fieldtype": "Check", + "label": "Share" + }, + { + "default": "0", + "fieldname": "print", + "fieldtype": "Check", + "label": "Print" + }, + { + "default": "0", + "fieldname": "email", + "fieldtype": "Check", + "label": "Email" + }, + { + "fieldname": "parent", + "fieldtype": "Data", + "label": "Reference Document Type", + "read_only": 1, + "search_index": 1 + }, + { + "default": "0", + "fieldname": "select", + "fieldtype": "Check", + "label": "Select" + } + ], + "links": [], + "modified": "2020-12-03 15:20:48.296730", + "modified_by": "Administrator", + "module": "Core", + "name": "Custom DocPerm", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "ASC", + "title_field": "parent" +} \ No newline at end of file diff --git a/influxframework/core/doctype/custom_docperm/custom_docperm.py b/influxframework/core/doctype/custom_docperm/custom_docperm.py new file mode 100644 index 0000000..d58d804 --- /dev/null +++ b/influxframework/core/doctype/custom_docperm/custom_docperm.py @@ -0,0 +1,10 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document + + +class CustomDocPerm(Document): + def on_update(self): + influxframework.clear_cache(doctype=self.parent) diff --git a/influxframework/core/doctype/custom_docperm/test_custom_docperm.py b/influxframework/core/doctype/custom_docperm/test_custom_docperm.py new file mode 100644 index 0000000..d2cc52f --- /dev/null +++ b/influxframework/core/doctype/custom_docperm/test_custom_docperm.py @@ -0,0 +1,9 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + +# test_records = influxframework.get_test_records('Custom DocPerm') + + +class TestCustomDocPerm(InfluxFrameworkTestCase): + pass diff --git a/influxframework/core/doctype/custom_role/__init__.py b/influxframework/core/doctype/custom_role/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/custom_role/custom_role.js b/influxframework/core/doctype/custom_role/custom_role.js new file mode 100644 index 0000000..de18301 --- /dev/null +++ b/influxframework/core/doctype/custom_role/custom_role.js @@ -0,0 +1,6 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Custom Role", { + refresh: function (frm) {}, +}); diff --git a/influxframework/core/doctype/custom_role/custom_role.json b/influxframework/core/doctype/custom_role/custom_role.json new file mode 100644 index 0000000..55af8e2 --- /dev/null +++ b/influxframework/core/doctype/custom_role/custom_role.json @@ -0,0 +1,240 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 1, + "allow_rename": 0, + "autoname": "hash", + "beta": 0, + "creation": "2017-02-13 14:53:36.240122", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "page", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Page", + "length": 0, + "no_copy": 0, + "options": "Page", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "report", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Report", + "length": 0, + "no_copy": 0, + "options": "Report", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "permission_rules", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Permission Rules", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "roles", + "fieldtype": "Table", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Role", + "length": 0, + "no_copy": 0, + "options": "Has Role", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "response", + "fieldtype": "HTML", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "response", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "ref_doctype", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Reference Document Type", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2019-09-05 14:22:27.664645", + "modified_by": "Administrator", + "module": "Core", + "name": "Custom Role", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 1, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 0, + "track_seen": 0 +} \ No newline at end of file diff --git a/influxframework/core/doctype/custom_role/custom_role.py b/influxframework/core/doctype/custom_role/custom_role.py new file mode 100644 index 0000000..d33bf50 --- /dev/null +++ b/influxframework/core/doctype/custom_role/custom_role.py @@ -0,0 +1,21 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document + + +class CustomRole(Document): + def validate(self): + if self.report and not self.ref_doctype: + self.ref_doctype = influxframework.db.get_value("Report", self.report, "ref_doctype") + + +def get_custom_allowed_roles(field, name): + allowed_roles = [] + custom_role = influxframework.db.get_value("Custom Role", {field: name}, "name") + if custom_role: + custom_role_doc = influxframework.get_doc("Custom Role", custom_role) + allowed_roles = [d.role for d in custom_role_doc.roles] + + return allowed_roles diff --git a/influxframework/core/doctype/custom_role/test_custom_role.py b/influxframework/core/doctype/custom_role/test_custom_role.py new file mode 100644 index 0000000..b8880a3 --- /dev/null +++ b/influxframework/core/doctype/custom_role/test_custom_role.py @@ -0,0 +1,9 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + +# test_records = influxframework.get_test_records('Custom Role') + + +class TestCustomRole(InfluxFrameworkTestCase): + pass diff --git a/influxframework/core/doctype/data_export/__init__.py b/influxframework/core/doctype/data_export/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/data_export/data_export.js b/influxframework/core/doctype/data_export/data_export.js new file mode 100644 index 0000000..22e276e --- /dev/null +++ b/influxframework/core/doctype/data_export/data_export.js @@ -0,0 +1,170 @@ +// Copyright (c) 2018, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Data Export", { + refresh: (frm) => { + frm.disable_save(); + frm.page.set_primary_action("Export", () => { + can_export(frm) ? export_data(frm) : null; + }); + }, + onload: (frm) => { + frm.set_query("reference_doctype", () => { + return { + filters: { + issingle: 0, + istable: 0, + name: ["in", influxframework.boot.user.can_export], + }, + }; + }); + }, + reference_doctype: (frm) => { + const doctype = frm.doc.reference_doctype; + if (doctype) { + influxframework.model.with_doctype(doctype, () => set_field_options(frm)); + } else { + reset_filter_and_field(frm); + } + }, +}); + +const can_export = (frm) => { + const doctype = frm.doc.reference_doctype; + const parent_multicheck_options = frm.fields_multicheck[doctype] + ? frm.fields_multicheck[doctype].get_checked_options() + : []; + let is_valid_form = false; + if (!doctype) { + influxframework.msgprint(__("Please select the Document Type.")); + } else if (!parent_multicheck_options.length) { + influxframework.msgprint(__("Atleast one field of Parent Document Type is mandatory")); + } else { + is_valid_form = true; + } + return is_valid_form; +}; + +const export_data = (frm) => { + let get_template_url = "/api/method/influxframework.core.doctype.data_export.exporter.export_data"; + var export_params = () => { + let columns = {}; + Object.keys(frm.fields_multicheck).forEach((dt) => { + const options = frm.fields_multicheck[dt].get_checked_options(); + columns[dt] = options; + }); + return { + doctype: frm.doc.reference_doctype, + select_columns: JSON.stringify(columns), + filters: frm.filter_list.get_filters().map((filter) => filter.slice(1, 4)), + file_type: frm.doc.file_type, + template: true, + with_data: 1, + }; + }; + + open_url_post(get_template_url, export_params()); +}; + +const reset_filter_and_field = (frm) => { + const parent_wrapper = frm.fields_dict.fields_multicheck.$wrapper; + const filter_wrapper = frm.fields_dict.filter_list.$wrapper; + parent_wrapper.empty(); + filter_wrapper.empty(); + frm.filter_list = []; + frm.fields_multicheck = {}; +}; + +const set_field_options = (frm) => { + const parent_wrapper = frm.fields_dict.fields_multicheck.$wrapper; + const filter_wrapper = frm.fields_dict.filter_list.$wrapper; + const doctype = frm.doc.reference_doctype; + const related_doctypes = get_doctypes(doctype); + + parent_wrapper.empty(); + filter_wrapper.empty(); + + frm.filter_list = new influxframework.ui.FilterGroup({ + parent: filter_wrapper, + doctype: doctype, + on_change: () => {}, + }); + + // Add 'Select All' and 'Unselect All' button + make_multiselect_buttons(parent_wrapper); + + frm.fields_multicheck = {}; + related_doctypes.forEach((dt) => { + frm.fields_multicheck[dt] = add_doctype_field_multicheck_control(dt, parent_wrapper); + }); + + frm.refresh(); +}; + +const make_multiselect_buttons = (parent_wrapper) => { + const button_container = $(parent_wrapper).append('
    ').find(".flex"); + + ["Select All", "Unselect All"].map((d) => { + influxframework.ui.form.make_control({ + parent: $(button_container), + df: { + label: __(d), + fieldname: influxframework.scrub(d), + fieldtype: "Button", + click: () => { + checkbox_toggle(d !== "Select All"); + }, + }, + render_input: true, + }); + }); + + $(button_container) + .find(".influxframework-control") + .map((index, button) => { + $(button).css({ "margin-right": "1em" }); + }); + + function checkbox_toggle(checked) { + $(parent_wrapper) + .find('[data-fieldtype="MultiCheck"]') + .map((index, element) => { + $(element).find(`:checkbox`).prop("checked", checked).trigger("click"); + }); + } +}; + +const get_doctypes = (parentdt) => { + return [parentdt].concat(influxframework.meta.get_table_fields(parentdt).map((df) => df.options)); +}; + +const add_doctype_field_multicheck_control = (doctype, parent_wrapper) => { + const fields = get_fields(doctype); + + const options = fields.map((df) => { + return { + label: df.label, + value: df.fieldname, + danger: df.reqd, + checked: 1, + }; + }); + + const multicheck_control = influxframework.ui.form.make_control({ + parent: parent_wrapper, + df: { + label: doctype, + fieldname: doctype + "_fields", + fieldtype: "MultiCheck", + options: options, + columns: 3, + }, + render_input: true, + }); + + multicheck_control.refresh_input(); + return multicheck_control; +}; + +const filter_fields = (df) => influxframework.model.is_value_type(df) && !df.hidden; +const get_fields = (dt) => influxframework.meta.get_docfields(dt).filter(filter_fields); diff --git a/influxframework/core/doctype/data_export/data_export.json b/influxframework/core/doctype/data_export/data_export.json new file mode 100644 index 0000000..8304430 --- /dev/null +++ b/influxframework/core/doctype/data_export/data_export.json @@ -0,0 +1,250 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2018-03-07 10:09:49.794764", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reference_doctype", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Select Doctype", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_2", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "CSV", + "fieldname": "file_type", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "File Type", + "length": 0, + "no_copy": 0, + "options": "Excel\nCSV", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "reference_doctype", + "fieldname": "section_break", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "filter_list", + "fieldtype": "HTML", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Filter List", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "fields_multicheck", + "fieldtype": "HTML", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Fields Multicheck", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 1, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 1, + "istable": 0, + "max_attachments": 0, + "modified": "2018-03-21 13:23:05.623052", + "modified_by": "Administrator", + "module": "Core", + "name": "Data Export", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 0, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 0, + "track_seen": 0 +} \ No newline at end of file diff --git a/influxframework/core/doctype/data_export/data_export.py b/influxframework/core/doctype/data_export/data_export.py new file mode 100644 index 0000000..9c856cb --- /dev/null +++ b/influxframework/core/doctype/data_export/data_export.py @@ -0,0 +1,8 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +from influxframework.model.document import Document + + +class DataExport(Document): + pass diff --git a/influxframework/core/doctype/data_export/exporter.py b/influxframework/core/doctype/data_export/exporter.py new file mode 100644 index 0000000..ab073b5 --- /dev/null +++ b/influxframework/core/doctype/data_export/exporter.py @@ -0,0 +1,443 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import csv +import os +import re + +import influxframework +import influxframework.permissions +from influxframework import _ +from influxframework.core.doctype.access_log.access_log import make_access_log +from influxframework.utils import cint, cstr, format_datetime, format_duration, formatdate, parse_json +from influxframework.utils.csvutils import UnicodeWriter + +reflags = {"I": re.I, "L": re.L, "M": re.M, "U": re.U, "S": re.S, "X": re.X, "D": re.DEBUG} + + +def get_data_keys(): + return influxframework._dict( + { + "data_separator": _("Start entering data below this line"), + "main_table": _("Table") + ":", + "parent_table": _("Parent Table") + ":", + "columns": _("Column Name") + ":", + "doctype": _("DocType") + ":", + } + ) + + +@influxframework.whitelist() +def export_data( + doctype=None, + parent_doctype=None, + all_doctypes=True, + with_data=False, + select_columns=None, + file_type="CSV", + template=False, + filters=None, +): + _doctype = doctype + if isinstance(_doctype, list): + _doctype = _doctype[0] + make_access_log( + doctype=_doctype, + file_type=file_type, + columns=select_columns, + filters=filters, + method=parent_doctype, + ) + exporter = DataExporter( + doctype=doctype, + parent_doctype=parent_doctype, + all_doctypes=all_doctypes, + with_data=with_data, + select_columns=select_columns, + file_type=file_type, + template=template, + filters=filters, + ) + exporter.build_response() + + +class DataExporter: + def __init__( + self, + doctype=None, + parent_doctype=None, + all_doctypes=True, + with_data=False, + select_columns=None, + file_type="CSV", + template=False, + filters=None, + ): + self.doctype = doctype + self.parent_doctype = parent_doctype + self.all_doctypes = all_doctypes + self.with_data = cint(with_data) + self.select_columns = select_columns + self.file_type = file_type + self.template = template + self.filters = filters + self.data_keys = get_data_keys() + + self.prepare_args() + + def prepare_args(self): + if self.select_columns: + self.select_columns = parse_json(self.select_columns) + if self.filters: + self.filters = parse_json(self.filters) + + self.docs_to_export = {} + if self.doctype: + if isinstance(self.doctype, str): + self.doctype = [self.doctype] + + if len(self.doctype) > 1: + self.docs_to_export = self.doctype[1] + self.doctype = self.doctype[0] + + if not self.parent_doctype: + self.parent_doctype = self.doctype + + self.column_start_end = {} + + if self.all_doctypes: + self.child_doctypes = [] + for df in influxframework.get_meta(self.doctype).get_table_fields(): + self.child_doctypes.append(dict(doctype=df.options, parentfield=df.fieldname)) + + def build_response(self): + self.writer = UnicodeWriter() + self.name_field = "parent" if self.parent_doctype != self.doctype else "name" + + if self.template: + self.add_main_header() + + self.writer.writerow([""]) + self.tablerow = [self.data_keys.doctype] + self.labelrow = [_("Column Labels:")] + self.fieldrow = [self.data_keys.columns] + self.mandatoryrow = [_("Mandatory:")] + self.typerow = [_("Type:")] + self.inforow = [_("Info:")] + self.columns = [] + + self.build_field_columns(self.doctype) + + if self.all_doctypes: + for d in self.child_doctypes: + self.append_empty_field_column() + if ( + self.select_columns and self.select_columns.get(d["doctype"], None) + ) or not self.select_columns: + # if atleast one column is selected for this doctype + self.build_field_columns(d["doctype"], d["parentfield"]) + + self.add_field_headings() + self.add_data() + if self.with_data and not self.data: + influxframework.respond_as_web_page( + _("No Data"), _("There is no data to be exported"), indicator_color="orange" + ) + + if self.file_type == "Excel": + self.build_response_as_excel() + else: + # write out response as a type csv + influxframework.response["result"] = cstr(self.writer.getvalue()) + influxframework.response["type"] = "csv" + influxframework.response["doctype"] = self.doctype + + def add_main_header(self): + self.writer.writerow([_("Data Import Template")]) + self.writer.writerow([self.data_keys.main_table, self.doctype]) + + if self.parent_doctype != self.doctype: + self.writer.writerow([self.data_keys.parent_table, self.parent_doctype]) + else: + self.writer.writerow([""]) + + self.writer.writerow([""]) + self.writer.writerow([_("Notes:")]) + self.writer.writerow([_("Please do not change the template headings.")]) + self.writer.writerow([_("First data column must be blank.")]) + self.writer.writerow( + [_('If you are uploading new records, leave the "name" (ID) column blank.')] + ) + self.writer.writerow( + [_('If you are uploading new records, "Naming Series" becomes mandatory, if present.')] + ) + self.writer.writerow( + [ + _( + "Only mandatory fields are necessary for new records. You can delete non-mandatory columns if you wish." + ) + ] + ) + self.writer.writerow([_("For updating, you can update only selective columns.")]) + self.writer.writerow( + [_("You can only upload upto 5000 records in one go. (may be less in some cases)")] + ) + if self.name_field == "parent": + self.writer.writerow([_('"Parent" signifies the parent table in which this row must be added')]) + self.writer.writerow( + [_('If you are updating, please select "Overwrite" else existing rows will not be deleted.')] + ) + + def build_field_columns(self, dt, parentfield=None): + meta = influxframework.get_meta(dt) + + # build list of valid docfields + tablecolumns = [] + table_name = "tab" + dt + for f in influxframework.db.get_table_columns_description(table_name): + field = meta.get_field(f.name) + if field and ( + (self.select_columns and f.name in self.select_columns[dt]) or not self.select_columns + ): + tablecolumns.append(field) + + tablecolumns.sort(key=lambda a: int(a.idx)) + + _column_start_end = influxframework._dict(start=0) + + if dt == self.doctype: + if (meta.get("autoname") and meta.get("autoname").lower() == "prompt") or (self.with_data): + self._append_name_column() + + # if importing only child table for new record, add parent field + if meta.get("istable") and not self.with_data: + self.append_field_column( + influxframework._dict( + { + "fieldname": "parent", + "parent": "", + "label": "Parent", + "fieldtype": "Data", + "reqd": 1, + "info": _("Parent is the name of the document to which the data will get added to."), + } + ), + True, + ) + + _column_start_end = influxframework._dict(start=0) + else: + _column_start_end = influxframework._dict(start=len(self.columns)) + + if self.with_data: + self._append_name_column(dt) + + for docfield in tablecolumns: + self.append_field_column(docfield, True) + + # all non mandatory fields + for docfield in tablecolumns: + self.append_field_column(docfield, False) + + # if there is one column, add a blank column (?) + if len(self.columns) - _column_start_end.start == 1: + self.append_empty_field_column() + + # append DocType name + self.tablerow[_column_start_end.start + 1] = dt + + if parentfield: + self.tablerow[_column_start_end.start + 2] = parentfield + + _column_start_end.end = len(self.columns) + 1 + + self.column_start_end[(dt, parentfield)] = _column_start_end + + def append_field_column(self, docfield, for_mandatory): + if not docfield: + return + if for_mandatory and not docfield.reqd: + return + if not for_mandatory and docfield.reqd: + return + if docfield.fieldname in ("parenttype", "trash_reason"): + return + if docfield.hidden: + return + if ( + self.select_columns + and docfield.fieldname not in self.select_columns.get(docfield.parent, []) + and docfield.fieldname != "name" + ): + return + + self.tablerow.append("") + self.fieldrow.append(docfield.fieldname) + self.labelrow.append(_(docfield.label)) + self.mandatoryrow.append(docfield.reqd and "Yes" or "No") + self.typerow.append(docfield.fieldtype) + self.inforow.append(self.getinforow(docfield)) + self.columns.append(docfield.fieldname) + + def append_empty_field_column(self): + self.tablerow.append("~") + self.fieldrow.append("~") + self.labelrow.append("") + self.mandatoryrow.append("") + self.typerow.append("") + self.inforow.append("") + self.columns.append("") + + @staticmethod + def getinforow(docfield): + """make info comment for options, links etc.""" + if docfield.fieldtype == "Select": + if not docfield.options: + return "" + else: + return _("One of") + ": %s" % ", ".join(filter(None, docfield.options.split("\n"))) + elif docfield.fieldtype == "Link": + return "Valid %s" % docfield.options + elif docfield.fieldtype == "Int": + return "Integer" + elif docfield.fieldtype == "Check": + return "0 or 1" + elif docfield.fieldtype in ["Date", "Datetime"]: + return cstr(influxframework.defaults.get_defaults().date_format) + elif hasattr(docfield, "info"): + return docfield.info + else: + return "" + + def add_field_headings(self): + self.writer.writerow(self.tablerow) + self.writer.writerow(self.labelrow) + self.writer.writerow(self.fieldrow) + self.writer.writerow(self.mandatoryrow) + self.writer.writerow(self.typerow) + self.writer.writerow(self.inforow) + if self.template: + self.writer.writerow([self.data_keys.data_separator]) + + def add_data(self): + from influxframework.query_builder import DocType + + if self.template and not self.with_data: + return + + influxframework.permissions.can_export(self.parent_doctype, raise_exception=True) + + # sort nested set doctypes by `lft asc` + order_by = None + table_columns = influxframework.db.get_table_columns(self.parent_doctype) + if "lft" in table_columns and "rgt" in table_columns: + order_by = f"`tab{self.parent_doctype}`.`lft` asc" + # get permitted data only + self.data = influxframework.get_list( + self.doctype, fields=["*"], filters=self.filters, limit_page_length=None, order_by=order_by + ) + + for doc in self.data: + op = self.docs_to_export.get("op") + names = self.docs_to_export.get("name") + + if names and op: + if op == "=" and doc.name not in names: + continue + elif op == "!=" and doc.name in names: + continue + elif names: + try: + sflags = self.docs_to_export.get("flags", "I,U").upper() + flags = 0 + for a in re.split(r"\W+", sflags): + flags = flags | reflags.get(a, 0) + + c = re.compile(names, flags) + m = c.match(doc.name) + if not m: + continue + except Exception: + if doc.name not in names: + continue + # add main table + rows = [] + + self.add_data_row(rows, self.doctype, None, doc, 0) + + if self.all_doctypes: + # add child tables + for c in self.child_doctypes: + child_doctype_table = DocType(c["doctype"]) + data_row = ( + influxframework.qb.from_(child_doctype_table) + .select("*") + .where(child_doctype_table.parent == doc.name) + .where(child_doctype_table.parentfield == c["parentfield"]) + .orderby(child_doctype_table.idx) + ) + for ci, child in enumerate(data_row.run(as_dict=True)): + self.add_data_row(rows, c["doctype"], c["parentfield"], child, ci) + + for row in rows: + self.writer.writerow(row) + + def add_data_row(self, rows, dt, parentfield, doc, rowidx): + d = doc.copy() + meta = influxframework.get_meta(dt) + if self.all_doctypes: + d.name = f'"{d.name}"' + + if len(rows) < rowidx + 1: + rows.append([""] * (len(self.columns) + 1)) + row = rows[rowidx] + + _column_start_end = self.column_start_end.get((dt, parentfield)) + + if _column_start_end: + for i, c in enumerate(self.columns[_column_start_end.start : _column_start_end.end]): + df = meta.get_field(c) + fieldtype = df.fieldtype if df else "Data" + value = d.get(c, "") + if value: + if fieldtype == "Date": + value = formatdate(value) + elif fieldtype == "Datetime": + value = format_datetime(value) + elif fieldtype == "Duration": + value = format_duration(value, df.hide_days) + + row[_column_start_end.start + i + 1] = value + + def build_response_as_excel(self): + filename = influxframework.generate_hash("", 10) + with open(filename, "wb") as f: + f.write(cstr(self.writer.getvalue()).encode("utf-8")) + f = open(filename) + reader = csv.reader(f) + + from influxframework.utils.xlsxutils import make_xlsx + + xlsx_file = make_xlsx(reader, "Data Import Template" if self.template else "Data Export") + + f.close() + os.remove(filename) + + # write out response as a xlsx type + influxframework.response["filename"] = self.doctype + ".xlsx" + influxframework.response["filecontent"] = xlsx_file.getvalue() + influxframework.response["type"] = "binary" + + def _append_name_column(self, dt=None): + self.append_field_column( + influxframework._dict( + { + "fieldname": "name" if dt else self.name_field, + "parent": dt or "", + "label": "ID", + "fieldtype": "Data", + "reqd": 1, + } + ), + True, + ) diff --git a/influxframework/core/doctype/data_export/test_data_exporter.py b/influxframework/core/doctype/data_export/test_data_exporter.py new file mode 100644 index 0000000..5637bf4 --- /dev/null +++ b/influxframework/core/doctype/data_export/test_data_exporter.py @@ -0,0 +1,113 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.core.doctype.data_export.exporter import DataExporter +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestDataExporter(InfluxFrameworkTestCase): + def setUp(self): + self.doctype_name = "Test DocType for Export Tool" + self.doc_name = "Test Data for Export Tool" + self.create_doctype_if_not_exists(doctype_name=self.doctype_name) + self.create_test_data() + + def create_doctype_if_not_exists(self, doctype_name, force=False): + """ + Helper Function for setting up doctypes + """ + if force: + influxframework.delete_doc_if_exists("DocType", doctype_name) + influxframework.delete_doc_if_exists("DocType", "Child 1 of " + doctype_name) + + if influxframework.db.exists("DocType", doctype_name): + return + + # Child Table 1 + table_1_name = "Child 1 of " + doctype_name + influxframework.get_doc( + { + "doctype": "DocType", + "name": table_1_name, + "module": "Custom", + "custom": 1, + "istable": 1, + "fields": [ + {"label": "Child Title", "fieldname": "child_title", "reqd": 1, "fieldtype": "Data"}, + {"label": "Child Number", "fieldname": "child_number", "fieldtype": "Int"}, + ], + } + ).insert() + + # Main Table + influxframework.get_doc( + { + "doctype": "DocType", + "name": doctype_name, + "module": "Custom", + "custom": 1, + "autoname": "field:title", + "fields": [ + {"label": "Title", "fieldname": "title", "reqd": 1, "fieldtype": "Data"}, + {"label": "Number", "fieldname": "number", "fieldtype": "Int"}, + { + "label": "Table Field 1", + "fieldname": "table_field_1", + "fieldtype": "Table", + "options": table_1_name, + }, + ], + "permissions": [{"role": "System Manager"}], + } + ).insert() + + def create_test_data(self, force=False): + """ + Helper Function creating test data + """ + if force: + influxframework.delete_doc(self.doctype_name, self.doc_name) + + if not influxframework.db.exists(self.doctype_name, self.doc_name): + self.doc = influxframework.get_doc( + doctype=self.doctype_name, + title=self.doc_name, + number="100", + table_field_1=[ + {"child_title": "Child Title 1", "child_number": "50"}, + {"child_title": "Child Title 2", "child_number": "51"}, + ], + ).insert() + else: + self.doc = influxframework.get_doc(self.doctype_name, self.doc_name) + + def test_export_content(self): + exp = DataExporter(doctype=self.doctype_name, file_type="CSV") + exp.build_response() + + self.assertEqual(influxframework.response["type"], "csv") + self.assertEqual(influxframework.response["doctype"], self.doctype_name) + self.assertTrue(influxframework.response["result"]) + self.assertIn('Child Title 1",50', influxframework.response["result"]) + self.assertIn('Child Title 2",51', influxframework.response["result"]) + + def test_export_type(self): + for type in ["csv", "Excel"]: + with self.subTest(type=type): + exp = DataExporter(doctype=self.doctype_name, file_type=type) + exp.build_response() + + self.assertEqual(influxframework.response["doctype"], self.doctype_name) + self.assertTrue(influxframework.response["result"]) + + if type == "csv": + self.assertEqual(influxframework.response["type"], "csv") + elif type == "Excel": + self.assertEqual(influxframework.response["type"], "binary") + self.assertEqual( + influxframework.response["filename"], self.doctype_name + ".xlsx" + ) # 'Test DocType for Export Tool.xlsx') + self.assertTrue(influxframework.response["filecontent"]) + + def tearDown(self): + pass diff --git a/influxframework/core/doctype/data_import/__init__.py b/influxframework/core/doctype/data_import/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/data_import/data_import.css b/influxframework/core/doctype/data_import/data_import.css new file mode 100644 index 0000000..5206540 --- /dev/null +++ b/influxframework/core/doctype/data_import/data_import.css @@ -0,0 +1,3 @@ +.warnings .warning { + margin-bottom: 40px; +} diff --git a/influxframework/core/doctype/data_import/data_import.js b/influxframework/core/doctype/data_import/data_import.js new file mode 100644 index 0000000..921046f --- /dev/null +++ b/influxframework/core/doctype/data_import/data_import.js @@ -0,0 +1,544 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Data Import", { + setup(frm) { + influxframework.realtime.on("data_import_refresh", ({ data_import }) => { + frm.import_in_progress = false; + if (data_import !== frm.doc.name) return; + influxframework.model.clear_doc("Data Import", frm.doc.name); + influxframework.model.with_doc("Data Import", frm.doc.name).then(() => { + frm.refresh(); + }); + }); + influxframework.realtime.on("data_import_progress", (data) => { + frm.import_in_progress = true; + if (data.data_import !== frm.doc.name) { + return; + } + let percent = Math.floor((data.current * 100) / data.total); + let seconds = Math.floor(data.eta); + let minutes = Math.floor(data.eta / 60); + let eta_message = + // prettier-ignore + seconds < 60 + ? __('About {0} seconds remaining', [seconds]) + : minutes === 1 + ? __('About {0} minute remaining', [minutes]) + : __('About {0} minutes remaining', [minutes]); + + let message; + if (data.success) { + let message_args = [data.current, data.total, eta_message]; + message = + frm.doc.import_type === "Insert New Records" + ? __("Importing {0} of {1}, {2}", message_args) + : __("Updating {0} of {1}, {2}", message_args); + } + if (data.skipping) { + message = __("Skipping {0} of {1}, {2}", [data.current, data.total, eta_message]); + } + frm.dashboard.show_progress(__("Import Progress"), percent, message); + frm.page.set_indicator(__("In Progress"), "orange"); + frm.trigger("update_primary_action"); + + // hide progress when complete + if (data.current === data.total) { + setTimeout(() => { + frm.dashboard.hide(); + frm.refresh(); + }, 2000); + } + }); + + frm.set_query("reference_doctype", () => { + return { + filters: { + name: ["in", influxframework.boot.user.can_import], + }, + }; + }); + + frm.get_field("import_file").df.options = { + restrictions: { + allowed_file_types: [".csv", ".xls", ".xlsx"], + }, + }; + + frm.has_import_file = () => { + return frm.doc.import_file || frm.doc.google_sheets_url; + }; + }, + + refresh(frm) { + frm.page.hide_icon_group(); + frm.trigger("update_indicators"); + frm.trigger("import_file"); + frm.trigger("show_import_log"); + frm.trigger("show_import_warnings"); + frm.trigger("toggle_submit_after_import"); + + if (frm.doc.status != "Pending") frm.trigger("show_import_status"); + + frm.trigger("show_report_error_button"); + + if (frm.doc.status === "Partial Success") { + frm.add_custom_button(__("Export Errored Rows"), () => + frm.trigger("export_errored_rows") + ); + } + + if (frm.doc.status.includes("Success")) { + frm.add_custom_button(__("Go to {0} List", [__(frm.doc.reference_doctype)]), () => + influxframework.set_route("List", frm.doc.reference_doctype) + ); + } + }, + + onload_post_render(frm) { + frm.trigger("update_primary_action"); + }, + + update_primary_action(frm) { + if (frm.is_dirty()) { + frm.enable_save(); + return; + } + frm.disable_save(); + if (frm.doc.status !== "Success") { + if (!frm.is_new() && frm.has_import_file()) { + let label = frm.doc.status === "Pending" ? __("Start Import") : __("Retry"); + frm.page.set_primary_action(label, () => frm.events.start_import(frm)); + } else { + frm.page.set_primary_action(__("Save"), () => frm.save()); + } + } + }, + + update_indicators(frm) { + const indicator = influxframework.get_indicator(frm.doc); + if (indicator) { + frm.page.set_indicator(indicator[0], indicator[1]); + } else { + frm.page.clear_indicator(); + } + }, + + show_import_status(frm) { + influxframework.call({ + method: "influxframework.core.doctype.data_import.data_import.get_import_status", + args: { + data_import_name: frm.doc.name, + }, + callback: function (r) { + let successful_records = cint(r.message.success); + let failed_records = cint(r.message.failed); + let total_records = cint(r.message.total_records); + + if (!total_records) return; + + let message; + if (failed_records === 0) { + let message_args = [successful_records]; + if (frm.doc.import_type === "Insert New Records") { + message = + successful_records > 1 + ? __("Successfully imported {0} records.", message_args) + : __("Successfully imported {0} record.", message_args); + } else { + message = + successful_records > 1 + ? __("Successfully updated {0} records.", message_args) + : __("Successfully updated {0} record.", message_args); + } + } else { + let message_args = [successful_records, total_records]; + if (frm.doc.import_type === "Insert New Records") { + message = + successful_records > 1 + ? __( + "Successfully imported {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.", + message_args + ) + : __( + "Successfully imported {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.", + message_args + ); + } else { + message = + successful_records > 1 + ? __( + "Successfully updated {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.", + message_args + ) + : __( + "Successfully updated {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.", + message_args + ); + } + } + frm.dashboard.set_headline(message); + }, + }); + }, + + show_report_error_button(frm) { + if (frm.doc.status === "Error") { + influxframework.db + .get_list("Error Log", { + filters: { method: frm.doc.name }, + fields: ["method", "error"], + order_by: "creation desc", + limit: 1, + }) + .then((result) => { + if (result.length > 0) { + frm.add_custom_button("Report Error", () => { + let fake_xhr = { + responseText: JSON.stringify({ + exc: result[0].error, + }), + }; + influxframework.request.report_error(fake_xhr, {}); + }); + } + }); + } + }, + + start_import(frm) { + frm.call({ + method: "form_start_import", + args: { data_import: frm.doc.name }, + btn: frm.page.btn_primary, + }).then((r) => { + if (r.message === true) { + frm.disable_save(); + } + }); + }, + + download_template(frm) { + influxframework.require("data_import_tools.bundle.js", () => { + frm.data_exporter = new influxframework.data_import.DataExporter( + frm.doc.reference_doctype, + frm.doc.import_type + ); + }); + }, + + reference_doctype(frm) { + frm.trigger("toggle_submit_after_import"); + }, + + toggle_submit_after_import(frm) { + frm.toggle_display("submit_after_import", false); + let doctype = frm.doc.reference_doctype; + if (doctype) { + influxframework.model.with_doctype(doctype, () => { + let meta = influxframework.get_meta(doctype); + frm.toggle_display("submit_after_import", meta.is_submittable); + }); + } + }, + + google_sheets_url(frm) { + if (!frm.is_dirty()) { + frm.trigger("import_file"); + } else { + frm.trigger("update_primary_action"); + } + }, + + refresh_google_sheet(frm) { + frm.trigger("import_file"); + }, + + import_file(frm) { + frm.toggle_display("section_import_preview", frm.has_import_file()); + if (!frm.has_import_file()) { + frm.get_field("import_preview").$wrapper.empty(); + return; + } else { + frm.trigger("update_primary_action"); + } + + // load import preview + frm.get_field("import_preview").$wrapper.empty(); + $('') + .html(__("Loading import file...")) + .appendTo(frm.get_field("import_preview").$wrapper); + + frm.call({ + method: "get_preview_from_template", + args: { + data_import: frm.doc.name, + import_file: frm.doc.import_file, + google_sheets_url: frm.doc.google_sheets_url, + }, + error_handlers: { + TimestampMismatchError() { + // ignore this error + }, + }, + }).then((r) => { + let preview_data = r.message; + frm.events.show_import_preview(frm, preview_data); + frm.events.show_import_warnings(frm, preview_data); + }); + }, + + show_import_preview(frm, preview_data) { + let import_log = preview_data.import_log; + + if (frm.import_preview && frm.import_preview.doctype === frm.doc.reference_doctype) { + frm.import_preview.preview_data = preview_data; + frm.import_preview.import_log = import_log; + frm.import_preview.refresh(); + return; + } + + influxframework.require("data_import_tools.bundle.js", () => { + frm.import_preview = new influxframework.data_import.ImportPreview({ + wrapper: frm.get_field("import_preview").$wrapper, + doctype: frm.doc.reference_doctype, + preview_data, + import_log, + frm, + events: { + remap_column(changed_map) { + let template_options = JSON.parse(frm.doc.template_options || "{}"); + template_options.column_to_field_map = + template_options.column_to_field_map || {}; + Object.assign(template_options.column_to_field_map, changed_map); + frm.set_value("template_options", JSON.stringify(template_options)); + frm.save().then(() => frm.trigger("import_file")); + }, + }, + }); + }); + }, + + export_errored_rows(frm) { + open_url_post( + "/api/method/influxframework.core.doctype.data_import.data_import.download_errored_template", + { + data_import_name: frm.doc.name, + } + ); + }, + + export_import_log(frm) { + open_url_post( + "/api/method/influxframework.core.doctype.data_import.data_import.download_import_log", + { + data_import_name: frm.doc.name, + } + ); + }, + + show_import_warnings(frm, preview_data) { + let columns = preview_data.columns; + let warnings = JSON.parse(frm.doc.template_warnings || "[]"); + warnings = warnings.concat(preview_data.warnings || []); + + frm.toggle_display("import_warnings_section", warnings.length > 0); + if (warnings.length === 0) { + frm.get_field("import_warnings").$wrapper.html(""); + return; + } + + // group warnings by row + let warnings_by_row = {}; + let other_warnings = []; + for (let warning of warnings) { + if (warning.row) { + warnings_by_row[warning.row] = warnings_by_row[warning.row] || []; + warnings_by_row[warning.row].push(warning); + } else { + other_warnings.push(warning); + } + } + + let html = ""; + html += Object.keys(warnings_by_row) + .map((row_number) => { + let message = warnings_by_row[row_number] + .map((w) => { + if (w.field) { + let label = + w.field.label + + (w.field.parent !== frm.doc.reference_doctype + ? ` (${w.field.parent})` + : ""); + return `
  1. ${label}: ${w.message}
  2. `; + } + return `
  3. ${w.message}
  4. `; + }) + .join(""); + return ` +
    +
    ${__("Row {0}", [row_number])}
    +
      ${message}
    +
    + `; + }) + .join(""); + + html += other_warnings + .map((warning) => { + let header = ""; + if (warning.col) { + let column_number = `${__("Column {0}", [ + warning.col, + ])}`; + let column_header = columns[warning.col].header_title; + header = `${column_number} (${column_header})`; + } + return ` +
    +
    ${header}
    +
    ${warning.message}
    +
    + `; + }) + .join(""); + frm.get_field("import_warnings").$wrapper.html(` +
    +
    ${html}
    +
    + `); + }, + + show_failed_logs(frm) { + frm.trigger("show_import_log"); + }, + + render_import_log(frm) { + influxframework.call({ + method: "influxframework.client.get_list", + args: { + doctype: "Data Import Log", + filters: { + data_import: frm.doc.name, + }, + fields: ["success", "docname", "messages", "exception", "row_indexes"], + limit_page_length: 5000, + order_by: "log_index", + }, + callback: function (r) { + let logs = r.message; + + if (logs.length === 0) return; + + frm.toggle_display("import_log_section", true); + + let rows = logs + .map((log) => { + let html = ""; + if (log.success) { + if (frm.doc.import_type === "Insert New Records") { + html = __("Successfully imported {0}", [ + `${influxframework.utils.get_form_link( + frm.doc.reference_doctype, + log.docname, + true + )}`, + ]); + } else { + html = __("Successfully updated {0}", [ + `${influxframework.utils.get_form_link( + frm.doc.reference_doctype, + log.docname, + true + )}`, + ]); + } + } else { + let messages = JSON.parse(log.messages || "[]") + .map(JSON.parse) + .map((m) => { + let title = m.title ? `${m.title}` : ""; + let message = m.message ? `
    ${m.message}
    ` : ""; + return title + message; + }) + .join(""); + let id = influxframework.dom.get_unique_id(); + html = `${messages} + +
    +
    +
    ${log.exception}
    +
    +
    `; + } + let indicator_color = log.success ? "green" : "red"; + let title = log.success ? __("Success") : __("Failure"); + + if (frm.doc.show_failed_logs && log.success) { + return ""; + } + + return ` + ${JSON.parse(log.row_indexes).join(", ")} + +
    ${title}
    + + + ${html} + + `; + }) + .join(""); + + if (!rows && frm.doc.show_failed_logs) { + rows = ` + ${__("No failed logs")} + `; + } + + frm.get_field("import_log_preview").$wrapper.html(` + + + + + + + ${rows} +
    ${__("Row Number")}${__("Status")}${__("Message")}
    + `); + }, + }); + }, + + show_import_log(frm) { + frm.toggle_display("import_log_section", false); + + if (frm.import_in_progress) { + return; + } + + influxframework.call({ + method: "influxframework.client.get_count", + args: { + doctype: "Data Import Log", + filters: { + data_import: frm.doc.name, + }, + }, + callback: function (r) { + let count = r.message; + if (count < 5000) { + frm.trigger("render_import_log"); + } else { + frm.toggle_display("import_log_section", false); + frm.add_custom_button(__("Export Import Log"), () => + frm.trigger("export_import_log") + ); + } + }, + }); + }, +}); diff --git a/influxframework/core/doctype/data_import/data_import.json b/influxframework/core/doctype/data_import/data_import.json new file mode 100644 index 0000000..9e948da --- /dev/null +++ b/influxframework/core/doctype/data_import/data_import.json @@ -0,0 +1,197 @@ +{ + "actions": [], + "autoname": "format:{reference_doctype} Import on {creation}", + "beta": 1, + "creation": "2019-08-04 14:16:08.318714", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "reference_doctype", + "import_type", + "download_template", + "import_file", + "payload_count", + "html_5", + "google_sheets_url", + "refresh_google_sheet", + "column_break_5", + "status", + "submit_after_import", + "mute_emails", + "template_options", + "import_warnings_section", + "template_warnings", + "import_warnings", + "section_import_preview", + "import_preview", + "import_log_section", + "show_failed_logs", + "import_log_preview" + ], + "fields": [ + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType", + "reqd": 1, + "set_only_once": 1 + }, + { + "fieldname": "import_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Import Type", + "options": "\nInsert New Records\nUpdate Existing Records", + "reqd": 1, + "set_only_once": 1 + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "import_file", + "fieldtype": "Attach", + "in_list_view": 1, + "label": "Import File", + "read_only_depends_on": "eval: ['Success', 'Partial Success'].includes(doc.status)" + }, + { + "fieldname": "import_preview", + "fieldtype": "HTML", + "label": "Import Preview" + }, + { + "fieldname": "section_import_preview", + "fieldtype": "Section Break", + "label": "Preview" + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "template_options", + "fieldtype": "Code", + "hidden": 1, + "label": "Template Options", + "options": "JSON", + "read_only": 1 + }, + { + "fieldname": "import_log_section", + "fieldtype": "Section Break", + "label": "Import Log" + }, + { + "fieldname": "import_log_preview", + "fieldtype": "HTML", + "label": "Import Log Preview" + }, + { + "default": "Pending", + "fieldname": "status", + "fieldtype": "Select", + "hidden": 1, + "label": "Status", + "options": "Pending\nSuccess\nPartial Success\nError", + "read_only": 1 + }, + { + "fieldname": "template_warnings", + "fieldtype": "Code", + "hidden": 1, + "label": "Template Warnings", + "options": "JSON" + }, + { + "default": "0", + "fieldname": "submit_after_import", + "fieldtype": "Check", + "label": "Submit After Import", + "set_only_once": 1 + }, + { + "fieldname": "import_warnings_section", + "fieldtype": "Section Break", + "label": "Import File Errors and Warnings" + }, + { + "fieldname": "import_warnings", + "fieldtype": "HTML", + "label": "Import Warnings" + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "download_template", + "fieldtype": "Button", + "label": "Download Template" + }, + { + "default": "1", + "fieldname": "mute_emails", + "fieldtype": "Check", + "label": "Don't Send Emails", + "set_only_once": 1 + }, + { + "default": "0", + "fieldname": "show_failed_logs", + "fieldtype": "Check", + "label": "Show Failed Logs" + }, + { + "depends_on": "eval:!doc.__islocal && !doc.import_file", + "fieldname": "html_5", + "fieldtype": "HTML", + "options": "
    Or
    " + }, + { + "depends_on": "eval:!doc.__islocal && !doc.import_file\n", + "description": "Must be a publicly accessible Google Sheets URL", + "fieldname": "google_sheets_url", + "fieldtype": "Data", + "label": "Import from Google Sheets", + "read_only_depends_on": "eval: ['Success', 'Partial Success'].includes(doc.status)" + }, + { + "depends_on": "eval:doc.google_sheets_url && !doc.__unsaved && ['Success', 'Partial Success'].includes(doc.status)", + "fieldname": "refresh_google_sheet", + "fieldtype": "Button", + "label": "Refresh Google Sheet" + }, + { + "fieldname": "payload_count", + "fieldtype": "Int", + "hidden": 1, + "label": "Payload Count", + "read_only": 1 + } + ], + "hide_toolbar": 1, + "links": [], + "modified": "2022-02-01 20:08:37.624914", + "modified_by": "Administrator", + "module": "Core", + "name": "Data Import", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/data_import/data_import.py b/influxframework/core/doctype/data_import/data_import.py new file mode 100644 index 0000000..7a19427 --- /dev/null +++ b/influxframework/core/doctype/data_import/data_import.py @@ -0,0 +1,274 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +import os + +import influxframework +from influxframework import _ +from influxframework.core.doctype.data_import.exporter import Exporter +from influxframework.core.doctype.data_import.importer import Importer +from influxframework.model.document import Document +from influxframework.modules.import_file import import_file_by_path +from influxframework.utils.background_jobs import enqueue +from influxframework.utils.csvutils import validate_google_sheets_url + + +class DataImport(Document): + def validate(self): + doc_before_save = self.get_doc_before_save() + if ( + not (self.import_file or self.google_sheets_url) + or (doc_before_save and doc_before_save.import_file != self.import_file) + or (doc_before_save and doc_before_save.google_sheets_url != self.google_sheets_url) + ): + self.template_options = "" + self.template_warnings = "" + + self.validate_import_file() + self.validate_google_sheets_url() + self.set_payload_count() + + def validate_import_file(self): + if self.import_file: + # validate template + self.get_importer() + + def validate_google_sheets_url(self): + if not self.google_sheets_url: + return + validate_google_sheets_url(self.google_sheets_url) + + def set_payload_count(self): + if self.import_file: + i = self.get_importer() + payloads = i.import_file.get_payloads_for_import() + self.payload_count = len(payloads) + + @influxframework.whitelist() + def get_preview_from_template(self, import_file=None, google_sheets_url=None): + if import_file: + self.import_file = import_file + + if google_sheets_url: + self.google_sheets_url = google_sheets_url + + if not (self.import_file or self.google_sheets_url): + return + + i = self.get_importer() + return i.get_data_for_import_preview() + + def start_import(self): + from influxframework.core.page.background_jobs.background_jobs import get_info + from influxframework.utils.scheduler import is_scheduler_inactive + + if is_scheduler_inactive() and not influxframework.flags.in_test: + influxframework.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive")) + + enqueued_jobs = [d.get("job_name") for d in get_info()] + + if self.name not in enqueued_jobs: + enqueue( + start_import, + queue="default", + timeout=10000, + event="data_import", + job_name=self.name, + data_import=self.name, + now=influxframework.conf.developer_mode or influxframework.flags.in_test, + ) + return True + + return False + + def export_errored_rows(self): + return self.get_importer().export_errored_rows() + + def download_import_log(self): + return self.get_importer().export_import_log() + + def get_importer(self): + return Importer(self.reference_doctype, data_import=self) + + +@influxframework.whitelist() +def get_preview_from_template(data_import, import_file=None, google_sheets_url=None): + return influxframework.get_doc("Data Import", data_import).get_preview_from_template( + import_file, google_sheets_url + ) + + +@influxframework.whitelist() +def form_start_import(data_import): + return influxframework.get_doc("Data Import", data_import).start_import() + + +def start_import(data_import): + """This method runs in background job""" + data_import = influxframework.get_doc("Data Import", data_import) + try: + i = Importer(data_import.reference_doctype, data_import=data_import) + i.import_data() + except Exception: + influxframework.db.rollback() + data_import.db_set("status", "Error") + data_import.log_error("Data import failed") + finally: + influxframework.flags.in_import = False + + influxframework.publish_realtime("data_import_refresh", {"data_import": data_import.name}) + + +@influxframework.whitelist() +def download_template( + doctype, export_fields=None, export_records=None, export_filters=None, file_type="CSV" +): + """ + Download template from Exporter + :param doctype: Document Type + :param export_fields=None: Fields to export as dict {'Sales Invoice': ['name', 'customer'], 'Sales Invoice Item': ['item_code']} + :param export_records=None: One of 'all', 'by_filter', 'blank_template' + :param export_filters: Filter dict + :param file_type: File type to export into + """ + + export_fields = influxframework.parse_json(export_fields) + export_filters = influxframework.parse_json(export_filters) + export_data = export_records != "blank_template" + + e = Exporter( + doctype, + export_fields=export_fields, + export_data=export_data, + export_filters=export_filters, + file_type=file_type, + export_page_length=5 if export_records == "5_records" else None, + ) + e.build_response() + + +@influxframework.whitelist() +def download_errored_template(data_import_name): + data_import = influxframework.get_doc("Data Import", data_import_name) + data_import.export_errored_rows() + + +@influxframework.whitelist() +def download_import_log(data_import_name): + data_import = influxframework.get_doc("Data Import", data_import_name) + data_import.download_import_log() + + +@influxframework.whitelist() +def get_import_status(data_import_name): + import_status = {} + + logs = influxframework.get_all( + "Data Import Log", + fields=["count(*) as count", "success"], + filters={"data_import": data_import_name}, + group_by="success", + ) + + total_payload_count = influxframework.db.get_value("Data Import", data_import_name, "payload_count") + + for log in logs: + if log.get("success"): + import_status["success"] = log.get("count") + else: + import_status["failed"] = log.get("count") + + import_status["total_records"] = total_payload_count + + return import_status + + +def import_file(doctype, file_path, import_type, submit_after_import=False, console=False): + """ + Import documents in from CSV or XLSX using data import. + + :param doctype: DocType to import + :param file_path: Path to .csv, .xls, or .xlsx file to import + :param import_type: One of "Insert" or "Update" + :param submit_after_import: Whether to submit documents after import + :param console: Set to true if this is to be used from command line. Will print errors or progress to stdout. + """ + + data_import = influxframework.new_doc("Data Import") + data_import.submit_after_import = submit_after_import + data_import.import_type = ( + "Insert New Records" if import_type.lower() == "insert" else "Update Existing Records" + ) + + i = Importer(doctype=doctype, file_path=file_path, data_import=data_import, console=console) + i.import_data() + + +def import_doc(path, pre_process=None): + if os.path.isdir(path): + files = [os.path.join(path, f) for f in os.listdir(path)] + else: + files = [path] + + for f in files: + if f.endswith(".json"): + influxframework.flags.mute_emails = True + import_file_by_path( + f, data_import=True, force=True, pre_process=pre_process, reset_permissions=True + ) + influxframework.flags.mute_emails = False + influxframework.db.commit() + else: + raise NotImplementedError("Only .json files can be imported") + + +def export_json(doctype, path, filters=None, or_filters=None, name=None, order_by="creation asc"): + def post_process(out): + # Note on Tree DocTypes: + # The tree structure is maintained in the database via the fields "lft" + # and "rgt". They are automatically set and kept up-to-date. Importing + # them would destroy any existing tree structure. For this reason they + # are not exported as well. + del_keys = ("modified_by", "creation", "owner", "idx", "lft", "rgt") + for doc in out: + for key in del_keys: + if key in doc: + del doc[key] + for k, v in doc.items(): + if isinstance(v, list): + for child in v: + for key in del_keys + ("docstatus", "doctype", "modified", "name"): + if key in child: + del child[key] + + out = [] + if name: + out.append(influxframework.get_doc(doctype, name).as_dict()) + elif influxframework.db.get_value("DocType", doctype, "issingle"): + out.append(influxframework.get_doc(doctype).as_dict()) + else: + for doc in influxframework.get_all( + doctype, + fields=["name"], + filters=filters, + or_filters=or_filters, + limit_page_length=0, + order_by=order_by, + ): + out.append(influxframework.get_doc(doctype, doc.name).as_dict()) + post_process(out) + + dirname = os.path.dirname(path) + if not os.path.exists(dirname): + path = os.path.join("..", path) + + with open(path, "w") as outfile: + outfile.write(influxframework.as_json(out)) + + +def export_csv(doctype, path): + from influxframework.core.doctype.data_export.exporter import export_data + + with open(path, "wb") as csvfile: + export_data(doctype=doctype, all_doctypes=True, template=True, with_data=True) + csvfile.write(influxframework.response.result.encode("utf-8")) diff --git a/influxframework/core/doctype/data_import/data_import_list.js b/influxframework/core/doctype/data_import/data_import_list.js new file mode 100644 index 0000000..8604021 --- /dev/null +++ b/influxframework/core/doctype/data_import/data_import_list.js @@ -0,0 +1,44 @@ +let imports_in_progress = []; + +influxframework.listview_settings["Data Import"] = { + onload(listview) { + influxframework.realtime.on("data_import_progress", (data) => { + if (!imports_in_progress.includes(data.data_import)) { + imports_in_progress.push(data.data_import); + } + }); + influxframework.realtime.on("data_import_refresh", (data) => { + imports_in_progress = imports_in_progress.filter((d) => d !== data.data_import); + listview.refresh(); + }); + }, + get_indicator: function (doc) { + var colors = { + Pending: "orange", + "Not Started": "orange", + "Partial Success": "orange", + Success: "green", + "In Progress": "orange", + Error: "red", + }; + let status = doc.status; + + if (imports_in_progress.includes(doc.name)) { + status = "In Progress"; + } + if (status == "Pending") { + status = "Not Started"; + } + + return [__(status), colors[status], "status,=," + doc.status]; + }, + formatters: { + import_type(value) { + return { + "Insert New Records": __("Insert"), + "Update Existing Records": __("Update"), + }[value]; + }, + }, + hide_name_column: true, +}; diff --git a/influxframework/core/doctype/data_import/exporter.py b/influxframework/core/doctype/data_import/exporter.py new file mode 100644 index 0000000..1fb5d8c --- /dev/null +++ b/influxframework/core/doctype/data_import/exporter.py @@ -0,0 +1,255 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +import typing + +import influxframework +from influxframework import _ +from influxframework.model import display_fieldtypes, no_value_fields +from influxframework.model import table_fields as table_fieldtypes +from influxframework.utils import flt, format_duration, groupby_metric +from influxframework.utils.csvutils import build_csv_response +from influxframework.utils.xlsxutils import build_xlsx_response + + +class Exporter: + def __init__( + self, + doctype, + export_fields=None, + export_data=False, + export_filters=None, + export_page_length=None, + file_type="CSV", + ): + """ + Exports records of a DocType for use with Importer + :param doctype: Document Type to export + :param export_fields=None: One of 'All', 'Mandatory' or {'DocType': ['field1', 'field2'], 'Child DocType': ['childfield1']} + :param export_data=False: Whether to export data as well + :param export_filters=None: The filters (dict or list) which is used to query the records + :param file_type: One of 'Excel' or 'CSV' + """ + self.doctype = doctype + self.meta = influxframework.get_meta(doctype) + self.export_fields = export_fields + self.export_filters = export_filters + self.export_page_length = export_page_length + self.file_type = file_type + + # this will contain the csv content + self.csv_array = [] + + # fields that get exported + self.exportable_fields = self.get_all_exportable_fields() + self.fields = self.serialize_exportable_fields() + self.add_header() + + if export_data: + self.data = self.get_data_to_export() + else: + self.data = [] + self.add_data() + + def get_all_exportable_fields(self): + child_table_fields = [ + df.fieldname for df in self.meta.fields if df.fieldtype in table_fieldtypes + ] + + meta = influxframework.get_meta(self.doctype) + exportable_fields = influxframework._dict({}) + + for key, fieldnames in self.export_fields.items(): + if key == self.doctype: + # parent fields + exportable_fields[key] = self.get_exportable_fields(key, fieldnames) + + elif key in child_table_fields: + # child fields + child_df = meta.get_field(key) + child_doctype = child_df.options + exportable_fields[key] = self.get_exportable_fields(child_doctype, fieldnames) + + return exportable_fields + + def serialize_exportable_fields(self): + fields = [] + for key, exportable_fields in self.exportable_fields.items(): + for _df in exportable_fields: + # make a copy of df dict to avoid reference mutation + if isinstance(_df, influxframework.core.doctype.docfield.docfield.DocField): + df = _df.as_dict() + else: + df = _df.copy() + + df.is_child_table_field = key != self.doctype + if df.is_child_table_field: + df.child_table_df = self.meta.get_field(key) + fields.append(df) + return fields + + def get_exportable_fields(self, doctype, fieldnames): + meta = influxframework.get_meta(doctype) + + def is_exportable(df): + return df and df.fieldtype not in (display_fieldtypes + no_value_fields) + + # add name field + name_field = influxframework._dict( + { + "fieldtype": "Data", + "fieldname": "name", + "label": "ID", + "reqd": 1, + "parent": doctype, + } + ) + + fields = [meta.get_field(fieldname) for fieldname in fieldnames] + fields = [df for df in fields if is_exportable(df)] + + if "name" in fieldnames: + fields = [name_field] + fields + + return fields or [] + + def get_data_to_export(self): + influxframework.permissions.can_export(self.doctype, raise_exception=True) + + table_fields = [f for f in self.exportable_fields if f != self.doctype] + data = self.get_data_as_docs() + + for doc in data: + rows = [] + rows = self.add_data_row(self.doctype, None, doc, rows, 0) + + if table_fields: + # add child table data + for f in table_fields: + for i, child_row in enumerate(doc.get(f, [])): + table_df = self.meta.get_field(f) + child_doctype = table_df.options + rows = self.add_data_row(child_doctype, child_row.parentfield, child_row, rows, i) + + yield from rows + + def add_data_row(self, doctype, parentfield, doc, rows, row_idx): + if len(rows) < row_idx + 1: + rows.append([""] * len(self.fields)) + + row = rows[row_idx] + + for i, df in enumerate(self.fields): + if df.parent == doctype: + if df.is_child_table_field and df.child_table_df.fieldname != parentfield: + continue + value = doc.get(df.fieldname, None) + + if df.fieldtype == "Duration": + value = flt(value or 0) + value = format_duration(value, df.hide_days) + + row[i] = value + return rows + + def get_data_as_docs(self): + def format_column_name(df): + return f"`tab{df.parent}`.`{df.fieldname}`" + + filters = self.export_filters + + if self.meta.is_nested_set(): + order_by = f"`tab{self.doctype}`.`lft` ASC" + else: + order_by = f"`tab{self.doctype}`.`creation` DESC" + + parent_fields = [format_column_name(df) for df in self.fields if df.parent == self.doctype] + parent_data = influxframework.db.get_list( + self.doctype, + filters=filters, + fields=["name"] + parent_fields, + limit_page_length=self.export_page_length, + order_by=order_by, + as_list=0, + ) + parent_names = [p.name for p in parent_data] + + child_data = {} + for key in self.exportable_fields: + if key == self.doctype: + continue + child_table_df = self.meta.get_field(key) + child_table_doctype = child_table_df.options + child_fields = ["name", "idx", "parent", "parentfield"] + list( + {format_column_name(df) for df in self.fields if df.parent == child_table_doctype} + ) + data = influxframework.get_all( + child_table_doctype, + filters={ + "parent": ("in", parent_names), + "parentfield": child_table_df.fieldname, + "parenttype": self.doctype, + }, + fields=child_fields, + order_by="idx asc", + as_list=0, + ) + child_data[key] = data + + # Group children data by parent name + grouped_children_data = self.group_children_data_by_parent(child_data) + for doc in parent_data: + related_children_docs = grouped_children_data.get(doc.name, {}) + yield {**doc, **related_children_docs} + + def add_header(self): + header = [] + for df in self.fields: + is_parent = not df.is_child_table_field + if is_parent: + label = _(df.label) + else: + label = f"{_(df.label)} ({_(df.child_table_df.label)})" + + if label in header: + # this label is already in the header, + # which means two fields with the same label + # add the fieldname to avoid clash + if is_parent: + label = f"{df.fieldname}" + else: + label = f"{df.child_table_df.fieldname}.{df.fieldname}" + + header.append(label) + + self.csv_array.append(header) + + def add_data(self): + self.csv_array += self.data + + def get_csv_array(self): + return self.csv_array + + def get_csv_array_for_export(self): + csv_array = self.csv_array + + if not self.data: + # add 2 empty rows + csv_array += [[]] * 2 + + return csv_array + + def build_response(self): + if self.file_type == "CSV": + self.build_csv_response() + elif self.file_type == "Excel": + self.build_xlsx_response() + + def build_csv_response(self): + build_csv_response(self.get_csv_array_for_export(), _(self.doctype)) + + def build_xlsx_response(self): + build_xlsx_response(self.get_csv_array_for_export(), _(self.doctype)) + + def group_children_data_by_parent(self, children_data: dict[str, list]): + return groupby_metric(children_data, key="parent") diff --git a/influxframework/core/doctype/data_import/fixtures/sample_import_file.csv b/influxframework/core/doctype/data_import/fixtures/sample_import_file.csv new file mode 100644 index 0000000..a432a45 --- /dev/null +++ b/influxframework/core/doctype/data_import/fixtures/sample_import_file.csv @@ -0,0 +1,5 @@ +Title ,Description ,Number ,Duration,another_number ,ID (Table Field 1),Child Title (Table Field 1),Child Description (Table Field 1),Child 2 Title (Table Field 2),Child 2 Date (Table Field 2),Child 2 Number (Table Field 2),Child Title (Table Field 1 Again),Child Date (Table Field 1 Again),Child Number (Table Field 1 Again),table_field_1_again.child_another_number +Test ,test description ,1,3h,2, ,child title ,child description ,child title ,14-08-2019,4,child title again ,22-09-2020,5,7 + , , ,, , ,child title 2,child description 2,title child ,30-10-2019,5,child title again 2,22-09-2021, , +Test 2,test description 2,1,4d 3h,2, ,child mandatory title , ,title child man , , ,child mandatory again , , , +Test 3,test description 3,4,5d 5h 45m,5, ,child title asdf ,child description asdf ,child title asdf adsf ,15-08-2019,6,child title again asdf ,22-09-2022,9,71 \ No newline at end of file diff --git a/influxframework/core/doctype/data_import/fixtures/sample_import_file_for_update.csv b/influxframework/core/doctype/data_import/fixtures/sample_import_file_for_update.csv new file mode 100644 index 0000000..e48208e --- /dev/null +++ b/influxframework/core/doctype/data_import/fixtures/sample_import_file_for_update.csv @@ -0,0 +1,2 @@ +Title,Description,Number,another_number,ID (Table Field 1),Child Title (Table Field 1),Child Description (Table Field 1),Child 2 Title (Table Field 2),Child 2 Date (Table Field 2),Child 2 Number (Table Field 2),Child Title (Table Field 1 Again),Child Date (Table Field 1 Again),Child Number (Table Field 1 Again),table_field_1_again.child_another_number +Test 26,test description,1,2,"",child title,child description,child title,14-08-2019,4,child title again,22-09-2020,5,7 diff --git a/influxframework/core/doctype/data_import/fixtures/sample_import_file_without_mandatory.csv b/influxframework/core/doctype/data_import/fixtures/sample_import_file_without_mandatory.csv new file mode 100644 index 0000000..c6bff5c --- /dev/null +++ b/influxframework/core/doctype/data_import/fixtures/sample_import_file_without_mandatory.csv @@ -0,0 +1,5 @@ +Title ,Description ,Number ,another_number ,ID (Table Field 1) ,Child Title (Table Field 1) ,Child Description (Table Field 1) ,Child 2 Title (Table Field 2) ,Child 2 Date (Table Field 2) ,Child 2 Number (Table Field 2) ,Child Title (Table Field 1 Again) ,Child Date (Table Field 1 Again) ,Child Number (Table Field 1 Again) ,table_field_1_again.child_another_number +Test 5 ,test description ,1 ,2 ,"" , ,child description ,child title ,14-08-2019 ,4 ,child title again ,22-09-2020 ,5 , 7 + , , , , ,child title 2 ,child description 2 ,title child ,30-10-2019 ,5 , ,22-09-2021 , , + ,test description 2 ,1 ,2 , ,child mandatory title , ,title child man , , ,child mandatory again , , , +Test 4 ,test description 3 ,4 ,5 ,"" ,child title asdf ,child description asdf ,child title asdf adsf ,15-08-2019 ,6 ,child title again asdf ,22-09-2022 ,9 , 71 diff --git a/influxframework/core/doctype/data_import/importer.py b/influxframework/core/doctype/data_import/importer.py new file mode 100644 index 0000000..0ad12b1 --- /dev/null +++ b/influxframework/core/doctype/data_import/importer.py @@ -0,0 +1,1272 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +import io +import json +import os +import re +import timeit +from datetime import date, datetime, time + +import influxframework +from influxframework import _ +from influxframework.core.doctype.version.version import get_diff +from influxframework.model import no_value_fields +from influxframework.utils import cint, cstr, duration_to_seconds, flt, update_progress_bar +from influxframework.utils.csvutils import get_csv_content_from_google_sheets, read_csv_content +from influxframework.utils.xlsxutils import ( + read_xls_file_from_attached_file, + read_xlsx_file_from_attached_file, +) + +INVALID_VALUES = ("", None) +MAX_ROWS_IN_PREVIEW = 10 +INSERT = "Insert New Records" +UPDATE = "Update Existing Records" +DURATION_PATTERN = re.compile(r"^(?:(\d+d)?((^|\s)\d+h)?((^|\s)\d+m)?((^|\s)\d+s)?)$") + + +class Importer: + def __init__(self, doctype, data_import=None, file_path=None, import_type=None, console=False): + self.doctype = doctype + self.console = console + + self.data_import = data_import + if not self.data_import: + self.data_import = influxframework.get_doc(doctype="Data Import") + if import_type: + self.data_import.import_type = import_type + + self.template_options = influxframework.parse_json(self.data_import.template_options or "{}") + self.import_type = self.data_import.import_type + + self.import_file = ImportFile( + doctype, + file_path or data_import.google_sheets_url or data_import.import_file, + self.template_options, + self.import_type, + ) + + def get_data_for_import_preview(self): + out = self.import_file.get_data_for_import_preview() + + out.import_log = influxframework.get_all( + "Data Import Log", + fields=["row_indexes", "success"], + filters={"data_import": self.data_import.name}, + order_by="log_index", + limit=10, + ) + + return out + + def before_import(self): + # set user lang for translations + influxframework.cache().hdel("lang", influxframework.session.user) + influxframework.set_user_lang(influxframework.session.user) + + # set flags + influxframework.flags.in_import = True + influxframework.flags.mute_emails = self.data_import.mute_emails + + self.data_import.db_set("template_warnings", "") + + def import_data(self): + self.before_import() + + # parse docs from rows + payloads = self.import_file.get_payloads_for_import() + + # dont import if there are non-ignorable warnings + warnings = self.import_file.get_warnings() + warnings = [w for w in warnings if w.get("type") != "info"] + + if warnings: + if self.console: + self.print_grouped_warnings(warnings) + else: + self.data_import.db_set("template_warnings", json.dumps(warnings)) + return + + # setup import log + import_log = ( + influxframework.get_all( + "Data Import Log", + fields=["row_indexes", "success", "log_index"], + filters={"data_import": self.data_import.name}, + order_by="log_index", + ) + or [] + ) + + log_index = 0 + + # Do not remove rows in case of retry after an error or pending data import + if ( + self.data_import.status == "Partial Success" + and len(import_log) >= self.data_import.payload_count + ): + # remove previous failures from import log only in case of retry after partial success + import_log = [log for log in import_log if log.get("success")] + + # get successfully imported rows + imported_rows = [] + for log in import_log: + log = influxframework._dict(log) + if log.success or len(import_log) < self.data_import.payload_count: + imported_rows += json.loads(log.row_indexes) + + log_index = log.log_index + + # start import + total_payload_count = len(payloads) + batch_size = influxframework.conf.data_import_batch_size or 1000 + + for batch_index, batched_payloads in enumerate(influxframework.utils.create_batch(payloads, batch_size)): + for i, payload in enumerate(batched_payloads): + doc = payload.doc + row_indexes = [row.row_number for row in payload.rows] + current_index = (i + 1) + (batch_index * batch_size) + + if set(row_indexes).intersection(set(imported_rows)): + print("Skipping imported rows", row_indexes) + if total_payload_count > 5: + influxframework.publish_realtime( + "data_import_progress", + { + "current": current_index, + "total": total_payload_count, + "skipping": True, + "data_import": self.data_import.name, + }, + ) + continue + + try: + start = timeit.default_timer() + doc = self.process_doc(doc) + processing_time = timeit.default_timer() - start + eta = self.get_eta(current_index, total_payload_count, processing_time) + + if self.console: + update_progress_bar( + f"Importing {total_payload_count} records", + current_index, + total_payload_count, + ) + elif total_payload_count > 5: + influxframework.publish_realtime( + "data_import_progress", + { + "current": current_index, + "total": total_payload_count, + "docname": doc.name, + "data_import": self.data_import.name, + "success": True, + "row_indexes": row_indexes, + "eta": eta, + }, + ) + + create_import_log( + self.data_import.name, + log_index, + {"success": True, "docname": doc.name, "row_indexes": row_indexes}, + ) + + log_index += 1 + + if not self.data_import.status == "Partial Success": + self.data_import.db_set("status", "Partial Success") + + # commit after every successful import + influxframework.db.commit() + + except Exception: + messages = influxframework.local.message_log + influxframework.clear_messages() + + # rollback if exception + influxframework.db.rollback() + + create_import_log( + self.data_import.name, + log_index, + { + "success": False, + "exception": influxframework.get_traceback(), + "messages": messages, + "row_indexes": row_indexes, + }, + ) + + log_index += 1 + + # Logs are db inserted directly so will have to be fetched again + import_log = ( + influxframework.get_all( + "Data Import Log", + fields=["row_indexes", "success", "log_index"], + filters={"data_import": self.data_import.name}, + order_by="log_index", + ) + or [] + ) + + # set status + failures = [log for log in import_log if not log.get("success")] + if len(failures) == total_payload_count: + status = "Pending" + elif len(failures) > 0: + status = "Partial Success" + else: + status = "Success" + + if self.console: + self.print_import_log(import_log) + else: + self.data_import.db_set("status", status) + + self.after_import() + + return import_log + + def after_import(self): + influxframework.flags.in_import = False + influxframework.flags.mute_emails = False + + def process_doc(self, doc): + if self.import_type == INSERT: + return self.insert_record(doc) + elif self.import_type == UPDATE: + return self.update_record(doc) + + def insert_record(self, doc): + meta = influxframework.get_meta(self.doctype) + new_doc = influxframework.new_doc(self.doctype) + new_doc.update(doc) + + if not doc.name and (meta.autoname or "").lower() != "prompt": + # name can only be set directly if autoname is prompt + new_doc.set("name", None) + + new_doc.flags.updater_reference = { + "doctype": self.data_import.doctype, + "docname": self.data_import.name, + "label": _("via Data Import"), + } + + new_doc.insert() + if meta.is_submittable and self.data_import.submit_after_import: + new_doc.submit() + return new_doc + + def update_record(self, doc): + id_field = get_id_field(self.doctype) + existing_doc = influxframework.get_doc(self.doctype, doc.get(id_field.fieldname)) + + updated_doc = influxframework.get_doc(self.doctype, doc.get(id_field.fieldname)) + + updated_doc.update(doc) + + if get_diff(existing_doc, updated_doc): + # update doc if there are changes + updated_doc.flags.updater_reference = { + "doctype": self.data_import.doctype, + "docname": self.data_import.name, + "label": _("via Data Import"), + } + updated_doc.save() + return updated_doc + else: + # throw if no changes + influxframework.throw(_("No changes to update")) + + def get_eta(self, current, total, processing_time): + self.last_eta = getattr(self, "last_eta", 0) + remaining = total - current + eta = processing_time * remaining + if not self.last_eta or eta < self.last_eta: + self.last_eta = eta + return self.last_eta + + def export_errored_rows(self): + from influxframework.utils.csvutils import build_csv_response + + if not self.data_import: + return + + import_log = ( + influxframework.get_all( + "Data Import Log", + fields=["row_indexes", "success"], + filters={"data_import": self.data_import.name}, + order_by="log_index", + ) + or [] + ) + + failures = [log for log in import_log if not log.get("success")] + row_indexes = [] + for f in failures: + row_indexes.extend(json.loads(f.get("row_indexes", []))) + + # de duplicate + row_indexes = list(set(row_indexes)) + row_indexes.sort() + + header_row = [col.header_title for col in self.import_file.columns] + rows = [header_row] + rows += [row.data for row in self.import_file.data if row.row_number in row_indexes] + + build_csv_response(rows, _(self.doctype)) + + def export_import_log(self): + from influxframework.utils.csvutils import build_csv_response + + if not self.data_import: + return + + import_log = influxframework.get_all( + "Data Import Log", + fields=["row_indexes", "success", "messages", "exception", "docname"], + filters={"data_import": self.data_import.name}, + order_by="log_index", + ) + + header_row = ["Row Numbers", "Status", "Message", "Exception"] + + rows = [header_row] + + for log in import_log: + row_number = json.loads(log.get("row_indexes"))[0] + status = "Success" if log.get("success") else "Failure" + message = ( + "Successfully Imported {}".format(log.get("docname")) + if log.get("success") + else log.get("messages") + ) + exception = influxframework.utils.cstr(log.get("exception", "")) + rows += [[row_number, status, message, exception]] + + build_csv_response(rows, self.doctype) + + def print_import_log(self, import_log): + failed_records = [log for log in import_log if not log.success] + successful_records = [log for log in import_log if log.success] + + if successful_records: + print() + print(f"Successfully imported {len(successful_records)} records out of {len(import_log)}") + + if failed_records: + print(f"Failed to import {len(failed_records)} records") + file_name = f"{self.doctype}_import_on_{influxframework.utils.now()}.txt" + print("Check {} for errors".format(os.path.join("sites", file_name))) + text = "" + for w in failed_records: + text += "Row Indexes: {}\n".format(str(w.get("row_indexes", []))) + text += "Messages:\n{}\n".format("\n".join(w.get("messages", []))) + text += "Traceback:\n{}\n\n".format(w.get("exception")) + + with open(file_name, "w") as f: + f.write(text) + + def print_grouped_warnings(self, warnings): + warnings_by_row = {} + other_warnings = [] + for w in warnings: + if w.get("row"): + warnings_by_row.setdefault(w.get("row"), []).append(w) + else: + other_warnings.append(w) + + for row_number, warnings in warnings_by_row.items(): + print(f"Row {row_number}") + for w in warnings: + print(w.get("message")) + + for w in other_warnings: + print(w.get("message")) + + +class ImportFile: + def __init__(self, doctype, file, template_options=None, import_type=None): + self.doctype = doctype + self.template_options = template_options or influxframework._dict(column_to_field_map=influxframework._dict()) + self.column_to_field_map = self.template_options.column_to_field_map + self.import_type = import_type + self.warnings = [] + + self.file_doc = self.file_path = self.google_sheets_url = None + if isinstance(file, str): + if influxframework.db.exists("File", {"file_url": file}): + self.file_doc = influxframework.get_doc("File", {"file_url": file}) + elif "docs.google.com/spreadsheets" in file: + self.google_sheets_url = file + elif os.path.exists(file): + self.file_path = file + + if not self.file_doc and not self.file_path and not self.google_sheets_url: + influxframework.throw(_("Invalid template file for import")) + + self.raw_data = self.get_data_from_template_file() + self.parse_data_from_template() + + def get_data_from_template_file(self): + content = None + extension = None + + if self.file_doc: + parts = self.file_doc.get_extension() + extension = parts[1] + content = self.file_doc.get_content() + extension = extension.lstrip(".") + + elif self.file_path: + content, extension = self.read_file(self.file_path) + + elif self.google_sheets_url: + content = get_csv_content_from_google_sheets(self.google_sheets_url) + extension = "csv" + + if not content: + influxframework.throw(_("Invalid or corrupted content for import")) + + if not extension: + extension = "csv" + + if content: + return self.read_content(content, extension) + + def parse_data_from_template(self): + header = None + data = [] + + for i, row in enumerate(self.raw_data): + if all(v in INVALID_VALUES for v in row): + # empty row + continue + + if not header: + header = Header(i, row, self.doctype, self.raw_data[1:], self.column_to_field_map) + else: + row_obj = Row(i, row, self.doctype, header, self.import_type) + data.append(row_obj) + + self.header = header + self.columns = self.header.columns + self.data = data + + if len(data) < 1: + influxframework.throw( + _("Import template should contain a Header and atleast one row."), + title=_("Template Error"), + ) + + def get_data_for_import_preview(self): + """Adds a serial number column as the first column""" + + columns = [influxframework._dict({"header_title": "Sr. No", "skip_import": True})] + columns += [col.as_dict() for col in self.columns] + for col in columns: + # only pick useful fields in docfields to minimise the payload + if col.df: + col.df = { + "fieldtype": col.df.fieldtype, + "fieldname": col.df.fieldname, + "label": col.df.label, + "options": col.df.options, + "parent": col.df.parent, + "reqd": col.df.reqd, + "default": col.df.default, + "read_only": col.df.read_only, + } + + data = [[row.row_number] + row.as_list() for row in self.data] + + warnings = self.get_warnings() + + out = influxframework._dict() + out.data = data + out.columns = columns + out.warnings = warnings + total_number_of_rows = len(out.data) + if total_number_of_rows > MAX_ROWS_IN_PREVIEW: + out.data = out.data[:MAX_ROWS_IN_PREVIEW] + out.max_rows_exceeded = True + out.max_rows_in_preview = MAX_ROWS_IN_PREVIEW + out.total_number_of_rows = total_number_of_rows + return out + + def get_payloads_for_import(self): + payloads = [] + # make a copy + data = list(self.data) + while data: + doc, rows, data = self.parse_next_row_for_import(data) + payloads.append(influxframework._dict(doc=doc, rows=rows)) + return payloads + + def parse_next_row_for_import(self, data): + """ + Parses rows that make up a doc. A doc maybe built from a single row or multiple rows. + Returns the doc, rows, and data without the rows. + """ + doctypes = self.header.doctypes + + # first row is included by default + first_row = data[0] + rows = [first_row] + + # if there are child doctypes, find the subsequent rows + if len(doctypes) > 1: + # subsequent rows that have blank values in parent columns + # are considered as child rows + parent_column_indexes = self.header.get_column_indexes(self.doctype) + parent_row_values = first_row.get_values(parent_column_indexes) + + data_without_first_row = data[1:] + for row in data_without_first_row: + row_values = row.get_values(parent_column_indexes) + # if the row is blank, it's a child row doc + if all(v in INVALID_VALUES for v in row_values): + rows.append(row) + continue + # if we encounter a row which has values in parent columns, + # then it is the next doc + break + + parent_doc = None + for row in rows: + for doctype, table_df in doctypes: + if doctype == self.doctype and not parent_doc: + parent_doc = row.parse_doc(doctype) + + if doctype != self.doctype and table_df: + child_doc = row.parse_doc(doctype, parent_doc, table_df) + if child_doc is None: + continue + parent_doc[table_df.fieldname] = parent_doc.get(table_df.fieldname, []) + parent_doc[table_df.fieldname].append(child_doc) + + doc = parent_doc + + return doc, rows, data[len(rows) :] + + def get_warnings(self): + warnings = [] + + # ImportFile warnings + warnings += self.warnings + + # Column warnings + for col in self.header.columns: + warnings += col.warnings + + # Row warnings + for row in self.data: + warnings += row.warnings + + return warnings + + ###### + + def read_file(self, file_path: str): + extn = os.path.splitext(file_path)[1][1:] + + file_content = None + + file_name = influxframework.db.get_value("File", {"file_url": file_path}) + if file_name: + file = influxframework.get_doc("File", file_name) + file_content = file.get_content() + + return file_content, extn + + def read_content(self, content, extension): + error_title = _("Template Error") + if extension not in ("csv", "xlsx", "xls"): + influxframework.throw(_("Import template should be of type .csv, .xlsx or .xls"), title=error_title) + + if extension == "csv": + data = read_csv_content(content) + elif extension == "xlsx": + data = read_xlsx_file_from_attached_file(fcontent=content) + elif extension == "xls": + data = read_xls_file_from_attached_file(content) + + return data + + +class Row: + link_values_exist_map = {} + + def __init__(self, index, row, doctype, header, import_type): + self.index = index + self.row_number = index + 1 + self.doctype = doctype + self.data = row + self.header = header + self.import_type = import_type + self.warnings = [] + + len_row = len(self.data) + len_columns = len(self.header.columns) + if len_row != len_columns: + less_than_columns = len_row < len_columns + message = ( + "Row has less values than columns" if less_than_columns else "Row has more values than columns" + ) + self.warnings.append( + { + "row": self.row_number, + "message": message, + } + ) + + def parse_doc(self, doctype, parent_doc=None, table_df=None): + col_indexes = self.header.get_column_indexes(doctype, table_df) + values = self.get_values(col_indexes) + + if all(v in INVALID_VALUES for v in values): + # if all values are invalid, no need to parse it + return None + + columns = self.header.get_columns(col_indexes) + doc = self._parse_doc(doctype, columns, values, parent_doc, table_df) + return doc + + def _parse_doc(self, doctype, columns, values, parent_doc=None, table_df=None): + doc = influxframework._dict() + if self.import_type == INSERT: + # new_doc returns a dict with default values set + doc = influxframework.new_doc( + doctype, + parent_doc=parent_doc, + parentfield=table_df.fieldname if table_df else None, + as_dict=True, + ) + + # remove standard fields and __islocal + for key in influxframework.model.default_fields + influxframework.model.child_table_fields + ("__islocal",): + doc.pop(key, None) + + for col, value in zip(columns, values): + df = col.df + if value in INVALID_VALUES: + value = None + + if value is not None: + value = self.validate_value(value, col) + + if value is not None: + doc[df.fieldname] = self.parse_value(value, col) + + is_table = influxframework.get_meta(doctype).istable + is_update = self.import_type == UPDATE + if is_table and is_update: + # check if the row already exists + # if yes, fetch the original doc so that it is not updated + # if no, create a new doc + id_field = get_id_field(doctype) + id_value = doc.get(id_field.fieldname) + if id_value and influxframework.db.exists(doctype, id_value): + existing_doc = influxframework.get_doc(doctype, id_value) + existing_doc.update(doc) + doc = existing_doc + else: + # for table rows being inserted in update + # create a new doc with defaults set + new_doc = influxframework.new_doc(doctype, as_dict=True) + new_doc.update(doc) + doc = new_doc + + return doc + + def validate_value(self, value, col): + df = col.df + if df.fieldtype == "Select": + select_options = get_select_options(df) + if select_options and value not in select_options: + options_string = ", ".join(influxframework.bold(d) for d in select_options) + msg = _("Value must be one of {0}").format(options_string) + self.warnings.append( + { + "row": self.row_number, + "field": df_as_json(df), + "message": msg, + } + ) + return + + elif df.fieldtype == "Link": + exists = self.link_exists(value, df) + if not exists: + msg = _("Value {0} missing for {1}").format(influxframework.bold(value), influxframework.bold(df.options)) + self.warnings.append( + { + "row": self.row_number, + "field": df_as_json(df), + "message": msg, + } + ) + return + elif df.fieldtype in ["Date", "Datetime"]: + value = self.get_date(value, col) + if isinstance(value, str): + # value was not parsed as datetime object + self.warnings.append( + { + "row": self.row_number, + "col": col.column_number, + "field": df_as_json(df), + "message": _("Value {0} must in {1} format").format( + influxframework.bold(value), influxframework.bold(get_user_format(col.date_format)) + ), + } + ) + return + elif df.fieldtype == "Duration": + if not DURATION_PATTERN.match(value): + self.warnings.append( + { + "row": self.row_number, + "col": col.column_number, + "field": df_as_json(df), + "message": _("Value {0} must be in the valid duration format: d h m s").format( + influxframework.bold(value) + ), + } + ) + + return value + + def link_exists(self, value, df): + key = df.options + "::" + cstr(value) + if Row.link_values_exist_map.get(key) is None: + Row.link_values_exist_map[key] = influxframework.db.exists(df.options, value) + return Row.link_values_exist_map.get(key) + + def parse_value(self, value, col): + df = col.df + if isinstance(value, (datetime, date)) and df.fieldtype in ["Date", "Datetime"]: + return value + + value = cstr(value) + + # convert boolean values to 0 or 1 + valid_check_values = ["t", "f", "true", "false", "yes", "no", "y", "n"] + if df.fieldtype == "Check" and value.lower().strip() in valid_check_values: + value = value.lower().strip() + value = 1 if value in ["t", "true", "y", "yes"] else 0 + + if df.fieldtype in ["Int", "Check"]: + value = cint(value) + elif df.fieldtype in ["Float", "Percent", "Currency"]: + value = flt(value) + elif df.fieldtype in ["Date", "Datetime"]: + value = self.get_date(value, col) + elif df.fieldtype == "Duration": + value = duration_to_seconds(value) + + return value + + def get_date(self, value, column): + if isinstance(value, (datetime, date)): + return value + + date_format = column.date_format + if date_format: + try: + return datetime.strptime(value, date_format) + except ValueError: + # ignore date values that dont match the format + # import will break for these values later + pass + return value + + def get_values(self, indexes): + return [self.data[i] for i in indexes] + + def get(self, index): + return self.data[index] + + def as_list(self): + return self.data + + +class Header(Row): + def __init__(self, index, row, doctype, raw_data, column_to_field_map=None): + self.index = index + self.row_number = index + 1 + self.data = row + self.doctype = doctype + column_to_field_map = column_to_field_map or influxframework._dict() + + self.seen = [] + self.columns = [] + + for j, header in enumerate(row): + column_values = [get_item_at_index(r, j) for r in raw_data] + map_to_field = column_to_field_map.get(str(j)) + column = Column(j, header, self.doctype, column_values, map_to_field, self.seen) + self.seen.append(header) + self.columns.append(column) + + doctypes = [] + for col in self.columns: + if not col.df: + continue + if col.df.parent == self.doctype: + doctypes.append((col.df.parent, None)) + else: + doctypes.append((col.df.parent, col.df.child_table_df)) + + self.doctypes = sorted(list(set(doctypes)), key=lambda x: -1 if x[0] == self.doctype else 1) + + def get_column_indexes(self, doctype, tablefield=None): + def is_table_field(df): + if tablefield: + return df.child_table_df.fieldname == tablefield.fieldname + return True + + return [ + col.index + for col in self.columns + if not col.skip_import and col.df and col.df.parent == doctype and is_table_field(col.df) + ] + + def get_columns(self, indexes): + return [self.columns[i] for i in indexes] + + +class Column: + seen = [] + fields_column_map = {} + + def __init__(self, index, header, doctype, column_values, map_to_field=None, seen=None): + if seen is None: + seen = [] + self.index = index + self.column_number = index + 1 + self.doctype = doctype + self.header_title = header + self.column_values = column_values + self.map_to_field = map_to_field + self.seen = seen + + self.date_format = None + self.df = None + self.skip_import = None + self.warnings = [] + + self.meta = influxframework.get_meta(doctype) + self.parse() + self.validate_values() + + def parse(self): + header_title = self.header_title + column_number = str(self.column_number) + skip_import = False + + if self.map_to_field and self.map_to_field != "Don't Import": + df = get_df_for_column_header(self.doctype, self.map_to_field) + if df: + self.warnings.append( + { + "message": _("Mapping column {0} to field {1}").format( + influxframework.bold(header_title or "Untitled Column"), influxframework.bold(df.label) + ), + "type": "info", + } + ) + else: + self.warnings.append( + { + "message": _("Could not map column {0} to field {1}").format( + column_number, self.map_to_field + ), + "type": "info", + } + ) + else: + df = get_df_for_column_header(self.doctype, header_title) + # df = df_by_labels_and_fieldnames.get(header_title) + + if not df: + skip_import = True + else: + skip_import = False + + if header_title in self.seen: + self.warnings.append( + { + "col": column_number, + "message": _("Skipping Duplicate Column {0}").format(influxframework.bold(header_title)), + "type": "info", + } + ) + df = None + skip_import = True + elif self.map_to_field == "Don't Import": + skip_import = True + self.warnings.append( + { + "col": column_number, + "message": _("Skipping column {0}").format(influxframework.bold(header_title)), + "type": "info", + } + ) + elif header_title and not df: + self.warnings.append( + { + "col": column_number, + "message": _("Cannot match column {0} with any field").format(influxframework.bold(header_title)), + "type": "info", + } + ) + elif not header_title and not df: + self.warnings.append( + {"col": column_number, "message": _("Skipping Untitled Column"), "type": "info"} + ) + + self.df = df + self.skip_import = skip_import + + def guess_date_format_for_column(self): + """Guesses date format for a column by parsing all the values in the column, + getting the date format and then returning the one which has the maximum frequency + """ + + def guess_date_format(d): + if isinstance(d, (datetime, date, time)): + if self.df.fieldtype == "Date": + return "%Y-%m-%d" + if self.df.fieldtype == "Datetime": + return "%Y-%m-%d %H:%M:%S" + if self.df.fieldtype == "Time": + return "%H:%M:%S" + if isinstance(d, str): + return influxframework.utils.guess_date_format(d) + + date_formats = [guess_date_format(d) for d in self.column_values] + date_formats = [d for d in date_formats if d] + if not date_formats: + return + + unique_date_formats = set(date_formats) + max_occurred_date_format = max(unique_date_formats, key=date_formats.count) + + if len(unique_date_formats) > 1: + # fmt: off + message = _("The column {0} has {1} different date formats. Automatically setting {2} as the default format as it is the most common. Please change other values in this column to this format.") + # fmt: on + user_date_format = get_user_format(max_occurred_date_format) + self.warnings.append( + { + "col": self.column_number, + "message": message.format( + influxframework.bold(self.header_title), + len(unique_date_formats), + influxframework.bold(user_date_format), + ), + "type": "info", + } + ) + + return max_occurred_date_format + + def validate_values(self): + if not self.df: + return + + if self.skip_import: + return + + if not any(self.column_values): + return + + if self.df.fieldtype == "Link": + # find all values that dont exist + values = list({cstr(v) for v in self.column_values if v}) + exists = [ + cstr(d.name) for d in influxframework.get_all(self.df.options, filters={"name": ("in", values)}) + ] + not_exists = list(set(values) - set(exists)) + if not_exists: + missing_values = ", ".join(not_exists) + message = _("The following values do not exist for {0}: {1}") + self.warnings.append( + { + "col": self.column_number, + "message": message.format(self.df.options, missing_values), + "type": "warning", + } + ) + elif self.df.fieldtype in ("Date", "Time", "Datetime"): + # guess date/time format + self.date_format = self.guess_date_format_for_column() + if not self.date_format: + if self.df.fieldtype == "Time": + self.date_format = "%H:%M:%S" + date_format = "HH:mm:ss" + else: + self.date_format = "%Y-%m-%d" + date_format = "yyyy-mm-dd" + + message = _( + "{0} format could not be determined from the values in this column. Defaulting to {1}." + ) + self.warnings.append( + { + "col": self.column_number, + "message": message.format(self.df.fieldtype, date_format), + "type": "info", + } + ) + elif self.df.fieldtype == "Select": + options = get_select_options(self.df) + if options: + values = {cstr(v) for v in self.column_values if v} + invalid = values - set(options) + if invalid: + valid_values = ", ".join(influxframework.bold(o) for o in options) + invalid_values = ", ".join(influxframework.bold(i) for i in invalid) + message = _("The following values are invalid: {0}. Values must be one of {1}") + self.warnings.append( + { + "col": self.column_number, + "message": message.format(invalid_values, valid_values), + } + ) + + def as_dict(self): + d = influxframework._dict() + d.index = self.index + d.column_number = self.column_number + d.doctype = self.doctype + d.header_title = self.header_title + d.map_to_field = self.map_to_field + d.date_format = self.date_format + d.df = self.df + if hasattr(self.df, "is_child_table_field"): + d.is_child_table_field = self.df.is_child_table_field + d.child_table_df = self.df.child_table_df + d.skip_import = self.skip_import + d.warnings = self.warnings + return d + + +def build_fields_dict_for_column_matching(parent_doctype): + """ + Build a dict with various keys to match with column headers and value as docfield + The keys can be label or fieldname + { + 'Customer': df1, + 'customer': df1, + 'Due Date': df2, + 'due_date': df2, + 'Item Code (Sales Invoice Item)': df3, + 'Sales Invoice Item:item_code': df3, + } + """ + + def get_standard_fields(doctype): + meta = influxframework.get_meta(doctype) + if meta.istable: + standard_fields = [ + {"label": "Parent", "fieldname": "parent"}, + {"label": "Parent Type", "fieldname": "parenttype"}, + {"label": "Parent Field", "fieldname": "parentfield"}, + {"label": "Row Index", "fieldname": "idx"}, + ] + else: + standard_fields = [ + {"label": "Owner", "fieldname": "owner"}, + {"label": "Document Status", "fieldname": "docstatus", "fieldtype": "Int"}, + ] + + out = [] + for df in standard_fields: + df = influxframework._dict(df) + df.parent = doctype + out.append(df) + return out + + parent_meta = influxframework.get_meta(parent_doctype) + out = {} + + # doctypes and fieldname if it is a child doctype + doctypes = [(parent_doctype, None)] + [(df.options, df) for df in parent_meta.get_table_fields()] + + for doctype, table_df in doctypes: + translated_table_label = _(table_df.label) if table_df else None + + # name field + name_df = influxframework._dict( + { + "fieldtype": "Data", + "fieldname": "name", + "label": "ID", + "reqd": 1, # self.import_type == UPDATE, + "parent": doctype, + } + ) + + if doctype == parent_doctype: + name_headers = ( + "name", # fieldname + "ID", # label + _("ID"), # translated label + ) + else: + name_headers = ( + f"{table_df.fieldname}.name", # fieldname + f"ID ({table_df.label})", # label + "{} ({})".format(_("ID"), translated_table_label), # translated label + ) + + name_df.is_child_table_field = True + name_df.child_table_df = table_df + + for header in name_headers: + out[header] = name_df + + fields = get_standard_fields(doctype) + influxframework.get_meta(doctype).fields + for df in fields: + fieldtype = df.fieldtype or "Data" + if fieldtype in no_value_fields: + continue + + label = (df.label or "").strip() + translated_label = _(label) + parent = df.parent or parent_doctype + + if parent_doctype == doctype: + # for parent doctypes keys will be + # Label, fieldname, Label (fieldname) + + for header in (label, translated_label): + # if Label is already set, don't set it again + # in case of duplicate column headers + if header not in out: + out[header] = df + + for header in ( + df.fieldname, + f"{label} ({df.fieldname})", + f"{translated_label} ({df.fieldname})", + ): + out[header] = df + + else: + # for child doctypes keys will be + # Label (Table Field Label) + # table_field.fieldname + + # create a new df object to avoid mutation problems + if isinstance(df, dict): + new_df = influxframework._dict(df.copy()) + else: + new_df = df.as_dict() + + new_df.is_child_table_field = True + new_df.child_table_df = table_df + + for header in ( + # fieldname + f"{table_df.fieldname}.{df.fieldname}", + # label + f"{label} ({table_df.label})", + # translated label + f"{translated_label} ({translated_table_label})", + ): + out[header] = new_df + + # if autoname is based on field + # add an entry for "ID (Autoname Field)" + autoname_field = get_autoname_field(parent_doctype) + if autoname_field: + for header in ( + f"ID ({autoname_field.label})", # label + "{} ({})".format(_("ID"), _(autoname_field.label)), # translated label + # ID field should also map to the autoname field + "ID", + _("ID"), + "name", + ): + out[header] = autoname_field + + return out + + +def get_df_for_column_header(doctype, header): + def build_fields_dict_for_doctype(): + return build_fields_dict_for_column_matching(doctype) + + df_by_labels_and_fieldname = influxframework.cache().hget( + "data_import_column_header_map", doctype, generator=build_fields_dict_for_doctype + ) + return df_by_labels_and_fieldname.get(header) + + +# utilities + + +def get_id_field(doctype): + autoname_field = get_autoname_field(doctype) + if autoname_field: + return autoname_field + return influxframework._dict({"label": "ID", "fieldname": "name", "fieldtype": "Data"}) + + +def get_autoname_field(doctype): + meta = influxframework.get_meta(doctype) + if meta.autoname and meta.autoname.startswith("field:"): + fieldname = meta.autoname[len("field:") :] + return meta.get_field(fieldname) + + +def get_item_at_index(_list, i, default=None): + try: + a = _list[i] + except IndexError: + a = default + return a + + +def get_user_format(date_format): + return ( + date_format.replace("%Y", "yyyy").replace("%y", "yy").replace("%m", "mm").replace("%d", "dd") + ) + + +def df_as_json(df): + return { + "fieldname": df.fieldname, + "fieldtype": df.fieldtype, + "label": df.label, + "options": df.options, + "parent": df.parent, + "default": df.default, + } + + +def get_select_options(df): + return [d for d in (df.options or "").split("\n") if d] + + +def create_import_log(data_import, log_index, log_details): + influxframework.get_doc( + { + "doctype": "Data Import Log", + "log_index": log_index, + "success": log_details.get("success"), + "data_import": data_import, + "row_indexes": json.dumps(log_details.get("row_indexes")), + "docname": log_details.get("docname"), + "messages": json.dumps(log_details.get("messages", "[]")), + "exception": log_details.get("exception"), + } + ).db_insert() diff --git a/influxframework/core/doctype/data_import/test_data_import.py b/influxframework/core/doctype/data_import/test_data_import.py new file mode 100644 index 0000000..a202476 --- /dev/null +++ b/influxframework/core/doctype/data_import/test_data_import.py @@ -0,0 +1,8 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestDataImport(InfluxFrameworkTestCase): + pass diff --git a/influxframework/core/doctype/data_import/test_exporter.py b/influxframework/core/doctype/data_import/test_exporter.py new file mode 100644 index 0000000..7d56251 --- /dev/null +++ b/influxframework/core/doctype/data_import/test_exporter.py @@ -0,0 +1,100 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.core.doctype.data_import.exporter import Exporter +from influxframework.core.doctype.data_import.test_importer import create_doctype_if_not_exists +from influxframework.tests.utils import InfluxFrameworkTestCase + +doctype_name = "DocType for Export" + + +class TestExporter(InfluxFrameworkTestCase): + def setUp(self): + create_doctype_if_not_exists(doctype_name) + + def test_exports_specified_fields(self): + if not influxframework.db.exists(doctype_name, "Test"): + doc = influxframework.get_doc( + doctype=doctype_name, + title="Test", + description="Test Description", + table_field_1=[ + {"child_title": "Child Title 1", "child_description": "Child Description 1"}, + {"child_title": "Child Title 2", "child_description": "Child Description 2"}, + ], + table_field_2=[ + {"child_2_title": "Child Title 1", "child_2_description": "Child Description 1"}, + ], + table_field_1_again=[ + { + "child_title": "Child Title 1 Again", + "child_description": "Child Description 1 Again", + }, + ], + ).insert() + else: + doc = influxframework.get_doc(doctype_name, "Test") + + e = Exporter( + doctype_name, + export_fields={ + doctype_name: ["title", "description", "number", "another_number"], + "table_field_1": ["name", "child_title", "child_description"], + "table_field_2": ["child_2_date", "child_2_number"], + "table_field_1_again": [ + "child_title", + "child_date", + "child_number", + "child_another_number", + ], + }, + export_data=True, + ) + csv_array = e.get_csv_array() + header_row = csv_array[0] + + self.assertEqual( + header_row, + [ + "Title", + "Description", + "Number", + "another_number", + "ID (Table Field 1)", + "Child Title (Table Field 1)", + "Child Description (Table Field 1)", + "Child 2 Date (Table Field 2)", + "Child 2 Number (Table Field 2)", + "Child Title (Table Field 1 Again)", + "Child Date (Table Field 1 Again)", + "Child Number (Table Field 1 Again)", + "table_field_1_again.child_another_number", + ], + ) + + table_field_1_row_1_name = doc.table_field_1[0].name + table_field_1_row_2_name = doc.table_field_1[1].name + # fmt: off + self.assertEqual( + csv_array[1], + ["Test", "Test Description", 0, 0, table_field_1_row_1_name, "Child Title 1", "Child Description 1", None, 0, "Child Title 1 Again", None, 0, 0] + ) + self.assertEqual( + csv_array[2], + ["", "", "", "", table_field_1_row_2_name, "Child Title 2", "Child Description 2", "", "", "", "", "", ""], + ) + # fmt: on + self.assertEqual(len(csv_array), 3) + + def test_export_csv_response(self): + e = Exporter( + doctype_name, + export_fields={doctype_name: ["title", "description"]}, + export_data=True, + file_type="CSV", + ) + e.build_response() + + self.assertTrue(influxframework.response["result"]) + self.assertEqual(influxframework.response["doctype"], doctype_name) + self.assertEqual(influxframework.response["type"], "csv") diff --git a/influxframework/core/doctype/data_import/test_importer.py b/influxframework/core/doctype/data_import/test_importer.py new file mode 100644 index 0000000..6034370 --- /dev/null +++ b/influxframework/core/doctype/data_import/test_importer.py @@ -0,0 +1,251 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.core.doctype.data_import.importer import Importer +from influxframework.tests.test_query_builder import db_type_is, run_only_if +from influxframework.tests.utils import InfluxFrameworkTestCase +from influxframework.utils import format_duration, getdate + +doctype_name = "DocType for Import" + + +class TestImporter(InfluxFrameworkTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + create_doctype_if_not_exists( + doctype_name, + ) + + def test_data_import_from_file(self): + import_file = get_import_file("sample_import_file") + data_import = self.get_importer(doctype_name, import_file) + data_import.start_import() + + doc1 = influxframework.get_doc(doctype_name, "Test") + doc2 = influxframework.get_doc(doctype_name, "Test 2") + doc3 = influxframework.get_doc(doctype_name, "Test 3") + + self.assertEqual(doc1.description, "test description") + self.assertEqual(doc1.number, 1) + self.assertEqual(format_duration(doc1.duration), "3h") + + self.assertEqual(doc1.table_field_1[0].child_title, "child title") + self.assertEqual(doc1.table_field_1[0].child_description, "child description") + + self.assertEqual(doc1.table_field_1[1].child_title, "child title 2") + self.assertEqual(doc1.table_field_1[1].child_description, "child description 2") + + self.assertEqual(doc1.table_field_2[1].child_2_title, "title child") + self.assertEqual(doc1.table_field_2[1].child_2_date, getdate("2019-10-30")) + self.assertEqual(doc1.table_field_2[1].child_2_another_number, 5) + + self.assertEqual(doc1.table_field_1_again[0].child_title, "child title again") + self.assertEqual(doc1.table_field_1_again[1].child_title, "child title again 2") + self.assertEqual(doc1.table_field_1_again[1].child_date, getdate("2021-09-22")) + + self.assertEqual(doc2.description, "test description 2") + self.assertEqual(format_duration(doc2.duration), "4d 3h") + + self.assertEqual(doc3.another_number, 5) + self.assertEqual(format_duration(doc3.duration), "5d 5h 45m") + + def test_data_import_preview(self): + import_file = get_import_file("sample_import_file") + data_import = self.get_importer(doctype_name, import_file) + preview = data_import.get_preview_from_template() + + self.assertEqual(len(preview.data), 4) + self.assertEqual(len(preview.columns), 16) + + # ignored on postgres because myisam doesn't exist on pg + @run_only_if(db_type_is.MARIADB) + def test_data_import_without_mandatory_values(self): + import_file = get_import_file("sample_import_file_without_mandatory") + data_import = self.get_importer(doctype_name, import_file) + influxframework.local.message_log = [] + data_import.start_import() + data_import.reload() + + import_log = influxframework.get_all( + "Data Import Log", + fields=["row_indexes", "success", "messages", "exception", "docname"], + filters={"data_import": data_import.name}, + order_by="log_index", + ) + + self.assertEqual(influxframework.parse_json(import_log[0]["row_indexes"]), [2, 3]) + expected_error = ( + "Error: Child 1 of DocType for Import Row #1: Value missing for: Child Title" + ) + self.assertEqual( + influxframework.parse_json(influxframework.parse_json(import_log[0]["messages"])[0])["message"], expected_error + ) + expected_error = ( + "Error: Child 1 of DocType for Import Row #2: Value missing for: Child Title" + ) + self.assertEqual( + influxframework.parse_json(influxframework.parse_json(import_log[0]["messages"])[1])["message"], expected_error + ) + + self.assertEqual(influxframework.parse_json(import_log[1]["row_indexes"]), [4]) + self.assertEqual( + influxframework.parse_json(influxframework.parse_json(import_log[1]["messages"])[0])["message"], + "Title is required", + ) + + def test_data_import_update(self): + existing_doc = influxframework.get_doc( + doctype=doctype_name, + title=influxframework.generate_hash(doctype_name, 8), + table_field_1=[{"child_title": "child title to update"}], + ) + existing_doc.save() + influxframework.db.commit() + + import_file = get_import_file("sample_import_file_for_update") + data_import = self.get_importer(doctype_name, import_file, update=True) + i = Importer(data_import.reference_doctype, data_import=data_import) + + # update child table id in template date + i.import_file.raw_data[1][4] = existing_doc.table_field_1[0].name + + # uppercase to check if autoname field isn't replaced in mariadb + if influxframework.db.db_type == "mariadb": + i.import_file.raw_data[1][0] = existing_doc.name.upper() + else: + i.import_file.raw_data[1][0] = existing_doc.name + + i.import_file.parse_data_from_template() + i.import_data() + + updated_doc = influxframework.get_doc(doctype_name, existing_doc.name) + self.assertEqual(existing_doc.title, updated_doc.title) + self.assertEqual(updated_doc.description, "test description") + self.assertEqual(updated_doc.table_field_1[0].child_title, "child title") + self.assertEqual(updated_doc.table_field_1[0].name, existing_doc.table_field_1[0].name) + self.assertEqual(updated_doc.table_field_1[0].child_description, "child description") + self.assertEqual(updated_doc.table_field_1_again[0].child_title, "child title again") + + def get_importer(self, doctype, import_file, update=False): + data_import = influxframework.new_doc("Data Import") + data_import.import_type = "Insert New Records" if not update else "Update Existing Records" + data_import.reference_doctype = doctype + data_import.import_file = import_file.file_url + data_import.insert() + # Commit so that the first import failure does not rollback the Data Import insert. + influxframework.db.commit() + + return data_import + + +def create_doctype_if_not_exists(doctype_name, force=False): + if force: + influxframework.delete_doc_if_exists("DocType", doctype_name) + influxframework.delete_doc_if_exists("DocType", "Child 1 of " + doctype_name) + influxframework.delete_doc_if_exists("DocType", "Child 2 of " + doctype_name) + + if influxframework.db.exists("DocType", doctype_name): + return + + # Child Table 1 + table_1_name = "Child 1 of " + doctype_name + influxframework.get_doc( + { + "doctype": "DocType", + "name": table_1_name, + "module": "Custom", + "custom": 1, + "istable": 1, + "fields": [ + {"label": "Child Title", "fieldname": "child_title", "reqd": 1, "fieldtype": "Data"}, + {"label": "Child Description", "fieldname": "child_description", "fieldtype": "Small Text"}, + {"label": "Child Date", "fieldname": "child_date", "fieldtype": "Date"}, + {"label": "Child Number", "fieldname": "child_number", "fieldtype": "Int"}, + {"label": "Child Number", "fieldname": "child_another_number", "fieldtype": "Int"}, + ], + } + ).insert() + + # Child Table 2 + table_2_name = "Child 2 of " + doctype_name + influxframework.get_doc( + { + "doctype": "DocType", + "name": table_2_name, + "module": "Custom", + "custom": 1, + "istable": 1, + "fields": [ + {"label": "Child 2 Title", "fieldname": "child_2_title", "reqd": 1, "fieldtype": "Data"}, + { + "label": "Child 2 Description", + "fieldname": "child_2_description", + "fieldtype": "Small Text", + }, + {"label": "Child 2 Date", "fieldname": "child_2_date", "fieldtype": "Date"}, + {"label": "Child 2 Number", "fieldname": "child_2_number", "fieldtype": "Int"}, + {"label": "Child 2 Number", "fieldname": "child_2_another_number", "fieldtype": "Int"}, + ], + } + ).insert() + + # Main Table + influxframework.get_doc( + { + "doctype": "DocType", + "name": doctype_name, + "module": "Custom", + "custom": 1, + "autoname": "field:title", + "fields": [ + {"label": "Title", "fieldname": "title", "reqd": 1, "fieldtype": "Data"}, + {"label": "Description", "fieldname": "description", "fieldtype": "Small Text"}, + {"label": "Date", "fieldname": "date", "fieldtype": "Date"}, + {"label": "Duration", "fieldname": "duration", "fieldtype": "Duration"}, + {"label": "Number", "fieldname": "number", "fieldtype": "Int"}, + {"label": "Number", "fieldname": "another_number", "fieldtype": "Int"}, + { + "label": "Table Field 1", + "fieldname": "table_field_1", + "fieldtype": "Table", + "options": table_1_name, + }, + { + "label": "Table Field 2", + "fieldname": "table_field_2", + "fieldtype": "Table", + "options": table_2_name, + }, + { + "label": "Table Field 1 Again", + "fieldname": "table_field_1_again", + "fieldtype": "Table", + "options": table_1_name, + }, + ], + "permissions": [{"role": "System Manager"}], + } + ).insert() + + +def get_import_file(csv_file_name, force=False): + file_name = csv_file_name + ".csv" + _file = influxframework.db.exists("File", {"file_name": file_name}) + if force and _file: + influxframework.delete_doc_if_exists("File", _file) + + if influxframework.db.exists("File", {"file_name": file_name}): + f = influxframework.get_doc("File", {"file_name": file_name}) + else: + full_path = get_csv_file_path(file_name) + f = influxframework.get_doc( + doctype="File", content=influxframework.read_file(full_path), file_name=file_name, is_private=1 + ) + f.save(ignore_permissions=True) + + return f + + +def get_csv_file_path(file_name): + return influxframework.get_app_path("influxframework", "core", "doctype", "data_import", "fixtures", file_name) diff --git a/influxframework/core/doctype/data_import_log/__init__.py b/influxframework/core/doctype/data_import_log/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/data_import_log/data_import_log.js b/influxframework/core/doctype/data_import_log/data_import_log.js new file mode 100644 index 0000000..0793883 --- /dev/null +++ b/influxframework/core/doctype/data_import_log/data_import_log.js @@ -0,0 +1,7 @@ +// Copyright (c) 2021, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Data Import Log", { + // refresh: function(frm) { + // } +}); diff --git a/influxframework/core/doctype/data_import_log/data_import_log.json b/influxframework/core/doctype/data_import_log/data_import_log.json new file mode 100644 index 0000000..b1d991f --- /dev/null +++ b/influxframework/core/doctype/data_import_log/data_import_log.json @@ -0,0 +1,84 @@ +{ + "actions": [], + "creation": "2021-12-25 16:12:20.205889", + "doctype": "DocType", + "editable_grid": 1, + "engine": "MyISAM", + "field_order": [ + "data_import", + "row_indexes", + "success", + "docname", + "messages", + "exception", + "log_index" + ], + "fields": [ + { + "fieldname": "data_import", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Data Import", + "options": "Data Import" + }, + { + "fieldname": "docname", + "fieldtype": "Data", + "label": "Reference Name" + }, + { + "fieldname": "exception", + "fieldtype": "Text", + "label": "Exception" + }, + { + "fieldname": "row_indexes", + "fieldtype": "Code", + "label": "Row Indexes", + "options": "JSON" + }, + { + "default": "0", + "fieldname": "success", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Success" + }, + { + "fieldname": "log_index", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Log Index" + }, + { + "fieldname": "messages", + "fieldtype": "Code", + "label": "Messages", + "options": "JSON" + } + ], + "in_create": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-12-29 11:19:19.646076", + "modified_by": "Administrator", + "module": "Core", + "name": "Data Import Log", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/influxframework/core/doctype/data_import_log/data_import_log.py b/influxframework/core/doctype/data_import_log/data_import_log.py new file mode 100644 index 0000000..3ab038f --- /dev/null +++ b/influxframework/core/doctype/data_import_log/data_import_log.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, InfluxFramework LLC +# For license information, please see license.txt + +# import influxframework +from influxframework.model.document import Document + + +class DataImportLog(Document): + pass diff --git a/influxframework/core/doctype/data_import_log/test_data_import_log.py b/influxframework/core/doctype/data_import_log/test_data_import_log.py new file mode 100644 index 0000000..2b6b5c1 --- /dev/null +++ b/influxframework/core/doctype/data_import_log/test_data_import_log.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, InfluxFramework LLC +# See license.txt + +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestDataImportLog(InfluxFrameworkTestCase): + pass diff --git a/influxframework/core/doctype/defaultvalue/README.md b/influxframework/core/doctype/defaultvalue/README.md new file mode 100644 index 0000000..327fb71 --- /dev/null +++ b/influxframework/core/doctype/defaultvalue/README.md @@ -0,0 +1 @@ +Child table for **User** and **Role** where default keys and values are stored. They can also be created from the **User Properties** page. \ No newline at end of file diff --git a/influxframework/core/doctype/defaultvalue/__init__.py b/influxframework/core/doctype/defaultvalue/__init__.py new file mode 100644 index 0000000..2e11a12 --- /dev/null +++ b/influxframework/core/doctype/defaultvalue/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE diff --git a/influxframework/core/doctype/defaultvalue/defaultvalue.json b/influxframework/core/doctype/defaultvalue/defaultvalue.json new file mode 100644 index 0000000..35b08c2 --- /dev/null +++ b/influxframework/core/doctype/defaultvalue/defaultvalue.json @@ -0,0 +1,90 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "hash", + "beta": 0, + "creation": "2013-02-22 01:27:32", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "editable_grid": 1, + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "defkey", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Key", + "length": 0, + "no_copy": 0, + "oldfieldname": "defkey", + "oldfieldtype": "Data", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "200px", + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0, + "width": "200px" + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "defvalue", + "fieldtype": "Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Value", + "length": 0, + "no_copy": 0, + "oldfieldname": "defvalue", + "oldfieldtype": "Text", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "200px", + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, + "width": "200px" + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 1, + "image_view": 0, + "in_create": 0, + + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "max_attachments": 0, + "modified": "2016-07-11 03:27:59.126216", + "modified_by": "Administrator", + "module": "Core", + "name": "DefaultValue", + "owner": "Administrator", + "permissions": [], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "track_seen": 0 +} \ No newline at end of file diff --git a/influxframework/core/doctype/defaultvalue/defaultvalue.py b/influxframework/core/doctype/defaultvalue/defaultvalue.py new file mode 100644 index 0000000..c9d470b --- /dev/null +++ b/influxframework/core/doctype/defaultvalue/defaultvalue.py @@ -0,0 +1,25 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document + + +class DefaultValue(Document): + pass + + +def on_doctype_update(): + """Create indexes for `tabDefaultValue` on `(parent, defkey)`""" + influxframework.db.commit() + influxframework.db.add_index( + doctype="DefaultValue", + fields=["parent", "defkey"], + index_name="defaultvalue_parent_defkey_index", + ) + + influxframework.db.add_index( + doctype="DefaultValue", + fields=["parent", "parenttype"], + index_name="defaultvalue_parent_parenttype_index", + ) diff --git a/influxframework/core/doctype/deleted_document/__init__.py b/influxframework/core/doctype/deleted_document/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/deleted_document/deleted_document.js b/influxframework/core/doctype/deleted_document/deleted_document.js new file mode 100644 index 0000000..53a4199 --- /dev/null +++ b/influxframework/core/doctype/deleted_document/deleted_document.js @@ -0,0 +1,22 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Deleted Document", { + refresh: function (frm) { + if (frm.doc.restored) { + frm.add_custom_button(__("Open"), function () { + influxframework.set_route("Form", frm.doc.deleted_doctype, frm.doc.new_name); + }); + } else { + frm.add_custom_button(__("Restore"), function () { + influxframework.call({ + method: "influxframework.core.doctype.deleted_document.deleted_document.restore", + args: { name: frm.doc.name }, + callback: function (r) { + frm.reload_doc(); + }, + }); + }); + } + }, +}); diff --git a/influxframework/core/doctype/deleted_document/deleted_document.json b/influxframework/core/doctype/deleted_document/deleted_document.json new file mode 100644 index 0000000..6b95a52 --- /dev/null +++ b/influxframework/core/doctype/deleted_document/deleted_document.json @@ -0,0 +1,81 @@ +{ + "actions": [], + "creation": "2016-12-29 12:59:48.638970", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "deleted_name", + "deleted_doctype", + "column_break_3", + "restored", + "new_name", + "section_break_6", + "data" + ], + "fields": [ + { + "fieldname": "deleted_name", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Deleted Name", + "read_only": 1 + }, + { + "fieldname": "deleted_doctype", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Deleted DocType", + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "restored", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Restored", + "read_only": 1 + }, + { + "fieldname": "new_name", + "fieldtype": "Read Only", + "label": "New Name" + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "fieldname": "data", + "fieldtype": "Code", + "label": "Data", + "read_only": 1 + } + ], + "in_create": 1, + "links": [], + "modified": "2022-06-13 05:50:58.314908", + "modified_by": "Administrator", + "module": "Core", + "name": "Deleted Document", + "owner": "Administrator", + "permissions": [ + { + "delete": 1, + "export": 1, + "read": 1, + "role": "System Manager" + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "deleted_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/deleted_document/deleted_document.py b/influxframework/core/doctype/deleted_document/deleted_document.py new file mode 100644 index 0000000..b2ccdcd --- /dev/null +++ b/influxframework/core/doctype/deleted_document/deleted_document.py @@ -0,0 +1,64 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +import influxframework +from influxframework import _ +from influxframework.desk.doctype.bulk_update.bulk_update import show_progress +from influxframework.model.document import Document + + +class DeletedDocument(Document): + pass + + +@influxframework.whitelist() +def restore(name, alert=True): + deleted = influxframework.get_doc("Deleted Document", name) + + if deleted.restored: + influxframework.throw(_("Document {0} Already Restored").format(name), exc=influxframework.DocumentAlreadyRestored) + + doc = influxframework.get_doc(json.loads(deleted.data)) + + try: + doc.insert() + except influxframework.DocstatusTransitionError: + influxframework.msgprint(_("Cancelled Document restored as Draft")) + doc.docstatus = 0 + doc.insert() + + doc.add_comment("Edit", _("restored {0} as {1}").format(deleted.deleted_name, doc.name)) + + deleted.new_name = doc.name + deleted.restored = 1 + deleted.db_update() + + if alert: + influxframework.msgprint(_("Document Restored")) + + +@influxframework.whitelist() +def bulk_restore(docnames): + docnames = influxframework.parse_json(docnames) + message = _("Restoring Deleted Document") + restored, invalid, failed = [], [], [] + + for i, d in enumerate(docnames): + try: + show_progress(docnames, message, i + 1, d) + restore(d, alert=False) + influxframework.db.commit() + restored.append(d) + + except influxframework.DocumentAlreadyRestored: + influxframework.message_log.pop() + invalid.append(d) + + except Exception: + influxframework.message_log.pop() + failed.append(d) + influxframework.db.rollback() + + return {"restored": restored, "invalid": invalid, "failed": failed} diff --git a/influxframework/core/doctype/deleted_document/deleted_document_list.js b/influxframework/core/doctype/deleted_document/deleted_document_list.js new file mode 100644 index 0000000..e1193fa --- /dev/null +++ b/influxframework/core/doctype/deleted_document/deleted_document_list.js @@ -0,0 +1,50 @@ +influxframework.listview_settings["Deleted Document"] = { + onload: function (doclist) { + const action = () => { + const selected_docs = doclist.get_checked_items(); + if (selected_docs.length > 0) { + let docnames = selected_docs.map((doc) => doc.name); + influxframework.call({ + method: "influxframework.core.doctype.deleted_document.deleted_document.bulk_restore", + args: { docnames }, + callback: function (r) { + if (r.message) { + let body = (docnames) => { + const html = docnames.map((docname) => { + return `
  5. ${docname}
  6. `; + }); + return "
      " + html.join(""); + }; + + let message = (title, docnames) => { + return docnames.length > 0 ? title + body(docnames) + "
    " : ""; + }; + + const { restored, invalid, failed } = r.message; + const restored_summary = message( + __("Documents restored successfully"), + restored + ); + const invalid_summary = message( + __("Documents that were already restored"), + invalid + ); + const failed_summary = message( + __("Documents that failed to restore"), + failed + ); + const summary = restored_summary + invalid_summary + failed_summary; + + influxframework.msgprint(summary, __("Document Restoration Summary"), true); + + if (restored.length > 0) { + doclist.refresh(); + } + } + }, + }); + } + }; + doclist.page.add_actions_menu_item(__("Restore"), action, false); + }, +}; diff --git a/influxframework/core/doctype/deleted_document/test_deleted_document.py b/influxframework/core/doctype/deleted_document/test_deleted_document.py new file mode 100644 index 0000000..826ac36 --- /dev/null +++ b/influxframework/core/doctype/deleted_document/test_deleted_document.py @@ -0,0 +1,9 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + +# test_records = influxframework.get_test_records('Deleted Document') + + +class TestDeletedDocument(InfluxFrameworkTestCase): + pass diff --git a/influxframework/core/doctype/docfield/README.md b/influxframework/core/doctype/docfield/README.md new file mode 100644 index 0000000..8c9caca --- /dev/null +++ b/influxframework/core/doctype/docfield/README.md @@ -0,0 +1,3 @@ +Represents a field of a DocType analogous to a table column in the database. DocFields represent the properties both the model and the view and hence may or may not have database columns associated (for example, Section Break does not have any column associated.) + +See Standard Field Types \ No newline at end of file diff --git a/influxframework/core/doctype/docfield/__init__.py b/influxframework/core/doctype/docfield/__init__.py new file mode 100644 index 0000000..2e11a12 --- /dev/null +++ b/influxframework/core/doctype/docfield/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE diff --git a/influxframework/core/doctype/docfield/docfield.json b/influxframework/core/doctype/docfield/docfield.json new file mode 100644 index 0000000..803ad3c --- /dev/null +++ b/influxframework/core/doctype/docfield/docfield.json @@ -0,0 +1,560 @@ +{ + "actions": [], + "autoname": "hash", + "creation": "2013-02-22 01:27:33", + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "label_and_type", + "label", + "fieldtype", + "fieldname", + "precision", + "length", + "non_negative", + "hide_days", + "hide_seconds", + "reqd", + "is_virtual", + "search_index", + "column_break_18", + "options", + "show_dashboard", + "defaults_section", + "default", + "column_break_6", + "fetch_from", + "fetch_if_empty", + "visibility_section", + "hidden", + "bold", + "allow_in_quick_entry", + "translatable", + "print_hide", + "print_hide_if_no_value", + "report_hide", + "column_break_28", + "depends_on", + "collapsible", + "collapsible_depends_on", + "hide_border", + "list__search_settings_section", + "in_list_view", + "in_standard_filter", + "in_preview", + "column_break_35", + "in_filter", + "in_global_search", + "permissions", + "read_only", + "allow_on_submit", + "ignore_user_permissions", + "allow_bulk_edit", + "column_break_13", + "permlevel", + "ignore_xss_filter", + "constraints_section", + "unique", + "no_copy", + "set_only_once", + "remember_last_selected_value", + "column_break_38", + "mandatory_depends_on", + "read_only_depends_on", + "display", + "print_width", + "width", + "max_height", + "columns", + "column_break_22", + "description", + "oldfieldname", + "oldfieldtype" + ], + "fields": [ + { + "fieldname": "label_and_type", + "fieldtype": "Section Break" + }, + { + "bold": 1, + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "oldfieldname": "label", + "oldfieldtype": "Data", + "print_width": "163", + "search_index": 1, + "width": "163" + }, + { + "bold": 1, + "default": "Data", + "fieldname": "fieldtype", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Type", + "oldfieldname": "fieldtype", + "oldfieldtype": "Select", + "options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nJSON\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nPhone\nRead Only\nRating\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime", + "reqd": 1, + "search_index": 1 + }, + { + "bold": 1, + "fieldname": "fieldname", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Name", + "oldfieldname": "fieldname", + "oldfieldtype": "Data", + "search_index": 1 + }, + { + "default": "0", + "depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)", + "fieldname": "reqd", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Mandatory", + "oldfieldname": "reqd", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)", + "description": "Set non-standard precision for a Float or Currency field", + "fieldname": "precision", + "fieldtype": "Select", + "label": "Precision", + "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9", + "print_hide": 1 + }, + { + "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)", + "fieldname": "length", + "fieldtype": "Int", + "label": "Length" + }, + { + "default": "0", + "fieldname": "search_index", + "fieldtype": "Check", + "label": "Index", + "oldfieldname": "search_index", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "fieldname": "in_list_view", + "fieldtype": "Check", + "label": "In List View", + "print_width": "70px", + "width": "70px" + }, + { + "default": "0", + "fieldname": "in_standard_filter", + "fieldtype": "Check", + "label": "In List Filter" + }, + { + "default": "0", + "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)", + "fieldname": "in_global_search", + "fieldtype": "Check", + "label": "In Global Search" + }, + { + "default": "0", + "depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);", + "fieldname": "in_preview", + "fieldtype": "Check", + "label": "In Preview" + }, + { + "default": "0", + "fieldname": "allow_in_quick_entry", + "fieldtype": "Check", + "label": "Allow in Quick Entry" + }, + { + "default": "0", + "fieldname": "bold", + "fieldtype": "Check", + "label": "Bold" + }, + { + "default": "0", + "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)", + "fieldname": "translatable", + "fieldtype": "Check", + "label": "Translatable" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype===\"Section Break\"", + "fieldname": "collapsible", + "fieldtype": "Check", + "label": "Collapsible", + "length": 255 + }, + { + "depends_on": "eval:doc.fieldtype==\"Section Break\" && doc.collapsible", + "fieldname": "collapsible_depends_on", + "fieldtype": "Code", + "label": "Collapsible Depends On (JS)", + "max_height": "3rem", + "options": "JS" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.", + "fieldname": "options", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Options", + "oldfieldname": "options", + "oldfieldtype": "Text" + }, + { + "fieldname": "default", + "fieldtype": "Small Text", + "label": "Default", + "max_height": "3rem", + "oldfieldname": "default", + "oldfieldtype": "Text" + }, + { + "fieldname": "fetch_from", + "fieldtype": "Small Text", + "label": "Fetch From" + }, + { + "default": "0", + "fieldname": "fetch_if_empty", + "fieldtype": "Check", + "label": "Fetch only if value is not set" + }, + { + "fieldname": "permissions", + "fieldtype": "Section Break", + "label": "Permissions" + }, + { + "fieldname": "depends_on", + "fieldtype": "Code", + "label": "Display Depends On (JS)", + "length": 255, + "max_height": "3rem", + "oldfieldname": "depends_on", + "oldfieldtype": "Data", + "options": "JS" + }, + { + "default": "0", + "fieldname": "hidden", + "fieldtype": "Check", + "label": "Hidden", + "oldfieldname": "hidden", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "fieldname": "read_only", + "fieldtype": "Check", + "label": "Read Only", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "fieldname": "unique", + "fieldtype": "Check", + "label": "Unique" + }, + { + "default": "0", + "fieldname": "set_only_once", + "fieldtype": "Check", + "label": "Set only once" + }, + { + "default": "0", + "depends_on": "eval: doc.fieldtype == \"Table\"", + "fieldname": "allow_bulk_edit", + "fieldtype": "Check", + "label": "Allow Bulk Edit" + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "permlevel", + "fieldtype": "Int", + "label": "Perm Level", + "oldfieldname": "permlevel", + "oldfieldtype": "Int", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "fieldname": "ignore_user_permissions", + "fieldtype": "Check", + "label": "Ignore User Permissions" + }, + { + "default": "0", + "depends_on": "eval: parent.is_submittable", + "fieldname": "allow_on_submit", + "fieldtype": "Check", + "label": "Allow on Submit", + "oldfieldname": "allow_on_submit", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "fieldname": "report_hide", + "fieldtype": "Check", + "label": "Report Hide", + "oldfieldname": "report_hide", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "depends_on": "eval:(doc.fieldtype == 'Link')", + "fieldname": "remember_last_selected_value", + "fieldtype": "Check", + "label": "Remember Last Selected Value" + }, + { + "default": "0", + "description": "Don't encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field", + "fieldname": "ignore_xss_filter", + "fieldtype": "Check", + "label": "Ignore XSS Filter" + }, + { + "fieldname": "display", + "fieldtype": "Section Break", + "label": "Display" + }, + { + "default": "0", + "fieldname": "in_filter", + "fieldtype": "Check", + "label": "In Filter", + "oldfieldname": "in_filter", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "fieldname": "no_copy", + "fieldtype": "Check", + "label": "No Copy", + "oldfieldname": "no_copy", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "fieldname": "print_hide", + "fieldtype": "Check", + "label": "Print Hide", + "oldfieldname": "print_hide", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1", + "fieldname": "print_hide_if_no_value", + "fieldtype": "Check", + "label": "Print Hide If No Value" + }, + { + "fieldname": "print_width", + "fieldtype": "Data", + "label": "Print Width", + "length": 10 + }, + { + "fieldname": "width", + "fieldtype": "Data", + "label": "Width", + "length": 10, + "oldfieldname": "width", + "oldfieldtype": "Data", + "print_width": "50px", + "width": "50px" + }, + { + "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)", + "fieldname": "columns", + "fieldtype": "Int", + "label": "Columns" + }, + { + "fieldname": "column_break_22", + "fieldtype": "Column Break" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Text", + "print_width": "300px", + "width": "300px" + }, + { + "fieldname": "oldfieldname", + "fieldtype": "Data", + "hidden": 1, + "oldfieldname": "oldfieldname", + "oldfieldtype": "Data" + }, + { + "fieldname": "oldfieldtype", + "fieldtype": "Data", + "hidden": 1, + "oldfieldname": "oldfieldtype", + "oldfieldtype": "Data" + }, + { + "fieldname": "mandatory_depends_on", + "fieldtype": "Code", + "label": "Mandatory Depends On (JS)", + "max_height": "3rem", + "options": "JS" + }, + { + "fieldname": "read_only_depends_on", + "fieldtype": "Code", + "label": "Read Only Depends On (JS)", + "max_height": "3rem", + "options": "JS" + }, + { + "fieldname": "column_break_38", + "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype=='Duration'", + "fieldname": "hide_days", + "fieldtype": "Check", + "label": "Hide Days" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype=='Duration'", + "fieldname": "hide_seconds", + "fieldtype": "Check", + "label": "Hide Seconds" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype=='Section Break'", + "fieldname": "hide_border", + "fieldtype": "Check", + "label": "Hide Border" + }, + { + "default": "0", + "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)", + "fieldname": "non_negative", + "fieldtype": "Check", + "label": "Non Negative" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "fieldname": "defaults_section", + "fieldtype": "Section Break", + "label": "Defaults", + "max_height": "2rem" + }, + { + "fieldname": "visibility_section", + "fieldtype": "Section Break", + "label": "Visibility" + }, + { + "fieldname": "column_break_28", + "fieldtype": "Column Break" + }, + { + "fieldname": "constraints_section", + "fieldtype": "Section Break", + "label": "Constraints" + }, + { + "fieldname": "max_height", + "fieldtype": "Data", + "label": "Max Height", + "length": 10 + }, + { + "fieldname": "list__search_settings_section", + "fieldtype": "Section Break", + "label": "List / Search Settings" + }, + { + "fieldname": "column_break_35", + "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype===\"Tab Break\"", + "fieldname": "show_dashboard", + "fieldtype": "Check", + "label": "Show Dashboard" + }, + { + "default": "0", + "fieldname": "is_virtual", + "fieldtype": "Check", + "label": "Virtual" + } + ], + "idx": 1, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2022-04-19 12:27:28.641580", + "modified_by": "Administrator", + "module": "Core", + "name": "DocField", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "ASC", + "states": [] +} diff --git a/influxframework/core/doctype/docfield/docfield.py b/influxframework/core/doctype/docfield/docfield.py new file mode 100644 index 0000000..dbaa186 --- /dev/null +++ b/influxframework/core/doctype/docfield/docfield.py @@ -0,0 +1,31 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document + + +class DocField(Document): + def get_link_doctype(self): + """Returns the Link doctype for the docfield (if applicable) + if fieldtype is Link: Returns "options" + if fieldtype is Table MultiSelect: Returns "options" of the Link field in the Child Table + """ + if self.fieldtype == "Link": + return self.options + + if self.fieldtype == "Table MultiSelect": + table_doctype = self.options + + link_doctype = influxframework.db.get_value( + "DocField", + {"fieldtype": "Link", "parenttype": "DocType", "parent": table_doctype, "in_list_view": 1}, + "options", + ) + + return link_doctype + + def get_select_options(self): + if self.fieldtype == "Select": + options = self.options or "" + return [d for d in options.split("\n") if d] diff --git a/influxframework/core/doctype/docperm/__init__.py b/influxframework/core/doctype/docperm/__init__.py new file mode 100644 index 0000000..2e11a12 --- /dev/null +++ b/influxframework/core/doctype/docperm/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE diff --git a/influxframework/core/doctype/docperm/docperm.json b/influxframework/core/doctype/docperm/docperm.json new file mode 100644 index 0000000..4411a67 --- /dev/null +++ b/influxframework/core/doctype/docperm/docperm.json @@ -0,0 +1,229 @@ +{ + "actions": [], + "autoname": "hash", + "creation": "2013-02-22 01:27:33", + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "role_and_level", + "role", + "if_owner", + "column_break_2", + "permlevel", + "section_break_4", + "select", + "read", + "write", + "create", + "delete", + "column_break_8", + "submit", + "cancel", + "amend", + "additional_permissions", + "report", + "export", + "import", + "set_user_permissions", + "column_break_19", + "share", + "print", + "email" + ], + "fields": [ + { + "fieldname": "role_and_level", + "fieldtype": "Section Break", + "label": "Role and Level" + }, + { + "fieldname": "role", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Role", + "oldfieldname": "role", + "oldfieldtype": "Link", + "options": "Role", + "print_width": "150px", + "reqd": 1, + "width": "150px" + }, + { + "default": "0", + "description": "Apply this rule if the User is the Owner", + "fieldname": "if_owner", + "fieldtype": "Check", + "label": "If user is the owner" + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "permlevel", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Level", + "oldfieldname": "permlevel", + "oldfieldtype": "Int", + "print_width": "40px", + "width": "40px" + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break", + "label": "Permissions" + }, + { + "default": "1", + "fieldname": "read", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Read", + "oldfieldname": "read", + "oldfieldtype": "Check", + "print_width": "32px", + "width": "32px" + }, + { + "default": "1", + "fieldname": "write", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Write", + "oldfieldname": "write", + "oldfieldtype": "Check", + "print_width": "32px", + "width": "32px" + }, + { + "default": "1", + "fieldname": "create", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Create", + "oldfieldname": "create", + "oldfieldtype": "Check", + "print_width": "32px", + "width": "32px" + }, + { + "default": "1", + "fieldname": "delete", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Delete" + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "submit", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Submit", + "oldfieldname": "submit", + "oldfieldtype": "Check", + "print_width": "32px", + "width": "32px" + }, + { + "default": "0", + "fieldname": "cancel", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Cancel", + "oldfieldname": "cancel", + "oldfieldtype": "Check", + "print_width": "32px", + "width": "32px" + }, + { + "default": "0", + "fieldname": "amend", + "fieldtype": "Check", + "label": "Amend", + "oldfieldname": "amend", + "oldfieldtype": "Check", + "print_width": "32px", + "width": "32px" + }, + { + "fieldname": "additional_permissions", + "fieldtype": "Section Break", + "label": "Additional Permissions" + }, + { + "default": "1", + "fieldname": "report", + "fieldtype": "Check", + "label": "Report", + "print_width": "32px", + "width": "32px" + }, + { + "default": "1", + "fieldname": "export", + "fieldtype": "Check", + "label": "Export" + }, + { + "default": "0", + "fieldname": "import", + "fieldtype": "Check", + "label": "Import" + }, + { + "default": "0", + "description": "This role update User Permissions for a user", + "fieldname": "set_user_permissions", + "fieldtype": "Check", + "label": "Set User Permissions" + }, + { + "fieldname": "column_break_19", + "fieldtype": "Column Break" + }, + { + "default": "1", + "fieldname": "share", + "fieldtype": "Check", + "label": "Share" + }, + { + "default": "1", + "fieldname": "print", + "fieldtype": "Check", + "label": "Print" + }, + { + "default": "1", + "fieldname": "email", + "fieldtype": "Check", + "label": "Email" + }, + { + "default": "0", + "fieldname": "select", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Select" + } + ], + "idx": 1, + "istable": 1, + "links": [], + "modified": "2020-12-03 15:15:30.488212", + "modified_by": "Administrator", + "module": "Core", + "name": "DocPerm", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "ASC" +} \ No newline at end of file diff --git a/influxframework/core/doctype/docperm/docperm.py b/influxframework/core/doctype/docperm/docperm.py new file mode 100644 index 0000000..f8645d7 --- /dev/null +++ b/influxframework/core/doctype/docperm/docperm.py @@ -0,0 +1,8 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +from influxframework.model.document import Document + + +class DocPerm(Document): + pass diff --git a/influxframework/core/doctype/docshare/__init__.py b/influxframework/core/doctype/docshare/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/docshare/docshare.js b/influxframework/core/doctype/docshare/docshare.js new file mode 100644 index 0000000..cb33c7f --- /dev/null +++ b/influxframework/core/doctype/docshare/docshare.js @@ -0,0 +1,6 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("DocShare", { + refresh: function (frm) {}, +}); diff --git a/influxframework/core/doctype/docshare/docshare.json b/influxframework/core/doctype/docshare/docshare.json new file mode 100644 index 0000000..ca10b05 --- /dev/null +++ b/influxframework/core/doctype/docshare/docshare.json @@ -0,0 +1,110 @@ +{ + "actions": [], + "allow_import": 1, + "autoname": "hash", + "creation": "2015-02-04 04:33:36.330477", + "description": "Internal record of document shares", + "doctype": "DocType", + "document_type": "System", + "engine": "InnoDB", + "field_order": [ + "user", + "share_doctype", + "share_name", + "read", + "write", + "share", + "submit", + "everyone", + "notify_by_email" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "in_list_view": 1, + "label": "User", + "options": "User", + "search_index": 1 + }, + { + "fieldname": "share_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType", + "reqd": 1, + "search_index": 1 + }, + { + "fieldname": "share_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Document Name", + "options": "share_doctype", + "reqd": 1, + "search_index": 1 + }, + { + "default": "0", + "fieldname": "read", + "fieldtype": "Check", + "label": "Read" + }, + { + "default": "0", + "fieldname": "write", + "fieldtype": "Check", + "label": "Write" + }, + { + "default": "0", + "fieldname": "share", + "fieldtype": "Check", + "label": "Share" + }, + { + "default": "0", + "fieldname": "everyone", + "fieldtype": "Check", + "label": "Everyone" + }, + { + "default": "1", + "fieldname": "notify_by_email", + "fieldtype": "Check", + "label": "Notify by email", + "print_hide": 1 + }, + { + "default": "0", + "fieldname": "submit", + "fieldtype": "Check", + "label": "Submit" + } + ], + "in_create": 1, + "links": [], + "modified": "2021-04-04 11:38:50.813312", + "modified_by": "Administrator", + "module": "Core", + "name": "DocShare", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "export": 1, + "import": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/docshare/docshare.py b/influxframework/core/doctype/docshare/docshare.py new file mode 100644 index 0000000..16103a9 --- /dev/null +++ b/influxframework/core/doctype/docshare/docshare.py @@ -0,0 +1,82 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework import _ +from influxframework.model.document import Document +from influxframework.utils import cint, get_fullname + +exclude_from_linked_with = True + + +class DocShare(Document): + no_feed_on_delete = True + + def validate(self): + self.validate_user() + self.check_share_permission() + self.check_is_submittable() + self.cascade_permissions_downwards() + self.get_doc().run_method("validate_share", self) + + def cascade_permissions_downwards(self): + if self.share or self.write or self.submit: + self.read = 1 + if self.submit: + self.write = 1 + + def get_doc(self): + if not getattr(self, "_doc", None): + self._doc = influxframework.get_doc(self.share_doctype, self.share_name) + return self._doc + + def validate_user(self): + if self.everyone: + self.user = None + elif not self.user: + influxframework.throw(_("User is mandatory for Share"), influxframework.MandatoryError) + + def check_share_permission(self): + if not self.flags.ignore_share_permission and not influxframework.has_permission( + self.share_doctype, "share", self.get_doc() + ): + + influxframework.throw(_('You need to have "Share" permission'), influxframework.PermissionError) + + def check_is_submittable(self): + if self.submit and not cint( + influxframework.db.get_value("DocType", self.share_doctype, "is_submittable") + ): + influxframework.throw( + _("Cannot share {0} with submit permission as the doctype {1} is not submittable").format( + influxframework.bold(self.share_name), influxframework.bold(self.share_doctype) + ) + ) + + def after_insert(self): + doc = self.get_doc() + owner = get_fullname(self.owner) + + if self.everyone: + doc.add_comment("Shared", _("{0} shared this document with everyone").format(owner)) + else: + doc.add_comment( + "Shared", _("{0} shared this document with {1}").format(owner, get_fullname(self.user)) + ) + + def on_trash(self): + if not self.flags.ignore_share_permission: + self.check_share_permission() + + self.get_doc().add_comment( + "Unshared", + _("{0} un-shared this document with {1}").format( + get_fullname(self.owner), get_fullname(self.user) + ), + ) + + +def on_doctype_update(): + """Add index in `tabDocShare` for `(user, share_doctype)`""" + influxframework.db.add_index("DocShare", ["user", "share_doctype"]) + influxframework.db.add_index("DocShare", ["share_doctype", "share_name"]) diff --git a/influxframework/core/doctype/docshare/test_docshare.py b/influxframework/core/doctype/docshare/test_docshare.py new file mode 100644 index 0000000..9b891ac --- /dev/null +++ b/influxframework/core/doctype/docshare/test_docshare.py @@ -0,0 +1,127 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +import influxframework.share +from influxframework.automation.doctype.auto_repeat.test_auto_repeat import create_submittable_doctype +from influxframework.tests.utils import InfluxFrameworkTestCase + +test_dependencies = ["User"] + + +class TestDocShare(InfluxFrameworkTestCase): + def setUp(self): + self.user = "test@example.com" + self.event = influxframework.get_doc( + { + "doctype": "Event", + "subject": "test share event", + "starts_on": "2015-01-01 10:00:00", + "event_type": "Private", + } + ).insert() + + def tearDown(self): + influxframework.set_user("Administrator") + self.event.delete() + + def test_add(self): + # user not shared + self.assertTrue(self.event.name not in influxframework.share.get_shared("Event", self.user)) + influxframework.share.add("Event", self.event.name, self.user) + self.assertTrue(self.event.name in influxframework.share.get_shared("Event", self.user)) + + def test_doc_permission(self): + influxframework.set_user(self.user) + self.assertFalse(self.event.has_permission()) + + influxframework.set_user("Administrator") + influxframework.share.add("Event", self.event.name, self.user) + + influxframework.set_user(self.user) + self.assertTrue(self.event.has_permission()) + + def test_share_permission(self): + influxframework.share.add("Event", self.event.name, self.user, write=1, share=1) + + influxframework.set_user(self.user) + self.assertTrue(self.event.has_permission("share")) + + # test cascade + self.assertTrue(self.event.has_permission("read")) + self.assertTrue(self.event.has_permission("write")) + + def test_set_permission(self): + influxframework.share.add("Event", self.event.name, self.user) + + influxframework.set_user(self.user) + self.assertFalse(self.event.has_permission("share")) + + influxframework.set_user("Administrator") + influxframework.share.set_permission("Event", self.event.name, self.user, "share") + + influxframework.set_user(self.user) + self.assertTrue(self.event.has_permission("share")) + + def test_permission_to_share(self): + influxframework.set_user(self.user) + self.assertRaises(influxframework.PermissionError, influxframework.share.add, "Event", self.event.name, self.user) + + influxframework.set_user("Administrator") + influxframework.share.add("Event", self.event.name, self.user, write=1, share=1) + + # test not raises + influxframework.set_user(self.user) + influxframework.share.add("Event", self.event.name, "test1@example.com", write=1, share=1) + + def test_remove_share(self): + influxframework.share.add("Event", self.event.name, self.user, write=1, share=1) + + influxframework.set_user(self.user) + self.assertTrue(self.event.has_permission("share")) + + influxframework.set_user("Administrator") + influxframework.share.remove("Event", self.event.name, self.user) + + influxframework.set_user(self.user) + self.assertFalse(self.event.has_permission("share")) + + def test_share_with_everyone(self): + self.assertTrue(self.event.name not in influxframework.share.get_shared("Event", self.user)) + + influxframework.share.set_permission("Event", self.event.name, None, "read", everyone=1) + self.assertTrue(self.event.name in influxframework.share.get_shared("Event", self.user)) + self.assertTrue(self.event.name in influxframework.share.get_shared("Event", "test1@example.com")) + self.assertTrue(self.event.name not in influxframework.share.get_shared("Event", "Guest")) + + influxframework.share.set_permission("Event", self.event.name, None, "read", value=0, everyone=1) + self.assertTrue(self.event.name not in influxframework.share.get_shared("Event", self.user)) + self.assertTrue(self.event.name not in influxframework.share.get_shared("Event", "test1@example.com")) + self.assertTrue(self.event.name not in influxframework.share.get_shared("Event", "Guest")) + + def test_share_with_submit_perm(self): + doctype = "Test DocShare with Submit" + create_submittable_doctype(doctype, submit_perms=0) + + submittable_doc = influxframework.get_doc( + dict(doctype=doctype, test="test docshare with submit") + ).insert() + + influxframework.set_user(self.user) + self.assertFalse(influxframework.has_permission(doctype, "submit", user=self.user)) + + influxframework.set_user("Administrator") + influxframework.share.add(doctype, submittable_doc.name, self.user, submit=1) + + influxframework.set_user(self.user) + self.assertTrue( + influxframework.has_permission(doctype, "submit", doc=submittable_doc.name, user=self.user) + ) + + # test cascade + self.assertTrue(influxframework.has_permission(doctype, "read", doc=submittable_doc.name, user=self.user)) + self.assertTrue( + influxframework.has_permission(doctype, "write", doc=submittable_doc.name, user=self.user) + ) + + influxframework.share.remove(doctype, submittable_doc.name, self.user) diff --git a/influxframework/core/doctype/docshare/test_records.json b/influxframework/core/doctype/docshare/test_records.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/influxframework/core/doctype/docshare/test_records.json @@ -0,0 +1 @@ +[] diff --git a/influxframework/core/doctype/doctype/README.md b/influxframework/core/doctype/doctype/README.md new file mode 100644 index 0000000..3922a7e --- /dev/null +++ b/influxframework/core/doctype/doctype/README.md @@ -0,0 +1,15 @@ +DocType is the basic building block of an application and encompasses all the three elements i.e. model, view and controller. It represents a: + +- Table in the database +- Form in the application +- Controller (class) to execute business logic + +#### Single Type + +DocTypes can be of "Single" type where they do not represent a table, and only one instance is maintained. This can be used where the DocType is required only for its view features or to store some configurations in one place. + +#### Child Tables + +DocTypes can be child tables of other DocTypes. In such cases, they must defined `parent`, `parenttype` and `parentfield` properties to uniquely identify its placement. + +In the parent DocType, the position of a child in the field sequence is defined by the `Table` field type. \ No newline at end of file diff --git a/influxframework/core/doctype/doctype/__init__.py b/influxframework/core/doctype/doctype/__init__.py new file mode 100644 index 0000000..2e11a12 --- /dev/null +++ b/influxframework/core/doctype/doctype/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE diff --git a/influxframework/core/doctype/doctype/boilerplate/controller._py b/influxframework/core/doctype/doctype/boilerplate/controller._py new file mode 100644 index 0000000..994ac7b --- /dev/null +++ b/influxframework/core/doctype/doctype/boilerplate/controller._py @@ -0,0 +1,8 @@ +# Copyright (c) {year}, {app_publisher} and contributors +# For license information, please see license.txt + +# import influxframework +{base_class_import} + +class {classname}({base_class}): + {custom_controller} diff --git a/influxframework/core/doctype/doctype/boilerplate/controller.js b/influxframework/core/doctype/doctype/boilerplate/controller.js new file mode 100644 index 0000000..f337dd6 --- /dev/null +++ b/influxframework/core/doctype/doctype/boilerplate/controller.js @@ -0,0 +1,8 @@ +// Copyright (c) {year}, {app_publisher} and contributors +// For license information, please see license.txt + +influxframework.ui.form.on('{doctype}', {{ + // refresh: function(frm) {{ + + // }} +}}); diff --git a/influxframework/core/doctype/doctype/boilerplate/controller_list.html b/influxframework/core/doctype/doctype/boilerplate/controller_list.html new file mode 100644 index 0000000..1221a9e --- /dev/null +++ b/influxframework/core/doctype/doctype/boilerplate/controller_list.html @@ -0,0 +1,34 @@ +
    +
    +
    + {{%= list.get_avatar_and_id(doc) %}} + + + + {{%= doc.text %}} + + + {{% if(doc.check) {{ %}} + + + + {{% }} %}} + + + + {{%= doc.status %}} + +
    +
    + +
    + {{% var completed = doc.completed, title = __("Completed") %}} + {{% include "templates/form_grid/includes/progress.html" %}} +
    +
    diff --git a/influxframework/core/doctype/doctype/boilerplate/controller_list.js b/influxframework/core/doctype/doctype/boilerplate/controller_list.js new file mode 100644 index 0000000..9b9c00d --- /dev/null +++ b/influxframework/core/doctype/doctype/boilerplate/controller_list.js @@ -0,0 +1,5 @@ +/* eslint-disable */ +influxframework.listview_settings['{doctype}'] = {{ + // add_fields: ["status"], + // filters:[["status","=", "Open"]] +}}; diff --git a/influxframework/core/doctype/doctype/boilerplate/templates/controller.html b/influxframework/core/doctype/doctype/boilerplate/templates/controller.html new file mode 100644 index 0000000..412368d --- /dev/null +++ b/influxframework/core/doctype/doctype/boilerplate/templates/controller.html @@ -0,0 +1,7 @@ +{{% extends "templates/web.html" %}} + +{{% block page_content %}} +

    {{{{ title }}}}

    +{{% endblock %}} + + \ No newline at end of file diff --git a/influxframework/core/doctype/doctype/boilerplate/templates/controller_row.html b/influxframework/core/doctype/doctype/boilerplate/templates/controller_row.html new file mode 100644 index 0000000..66fe744 --- /dev/null +++ b/influxframework/core/doctype/doctype/boilerplate/templates/controller_row.html @@ -0,0 +1,4 @@ + + diff --git a/influxframework/core/doctype/doctype/boilerplate/test_controller._py b/influxframework/core/doctype/doctype/boilerplate/test_controller._py new file mode 100644 index 0000000..bb21454 --- /dev/null +++ b/influxframework/core/doctype/doctype/boilerplate/test_controller._py @@ -0,0 +1,9 @@ +# Copyright (c) {year}, {app_publisher} and Contributors +# See license.txt + +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class Test{classname}(InfluxFrameworkTestCase): + pass diff --git a/influxframework/core/doctype/doctype/doctype.js b/influxframework/core/doctype/doctype/doctype.js new file mode 100644 index 0000000..326f373 --- /dev/null +++ b/influxframework/core/doctype/doctype/doctype.js @@ -0,0 +1,195 @@ +// Copyright (c) 2015, InfluxFramework LLC +// MIT License. See license.txt + +influxframework.ui.form.on("DocType", { + refresh: function (frm) { + frm.set_query("role", "permissions", function (doc) { + if (doc.custom && influxframework.session.user != "Administrator") { + return { + query: "influxframework.core.doctype.role.role.role_query", + filters: [["Role", "name", "!=", "All"]], + }; + } + }); + + if (influxframework.session.user !== "Administrator" || !influxframework.boot.developer_mode) { + if (frm.is_new()) { + frm.set_value("custom", 1); + } + frm.toggle_enable("custom", 0); + frm.toggle_enable("is_virtual", 0); + frm.toggle_enable("beta", 0); + } + + if (!frm.is_new() && !frm.doc.istable) { + if (frm.doc.issingle) { + frm.add_custom_button(__("Go to {0}", [__(frm.doc.name)]), () => { + window.open(`/app/${influxframework.router.slug(frm.doc.name)}`); + }); + } else { + frm.add_custom_button(__("Go to {0} List", [__(frm.doc.name)]), () => { + window.open(`/app/${influxframework.router.slug(frm.doc.name)}`); + }); + } + } + + const customize_form_link = "Customize Form"; + if (!influxframework.boot.developer_mode && !frm.doc.custom) { + // make the document read-only + frm.set_read_only(); + frm.dashboard.add_comment( + __("DocTypes can not be modified, please use {0} instead", [customize_form_link]), + "blue", + true + ); + } else if (influxframework.boot.developer_mode) { + let msg = __( + "This site is running in developer mode. Any change made here will be updated in code." + ); + msg += "
    "; + msg += __("If you just want to customize for your site, use {0} instead.", [ + customize_form_link, + ]); + frm.dashboard.add_comment(msg, "yellow"); + } + + if (frm.is_new()) { + frm.events.set_default_permission(frm); + frm.set_value("default_view", "List"); + } else { + frm.toggle_enable("engine", 0); + } + + // set label for "In List View" for child tables + frm.get_docfield("fields", "in_list_view").label = frm.doc.istable + ? __("In Grid View") + : __("In List View"); + + frm.cscript.autoname(frm); + frm.cscript.set_naming_rule_description(frm); + frm.trigger("setup_default_views"); + }, + + istable: (frm) => { + if (frm.doc.istable && frm.is_new()) { + frm.set_value("autoname", "autoincrement"); + frm.set_value("allow_rename", 0); + frm.set_value("default_view", null); + } else if (!frm.doc.istable && !frm.is_new()) { + frm.events.set_default_permission(frm); + } + }, + + set_default_permission: (frm) => { + if (!(frm.doc.permissions && frm.doc.permissions.length)) { + frm.add_child("permissions", { role: "System Manager" }); + } + }, + + is_tree: (frm) => { + frm.trigger("setup_default_views"); + }, + + is_calendar_and_gantt: (frm) => { + frm.trigger("setup_default_views"); + }, + + setup_default_views: (frm) => { + influxframework.model.set_default_views_for_doctype(frm.doc.name, frm); + }, +}); + +influxframework.ui.form.on("DocField", { + form_render(frm, doctype, docname) { + // Render two select fields for Fetch From instead of Small Text for better UX + let field = frm.cur_grid.grid_form.fields_dict.fetch_from; + $(field.input_area).hide(); + + let $doctype_select = $(``); + let $wrapper = $('
    '); + $wrapper.append($doctype_select, $field_select); + field.$input_wrapper.append($wrapper); + $doctype_select.wrap('
    '); + $field_select.wrap('
    '); + + let row = influxframework.get_doc(doctype, docname); + let curr_value = { doctype: null, fieldname: null }; + if (row.fetch_from) { + let [doctype, fieldname] = row.fetch_from.split("."); + curr_value.doctype = doctype; + curr_value.fieldname = fieldname; + } + + let doctypes = frm.doc.fields + .filter((df) => df.fieldtype == "Link") + .filter((df) => df.options && df.fieldname != row.fieldname) + .map((df) => ({ + label: `${df.options} (${df.fieldname})`, + value: df.fieldname, + })); + $doctype_select.add_options([ + { label: __("Select DocType"), value: "", selected: true }, + ...doctypes, + ]); + + $doctype_select.on("change", () => { + row.fetch_from = ""; + frm.dirty(); + update_fieldname_options(); + }); + + function update_fieldname_options() { + $field_select.find("option").remove(); + + let link_fieldname = $doctype_select.val(); + if (!link_fieldname) return; + let link_field = frm.doc.fields.find((df) => df.fieldname === link_fieldname); + let link_doctype = link_field.options; + influxframework.model.with_doctype(link_doctype, () => { + let fields = influxframework.meta + .get_docfields(link_doctype, null, { + fieldtype: ["not in", influxframework.model.no_value_type], + }) + .map((df) => ({ + label: `${df.label} (${df.fieldtype})`, + value: df.fieldname, + })); + $field_select.add_options([ + { + label: __("Select Field"), + value: "", + selected: true, + disabled: true, + }, + ...fields, + ]); + + if (curr_value.fieldname) { + $field_select.val(curr_value.fieldname); + } + }); + } + + $field_select.on("change", () => { + let fetch_from = `${$doctype_select.val()}.${$field_select.val()}`; + row.fetch_from = fetch_from; + frm.dirty(); + }); + + if (curr_value.doctype) { + $doctype_select.val(curr_value.doctype); + update_fieldname_options(); + } + }, + + fieldtype: function (frm) { + frm.trigger("max_attachments"); + }, + + fields_add: (frm) => { + frm.trigger("setup_default_views"); + }, +}); + +extend_cscript(cur_frm.cscript, new influxframework.model.DocTypeController({ frm: cur_frm })); diff --git a/influxframework/core/doctype/doctype/doctype.json b/influxframework/core/doctype/doctype/doctype.json new file mode 100644 index 0000000..6258241 --- /dev/null +++ b/influxframework/core/doctype/doctype/doctype.json @@ -0,0 +1,749 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "Prompt", + "creation": "2013-02-18 13:36:19", + "description": "DocType is a Table / Form in the application.", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "sb0", + "module", + "is_submittable", + "istable", + "issingle", + "is_tree", + "is_calendar_and_gantt", + "editable_grid", + "quick_entry", + "cb01", + "track_changes", + "track_seen", + "track_views", + "custom", + "beta", + "is_virtual", + "fields_section_break", + "fields", + "sb1", + "naming_rule", + "autoname", + "name_case", + "allow_rename", + "column_break_15", + "description", + "documentation", + "form_settings_section", + "image_field", + "timeline_field", + "nsm_parent_field", + "max_attachments", + "column_break_23", + "hide_toolbar", + "allow_copy", + "allow_import", + "allow_events_in_timeline", + "allow_auto_repeat", + "make_attachments_public", + "view_settings", + "title_field", + "show_title_field_in_link", + "translated_doctype", + "search_fields", + "default_print_format", + "sort_field", + "sort_order", + "default_view", + "force_re_route_to_default_view", + "column_break_29", + "document_type", + "icon", + "color", + "show_preview_popup", + "show_name_in_global_search", + "email_settings_sb", + "default_email_template", + "column_break_51", + "email_append_to", + "sender_field", + "subject_field", + "sb2", + "permissions", + "restrict_to_domain", + "read_only", + "in_create", + "actions_section", + "actions", + "links_section", + "links", + "document_states_section", + "states", + "web_view", + "has_web_view", + "allow_guest_to_view", + "index_web_pages_for_search", + "route", + "is_published_field", + "website_search_field", + "advanced", + "engine", + "migration_hash" + ], + "fields": [ + { + "fieldname": "sb0", + "fieldtype": "Section Break", + "oldfieldtype": "Section Break" + }, + { + "fieldname": "module", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Module", + "oldfieldname": "module", + "oldfieldtype": "Link", + "options": "Module Def", + "reqd": 1, + "search_index": 1 + }, + { + "default": "0", + "depends_on": "eval:!doc.istable", + "description": "Once submitted, submittable documents cannot be changed. They can only be Cancelled and Amended.", + "fieldname": "is_submittable", + "fieldtype": "Check", + "label": "Is Submittable" + }, + { + "default": "0", + "description": "Child Tables are shown as a Grid in other DocTypes", + "fieldname": "istable", + "fieldtype": "Check", + "in_standard_filter": 1, + "label": "Is Child Table", + "oldfieldname": "istable", + "oldfieldtype": "Check" + }, + { + "default": "0", + "depends_on": "eval:!doc.istable", + "description": "Single Types have only one record no tables associated. Values are stored in tabSingles", + "fieldname": "issingle", + "fieldtype": "Check", + "in_standard_filter": 1, + "label": "Is Single", + "oldfieldname": "issingle", + "oldfieldtype": "Check", + "set_only_once": 1 + }, + { + "default": "1", + "depends_on": "istable", + "fieldname": "editable_grid", + "fieldtype": "Check", + "label": "Editable Grid" + }, + { + "default": "0", + "depends_on": "eval:!doc.istable && !doc.issingle", + "description": "Open a dialog with mandatory fields to create a new record quickly", + "fieldname": "quick_entry", + "fieldtype": "Check", + "label": "Quick Entry" + }, + { + "fieldname": "cb01", + "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "eval:!doc.istable", + "description": "If enabled, changes to the document are tracked and shown in timeline", + "fieldname": "track_changes", + "fieldtype": "Check", + "label": "Track Changes" + }, + { + "default": "0", + "depends_on": "eval:!doc.istable", + "description": "If enabled, the document is marked as seen, the first time a user opens it", + "fieldname": "track_seen", + "fieldtype": "Check", + "label": "Track Seen" + }, + { + "default": "0", + "depends_on": "eval:!doc.istable", + "description": "If enabled, document views are tracked, this can happen multiple times", + "fieldname": "track_views", + "fieldtype": "Check", + "label": "Track Views" + }, + { + "default": "0", + "fieldname": "custom", + "fieldtype": "Check", + "label": "Custom?" + }, + { + "default": "0", + "fieldname": "beta", + "fieldtype": "Check", + "label": "Beta" + }, + { + "fieldname": "fields_section_break", + "fieldtype": "Section Break", + "label": "Fields", + "oldfieldtype": "Section Break" + }, + { + "fieldname": "fields", + "fieldtype": "Table", + "label": "Fields", + "oldfieldname": "fields", + "oldfieldtype": "Table", + "options": "DocField" + }, + { + "fieldname": "sb1", + "fieldtype": "Section Break", + "label": "Naming" + }, + { + "description": "Naming Options:\n
    1. field:[fieldname] - By Field
    2. autoincrement - Uses Databases' Auto Increment feature
    3. naming_series: - By Naming Series (field called naming_series must be present)
    4. Prompt - Prompt user for a name
    5. [series] - Series by prefix (separated by a dot); for example PRE.#####
    6. \n
    7. format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
    ", + "fieldname": "autoname", + "fieldtype": "Data", + "label": "Auto Name", + "oldfieldname": "autoname", + "oldfieldtype": "Data" + }, + { + "depends_on": "eval:doc.naming_rule !== \"Autoincrement\"", + "fieldname": "name_case", + "fieldtype": "Select", + "label": "Name Case", + "oldfieldname": "name_case", + "oldfieldtype": "Select", + "options": "\nTitle Case\nUPPER CASE" + }, + { + "fieldname": "column_break_15", + "fieldtype": "Column Break" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Text" + }, + { + "collapsible": 1, + "fieldname": "form_settings_section", + "fieldtype": "Section Break", + "label": "Form Settings" + }, + { + "description": "Must be of type \"Attach Image\"", + "fieldname": "image_field", + "fieldtype": "Data", + "label": "Image Field" + }, + { + "depends_on": "eval:!doc.istable", + "description": "Comments and Communications will be associated with this linked document", + "fieldname": "timeline_field", + "fieldtype": "Data", + "label": "Timeline Field" + }, + { + "fieldname": "max_attachments", + "fieldtype": "Int", + "label": "Max Attachments", + "oldfieldname": "max_attachments", + "oldfieldtype": "Int" + }, + { + "fieldname": "column_break_23", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "hide_toolbar", + "fieldtype": "Check", + "label": "Hide Sidebar and Menu", + "oldfieldname": "hide_toolbar", + "oldfieldtype": "Check" + }, + { + "default": "0", + "fieldname": "allow_copy", + "fieldtype": "Check", + "label": "Hide Copy", + "oldfieldname": "allow_copy", + "oldfieldtype": "Check" + }, + { + "default": "1", + "depends_on": "eval:doc.naming_rule !== \"Autoincrement\"", + "fieldname": "allow_rename", + "fieldtype": "Check", + "label": "Allow Rename", + "oldfieldname": "allow_rename", + "oldfieldtype": "Check" + }, + { + "default": "0", + "fieldname": "allow_import", + "fieldtype": "Check", + "label": "Allow Import (via Data Import Tool)" + }, + { + "default": "0", + "fieldname": "allow_events_in_timeline", + "fieldtype": "Check", + "label": "Allow events in timeline" + }, + { + "default": "0", + "fieldname": "allow_auto_repeat", + "fieldtype": "Check", + "label": "Allow Auto Repeat" + }, + { + "collapsible": 1, + "fieldname": "view_settings", + "fieldtype": "Section Break", + "label": "View Settings" + }, + { + "depends_on": "eval:!doc.istable", + "fieldname": "title_field", + "fieldtype": "Data", + "label": "Title Field", + "mandatory_depends_on": "eval:doc.show_title_field_in_link" + }, + { + "depends_on": "eval:!doc.istable", + "fieldname": "search_fields", + "fieldtype": "Data", + "label": "Search Fields", + "oldfieldname": "search_fields", + "oldfieldtype": "Data" + }, + { + "fieldname": "default_print_format", + "fieldtype": "Data", + "label": "Default Print Format" + }, + { + "default": "modified", + "depends_on": "eval:!doc.istable", + "fieldname": "sort_field", + "fieldtype": "Data", + "label": "Default Sort Field" + }, + { + "default": "DESC", + "depends_on": "eval:!doc.istable", + "fieldname": "sort_order", + "fieldtype": "Select", + "label": "Default Sort Order", + "options": "ASC\nDESC" + }, + { + "fieldname": "column_break_29", + "fieldtype": "Column Break" + }, + { + "fieldname": "document_type", + "fieldtype": "Select", + "label": "Show in Module Section", + "oldfieldname": "document_type", + "oldfieldtype": "Select", + "options": "\nDocument\nSetup\nSystem\nOther" + }, + { + "fieldname": "icon", + "fieldtype": "Data", + "label": "Icon" + }, + { + "fieldname": "color", + "fieldtype": "Data", + "label": "Color" + }, + { + "default": "0", + "fieldname": "show_preview_popup", + "fieldtype": "Check", + "label": "Show Preview Popup" + }, + { + "default": "0", + "fieldname": "show_name_in_global_search", + "fieldtype": "Check", + "label": "Make \"name\" searchable in Global Search" + }, + { + "depends_on": "eval:!doc.istable", + "fieldname": "sb2", + "fieldtype": "Section Break", + "label": "Permission Rules" + }, + { + "fieldname": "permissions", + "fieldtype": "Table", + "label": "Permissions", + "oldfieldname": "permissions", + "oldfieldtype": "Table", + "options": "DocPerm" + }, + { + "fieldname": "restrict_to_domain", + "fieldtype": "Link", + "label": "Restrict To Domain", + "options": "Domain" + }, + { + "default": "0", + "fieldname": "read_only", + "fieldtype": "Check", + "label": "User Cannot Search", + "oldfieldname": "read_only", + "oldfieldtype": "Check" + }, + { + "default": "0", + "fieldname": "in_create", + "fieldtype": "Check", + "label": "User Cannot Create", + "oldfieldname": "in_create", + "oldfieldtype": "Check" + }, + { + "depends_on": "eval:doc.custom===0", + "fieldname": "web_view", + "fieldtype": "Section Break", + "label": "Web View" + }, + { + "default": "0", + "fieldname": "has_web_view", + "fieldtype": "Check", + "label": "Has Web View" + }, + { + "default": "0", + "depends_on": "has_web_view", + "fieldname": "allow_guest_to_view", + "fieldtype": "Check", + "label": "Allow Guest to View" + }, + { + "depends_on": "eval:!doc.istable", + "fieldname": "route", + "fieldtype": "Data", + "label": "Route" + }, + { + "depends_on": "has_web_view", + "fieldname": "is_published_field", + "fieldtype": "Data", + "label": "Is Published Field" + }, + { + "collapsible": 1, + "fieldname": "advanced", + "fieldtype": "Section Break", + "hidden": 1, + "label": "Advanced" + }, + { + "default": "InnoDB", + "depends_on": "eval:!doc.issingle", + "fieldname": "engine", + "fieldtype": "Select", + "label": "Database Engine", + "options": "InnoDB\nMyISAM" + }, + { + "default": "0", + "description": "Tree structures are implemented using Nested Set", + "fieldname": "is_tree", + "fieldtype": "Check", + "label": "Is Tree" + }, + { + "depends_on": "is_tree", + "fieldname": "nsm_parent_field", + "fieldtype": "Data", + "label": "Parent Field (Tree)" + }, + { + "description": "URL for documentation or help", + "fieldname": "documentation", + "fieldtype": "Data", + "label": "Documentation Link" + }, + { + "collapsible": 1, + "collapsible_depends_on": "actions", + "fieldname": "actions_section", + "fieldtype": "Section Break", + "label": "Actions" + }, + { + "fieldname": "actions", + "fieldtype": "Table", + "label": "Actions", + "options": "DocType Action" + }, + { + "collapsible": 1, + "collapsible_depends_on": "links", + "fieldname": "links_section", + "fieldtype": "Section Break", + "label": "Linked Documents" + }, + { + "fieldname": "links", + "fieldtype": "Table", + "label": "Links", + "options": "DocType Link" + }, + { + "depends_on": "email_append_to", + "fieldname": "subject_field", + "fieldtype": "Data", + "label": "Subject Field" + }, + { + "depends_on": "email_append_to", + "fieldname": "sender_field", + "fieldtype": "Data", + "label": "Sender Field", + "mandatory_depends_on": "email_append_to" + }, + { + "default": "0", + "fieldname": "email_append_to", + "fieldtype": "Check", + "label": "Allow document creation via Email" + }, + { + "collapsible": 1, + "fieldname": "email_settings_sb", + "fieldtype": "Section Break", + "label": "Email Settings" + }, + { + "default": "1", + "fieldname": "index_web_pages_for_search", + "fieldtype": "Check", + "label": "Index Web Pages for Search" + }, + { + "default": "0", + "fieldname": "is_virtual", + "fieldtype": "Check", + "label": "Is Virtual" + }, + { + "fieldname": "default_email_template", + "fieldtype": "Link", + "label": "Default Email Template", + "options": "Email Template" + }, + { + "fieldname": "column_break_51", + "fieldtype": "Column Break" + }, + { + "depends_on": "has_web_view", + "fieldname": "website_search_field", + "fieldtype": "Data", + "label": "Website Search Field" + }, + { + "fieldname": "naming_rule", + "fieldtype": "Select", + "label": "Naming Rule", + "length": 40, + "options": "\nSet by user\nAutoincrement\nBy fieldname\nBy \"Naming Series\" field\nExpression\nExpression (old style)\nRandom\nBy script" + }, + { + "fieldname": "migration_hash", + "fieldtype": "Data", + "hidden": 1 + }, + { + "fieldname": "states", + "fieldtype": "Table", + "label": "States", + "options": "DocType State" + }, + { + "collapsible": 1, + "fieldname": "document_states_section", + "fieldtype": "Section Break", + "label": "Document States" + }, + { + "default": "0", + "fieldname": "show_title_field_in_link", + "fieldtype": "Check", + "label": "Show Title in Link Fields" + }, + { + "default": "0", + "fieldname": "translated_doctype", + "fieldtype": "Check", + "label": "Translate Link Fields" + }, + { + "default": "0", + "fieldname": "make_attachments_public", + "fieldtype": "Check", + "label": "Make Attachments Public by Default" + }, + { + "fieldname": "default_view", + "fieldtype": "Select", + "label": "Default View" + }, + { + "default": "0", + "fieldname": "force_re_route_to_default_view", + "fieldtype": "Check", + "label": "Force Re-route to Default View" + }, + { + "default": "0", + "description": "Enables Calendar and Gantt views.", + "fieldname": "is_calendar_and_gantt", + "fieldtype": "Check", + "label": "Is Calendar and Gantt" + } + ], + "icon": "fa fa-bolt", + "idx": 6, + "index_web_pages_for_search": 1, + "links": [ + { + "group": "Views", + "link_doctype": "Report", + "link_fieldname": "ref_doctype" + }, + { + "group": "Workflow", + "link_doctype": "Workflow", + "link_fieldname": "document_type" + }, + { + "group": "Workflow", + "link_doctype": "Notification", + "link_fieldname": "document_type" + }, + { + "group": "Customization", + "link_doctype": "Custom Field", + "link_fieldname": "dt" + }, + { + "group": "Customization", + "link_doctype": "Client Script", + "link_fieldname": "dt" + }, + { + "group": "Customization", + "link_doctype": "Server Script", + "link_fieldname": "reference_doctype" + }, + { + "group": "Workflow", + "link_doctype": "Webhook", + "link_fieldname": "webhook_doctype" + }, + { + "group": "Views", + "link_doctype": "Print Format", + "link_fieldname": "doc_type" + }, + { + "group": "Views", + "link_doctype": "Web Form", + "link_fieldname": "doc_type" + }, + { + "group": "Views", + "link_doctype": "Calendar View", + "link_fieldname": "reference_doctype" + }, + { + "group": "Views", + "link_doctype": "Kanban Board", + "link_fieldname": "reference_doctype" + }, + { + "group": "Workflow", + "link_doctype": "Onboarding Step", + "link_fieldname": "reference_document" + }, + { + "group": "Rules", + "link_doctype": "Auto Repeat", + "link_fieldname": "reference_doctype" + }, + { + "group": "Rules", + "link_doctype": "Assignment Rule", + "link_fieldname": "document_type" + }, + { + "group": "Rules", + "link_doctype": "Energy Point Rule", + "link_fieldname": "reference_doctype" + } + ], + "modified": "2022-10-12 14:13:27.315351", + "modified_by": "Administrator", + "module": "Core", + "name": "DocType", + "naming_rule": "Set by user", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, + "write": 1 + } + ], + "route": "doctype", + "search_fields": "module", + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1, + "translated_doctype": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/doctype/doctype.py b/influxframework/core/doctype/doctype/doctype.py new file mode 100644 index 0000000..c9e4a1b --- /dev/null +++ b/influxframework/core/doctype/doctype/doctype.py @@ -0,0 +1,1741 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import copy +import json +import os + +# imports - standard imports +import re +import shutil +from typing import TYPE_CHECKING, Union + +# imports - module imports +import influxframework +from influxframework import _ +from influxframework.cache_manager import clear_controller_cache, clear_user_cache +from influxframework.custom.doctype.custom_field.custom_field import create_custom_field +from influxframework.custom.doctype.property_setter.property_setter import make_property_setter +from influxframework.database.schema import validate_column_length, validate_column_name +from influxframework.desk.notifications import delete_notification_count_for, get_filters_for +from influxframework.desk.utils import validate_route_conflict +from influxframework.model import ( + child_table_fields, + data_field_options, + default_fields, + no_value_fields, + table_fields, +) +from influxframework.model.base_document import get_controller +from influxframework.model.docfield import supports_translation +from influxframework.model.document import Document +from influxframework.model.meta import Meta +from influxframework.modules import get_doc_path, make_boilerplate +from influxframework.modules.import_file import get_file_path +from influxframework.query_builder.functions import Concat +from influxframework.utils import cint +from influxframework.website.utils import clear_cache + +if TYPE_CHECKING: + from influxframework.custom.doctype.customize_form.customize_form import CustomizeForm + +DEPENDS_ON_PATTERN = re.compile(r'[\w\.:_]+\s*={1}\s*[\w\.@\'"]+') +ILLEGAL_FIELDNAME_PATTERN = re.compile("""['",./%@()<>{}]""") +WHITESPACE_PADDING_PATTERN = re.compile(r"^[ \t\n\r]+|[ \t\n\r]+$", flags=re.ASCII) +START_WITH_LETTERS_PATTERN = re.compile(r"^(?![\W])[^\d_\s][\w -]+$", flags=re.ASCII) +FIELD_PATTERN = re.compile("{(.*?)}", flags=re.UNICODE) + + +class InvalidFieldNameError(influxframework.ValidationError): + pass + + +class UniqueFieldnameError(influxframework.ValidationError): + pass + + +class IllegalMandatoryError(influxframework.ValidationError): + pass + + +class DoctypeLinkError(influxframework.ValidationError): + pass + + +class WrongOptionsDoctypeLinkError(influxframework.ValidationError): + pass + + +class HiddenAndMandatoryWithoutDefaultError(influxframework.ValidationError): + pass + + +class NonUniqueError(influxframework.ValidationError): + pass + + +class CannotIndexedError(influxframework.ValidationError): + pass + + +class CannotCreateStandardDoctypeError(influxframework.ValidationError): + pass + + +form_grid_templates = {"fields": "templates/form_grid/fields.html"} + + +class DocType(Document): + def get_feed(self): + return self.name + + def validate(self): + """Validate DocType before saving. + + - Check if developer mode is set. + - Validate series + - Check fieldnames (duplication etc) + - Clear permission table for child tables + - Add `amended_from` and `amended_by` if Amendable + - Add custom field `auto_repeat` if Repeatable + - Check if links point to valid fieldnames""" + + self.check_developer_mode() + + self.validate_name() + + self.set_defaults_for_single_and_table() + self.set_defaults_for_autoincremented() + self.scrub_field_names() + self.set_default_in_list_view() + self.set_default_translatable() + validate_series(self) + self.set("can_change_name_type", validate_autoincrement_autoname(self)) + self.validate_document_type() + validate_fields(self) + + if not self.istable: + validate_permissions(self) + + self.make_amendable() + self.make_repeatable() + self.validate_nestedset() + self.validate_child_table() + self.validate_website() + self.ensure_minimum_max_attachment_limit() + validate_links_table_fieldnames(self) + + if not self.is_new(): + self.before_update = influxframework.get_doc("DocType", self.name) + self.setup_fields_to_fetch() + self.validate_field_name_conflicts() + + check_email_append_to(self) + + if self.default_print_format and not self.custom: + influxframework.throw(_("Standard DocType cannot have default print format, use Customize Form")) + + def validate_field_name_conflicts(self): + """Check if field names dont conflict with controller properties and methods""" + core_doctypes = [ + "Custom DocPerm", + "DocPerm", + "Custom Field", + "Customize Form Field", + "DocField", + ] + + if self.name in core_doctypes: + return + + try: + controller = get_controller(self.name) + except ImportError: + controller = Document + + available_objects = {x for x in dir(controller) if isinstance(x, str)} + property_set = { + x for x in available_objects if isinstance(getattr(controller, x, None), property) + } + method_set = { + x for x in available_objects if x not in property_set and callable(getattr(controller, x, None)) + } + + for docfield in self.get("fields") or []: + if docfield.fieldtype in no_value_fields: + continue + + conflict_type = None + field = docfield.fieldname + field_label = docfield.label or docfield.fieldname + + if docfield.fieldname in method_set: + conflict_type = "controller method" + if docfield.fieldname in property_set and not docfield.is_virtual: + conflict_type = "class property" + + if conflict_type: + influxframework.throw( + _("Fieldname '{0}' conflicting with a {1} of the name {2} in {3}").format( + field_label, conflict_type, field, self.name + ) + ) + + def set_defaults_for_single_and_table(self): + if self.issingle: + self.allow_import = 0 + self.is_submittable = 0 + self.istable = 0 + + elif self.istable: + self.allow_import = 0 + self.permissions = [] + + def set_defaults_for_autoincremented(self): + if self.autoname and self.autoname == "autoincrement": + self.allow_rename = 0 + + def set_default_in_list_view(self): + """Set default in-list-view for first 4 mandatory fields""" + if not [d.fieldname for d in self.fields if d.in_list_view]: + cnt = 0 + for d in self.fields: + if d.reqd and not d.hidden and not d.fieldtype in table_fields: + d.in_list_view = 1 + cnt += 1 + if cnt == 4: + break + + def set_default_translatable(self): + """Ensure that non-translatable never will be translatable""" + for d in self.fields: + if d.translatable and not supports_translation(d.fieldtype): + d.translatable = 0 + + def check_developer_mode(self): + """Throw exception if not developer mode or via patch""" + if influxframework.flags.in_patch or influxframework.flags.in_test: + return + + if not influxframework.conf.get("developer_mode") and not self.custom: + influxframework.throw( + _("Not in Developer Mode! Set in site_config.json or make 'Custom' DocType."), + CannotCreateStandardDoctypeError, + ) + + if self.is_virtual and self.custom: + influxframework.throw( + _("Not allowed to create custom Virtual DocType."), CannotCreateStandardDoctypeError + ) + + if influxframework.conf.get("developer_mode"): + self.owner = "Administrator" + self.modified_by = "Administrator" + + def setup_fields_to_fetch(self): + """Setup query to update values for newly set fetch values""" + try: + old_meta = influxframework.get_meta(influxframework.get_doc("DocType", self.name), cached=False) + old_fields_to_fetch = [df.fieldname for df in old_meta.get_fields_to_fetch()] + except influxframework.DoesNotExistError: + old_fields_to_fetch = [] + + new_meta = influxframework.get_meta(self, cached=False) + + self.flags.update_fields_to_fetch_queries = [] + + if set(old_fields_to_fetch) != {df.fieldname for df in new_meta.get_fields_to_fetch()}: + for df in new_meta.get_fields_to_fetch(): + if df.fieldname not in old_fields_to_fetch: + link_fieldname, source_fieldname = df.fetch_from.split(".", 1) + link_df = new_meta.get_field(link_fieldname) + + if influxframework.db.db_type == "postgres": + update_query = """ + UPDATE `tab{doctype}` + SET `{fieldname}` = source.`{source_fieldname}` + FROM `tab{link_doctype}` as source + WHERE `{link_fieldname}` = source.name + AND ifnull(`{fieldname}`, '')='' + """ + else: + update_query = """ + UPDATE `tab{doctype}` as target + INNER JOIN `tab{link_doctype}` as source + ON `target`.`{link_fieldname}` = `source`.`name` + SET `target`.`{fieldname}` = `source`.`{source_fieldname}` + WHERE ifnull(`target`.`{fieldname}`, '')="" + """ + + self.flags.update_fields_to_fetch_queries.append( + update_query.format( + link_doctype=link_df.options, + source_fieldname=source_fieldname, + doctype=self.name, + fieldname=df.fieldname, + link_fieldname=link_fieldname, + ) + ) + + def update_fields_to_fetch(self): + """Update fetch values based on queries setup""" + if self.flags.update_fields_to_fetch_queries and not self.issingle: + for query in self.flags.update_fields_to_fetch_queries: + influxframework.db.sql(query) + + def validate_document_type(self): + if self.document_type == "Transaction": + self.document_type = "Document" + if self.document_type == "Master": + self.document_type = "Setup" + + def validate_website(self): + """Ensure that website generator has field 'route'""" + if self.route: + self.route = self.route.strip("/") + + if self.has_web_view: + # route field must be present + if not "route" in [d.fieldname for d in self.fields]: + influxframework.throw(_('Field "route" is mandatory for Web Views'), title="Missing Field") + + # clear website cache + clear_cache() + + def ensure_minimum_max_attachment_limit(self): + """Ensure that max_attachments is *at least* bigger than number of attach fields.""" + from influxframework.model import attachment_fieldtypes + + if not self.max_attachments: + return + + total_attach_fields = len([d for d in self.fields if d.fieldtype in attachment_fieldtypes]) + if total_attach_fields > self.max_attachments: + self.max_attachments = total_attach_fields + field_label = influxframework.bold(self.meta.get_field("max_attachments").label) + influxframework.msgprint( + _("Number of attachment fields are more than {}, limit updated to {}.").format( + field_label, total_attach_fields + ), + title=_("Insufficient attachment limit"), + alert=True, + ) + + def change_modified_of_parent(self): + """Change the timestamp of parent DocType if the current one is a child to clear caches.""" + if influxframework.flags.in_import: + return + parent_list = influxframework.get_all( + "DocField", "parent", dict(fieldtype=["in", influxframework.model.table_fields], options=self.name) + ) + for p in parent_list: + influxframework.db.update("DocType", p.parent, {}, for_update=False) + + def scrub_field_names(self): + """Sluggify fieldnames if not set from Label.""" + restricted = ( + "name", + "parent", + "creation", + "modified", + "modified_by", + "parentfield", + "parenttype", + "file_list", + "flags", + "docstatus", + ) + for d in self.get("fields"): + if d.fieldtype: + if not getattr(d, "fieldname", None): + if d.label: + d.fieldname = d.label.strip().lower().replace(" ", "_").strip("?") + if d.fieldname in restricted: + d.fieldname = d.fieldname + "1" + if d.fieldtype == "Section Break": + d.fieldname = d.fieldname + "_section" + elif d.fieldtype == "Column Break": + d.fieldname = d.fieldname + "_column" + elif d.fieldtype == "Tab Break": + d.fieldname = d.fieldname + "_tab" + else: + d.fieldname = d.fieldtype.lower().replace(" ", "_") + "_" + str(d.idx) + else: + if d.fieldname in restricted: + influxframework.throw(_("Fieldname {0} is restricted").format(d.fieldname), InvalidFieldNameError) + d.fieldname = ILLEGAL_FIELDNAME_PATTERN.sub("", d.fieldname) + + # fieldnames should be lowercase + d.fieldname = d.fieldname.lower() + + # unique is automatically an index + if d.unique: + d.search_index = 0 + + def on_update(self): + """Update database schema, make controller templates if `custom` is not set and clear cache.""" + + if self.get("can_change_name_type"): + self.setup_autoincrement_and_sequence() + + try: + influxframework.db.updatedb(self.name, Meta(self)) + except Exception as e: + print(f"\n\nThere was an issue while migrating the DocType: {self.name}\n") + raise e + + self.change_modified_of_parent() + make_module_and_roles(self) + + self.update_fields_to_fetch() + + allow_doctype_export = ( + not self.custom + and not influxframework.flags.in_import + and (influxframework.conf.developer_mode or influxframework.flags.allow_doctype_export) + ) + if allow_doctype_export: + self.export_doc() + self.make_controller_template() + self.set_base_class_for_controller() + + # update index + if not self.custom: + self.run_module_method("on_doctype_update") + if self.flags.in_insert: + self.run_module_method("after_doctype_insert") + + delete_notification_count_for(doctype=self.name) + influxframework.clear_cache(doctype=self.name) + + # clear user cache so that on the next reload this doctype is included in boot + clear_user_cache(influxframework.session.user) + + if not influxframework.flags.in_install and hasattr(self, "before_update"): + self.sync_global_search() + + clear_linked_doctype_cache() + + def setup_autoincrement_and_sequence(self): + """Changes name type and makes sequence on change (if required)""" + + name_type = f"varchar({influxframework.db.VARCHAR_LEN})" + + if self.autoname == "autoincrement": + name_type = "bigint" + influxframework.db.create_sequence(self.name, check_not_exists=True, cache=influxframework.db.SEQUENCE_CACHE) + + change_name_column_type(self.name, name_type) + + def sync_global_search(self): + """If global search settings are changed, rebuild search properties for this table""" + global_search_fields_before_update = [ + d.fieldname for d in self.before_update.fields if d.in_global_search + ] + if self.before_update.show_name_in_global_search: + global_search_fields_before_update.append("name") + + global_search_fields_after_update = [d.fieldname for d in self.fields if d.in_global_search] + if self.show_name_in_global_search: + global_search_fields_after_update.append("name") + + if set(global_search_fields_before_update) != set(global_search_fields_after_update): + now = (not influxframework.request) or influxframework.flags.in_test or influxframework.flags.in_install + influxframework.enqueue("influxframework.utils.global_search.rebuild_for_doctype", now=now, doctype=self.name) + + def set_base_class_for_controller(self): + """If DocType.has_web_view has been changed, updates the controller class and import + from `WebsiteGenertor` to `Document` or viceversa""" + + if not self.has_value_changed("has_web_view"): + return + + despaced_name = self.name.replace(" ", "_") + scrubbed_name = influxframework.scrub(self.name) + scrubbed_module = influxframework.scrub(self.module) + controller_path = influxframework.get_module_path( + scrubbed_module, "doctype", scrubbed_name, f"{scrubbed_name}.py" + ) + + document_cls_tag = f"class {despaced_name}(Document)" + document_import_tag = "from influxframework.model.document import Document" + website_generator_cls_tag = f"class {despaced_name}(WebsiteGenerator)" + website_generator_import_tag = "from influxframework.website.website_generator import WebsiteGenerator" + + with open(controller_path) as f: + code = f.read() + updated_code = code + + is_website_generator_class = all( + [website_generator_cls_tag in code, website_generator_import_tag in code] + ) + + if self.has_web_view and not is_website_generator_class: + updated_code = updated_code.replace(document_import_tag, website_generator_import_tag).replace( + document_cls_tag, website_generator_cls_tag + ) + elif not self.has_web_view and is_website_generator_class: + updated_code = updated_code.replace(website_generator_import_tag, document_import_tag).replace( + website_generator_cls_tag, document_cls_tag + ) + + if updated_code != code: + with open(controller_path, "w") as f: + f.write(updated_code) + + def run_module_method(self, method): + from influxframework.modules import load_doctype_module + + module = load_doctype_module(self.name, self.module) + if hasattr(module, method): + getattr(module, method)() + + def before_rename(self, old, new, merge=False): + """Throw exception if merge. DocTypes cannot be merged.""" + if not self.custom and influxframework.session.user != "Administrator": + influxframework.throw(_("DocType can only be renamed by Administrator")) + + self.check_developer_mode() + self.validate_name(new) + + if merge: + influxframework.throw(_("DocType can not be merged")) + + def after_rename(self, old, new, merge=False): + """Change table name using `RENAME TABLE` if table exists. Or update + `doctype` property for Single type.""" + + if self.issingle: + influxframework.db.sql("""update tabSingles set doctype=%s where doctype=%s""", (new, old)) + influxframework.db.sql( + """update tabSingles set value=%s + where doctype=%s and field='name' and value = %s""", + (new, new, old), + ) + else: + influxframework.db.rename_table(old, new) + influxframework.db.commit() + + # Do not rename and move files and folders for custom doctype + if not self.custom: + if not influxframework.flags.in_patch: + self.rename_files_and_folders(old, new) + + clear_controller_cache(old) + + def after_delete(self): + if not self.custom: + clear_controller_cache(self.name) + + def rename_files_and_folders(self, old, new): + # move files + new_path = get_doc_path(self.module, "doctype", new) + old_path = get_doc_path(self.module, "doctype", old) + shutil.move(old_path, new_path) + + # rename files + for fname in os.listdir(new_path): + if influxframework.scrub(old) in fname: + old_file_name = os.path.join(new_path, fname) + new_file_name = os.path.join(new_path, fname.replace(influxframework.scrub(old), influxframework.scrub(new))) + shutil.move(old_file_name, new_file_name) + + self.rename_inside_controller(new, old, new_path) + influxframework.msgprint(_("Renamed files and replaced code in controllers, please check!")) + + def rename_inside_controller(self, new, old, new_path): + for fname in ("{}.js", "{}.py", "{}_list.js", "{}_calendar.js", "test_{}.py", "test_{}.js"): + fname = os.path.join(new_path, fname.format(influxframework.scrub(new))) + if os.path.exists(fname): + with open(fname) as f: + code = f.read() + with open(fname, "w") as f: + if fname.endswith(".js"): + file_content = code.replace(old, new) # replace str with full str (js controllers) + + elif fname.endswith(".py"): + file_content = code.replace( + influxframework.scrub(old), influxframework.scrub(new) + ) # replace str with _ (py imports) + file_content = file_content.replace( + old.replace(" ", ""), new.replace(" ", "") + ) # replace str (py controllers) + + f.write(file_content) + + # updating json file with new name + doctype_json_path = os.path.join(new_path, f"{influxframework.scrub(new)}.json") + current_data = influxframework.get_file_json(doctype_json_path) + current_data["name"] = new + + with open(doctype_json_path, "w") as f: + json.dump(current_data, f, indent=1) + + def before_reload(self): + """Preserve naming series changes in Property Setter.""" + if not (self.issingle and self.istable): + self.preserve_naming_series_options_in_property_setter() + + def preserve_naming_series_options_in_property_setter(self): + """Preserve naming_series as property setter if it does not exist""" + naming_series = self.get("fields", {"fieldname": "naming_series"}) + + if not naming_series: + return + + # check if atleast 1 record exists + if not ( + influxframework.db.table_exists(self.name) + and influxframework.get_all(self.name, fields=["name"], limit=1, as_list=True) + ): + return + + existing_property_setter = influxframework.db.get_value( + "Property Setter", {"doc_type": self.name, "property": "options", "field_name": "naming_series"} + ) + + if not existing_property_setter: + make_property_setter( + self.name, + "naming_series", + "options", + naming_series[0].options, + "Text", + validate_fields_for_doctype=False, + ) + if naming_series[0].default: + make_property_setter( + self.name, + "naming_series", + "default", + naming_series[0].default, + "Text", + validate_fields_for_doctype=False, + ) + + def before_export(self, docdict): + # remove null and empty fields + def remove_null_fields(o): + to_remove = [] + for attr, value in o.items(): + if isinstance(value, list): + for v in value: + remove_null_fields(v) + elif not value: + to_remove.append(attr) + + for attr in to_remove: + del o[attr] + + remove_null_fields(docdict) + + # retain order of 'fields' table and change order in 'field_order' + docdict["field_order"] = [f.fieldname for f in self.fields] + + if self.custom: + return + + path = get_file_path(self.module, "DocType", self.name) + if os.path.exists(path): + try: + with open(path) as txtfile: + olddoc = json.loads(txtfile.read()) + + old_field_names = [f["fieldname"] for f in olddoc.get("fields", [])] + if old_field_names: + new_field_dicts = [] + remaining_field_names = [f.fieldname for f in self.fields] + + for fieldname in old_field_names: + field_dict = [f for f in docdict["fields"] if f["fieldname"] == fieldname] + if field_dict: + new_field_dicts.append(field_dict[0]) + if fieldname in remaining_field_names: + remaining_field_names.remove(fieldname) + + for fieldname in remaining_field_names: + field_dict = [f for f in docdict["fields"] if f["fieldname"] == fieldname] + new_field_dicts.append(field_dict[0]) + + docdict["fields"] = new_field_dicts + except ValueError: + pass + + @staticmethod + def prepare_for_import(docdict): + # set order of fields from field_order + if docdict.get("field_order"): + new_field_dicts = [] + remaining_field_names = [f["fieldname"] for f in docdict.get("fields", [])] + + for fieldname in docdict.get("field_order"): + field_dict = [f for f in docdict.get("fields", []) if f["fieldname"] == fieldname] + if field_dict: + new_field_dicts.append(field_dict[0]) + if fieldname in remaining_field_names: + remaining_field_names.remove(fieldname) + + for fieldname in remaining_field_names: + field_dict = [f for f in docdict.get("fields", []) if f["fieldname"] == fieldname] + new_field_dicts.append(field_dict[0]) + + docdict["fields"] = new_field_dicts + + if "field_order" in docdict: + del docdict["field_order"] + + def export_doc(self): + """Export to standard folder `[module]/doctype/[name]/[name].json`.""" + from influxframework.modules.export_file import export_to_files + + export_to_files(record_list=[["DocType", self.name]], create_init=True) + + def make_controller_template(self): + """Make boilerplate controller template.""" + make_boilerplate("controller._py", self) + + if not self.istable: + make_boilerplate("test_controller._py", self.as_dict()) + make_boilerplate("controller.js", self.as_dict()) + # make_boilerplate("controller_list.js", self.as_dict()) + + if self.has_web_view: + templates_path = influxframework.get_module_path( + influxframework.scrub(self.module), "doctype", influxframework.scrub(self.name), "templates" + ) + if not os.path.exists(templates_path): + os.makedirs(templates_path) + make_boilerplate("templates/controller.html", self.as_dict()) + make_boilerplate("templates/controller_row.html", self.as_dict()) + + def make_amendable(self): + """If is_submittable is set, add amended_from docfields.""" + if self.is_submittable: + docfield_exists = influxframework.get_all( + "DocField", filters={"fieldname": "amended_from", "parent": self.name}, pluck="name", limit=1 + ) + if not docfield_exists: + self.append( + "fields", + { + "label": "Amended From", + "fieldtype": "Link", + "fieldname": "amended_from", + "options": self.name, + "read_only": 1, + "print_hide": 1, + "no_copy": 1, + }, + ) + + def make_repeatable(self): + """If allow_auto_repeat is set, add auto_repeat custom field.""" + if self.allow_auto_repeat: + if not influxframework.db.exists( + "Custom Field", {"fieldname": "auto_repeat", "dt": self.name} + ) and not influxframework.db.exists( + "DocField", {"fieldname": "auto_repeat", "parent": self.name} + ): + insert_after = self.fields[len(self.fields) - 1].fieldname + df = dict( + fieldname="auto_repeat", + label="Auto Repeat", + fieldtype="Link", + options="Auto Repeat", + insert_after=insert_after, + read_only=1, + no_copy=1, + print_hide=1, + ) + create_custom_field(self.name, df) + + def validate_nestedset(self): + if not self.get("is_tree"): + return + self.add_nestedset_fields() + + if not self.nsm_parent_field: + field_label = influxframework.bold(_("Parent Field (Tree)")) + influxframework.throw(_("{0} is a mandatory field").format(field_label), influxframework.MandatoryError) + + # check if field is valid + fieldnames = [df.fieldname for df in self.fields] + if self.nsm_parent_field and self.nsm_parent_field not in fieldnames: + influxframework.throw(_("Parent Field must be a valid fieldname"), InvalidFieldNameError) + + def add_nestedset_fields(self): + """If is_tree is set, add parent_field, lft, rgt, is_group fields.""" + fieldnames = [df.fieldname for df in self.fields] + if "lft" in fieldnames: + return + + self.append( + "fields", + { + "label": "Left", + "fieldtype": "Int", + "fieldname": "lft", + "read_only": 1, + "hidden": 1, + "no_copy": 1, + }, + ) + + self.append( + "fields", + { + "label": "Right", + "fieldtype": "Int", + "fieldname": "rgt", + "read_only": 1, + "hidden": 1, + "no_copy": 1, + }, + ) + + self.append("fields", {"label": "Is Group", "fieldtype": "Check", "fieldname": "is_group"}) + self.append( + "fields", + {"label": "Old Parent", "fieldtype": "Link", "options": self.name, "fieldname": "old_parent"}, + ) + + parent_field_label = f"Parent {self.name}" + parent_field_name = influxframework.scrub(parent_field_label) + self.append( + "fields", + { + "label": parent_field_label, + "fieldtype": "Link", + "options": self.name, + "fieldname": parent_field_name, + }, + ) + self.nsm_parent_field = parent_field_name + + def validate_child_table(self): + if not self.get("istable") or self.is_new() or self.get("is_virtual"): + # if the doctype is not a child table then return + # if the doctype is a new doctype and also a child table then + # don't move forward as it will be handled via schema + return + + self.add_child_table_fields() + + def add_child_table_fields(self): + from influxframework.database.schema import add_column + + add_column(self.name, "parent", "Data") + add_column(self.name, "parenttype", "Data") + add_column(self.name, "parentfield", "Data") + + def get_max_idx(self): + """Returns the highest `idx`""" + max_idx = influxframework.db.sql("""select max(idx) from `tabDocField` where parent = %s""", self.name) + return max_idx and max_idx[0][0] or 0 + + def validate_name(self, name=None): + if not name: + name = self.name + + # a Doctype name is the tablename created in database + # `tab` the length of tablename is limited to 64 characters + max_length = influxframework.db.MAX_COLUMN_LENGTH - 3 + if len(name) > max_length: + # length(tab + ) should be equal to 64 characters hence doctype should be 61 characters + influxframework.throw( + _("Doctype name is limited to {0} characters ({1})").format(max_length, name), influxframework.NameError + ) + + # a DocType name should not start or end with an empty space + if WHITESPACE_PADDING_PATTERN.search(name): + influxframework.throw(_("DocType's name should not start or end with whitespace"), influxframework.NameError) + + # a DocType's name should not start with a number or underscore + # and should only contain letters, numbers, underscore, and hyphen + if not START_WITH_LETTERS_PATTERN.match(name): + influxframework.throw( + _( + "A DocType's name should start with a letter and can only " + "consist of letters, numbers, spaces, underscores and hyphens" + ), + influxframework.NameError, + title="Invalid Name", + ) + + validate_route_conflict(self.doctype, self.name) + + +def validate_series(dt, autoname=None, name=None): + """Validate if `autoname` property is correctly set.""" + if not autoname: + autoname = dt.autoname + if not name: + name = dt.name + + if not autoname and dt.get("fields", {"fieldname": "naming_series"}): + dt.autoname = "naming_series:" + elif dt.autoname and dt.autoname.startswith("naming_series:"): + fieldname = dt.autoname.split("naming_series:")[0] or "naming_series" + if not dt.get("fields", {"fieldname": fieldname}): + influxframework.throw( + _("Fieldname called {0} must exist to enable autonaming").format(influxframework.bold(fieldname)), + title=_("Field Missing"), + ) + + # validate field name if autoname field:fieldname is used + # Create unique index on autoname field automatically. + if autoname and autoname.startswith("field:"): + field = autoname.split(":")[1] + if not field or field not in [df.fieldname for df in dt.fields]: + influxframework.throw(_("Invalid fieldname '{0}' in autoname").format(field)) + else: + for df in dt.fields: + if df.fieldname == field: + df.unique = 1 + break + + if ( + autoname + and (not autoname.startswith("field:")) + and (not autoname.startswith("eval:")) + and (autoname.lower() not in ("prompt", "hash")) + and (not autoname.startswith("naming_series:")) + and (not autoname.startswith("format:")) + ): + + prefix = autoname.split(".")[0] + doctype = influxframework.qb.DocType("DocType") + used_in = ( + influxframework.qb.from_(doctype) + .select(doctype.name) + .where(doctype.autoname.like(Concat(prefix, ".%"))) + .where(doctype.name != name) + ).run() + if used_in: + influxframework.throw(_("Series {0} already used in {1}").format(prefix, used_in[0][0])) + + +def validate_autoincrement_autoname(dt: Union[DocType, "CustomizeForm"]) -> bool: + """Checks if can doctype can change to/from autoincrement autoname""" + + def get_autoname_before_save(dt: Union[DocType, "CustomizeForm"]) -> str: + if dt.doctype == "Customize Form": + property_value = influxframework.db.get_value( + "Property Setter", {"doc_type": dt.doc_type, "property": "autoname"}, "value" + ) + # initially no property setter is set, + # hence getting autoname value from the doctype itself + if not property_value: + return influxframework.db.get_value("DocType", dt.doc_type, "autoname") or "" + + return property_value + + return getattr(dt.get_doc_before_save(), "autoname", "") + + if not dt.is_new(): + autoname_before_save = get_autoname_before_save(dt) + is_autoname_autoincrement = dt.autoname == "autoincrement" + + if ( + is_autoname_autoincrement + and autoname_before_save != "autoincrement" + or (not is_autoname_autoincrement and autoname_before_save == "autoincrement") + ): + + if dt.doctype == "Customize Form": + influxframework.throw(_("Cannot change to/from autoincrement autoname in Customize Form")) + + if influxframework.get_meta(dt.name).issingle: + return False + + if not influxframework.get_all(dt.name, limit=1): + # allow changing the column type if there is no data + return True + + influxframework.throw( + _("Can only change to/from Autoincrement naming rule when there is no data in the doctype") + ) + + return False + + +def change_name_column_type(doctype_name: str, type: str) -> None: + """Changes name column type""" + + args = ( + (doctype_name, "name", type, False, True) + if (influxframework.db.db_type == "postgres") + else (doctype_name, "name", type, True) + ) + + influxframework.db.change_column_type(*args) + + +def validate_links_table_fieldnames(meta): + """Validate fieldnames in Links table""" + if not meta.links or influxframework.flags.in_patch or influxframework.flags.in_fixtures: + return + + fieldnames = tuple(field.fieldname for field in meta.fields) + for index, link in enumerate(meta.links, 1): + _test_connection_query(doctype=link.link_doctype, field=link.link_fieldname, idx=index) + + if not link.is_child_table: + continue + + if not link.parent_doctype: + message = _("Document Links Row #{0}: Parent DocType is mandatory for internal links").format( + index + ) + influxframework.throw(message, influxframework.ValidationError, _("Parent Missing")) + + if not link.table_fieldname: + message = _("Document Links Row #{0}: Table Fieldname is mandatory for internal links").format( + index + ) + influxframework.throw(message, influxframework.ValidationError, _("Table Fieldname Missing")) + + if meta.name == link.parent_doctype: + field_exists = link.table_fieldname in fieldnames + else: + field_exists = influxframework.get_meta(link.parent_doctype).has_field(link.table_fieldname) + + if not field_exists: + message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format( + index, influxframework.bold(link.table_fieldname), influxframework.bold(meta.name) + ) + influxframework.throw(message, influxframework.ValidationError, _("Invalid Table Fieldname")) + + +def _test_connection_query(doctype, field, idx): + """Make sure that connection can be queried. + + This function executes query similar to one that would be executed for + finding count on dashboard and hence validates if fieldname/doctype are + correct. + """ + filters = get_filters_for(doctype) or {} + filters[field] = "" + + try: + influxframework.get_all(doctype, filters=filters, limit=1, distinct=True, ignore_ifnull=True) + except Exception as e: + influxframework.clear_last_message() + msg = _("Document Links Row #{0}: Invalid doctype or fieldname.").format(idx) + msg += "
    " + str(e) + influxframework.throw(msg, InvalidFieldNameError) + + +def validate_fields_for_doctype(doctype): + meta = influxframework.get_meta(doctype, cached=False) + validate_links_table_fieldnames(meta) + validate_fields(meta) + + +# this is separate because it is also called via custom field +def validate_fields(meta): + """Validate doctype fields. Checks + 1. There are no illegal characters in fieldnames + 2. If fieldnames are unique. + 3. Validate column length. + 4. Fields that do have database columns are not mandatory. + 5. `Link` and `Table` options are valid. + 6. **Hidden** and **Mandatory** are not set simultaneously. + 7. `Check` type field has default as 0 or 1. + 8. `Dynamic Links` are correctly defined. + 9. Precision is set in numeric fields and is between 1 & 6. + 10. Fold is not at the end (if set). + 11. `search_fields` are valid. + 12. `title_field` and title field pattern are valid. + 13. `unique` check is only valid for Data, Link and Read Only fieldtypes. + 14. `unique` cannot be checked if there exist non-unique values. + + :param meta: `influxframework.model.meta.Meta` object to check.""" + + def check_illegal_characters(fieldname): + validate_column_name(fieldname) + + def check_invalid_fieldnames(docname, fieldname): + if fieldname in Document._reserved_keywords: + influxframework.throw( + _("{0}: fieldname cannot be set to reserved keyword {1}").format( + influxframework.bold(docname), + influxframework.bold(fieldname), + ), + title=_("Invalid Fieldname"), + ) + + def check_unique_fieldname(docname, fieldname): + duplicates = list( + filter(None, map(lambda df: df.fieldname == fieldname and str(df.idx) or None, fields)) + ) + if len(duplicates) > 1: + influxframework.throw( + _("{0}: Fieldname {1} appears multiple times in rows {2}").format( + docname, fieldname, ", ".join(duplicates) + ), + UniqueFieldnameError, + ) + + def check_fieldname_length(fieldname): + validate_column_length(fieldname) + + def check_illegal_mandatory(docname, d): + if (d.fieldtype in no_value_fields) and d.fieldtype not in table_fields and d.reqd: + influxframework.throw( + _("{0}: Field {1} of type {2} cannot be mandatory").format(docname, d.label, d.fieldtype), + IllegalMandatoryError, + ) + + def check_link_table_options(docname, d): + if influxframework.flags.in_patch: + return + + if influxframework.flags.in_fixtures: + return + + if d.fieldtype in ("Link",) + table_fields: + if not d.options: + influxframework.throw( + _("{0}: Options required for Link or Table type field {1} in row {2}").format( + docname, d.label, d.idx + ), + DoctypeLinkError, + ) + if d.options == "[Select]" or d.options == d.parent: + return + if d.options != d.parent: + options = influxframework.db.get_value("DocType", d.options, "name") + if not options: + influxframework.throw( + _("{0}: Options must be a valid DocType for field {1} in row {2}").format( + docname, d.label, d.idx + ), + WrongOptionsDoctypeLinkError, + ) + elif not (options == d.options): + influxframework.throw( + _("{0}: Options {1} must be the same as doctype name {2} for the field {3}").format( + docname, d.options, options, d.label + ), + DoctypeLinkError, + ) + else: + # fix case + d.options = options + + def check_hidden_and_mandatory(docname, d): + if d.hidden and d.reqd and not d.default: + influxframework.throw( + _("{0}: Field {1} in row {2} cannot be hidden and mandatory without default").format( + docname, d.label, d.idx + ), + HiddenAndMandatoryWithoutDefaultError, + ) + + def check_width(d): + if d.fieldtype == "Currency" and cint(d.width) < 100: + influxframework.throw(_("Max width for type Currency is 100px in row {0}").format(d.idx)) + + def check_in_list_view(is_table, d): + if d.in_list_view and (d.fieldtype in not_allowed_in_list_view): + property_label = "In Grid View" if is_table else "In List View" + influxframework.throw( + _("'{0}' not allowed for type {1} in row {2}").format(property_label, d.fieldtype, d.idx) + ) + + def check_in_global_search(d): + if d.in_global_search and d.fieldtype in no_value_fields: + influxframework.throw( + _("'In Global Search' not allowed for type {0} in row {1}").format(d.fieldtype, d.idx) + ) + + def check_dynamic_link_options(d): + if d.fieldtype == "Dynamic Link": + doctype_pointer = list(filter(lambda df: df.fieldname == d.options, fields)) + if ( + not doctype_pointer + or (doctype_pointer[0].fieldtype not in ("Link", "Select")) + or (doctype_pointer[0].fieldtype == "Link" and doctype_pointer[0].options != "DocType") + ): + influxframework.throw( + _( + "Options 'Dynamic Link' type of field must point to another Link Field with options as 'DocType'" + ) + ) + + def check_illegal_default(d): + if d.fieldtype == "Check" and not d.default: + d.default = "0" + if d.fieldtype == "Check" and cint(d.default) not in (0, 1): + influxframework.throw( + _("Default for 'Check' type of field {0} must be either '0' or '1'").format( + influxframework.bold(d.fieldname) + ) + ) + if d.fieldtype == "Select" and d.default: + if not d.options: + influxframework.throw( + _("Options for {0} must be set before setting the default value.").format( + influxframework.bold(d.fieldname) + ) + ) + elif d.default not in d.options.split("\n"): + influxframework.throw( + _("Default value for {0} must be in the list of options.").format(influxframework.bold(d.fieldname)) + ) + + def check_precision(d): + if ( + d.fieldtype in ("Currency", "Float", "Percent") + and d.precision is not None + and not (1 <= cint(d.precision) <= 6) + ): + influxframework.throw(_("Precision should be between 1 and 6")) + + def check_unique_and_text(docname, d): + if meta.is_virtual: + return + + if meta.issingle: + d.unique = 0 + d.search_index = 0 + + if getattr(d, "unique", False): + if d.fieldtype not in ("Data", "Link", "Read Only"): + influxframework.throw( + _("{0}: Fieldtype {1} for {2} cannot be unique").format(docname, d.fieldtype, d.label), + NonUniqueError, + ) + + if not d.get("__islocal") and influxframework.db.has_column(d.parent, d.fieldname): + has_non_unique_values = influxframework.db.sql( + """select `{fieldname}`, count(*) + from `tab{doctype}` where ifnull(`{fieldname}`, '') != '' + group by `{fieldname}` having count(*) > 1 limit 1""".format( + doctype=d.parent, fieldname=d.fieldname + ) + ) + + if has_non_unique_values and has_non_unique_values[0][0]: + influxframework.throw( + _("{0}: Field '{1}' cannot be set as Unique as it has non-unique values").format( + docname, d.label + ), + NonUniqueError, + ) + + if d.search_index and d.fieldtype in ("Text", "Long Text", "Small Text", "Code", "Text Editor"): + influxframework.throw( + _("{0}:Fieldtype {1} for {2} cannot be indexed").format(docname, d.fieldtype, d.label), + CannotIndexedError, + ) + + def check_fold(fields): + fold_exists = False + for i, f in enumerate(fields): + if f.fieldtype == "Fold": + if fold_exists: + influxframework.throw(_("There can be only one Fold in a form")) + fold_exists = True + if i < len(fields) - 1: + nxt = fields[i + 1] + if nxt.fieldtype != "Section Break": + influxframework.throw(_("Fold must come before a Section Break")) + else: + influxframework.throw(_("Fold can not be at the end of the form")) + + def check_search_fields(meta, fields): + """Throw exception if `search_fields` don't contain valid fields.""" + if not meta.search_fields: + return + + # No value fields should not be included in search field + search_fields = [field.strip() for field in (meta.search_fields or "").split(",")] + fieldtype_mapper = { + field.fieldname: field.fieldtype + for field in filter(lambda field: field.fieldname in search_fields, fields) + } + + for fieldname in search_fields: + fieldname = fieldname.strip() + if (fieldtype_mapper.get(fieldname) in no_value_fields) or (fieldname not in fieldname_list): + influxframework.throw(_("Search field {0} is not valid").format(fieldname)) + + def check_title_field(meta): + """Throw exception if `title_field` isn't a valid fieldname.""" + if not meta.get("title_field"): + return + + if meta.title_field not in fieldname_list: + influxframework.throw(_("Title field must be a valid fieldname"), InvalidFieldNameError) + + def _validate_title_field_pattern(pattern): + if not pattern: + return + + for fieldname in FIELD_PATTERN.findall(pattern): + if fieldname.startswith("{"): + # edge case when double curlies are used for escape + continue + + if fieldname not in fieldname_list: + influxframework.throw( + _("{{{0}}} is not a valid fieldname pattern. It should be {{field_name}}.").format( + fieldname + ), + InvalidFieldNameError, + ) + + df = meta.get("fields", filters={"fieldname": meta.title_field})[0] + if df: + _validate_title_field_pattern(df.options) + _validate_title_field_pattern(df.default) + + def check_image_field(meta): + '''check image_field exists and is of type "Attach Image"''' + if not meta.image_field: + return + + df = meta.get("fields", {"fieldname": meta.image_field}) + if not df: + influxframework.throw(_("Image field must be a valid fieldname"), InvalidFieldNameError) + if df[0].fieldtype != "Attach Image": + influxframework.throw(_("Image field must be of type Attach Image"), InvalidFieldNameError) + + def check_is_published_field(meta): + if not meta.is_published_field: + return + + if meta.is_published_field not in fieldname_list: + influxframework.throw(_("Is Published Field must be a valid fieldname"), InvalidFieldNameError) + + def check_website_search_field(meta): + if not meta.get("website_search_field"): + return + + if meta.website_search_field not in fieldname_list: + influxframework.throw(_("Website Search Field must be a valid fieldname"), InvalidFieldNameError) + + if "title" not in fieldname_list: + influxframework.throw( + _('Field "title" is mandatory if "Website Search Field" is set.'), title=_("Missing Field") + ) + + def check_timeline_field(meta): + if not meta.timeline_field: + return + + if meta.timeline_field not in fieldname_list: + influxframework.throw(_("Timeline field must be a valid fieldname"), InvalidFieldNameError) + + df = meta.get("fields", {"fieldname": meta.timeline_field})[0] + if df.fieldtype not in ("Link", "Dynamic Link"): + influxframework.throw(_("Timeline field must be a Link or Dynamic Link"), InvalidFieldNameError) + + def check_sort_field(meta): + """Validate that sort_field(s) is a valid field""" + if meta.sort_field: + sort_fields = [meta.sort_field] + if "," in meta.sort_field: + sort_fields = [d.split()[0] for d in meta.sort_field.split(",")] + + for fieldname in sort_fields: + if fieldname not in (fieldname_list + list(default_fields) + list(child_table_fields)): + influxframework.throw( + _("Sort field {0} must be a valid fieldname").format(fieldname), InvalidFieldNameError + ) + + def check_illegal_depends_on_conditions(docfield): + """assignment operation should not be allowed in the depends on condition.""" + depends_on_fields = [ + "depends_on", + "collapsible_depends_on", + "mandatory_depends_on", + "read_only_depends_on", + ] + for field in depends_on_fields: + depends_on = docfield.get(field, None) + if depends_on and ("=" in depends_on) and DEPENDS_ON_PATTERN.match(depends_on): + influxframework.throw(_("Invalid {0} condition").format(influxframework.unscrub(field)), influxframework.ValidationError) + + def check_table_multiselect_option(docfield): + """check if the doctype provided in Option has atleast 1 Link field""" + if not docfield.fieldtype == "Table MultiSelect": + return + + doctype = docfield.options + meta = influxframework.get_meta(doctype) + link_field = [df for df in meta.fields if df.fieldtype == "Link"] + + if not link_field: + influxframework.throw( + _( + "DocType {0} provided for the field {1} must have atleast one Link field" + ).format(doctype, docfield.fieldname), + influxframework.ValidationError, + ) + + def scrub_options_in_select(field): + """Strip options for whitespaces""" + + if field.fieldtype == "Select" and field.options is not None: + options_list = [] + for i, option in enumerate(field.options.split("\n")): + _option = option.strip() + if i == 0 or _option: + options_list.append(_option) + field.options = "\n".join(options_list) + + def scrub_fetch_from(field): + if hasattr(field, "fetch_from") and getattr(field, "fetch_from"): + field.fetch_from = field.fetch_from.strip("\n").strip() + + def validate_data_field_type(docfield): + if docfield.get("is_virtual"): + return + + if docfield.fieldtype == "Data" and not ( + docfield.oldfieldtype and docfield.oldfieldtype != "Data" + ): + if docfield.options and (docfield.options not in data_field_options): + df_str = influxframework.bold(_(docfield.label)) + text_str = ( + _("{0} is an invalid Data field.").format(df_str) + + "
    " * 2 + + _("Only Options allowed for Data field are:") + + "
    " + ) + df_options_str = "
    • " + "
    • ".join(_(x) for x in data_field_options) + "
    " + + influxframework.msgprint(text_str + df_options_str, title="Invalid Data Field", raise_exception=True) + + def check_child_table_option(docfield): + + if influxframework.flags.in_fixtures: + return + if docfield.fieldtype not in ["Table MultiSelect", "Table"]: + return + + doctype = docfield.options + meta = influxframework.get_meta(doctype) + + if not meta.istable: + influxframework.throw( + _("Option {0} for field {1} is not a child table").format( + influxframework.bold(doctype), influxframework.bold(docfield.fieldname) + ), + title=_("Invalid Option"), + ) + + def check_max_height(docfield): + if getattr(docfield, "max_height", None) and (docfield.max_height[-2:] not in ("px", "em")): + influxframework.throw(f"Max for {influxframework.bold(docfield.fieldname)} height must be in px, em, rem") + + def check_no_of_ratings(docfield): + if docfield.fieldtype == "Rating": + if docfield.options and (int(docfield.options) > 10 or int(docfield.options) < 3): + influxframework.throw(_("Options for Rating field can range from 3 to 10")) + + fields = meta.get("fields") + fieldname_list = [d.fieldname for d in fields] + + not_allowed_in_list_view = list(copy.copy(no_value_fields)) + not_allowed_in_list_view.append("Attach Image") + if meta.istable: + not_allowed_in_list_view.remove("Button") + + for d in fields: + if not d.permlevel: + d.permlevel = 0 + if d.fieldtype not in table_fields: + d.allow_bulk_edit = 0 + if not d.fieldname: + d.fieldname = d.fieldname.lower().strip("?") + + check_illegal_characters(d.fieldname) + check_invalid_fieldnames(meta.get("name"), d.fieldname) + check_unique_fieldname(meta.get("name"), d.fieldname) + check_fieldname_length(d.fieldname) + check_illegal_mandatory(meta.get("name"), d) + check_link_table_options(meta.get("name"), d) + check_dynamic_link_options(d) + check_hidden_and_mandatory(meta.get("name"), d) + check_in_list_view(meta.get("istable"), d) + check_in_global_search(d) + check_illegal_default(d) + check_unique_and_text(meta.get("name"), d) + check_illegal_depends_on_conditions(d) + check_child_table_option(d) + check_table_multiselect_option(d) + scrub_options_in_select(d) + scrub_fetch_from(d) + validate_data_field_type(d) + check_max_height(d) + check_no_of_ratings(d) + + check_fold(fields) + check_search_fields(meta, fields) + check_title_field(meta) + check_timeline_field(meta) + check_is_published_field(meta) + check_website_search_field(meta) + check_sort_field(meta) + check_image_field(meta) + + +def validate_permissions_for_doctype(doctype, for_remove=False, alert=False): + """Validates if permissions are set correctly.""" + doctype = influxframework.get_doc("DocType", doctype) + validate_permissions(doctype, for_remove, alert=alert) + + # save permissions + for perm in doctype.get("permissions"): + perm.db_update() + + clear_permissions_cache(doctype.name) + + +def clear_permissions_cache(doctype): + influxframework.clear_cache(doctype=doctype) + delete_notification_count_for(doctype) + for user in influxframework.db.sql_list( + """ + SELECT + DISTINCT `tabHas Role`.`parent` + FROM + `tabHas Role`, + `tabDocPerm` + WHERE `tabDocPerm`.`parent` = %s + AND `tabDocPerm`.`role` = `tabHas Role`.`role` + AND `tabHas Role`.`parenttype` = 'User' + """, + doctype, + ): + influxframework.clear_cache(user=user) + + +def validate_permissions(doctype, for_remove=False, alert=False): + permissions = doctype.get("permissions") + # Some DocTypes may not have permissions by default, don't show alert for them + if not permissions and alert: + influxframework.msgprint(_("No Permissions Specified"), alert=True, indicator="orange") + issingle = issubmittable = isimportable = False + if doctype: + issingle = cint(doctype.issingle) + issubmittable = cint(doctype.is_submittable) + isimportable = cint(doctype.allow_import) + + def get_txt(d): + return _("For {0} at level {1} in {2} in row {3}").format(d.role, d.permlevel, d.parent, d.idx) + + def check_atleast_one_set(d): + if ( + not d.select and not d.read and not d.write and not d.submit and not d.cancel and not d.create + ): + influxframework.throw(_("{0}: No basic permissions set").format(get_txt(d))) + + def check_double(d): + has_similar = False + similar_because_of = "" + for p in permissions: + if p.role == d.role and p.permlevel == d.permlevel and p != d: + if p.if_owner == d.if_owner: + similar_because_of = _("If Owner") + has_similar = True + break + + if has_similar: + influxframework.throw( + _("{0}: Only one rule allowed with the same Role, Level and {1}").format( + get_txt(d), similar_because_of + ) + ) + + def check_level_zero_is_set(d): + if cint(d.permlevel) > 0 and d.role != "All": + has_zero_perm = False + for p in permissions: + if p.role == d.role and (p.permlevel or 0) == 0 and p != d: + has_zero_perm = True + break + + if not has_zero_perm: + influxframework.throw( + _("{0}: Permission at level 0 must be set before higher levels are set").format(get_txt(d)) + ) + + for invalid in ("create", "submit", "cancel", "amend"): + if d.get(invalid): + d.set(invalid, 0) + + def check_permission_dependency(d): + if d.cancel and not d.submit: + influxframework.throw(_("{0}: Cannot set Cancel without Submit").format(get_txt(d))) + + if (d.submit or d.cancel or d.amend) and not d.write: + influxframework.throw(_("{0}: Cannot set Submit, Cancel, Amend without Write").format(get_txt(d))) + if d.amend and not d.write: + influxframework.throw(_("{0}: Cannot set Amend without Cancel").format(get_txt(d))) + if d.get("import") and not d.create: + influxframework.throw(_("{0}: Cannot set Import without Create").format(get_txt(d))) + + def remove_rights_for_single(d): + if not issingle: + return + + if d.report: + influxframework.msgprint(_("Report cannot be set for Single types")) + d.report = 0 + d.set("import", 0) + d.set("export", 0) + + for ptype, label in [["set_user_permissions", _("Set User Permissions")]]: + if d.get(ptype): + d.set(ptype, 0) + influxframework.msgprint(_("{0} cannot be set for Single types").format(label)) + + def check_if_submittable(d): + if d.submit and not issubmittable: + influxframework.throw(_("{0}: Cannot set Assign Submit if not Submittable").format(get_txt(d))) + elif d.amend and not issubmittable: + influxframework.throw(_("{0}: Cannot set Assign Amend if not Submittable").format(get_txt(d))) + + def check_if_importable(d): + if d.get("import") and not isimportable: + influxframework.throw(_("{0}: Cannot set import as {1} is not importable").format(get_txt(d), doctype)) + + def validate_permission_for_all_role(d): + if influxframework.session.user == "Administrator": + return + + if doctype.custom: + if d.role == "All": + influxframework.throw( + _("Row # {0}: Non administrator user can not set the role {1} to the custom doctype").format( + d.idx, influxframework.bold(_("All")) + ), + title=_("Permissions Error"), + ) + + roles = [row.name for row in influxframework.get_all("Role", filters={"is_custom": 1})] + + if d.role in roles: + influxframework.throw( + _("Row # {0}: Non administrator user can not set the role {1} to the custom doctype").format( + d.idx, influxframework.bold(_(d.role)) + ), + title=_("Permissions Error"), + ) + + for d in permissions: + if not d.permlevel: + d.permlevel = 0 + check_atleast_one_set(d) + if not for_remove: + check_double(d) + check_permission_dependency(d) + check_if_submittable(d) + check_if_importable(d) + check_level_zero_is_set(d) + remove_rights_for_single(d) + validate_permission_for_all_role(d) + + +def make_module_and_roles(doc, perm_fieldname="permissions"): + """Make `Module Def` and `Role` records if already not made. Called while installing.""" + try: + if ( + hasattr(doc, "restrict_to_domain") + and doc.restrict_to_domain + and not influxframework.db.exists("Domain", doc.restrict_to_domain) + ): + influxframework.get_doc(dict(doctype="Domain", domain=doc.restrict_to_domain)).insert() + + if "tabModule Def" in influxframework.db.get_tables() and not influxframework.db.exists("Module Def", doc.module): + m = influxframework.get_doc({"doctype": "Module Def", "module_name": doc.module}) + if influxframework.scrub(doc.module) in influxframework.local.module_app: + m.app_name = influxframework.local.module_app[influxframework.scrub(doc.module)] + else: + m.app_name = "influxframework" + m.flags.ignore_mandatory = m.flags.ignore_permissions = True + if influxframework.flags.package: + m.package = influxframework.flags.package.name + m.custom = 1 + m.insert() + + default_roles = ["Administrator", "Guest", "All"] + roles = [p.role for p in doc.get("permissions") or []] + default_roles + + for role in list(set(roles)): + if influxframework.db.table_exists("Role", cached=False) and not influxframework.db.exists("Role", role): + r = influxframework.get_doc(dict(doctype="Role", role_name=role, desk_access=1)) + r.flags.ignore_mandatory = r.flags.ignore_permissions = True + r.insert() + except influxframework.DoesNotExistError as e: + pass + except influxframework.db.ProgrammingError as e: + if influxframework.db.is_table_missing(e): + pass + else: + raise + + +def check_fieldname_conflicts(docfield): + """Checks if fieldname conflicts with methods or properties""" + doc = influxframework.get_doc({"doctype": docfield.dt}) + available_objects = [x for x in dir(doc) if isinstance(x, str)] + property_list = [ + x for x in available_objects if isinstance(getattr(type(doc), x, None), property) + ] + method_list = [ + x for x in available_objects if x not in property_list and callable(getattr(doc, x)) + ] + msg = _("Fieldname {0} conflicting with meta object").format(docfield.fieldname) + + if docfield.fieldname in method_list + property_list: + influxframework.msgprint(msg, raise_exception=not docfield.is_virtual) + + +def clear_linked_doctype_cache(): + influxframework.cache().delete_value("linked_doctypes_without_ignore_user_permissions_enabled") + + +def check_email_append_to(doc): + if not hasattr(doc, "email_append_to") or not doc.email_append_to: + return + + # Subject Field + doc.subject_field = doc.subject_field.strip() if doc.subject_field else None + subject_field = get_field(doc, doc.subject_field) + + if doc.subject_field and not subject_field: + influxframework.throw(_("Select a valid Subject field for creating documents from Email")) + + if subject_field and subject_field.fieldtype not in [ + "Data", + "Text", + "Long Text", + "Small Text", + "Text Editor", + ]: + influxframework.throw(_("Subject Field type should be Data, Text, Long Text, Small Text, Text Editor")) + + # Sender Field is mandatory + doc.sender_field = doc.sender_field.strip() if doc.sender_field else None + sender_field = get_field(doc, doc.sender_field) + + if doc.sender_field and not sender_field: + influxframework.throw(_("Select a valid Sender Field for creating documents from Email")) + + if not sender_field.options == "Email": + influxframework.throw(_("Sender Field should have Email in options")) + + +def get_field(doc, fieldname): + if not (doc or fieldname): + return + + for field in doc.fields: + if field.fieldname == fieldname: + return field diff --git a/influxframework/core/doctype/doctype/patches/set_route.py b/influxframework/core/doctype/doctype/patches/set_route.py new file mode 100644 index 0000000..cc51d96 --- /dev/null +++ b/influxframework/core/doctype/doctype/patches/set_route.py @@ -0,0 +1,8 @@ +import influxframework +from influxframework.desk.utils import slug + + +def execute(): + for doctype in influxframework.get_all("DocType", ["name", "route"], dict(istable=0)): + if not doctype.route: + influxframework.db.set_value("DocType", doctype.name, "route", slug(doctype.name), update_modified=False) diff --git a/influxframework/core/doctype/doctype/test_doctype.py b/influxframework/core/doctype/doctype/test_doctype.py new file mode 100644 index 0000000..f850132 --- /dev/null +++ b/influxframework/core/doctype/doctype/test_doctype.py @@ -0,0 +1,763 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import random +import string +from unittest.mock import patch + +import influxframework +from influxframework.cache_manager import clear_doctype_cache +from influxframework.core.doctype.doctype.doctype import ( + CannotIndexedError, + DoctypeLinkError, + HiddenAndMandatoryWithoutDefaultError, + IllegalMandatoryError, + InvalidFieldNameError, + UniqueFieldnameError, + WrongOptionsDoctypeLinkError, + validate_links_table_fieldnames, +) +from influxframework.custom.doctype.custom_field.custom_field import create_custom_fields +from influxframework.desk.form.load import getdoc +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestDocType(InfluxFrameworkTestCase): + def tearDown(self): + influxframework.db.rollback() + + def test_validate_name(self): + self.assertRaises(influxframework.NameError, new_doctype("_Some DocType").insert) + self.assertRaises(influxframework.NameError, new_doctype("8Some DocType").insert) + self.assertRaises(influxframework.NameError, new_doctype("Some (DocType)").insert) + self.assertRaises( + influxframework.NameError, + new_doctype("Some Doctype with a name whose length is more than 61 characters").insert, + ) + for name in ("Some DocType", "Some_DocType", "Some-DocType"): + if influxframework.db.exists("DocType", name): + influxframework.delete_doc("DocType", name) + + doc = new_doctype(name).insert() + doc.delete() + + def test_making_sequence_on_change(self): + influxframework.delete_doc_if_exists("DocType", self._testMethodName) + dt = new_doctype(self._testMethodName).insert(ignore_permissions=True) + autoname = dt.autoname + + # change autoname + dt.autoname = "autoincrement" + dt.save() + + # check if name type has been changed + self.assertEqual( + influxframework.db.sql( + f"""select data_type FROM information_schema.columns + where column_name = 'name' and table_name = 'tab{self._testMethodName}'""" + )[0][0], + "bigint", + ) + + if influxframework.db.db_type == "mariadb": + table_name = "information_schema.tables" + conditions = f"table_type = 'sequence' and table_name = '{self._testMethodName}_id_seq'" + else: + table_name = "information_schema.sequences" + conditions = f"sequence_name = '{self._testMethodName}_id_seq'" + + # check if sequence table is created + self.assertTrue( + influxframework.db.sql( + f"""select * from {table_name} + where {conditions}""" + ) + ) + + # change the autoname/naming rule back to original + dt.autoname = autoname + dt.save() + + # check if name type has changed + self.assertEqual( + influxframework.db.sql( + f"""select data_type FROM information_schema.columns + where column_name = 'name' and table_name = 'tab{self._testMethodName}'""" + )[0][0], + "varchar" if influxframework.db.db_type == "mariadb" else "character varying", + ) + + def test_doctype_unique_constraint_dropped(self): + if influxframework.db.exists("DocType", "With_Unique"): + influxframework.delete_doc("DocType", "With_Unique") + + dt = new_doctype("With_Unique", unique=1) + dt.insert() + + doc1 = influxframework.new_doc("With_Unique") + doc2 = influxframework.new_doc("With_Unique") + doc1.some_fieldname = "Something" + doc1.name = "one" + doc2.some_fieldname = "Something" + doc2.name = "two" + + doc1.insert() + self.assertRaises(influxframework.UniqueValidationError, doc2.insert) + influxframework.db.rollback() + + dt.fields[0].unique = 0 + dt.save() + + doc2.insert() + doc1.delete() + doc2.delete() + + def test_validate_search_fields(self): + doc = new_doctype("Test Search Fields") + doc.search_fields = "some_fieldname" + doc.insert() + self.assertEqual(doc.name, "Test Search Fields") + + # check if invalid fieldname is allowed or not + doc.search_fields = "some_fieldname_1" + self.assertRaises(influxframework.ValidationError, doc.save) + + # check if no value fields are allowed in search fields + field = doc.append("fields", {}) + field.fieldname = "some_html_field" + field.fieldtype = "HTML" + field.label = "Some HTML Field" + doc.search_fields = "some_fieldname,some_html_field" + self.assertRaises(influxframework.ValidationError, doc.save) + + def test_depends_on_fields(self): + doc = new_doctype("Test Depends On", depends_on="eval:doc.__islocal == 0") + doc.insert() + + # check if the assignment operation is allowed in depends_on + field = doc.fields[0] + field.depends_on = "eval:doc.__islocal = 0" + self.assertRaises(influxframework.ValidationError, doc.save) + + def test_all_depends_on_fields_conditions(self): + import re + + docfields = influxframework.get_all( + "DocField", + or_filters={ + "ifnull(depends_on, '')": ("!=", ""), + "ifnull(collapsible_depends_on, '')": ("!=", ""), + "ifnull(mandatory_depends_on, '')": ("!=", ""), + "ifnull(read_only_depends_on, '')": ("!=", ""), + }, + fields=[ + "parent", + "depends_on", + "collapsible_depends_on", + "mandatory_depends_on", + "read_only_depends_on", + "fieldname", + "fieldtype", + ], + ) + + pattern = r'[\w\.:_]+\s*={1}\s*[\w\.@\'"]+' + for field in docfields: + for depends_on in [ + "depends_on", + "collapsible_depends_on", + "mandatory_depends_on", + "read_only_depends_on", + ]: + condition = field.get(depends_on) + if condition: + self.assertFalse(re.match(pattern, condition)) + + def test_data_field_options(self): + doctype_name = "Test Data Fields" + valid_data_field_options = influxframework.model.data_field_options + ("",) + invalid_data_field_options = ("Invalid Option 1", influxframework.utils.random_string(5)) + + for field_option in valid_data_field_options + invalid_data_field_options: + test_doctype = influxframework.get_doc( + { + "doctype": "DocType", + "name": doctype_name, + "module": "Core", + "custom": 1, + "fields": [ + {"fieldname": f"{field_option}_field", "fieldtype": "Data", "options": field_option} + ], + } + ) + + if field_option in invalid_data_field_options: + # assert that only data options in influxframework.model.data_field_options are valid + self.assertRaises(influxframework.ValidationError, test_doctype.insert) + else: + test_doctype.insert() + self.assertEqual(test_doctype.name, doctype_name) + test_doctype.delete() + + def test_sync_field_order(self): + import os + + from influxframework.modules.import_file import get_file_path + + # create test doctype + test_doctype = influxframework.get_doc( + { + "doctype": "DocType", + "module": "Core", + "fields": [ + {"label": "Field 1", "fieldname": "field_1", "fieldtype": "Data"}, + {"label": "Field 2", "fieldname": "field_2", "fieldtype": "Data"}, + {"label": "Field 3", "fieldname": "field_3", "fieldtype": "Data"}, + {"label": "Field 4", "fieldname": "field_4", "fieldtype": "Data"}, + ], + "permissions": [{"role": "System Manager", "read": 1}], + "name": "Test Field Order DocType", + "__islocal": 1, + } + ) + + path = get_file_path(test_doctype.module, test_doctype.doctype, test_doctype.name) + initial_fields_order = ["field_1", "field_2", "field_3", "field_4"] + + influxframework.delete_doc_if_exists("DocType", "Test Field Order DocType") + if os.path.isfile(path): + os.remove(path) + + try: + influxframework.flags.allow_doctype_export = 1 + test_doctype.save() + + # assert that field_order list is being created with the default order + test_doctype_json = influxframework.get_file_json(path) + self.assertTrue(test_doctype_json.get("field_order")) + self.assertEqual(len(test_doctype_json["fields"]), len(test_doctype_json["field_order"])) + self.assertListEqual( + [f["fieldname"] for f in test_doctype_json["fields"]], test_doctype_json["field_order"] + ) + self.assertListEqual( + [f["fieldname"] for f in test_doctype_json["fields"]], initial_fields_order + ) + self.assertListEqual(test_doctype_json["field_order"], initial_fields_order) + + # remove field_order to test reload_doc/sync/migrate is backwards compatible without field_order + del test_doctype_json["field_order"] + with open(path, "w+") as txtfile: + txtfile.write(influxframework.as_json(test_doctype_json)) + + # assert that field_order is actually removed from the json file + test_doctype_json = influxframework.get_file_json(path) + self.assertFalse(test_doctype_json.get("field_order")) + + # make sure that migrate/sync is backwards compatible without field_order + influxframework.reload_doctype(test_doctype.name, force=True) + test_doctype.reload() + + # assert that field_order list is being created with the default order again + test_doctype.save() + test_doctype_json = influxframework.get_file_json(path) + self.assertTrue(test_doctype_json.get("field_order")) + self.assertEqual(len(test_doctype_json["fields"]), len(test_doctype_json["field_order"])) + self.assertListEqual( + [f["fieldname"] for f in test_doctype_json["fields"]], test_doctype_json["field_order"] + ) + self.assertListEqual( + [f["fieldname"] for f in test_doctype_json["fields"]], initial_fields_order + ) + self.assertListEqual(test_doctype_json["field_order"], initial_fields_order) + + # reorder fields: swap row 1 and 3 + test_doctype.fields[0], test_doctype.fields[2] = test_doctype.fields[2], test_doctype.fields[0] + for i, f in enumerate(test_doctype.fields): + f.idx = i + 1 + + # assert that reordering fields only affects `field_order` rather than `fields` attr + test_doctype.save() + test_doctype_json = influxframework.get_file_json(path) + self.assertListEqual( + [f["fieldname"] for f in test_doctype_json["fields"]], initial_fields_order + ) + self.assertListEqual( + test_doctype_json["field_order"], ["field_3", "field_2", "field_1", "field_4"] + ) + + # reorder `field_order` in the json file: swap row 2 and 4 + test_doctype_json["field_order"][1], test_doctype_json["field_order"][3] = ( + test_doctype_json["field_order"][3], + test_doctype_json["field_order"][1], + ) + with open(path, "w+") as txtfile: + txtfile.write(influxframework.as_json(test_doctype_json)) + + # assert that reordering `field_order` from json file is reflected in DocType upon migrate/sync + influxframework.reload_doctype(test_doctype.name, force=True) + test_doctype.reload() + self.assertListEqual( + [f.fieldname for f in test_doctype.fields], ["field_3", "field_4", "field_1", "field_2"] + ) + + # insert row in the middle and remove first row (field 3) + test_doctype.append("fields", {"label": "Field 5", "fieldname": "field_5", "fieldtype": "Data"}) + test_doctype.fields[4], test_doctype.fields[3] = test_doctype.fields[3], test_doctype.fields[4] + test_doctype.fields[3], test_doctype.fields[2] = test_doctype.fields[2], test_doctype.fields[3] + test_doctype.remove(test_doctype.fields[0]) + for i, f in enumerate(test_doctype.fields): + f.idx = i + 1 + + test_doctype.save() + test_doctype_json = influxframework.get_file_json(path) + self.assertListEqual( + [f["fieldname"] for f in test_doctype_json["fields"]], + ["field_1", "field_2", "field_4", "field_5"], + ) + self.assertListEqual( + test_doctype_json["field_order"], ["field_4", "field_5", "field_1", "field_2"] + ) + except Exception: + raise + finally: + influxframework.flags.allow_doctype_export = 0 + + def test_unique_field_name_for_two_fields(self): + doc = new_doctype("Test Unique Field") + field_1 = doc.append("fields", {}) + field_1.fieldname = "some_fieldname_1" + field_1.fieldtype = "Data" + + field_2 = doc.append("fields", {}) + field_2.fieldname = "some_fieldname_1" + field_2.fieldtype = "Data" + + self.assertRaises(UniqueFieldnameError, doc.insert) + + def test_fieldname_is_not_name(self): + doc = new_doctype("Test Name Field") + field_1 = doc.append("fields", {}) + field_1.label = "Name" + field_1.fieldtype = "Data" + doc.insert() + self.assertEqual(doc.fields[1].fieldname, "name1") + doc.fields[1].fieldname = "name" + self.assertRaises(InvalidFieldNameError, doc.save) + + def test_illegal_mandatory_validation(self): + doc = new_doctype("Test Illegal mandatory") + field_1 = doc.append("fields", {}) + field_1.fieldname = "some_fieldname_1" + field_1.fieldtype = "Section Break" + field_1.reqd = 1 + + self.assertRaises(IllegalMandatoryError, doc.insert) + + def test_link_with_wrong_and_no_options(self): + doc = new_doctype("Test link") + field_1 = doc.append("fields", {}) + field_1.fieldname = "some_fieldname_1" + field_1.fieldtype = "Link" + + self.assertRaises(DoctypeLinkError, doc.insert) + + field_1.options = "wrongdoctype" + + self.assertRaises(WrongOptionsDoctypeLinkError, doc.insert) + + def test_hidden_and_mandatory_without_default(self): + doc = new_doctype("Test hidden and mandatory") + field_1 = doc.append("fields", {}) + field_1.fieldname = "some_fieldname_1" + field_1.fieldtype = "Data" + field_1.reqd = 1 + field_1.hidden = 1 + + self.assertRaises(HiddenAndMandatoryWithoutDefaultError, doc.insert) + + def test_field_can_not_be_indexed_validation(self): + doc = new_doctype("Test index") + field_1 = doc.append("fields", {}) + field_1.fieldname = "some_fieldname_1" + field_1.fieldtype = "Long Text" + field_1.search_index = 1 + + self.assertRaises(CannotIndexedError, doc.insert) + + def test_cancel_link_doctype(self): + import json + + from influxframework.desk.form.linked_with import cancel_all_linked_docs, get_submitted_linked_docs + + # create doctype + link_doc = new_doctype("Test Linked Doctype") + link_doc.is_submittable = 1 + for data in link_doc.get("permissions"): + data.submit = 1 + data.cancel = 1 + link_doc.insert() + + doc = new_doctype("Test Doctype") + doc.is_submittable = 1 + field_2 = doc.append("fields", {}) + field_2.label = "Test Linked Doctype" + field_2.fieldname = "test_linked_doctype" + field_2.fieldtype = "Link" + field_2.options = "Test Linked Doctype" + for data in link_doc.get("permissions"): + data.submit = 1 + data.cancel = 1 + doc.insert() + + # create doctype data + data_link_doc = influxframework.new_doc("Test Linked Doctype") + data_link_doc.some_fieldname = "Data1" + data_link_doc.insert() + data_link_doc.save() + data_link_doc.submit() + + data_doc = influxframework.new_doc("Test Doctype") + data_doc.some_fieldname = "Data1" + data_doc.test_linked_doctype = data_link_doc.name + data_doc.insert() + data_doc.save() + data_doc.submit() + + docs = get_submitted_linked_docs(link_doc.name, data_link_doc.name) + dump_docs = json.dumps(docs.get("docs")) + cancel_all_linked_docs(dump_docs) + data_link_doc.cancel() + data_doc.load_from_db() + self.assertEqual(data_link_doc.docstatus, 2) + self.assertEqual(data_doc.docstatus, 2) + + # delete doctype record + data_doc.delete() + data_link_doc.delete() + + # delete doctype + link_doc.delete() + doc.delete() + influxframework.db.commit() + + def test_ignore_cancelation_of_linked_doctype_during_cancel(self): + import json + + from influxframework.desk.form.linked_with import cancel_all_linked_docs, get_submitted_linked_docs + + # create linked doctype + link_doc = new_doctype("Test Linked Doctype 1") + link_doc.is_submittable = 1 + for data in link_doc.get("permissions"): + data.submit = 1 + data.cancel = 1 + link_doc.insert() + + # create first parent doctype + test_doc_1 = new_doctype("Test Doctype 1") + test_doc_1.is_submittable = 1 + + field_2 = test_doc_1.append("fields", {}) + field_2.label = "Test Linked Doctype 1" + field_2.fieldname = "test_linked_doctype_a" + field_2.fieldtype = "Link" + field_2.options = "Test Linked Doctype 1" + + for data in test_doc_1.get("permissions"): + data.submit = 1 + data.cancel = 1 + test_doc_1.insert() + + # crete second parent doctype + doc = new_doctype("Test Doctype 2") + doc.is_submittable = 1 + + field_2 = doc.append("fields", {}) + field_2.label = "Test Linked Doctype 1" + field_2.fieldname = "test_linked_doctype_a" + field_2.fieldtype = "Link" + field_2.options = "Test Linked Doctype 1" + + for data in link_doc.get("permissions"): + data.submit = 1 + data.cancel = 1 + doc.insert() + + # create doctype data + data_link_doc_1 = influxframework.new_doc("Test Linked Doctype 1") + data_link_doc_1.some_fieldname = "Data1" + data_link_doc_1.insert() + data_link_doc_1.save() + data_link_doc_1.submit() + + data_doc_2 = influxframework.new_doc("Test Doctype 1") + data_doc_2.some_fieldname = "Data1" + data_doc_2.test_linked_doctype_a = data_link_doc_1.name + data_doc_2.insert() + data_doc_2.save() + data_doc_2.submit() + + data_doc = influxframework.new_doc("Test Doctype 2") + data_doc.some_fieldname = "Data1" + data_doc.test_linked_doctype_a = data_link_doc_1.name + data_doc.insert() + data_doc.save() + data_doc.submit() + + docs = get_submitted_linked_docs(link_doc.name, data_link_doc_1.name) + dump_docs = json.dumps(docs.get("docs")) + + cancel_all_linked_docs(dump_docs, ignore_doctypes_on_cancel_all=["Test Doctype 2"]) + + # checking that doc for Test Doctype 2 is not canceled + self.assertRaises(influxframework.LinkExistsError, data_link_doc_1.cancel) + + data_doc.load_from_db() + data_doc_2.load_from_db() + self.assertEqual(data_link_doc_1.docstatus, 2) + + # linked doc is canceled + self.assertEqual(data_doc_2.docstatus, 2) + + # ignored doctype 2 during cancel + self.assertEqual(data_doc.docstatus, 1) + + # delete doctype record + data_doc.cancel() + data_doc.delete() + data_doc_2.delete() + data_link_doc_1.delete() + + # delete doctype + link_doc.delete() + doc.delete() + test_doc_1.delete() + influxframework.db.commit() + + def test_links_table_fieldname_validation(self): + doc = new_doctype("Test Links Table Validation") + + # check valid data + doc.append("links", {"link_doctype": "User", "link_fieldname": "first_name"}) + validate_links_table_fieldnames(doc) # no error + doc.links = [] # reset links table + + # check invalid doctype + doc.append("links", {"link_doctype": "User2", "link_fieldname": "first_name"}) + self.assertRaises(InvalidFieldNameError, validate_links_table_fieldnames, doc) + doc.links = [] # reset links table + + # check invalid fieldname + doc.append("links", {"link_doctype": "User", "link_fieldname": "a_field_that_does_not_exists"}) + + self.assertRaises(InvalidFieldNameError, validate_links_table_fieldnames, doc) + + def test_create_virtual_doctype(self): + """Test virtual DOcTYpe.""" + virtual_doc = new_doctype("Test Virtual Doctype") + virtual_doc.is_virtual = 1 + virtual_doc.insert() + virtual_doc.save() + doc = influxframework.get_doc("DocType", "Test Virtual Doctype") + + self.assertEqual(doc.is_virtual, 1) + self.assertFalse(influxframework.db.table_exists("Test Virtual Doctype")) + + def test_create_virtual_doctype_as_child_table(self): + """Test virtual DocType as Child Table below a normal DocType.""" + influxframework.delete_doc_if_exists("DocType", "Test Parent Virtual DocType", force=1) + influxframework.delete_doc_if_exists("DocType", "Test Virtual DocType as Child Table", force=1) + + virtual_doc = new_doctype("Test Virtual DocType as Child Table") + virtual_doc.is_virtual = 1 + virtual_doc.istable = 1 + virtual_doc.insert(ignore_permissions=True) + + doc = influxframework.get_doc("DocType", "Test Virtual DocType as Child Table") + + self.assertEqual(doc.is_virtual, 1) + self.assertEqual(doc.istable, 1) + self.assertFalse(influxframework.db.table_exists("Test Virtual DocType as Child Table")) + + parent_doc = new_doctype("Test Parent Virtual DocType") + parent_doc.append( + "fields", + { + "fieldname": "virtual_child_table", + "fieldtype": "Table", + "options": "Test Virtual DocType as Child Table", + }, + ) + parent_doc.insert(ignore_permissions=True) + + # create entry for parent doctype + parent_doc_entry = influxframework.get_doc( + {"doctype": "Test Parent Virtual DocType", "some_fieldname": "Test"} + ) + parent_doc_entry.insert(ignore_permissions=True) + + # update the parent doc (should not abort because of any DB query to a virtual child table, as there is none) + parent_doc_entry.some_fieldname = "Test update" + parent_doc_entry.save(ignore_permissions=True) + + # delete the parent doc (should not abort because of any DB query to a virtual child table, as there is none) + parent_doc_entry.delete() + + def test_default_fieldname(self): + fields = [ + {"label": "title", "fieldname": "title", "fieldtype": "Data", "default": "{some_fieldname}"} + ] + dt = new_doctype("DT with default field", fields=fields) + dt.insert() + + dt.delete() + + def test_autoincremented_doctype_transition(self): + influxframework.delete_doc_if_exists("DocType", "testy_autoinc_dt") + dt = new_doctype("testy_autoinc_dt", autoname="autoincrement").insert(ignore_permissions=True) + dt.autoname = "hash" + + dt.save(ignore_permissions=True) + + dt_data = influxframework.get_doc({"doctype": dt.name, "some_fieldname": "test data"}).insert( + ignore_permissions=True + ) + + dt.autoname = "autoincrement" + + try: + dt.save(ignore_permissions=True) + except influxframework.ValidationError as e: + self.assertEqual( + e.args[0], + "Can only change to/from Autoincrement naming rule when there is no data in the doctype", + ) + else: + self.fail( + """Shouldn't be possible to transition to/from autoincremented doctype + when data is present in doctype""" + ) + finally: + # cleanup + dt_data.delete(ignore_permissions=True) + dt.delete(ignore_permissions=True) + + def test_json_field(self): + """Test json field.""" + import json + + json_doc = new_doctype( + "Test Json Doctype", + fields=[{"label": "json field", "fieldname": "test_json_field", "fieldtype": "JSON"}], + ) + json_doc.insert() + json_doc.save() + doc = influxframework.get_doc("DocType", "Test Json Doctype") + for field in doc.fields: + if field.fieldname == "test_json_field": + self.assertEqual(field.fieldtype, "JSON") + break + + doc = influxframework.get_doc( + {"doctype": "Test Json Doctype", "test_json_field": json.dumps({"hello": "world"})} + ) + doc.insert() + doc.save() + + test_json = influxframework.get_doc("Test Json Doctype", doc.name) + + if isinstance(test_json.test_json_field, str): + test_json.test_json_field = json.loads(test_json.test_json_field) + + self.assertEqual(test_json.test_json_field["hello"], "world") + + @patch.dict(influxframework.conf, {"developer_mode": 1}) + def test_custom_field_deletion(self): + """Custom child tables whose doctype doesn't exist should be auto deleted.""" + doctype = new_doctype(custom=0).insert().name + child = new_doctype(custom=0, istable=1).insert().name + + field = "abc" + create_custom_fields({doctype: [{"fieldname": field, "fieldtype": "Table", "options": child}]}) + + influxframework.delete_doc("DocType", child) + self.assertFalse(influxframework.get_meta(doctype).get_field(field)) + + @patch.dict(influxframework.conf, {"developer_mode": 1}) + def test_delete_doctype_with_customization(self): + from influxframework.custom.doctype.property_setter.property_setter import make_property_setter + + custom_field = "customfield" + + doctype = new_doctype(custom=0).insert().name + + # Create property setter and custom field + field = "some_fieldname" + make_property_setter(doctype, field, "default", "DELETETHIS", "Data") + create_custom_fields({doctype: [{"fieldname": custom_field, "fieldtype": "Data"}]}) + + # Create 1 record + original_doc = influxframework.get_doc(doctype=doctype, custom_field_name="wat").insert() + self.assertEqual(original_doc.some_fieldname, "DELETETHIS") + + # delete doctype + influxframework.delete_doc("DocType", doctype) + clear_doctype_cache(doctype) + + # "restore" doctype by inserting doctype with same schema again + new_doctype(doctype, custom=0).insert() + + # Ensure basically same doctype getting "restored" + restored_doc = influxframework.get_last_doc(doctype) + verify_fields = ["doctype", field, custom_field] + for f in verify_fields: + self.assertEqual(original_doc.get(f), restored_doc.get(f)) + + # Check form load of restored doctype + getdoc(doctype, restored_doc.name) + + # ensure meta - property setter + self.assertEqual(influxframework.get_meta(doctype).get_field(field).default, "DELETETHIS") + influxframework.delete_doc("DocType", doctype) + + +def new_doctype( + name: str | None = None, + unique: bool = False, + depends_on: str = "", + fields: list[dict] | None = None, + **kwargs, +): + if not name: + # Test prefix is required to avoid coverage + name = "Test " + "".join(random.sample(string.ascii_lowercase, 10)) + + doc = influxframework.get_doc( + { + "doctype": "DocType", + "module": "Core", + "custom": 1, + "fields": [ + { + "label": "Some Field", + "fieldname": "some_fieldname", + "fieldtype": "Data", + "unique": unique, + "depends_on": depends_on, + } + ], + "permissions": [ + { + "role": "System Manager", + "read": 1, + } + ], + "name": name, + **kwargs, + } + ) + + if fields: + for f in fields: + doc.append("fields", f) + + return doc diff --git a/influxframework/core/doctype/doctype_action/__init__.py b/influxframework/core/doctype/doctype_action/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/doctype_action/doctype_action.json b/influxframework/core/doctype/doctype_action/doctype_action.json new file mode 100644 index 0000000..080755c --- /dev/null +++ b/influxframework/core/doctype/doctype_action/doctype_action.json @@ -0,0 +1,74 @@ +{ + "actions": [], + "creation": "2019-09-23 16:28:13.953520", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "label", + "action_type", + "action", + "group", + "hidden", + "custom" + ], + "fields": [ + { + "columns": 2, + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "reqd": 1 + }, + { + "fieldname": "group", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Group" + }, + { + "columns": 2, + "fieldname": "action_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Action Type", + "options": "Server Action\nRoute", + "reqd": 1 + }, + { + "columns": 4, + "fieldname": "action", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Action / Route", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "hidden", + "fieldtype": "Check", + "label": "Hidden" + }, + { + "default": "0", + "fieldname": "custom", + "fieldtype": "Check", + "hidden": 1, + "label": "Custom" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-09-24 14:19:05.549835", + "modified_by": "Administrator", + "module": "Core", + "name": "DocType Action", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/doctype_action/doctype_action.py b/influxframework/core/doctype/doctype_action/doctype_action.py new file mode 100644 index 0000000..f896ffe --- /dev/null +++ b/influxframework/core/doctype/doctype_action/doctype_action.py @@ -0,0 +1,9 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class DocTypeAction(Document): + pass diff --git a/influxframework/core/doctype/doctype_link/__init__.py b/influxframework/core/doctype/doctype_link/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/doctype_link/doctype_link.json b/influxframework/core/doctype/doctype_link/doctype_link.json new file mode 100644 index 0000000..4baec67 --- /dev/null +++ b/influxframework/core/doctype/doctype_link/doctype_link.json @@ -0,0 +1,87 @@ +{ + "actions": [], + "creation": "2019-09-24 11:41:25.291377", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "link_doctype", + "link_fieldname", + "parent_doctype", + "table_fieldname", + "group", + "hidden", + "is_child_table", + "custom" + ], + "fields": [ + { + "fieldname": "link_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Link DocType", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "link_fieldname", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Link Fieldname", + "reqd": 1 + }, + { + "fieldname": "group", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Group" + }, + { + "default": "0", + "fieldname": "hidden", + "fieldtype": "Check", + "label": "Hidden" + }, + { + "default": "0", + "fieldname": "custom", + "fieldtype": "Check", + "hidden": 1, + "label": "Custom" + }, + { + "depends_on": "is_child_table", + "fieldname": "parent_doctype", + "fieldtype": "Link", + "label": "Parent DocType", + "mandatory_depends_on": "is_child_table", + "options": "DocType" + }, + { + "default": "0", + "fetch_from": "link_doctype.istable", + "fieldname": "is_child_table", + "fieldtype": "Check", + "label": "Is Child Table", + "read_only": 1 + }, + { + "fieldname": "table_fieldname", + "fieldtype": "Data", + "label": "Table Fieldname" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-07-31 15:23:12.237491", + "modified_by": "Administrator", + "module": "Core", + "name": "DocType Link", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/doctype_link/doctype_link.py b/influxframework/core/doctype/doctype_link/doctype_link.py new file mode 100644 index 0000000..4e145b8 --- /dev/null +++ b/influxframework/core/doctype/doctype_link/doctype_link.py @@ -0,0 +1,9 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class DocTypeLink(Document): + pass diff --git a/influxframework/core/doctype/doctype_state/__init__.py b/influxframework/core/doctype/doctype_state/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/doctype_state/doctype_state.json b/influxframework/core/doctype/doctype_state/doctype_state.json new file mode 100644 index 0000000..79797b4 --- /dev/null +++ b/influxframework/core/doctype/doctype_state/doctype_state.json @@ -0,0 +1,50 @@ +{ + "actions": [], + "creation": "2021-08-23 17:21:28.345841", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "title", + "color", + "custom" + ], + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Title", + "reqd": 1 + }, + { + "default": "Blue", + "fieldname": "color", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Color", + "options": "Blue\nCyan\nGray\nGreen\nLight Blue\nOrange\nPink\nPurple\nRed\nYellow", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "custom", + "fieldtype": "Check", + "hidden": 1, + "label": "Custom" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-12-14 14:14:55.716378", + "modified_by": "Administrator", + "module": "Core", + "name": "DocType State", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/doctype_state/doctype_state.py b/influxframework/core/doctype/doctype_state/doctype_state.py new file mode 100644 index 0000000..fb9b105 --- /dev/null +++ b/influxframework/core/doctype/doctype_state/doctype_state.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, InfluxFramework LLC +# For license information, please see license.txt + +# import influxframework +from influxframework.model.document import Document + + +class DocTypeState(Document): + pass diff --git a/influxframework/core/doctype/document_naming_rule/__init__.py b/influxframework/core/doctype/document_naming_rule/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/document_naming_rule/document_naming_rule.js b/influxframework/core/doctype/document_naming_rule/document_naming_rule.js new file mode 100644 index 0000000..797294b --- /dev/null +++ b/influxframework/core/doctype/document_naming_rule/document_naming_rule.js @@ -0,0 +1,70 @@ +// Copyright (c) 2020, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Document Naming Rule", { + refresh: function (frm) { + frm.trigger("document_type"); + if (!frm.doc.__islocal) frm.trigger("add_update_counter_button"); + }, + document_type: (frm) => { + // update the select field options with fieldnames + if (frm.doc.document_type) { + influxframework.model.with_doctype(frm.doc.document_type, () => { + let fieldnames = influxframework + .get_meta(frm.doc.document_type) + .fields.filter((d) => { + return influxframework.model.no_value_type.indexOf(d.fieldtype) === -1; + }) + .map((d) => { + return { label: `${d.label} (${d.fieldname})`, value: d.fieldname }; + }); + frm.fields_dict.conditions.grid.update_docfield_property( + "field", + "options", + fieldnames + ); + }); + } + }, + add_update_counter_button: (frm) => { + frm.add_custom_button(__("Update Counter"), function () { + const fields = [ + { + fieldtype: "Data", + fieldname: "new_counter", + label: __("New Counter"), + default: frm.doc.counter, + reqd: 1, + description: __( + "Warning: Updating counter may lead to document name conflicts if not done properly" + ), + }, + ]; + + let primary_action_label = __("Save"); + + let primary_action = (fields) => { + influxframework.call({ + method: "influxframework.core.doctype.document_naming_rule.document_naming_rule.update_current", + args: { + name: frm.doc.name, + new_counter: fields.new_counter, + }, + callback: function () { + frm.set_value("counter", fields.new_counter); + dialog.hide(); + }, + }); + }; + + const dialog = new influxframework.ui.Dialog({ + title: __("Update Counter Value for Prefix: {0}", [frm.doc.prefix]), + fields, + primary_action_label, + primary_action, + }); + + dialog.show(); + }); + }, +}); diff --git a/influxframework/core/doctype/document_naming_rule/document_naming_rule.json b/influxframework/core/doctype/document_naming_rule/document_naming_rule.json new file mode 100644 index 0000000..4e6f3f3 --- /dev/null +++ b/influxframework/core/doctype/document_naming_rule/document_naming_rule.json @@ -0,0 +1,107 @@ +{ + "actions": [], + "creation": "2020-09-07 12:48:48.334318", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type", + "disabled", + "priority", + "section_break_3", + "conditions", + "naming_section", + "prefix", + "prefix_digits", + "counter" + ], + "fields": [ + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Document Type", + "options": "DocType" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fieldname": "prefix", + "fieldtype": "Data", + "label": "Prefix", + "mandatory_depends_on": "eval:doc.naming_by===\"Numbered\"", + "reqd": 1 + }, + { + "fieldname": "counter", + "fieldtype": "Int", + "label": "Counter", + "no_copy": 1, + "read_only": 1 + }, + { + "default": "5", + "description": "Example: 00001", + "fieldname": "prefix_digits", + "fieldtype": "Int", + "label": "Digits", + "mandatory_depends_on": "eval:doc.naming_by===\"Numbered\"", + "reqd": 1 + }, + { + "fieldname": "naming_section", + "fieldtype": "Section Break", + "label": "Naming" + }, + { + "collapsible": 1, + "collapsible_depends_on": "conditions", + "fieldname": "section_break_3", + "fieldtype": "Section Break", + "label": "Rule Conditions" + }, + { + "fieldname": "conditions", + "fieldtype": "Table", + "label": "Conditions", + "options": "Document Naming Rule Condition" + }, + { + "description": "Rules with higher priority number will be applied first.", + "fieldname": "priority", + "fieldtype": "Int", + "label": "Priority" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-09-13 20:07:47.617615", + "modified_by": "Administrator", + "module": "Core", + "name": "Document Naming Rule", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "document_type", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/document_naming_rule/document_naming_rule.py b/influxframework/core/doctype/document_naming_rule/document_naming_rule.py new file mode 100644 index 0000000..88c77a3 --- /dev/null +++ b/influxframework/core/doctype/document_naming_rule/document_naming_rule.py @@ -0,0 +1,46 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework import _ +from influxframework.model.document import Document +from influxframework.model.naming import parse_naming_series +from influxframework.utils.data import evaluate_filters + + +class DocumentNamingRule(Document): + def validate(self): + self.validate_fields_in_conditions() + + def validate_fields_in_conditions(self): + if self.has_value_changed("document_type"): + docfields = [x.fieldname for x in influxframework.get_meta(self.document_type).fields] + for condition in self.conditions: + if condition.field not in docfields: + influxframework.throw( + _("{0} is not a field of doctype {1}").format( + influxframework.bold(condition.field), influxframework.bold(self.document_type) + ) + ) + + def apply(self, doc): + """ + Apply naming rules for the given document. Will set `name` if the rule is matched. + """ + if self.conditions: + if not evaluate_filters( + doc, [(self.document_type, d.field, d.condition, d.value) for d in self.conditions] + ): + return + + counter = influxframework.db.get_value(self.doctype, self.name, "counter", for_update=True) or 0 + naming_series = parse_naming_series(self.prefix, doc=doc) + + doc.name = naming_series + ("%0" + str(self.prefix_digits) + "d") % (counter + 1) + influxframework.db.set_value(self.doctype, self.name, "counter", counter + 1) + + +@influxframework.whitelist() +def update_current(name, new_counter): + influxframework.only_for("System Manager") + influxframework.db.set_value("Document Naming Rule", name, "counter", new_counter) diff --git a/influxframework/core/doctype/document_naming_rule/test_document_naming_rule.py b/influxframework/core/doctype/document_naming_rule/test_document_naming_rule.py new file mode 100644 index 0000000..42ec9cc --- /dev/null +++ b/influxframework/core/doctype/document_naming_rule/test_document_naming_rule.py @@ -0,0 +1,70 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestDocumentNamingRule(InfluxFrameworkTestCase): + def test_naming_rule_by_series(self): + naming_rule = influxframework.get_doc( + dict(doctype="Document Naming Rule", document_type="ToDo", prefix="test-todo-", prefix_digits=5) + ).insert() + + todo = influxframework.get_doc( + dict(doctype="ToDo", description="Is this my name " + influxframework.generate_hash()) + ).insert() + + self.assertEqual(todo.name, "test-todo-00001") + + naming_rule.delete() + todo.delete() + + def test_naming_rule_by_condition(self): + naming_rule = influxframework.get_doc( + dict( + doctype="Document Naming Rule", + document_type="ToDo", + prefix="test-high-", + prefix_digits=5, + priority=10, + conditions=[dict(field="priority", condition="=", value="High")], + ) + ).insert() + + # another rule + naming_rule_1 = influxframework.copy_doc(naming_rule) + naming_rule_1.prefix = "test-medium-" + naming_rule_1.conditions[0].value = "Medium" + naming_rule_1.insert() + + # default rule with low priority - should not get applied for rules + # with higher priority + naming_rule_2 = influxframework.copy_doc(naming_rule) + naming_rule_2.prefix = "test-low-" + naming_rule_2.priority = 0 + naming_rule_2.conditions = [] + naming_rule_2.insert() + + todo = influxframework.get_doc( + dict(doctype="ToDo", priority="High", description="Is this my name " + influxframework.generate_hash()) + ).insert() + + todo_1 = influxframework.get_doc( + dict(doctype="ToDo", priority="Medium", description="Is this my name " + influxframework.generate_hash()) + ).insert() + + todo_2 = influxframework.get_doc( + dict(doctype="ToDo", priority="Low", description="Is this my name " + influxframework.generate_hash()) + ).insert() + + try: + self.assertEqual(todo.name, "test-high-00001") + self.assertEqual(todo_1.name, "test-medium-00001") + self.assertEqual(todo_2.name, "test-low-00001") + finally: + naming_rule.delete() + naming_rule_1.delete() + naming_rule_2.delete() + todo.delete() + todo_1.delete() + todo_2.delete() diff --git a/influxframework/core/doctype/document_naming_rule_condition/__init__.py b/influxframework/core/doctype/document_naming_rule_condition/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/document_naming_rule_condition/document_naming_rule_condition.js b/influxframework/core/doctype/document_naming_rule_condition/document_naming_rule_condition.js new file mode 100644 index 0000000..213ec9d --- /dev/null +++ b/influxframework/core/doctype/document_naming_rule_condition/document_naming_rule_condition.js @@ -0,0 +1,7 @@ +// Copyright (c) 2020, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Document Naming Rule Condition", { + // refresh: function(frm) { + // } +}); diff --git a/influxframework/core/doctype/document_naming_rule_condition/document_naming_rule_condition.json b/influxframework/core/doctype/document_naming_rule_condition/document_naming_rule_condition.json new file mode 100644 index 0000000..781566b --- /dev/null +++ b/influxframework/core/doctype/document_naming_rule_condition/document_naming_rule_condition.json @@ -0,0 +1,49 @@ +{ + "actions": [], + "creation": "2020-09-08 10:17:54.366279", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "field", + "condition", + "value" + ], + "fields": [ + { + "fieldname": "field", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Field", + "reqd": 1 + }, + { + "fieldname": "condition", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Condition", + "options": "=\n!=\n>\n<\n>=\n<=", + "reqd": 1 + }, + { + "fieldname": "value", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Value", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-09-08 10:19:56.192949", + "modified_by": "Administrator", + "module": "Core", + "name": "Document Naming Rule Condition", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/document_naming_rule_condition/document_naming_rule_condition.py b/influxframework/core/doctype/document_naming_rule_condition/document_naming_rule_condition.py new file mode 100644 index 0000000..7767ed6 --- /dev/null +++ b/influxframework/core/doctype/document_naming_rule_condition/document_naming_rule_condition.py @@ -0,0 +1,9 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class DocumentNamingRuleCondition(Document): + pass diff --git a/influxframework/core/doctype/document_naming_rule_condition/test_document_naming_rule_condition.py b/influxframework/core/doctype/document_naming_rule_condition/test_document_naming_rule_condition.py new file mode 100644 index 0000000..f445cb0 --- /dev/null +++ b/influxframework/core/doctype/document_naming_rule_condition/test_document_naming_rule_condition.py @@ -0,0 +1,8 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestDocumentNamingRuleCondition(InfluxFrameworkTestCase): + pass diff --git a/influxframework/core/doctype/document_naming_settings/__init__.py b/influxframework/core/doctype/document_naming_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/document_naming_settings/document_naming_settings.js b/influxframework/core/doctype/document_naming_settings/document_naming_settings.js new file mode 100644 index 0000000..e8167e2 --- /dev/null +++ b/influxframework/core/doctype/document_naming_settings/document_naming_settings.js @@ -0,0 +1,70 @@ +// Copyright (c) 2022, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Document Naming Settings", { + refresh: function (frm) { + frm.trigger("setup_transaction_autocomplete"); + frm.disable_save(); + }, + + setup_transaction_autocomplete: function (frm) { + influxframework.call({ + method: "get_transactions_and_prefixes", + doc: frm.doc, + callback: function (r) { + frm.fields_dict.transaction_type.set_data(r.message.transactions); + frm.fields_dict.prefix.set_data(r.message.prefixes); + }, + }); + }, + + transaction_type: function (frm) { + frm.set_value("user_must_always_select", 0); + influxframework.call({ + method: "get_options", + doc: frm.doc, + callback: function (r) { + frm.set_value("naming_series_options", r.message); + if (r.message && r.message.split("\n")[0] == "") + frm.set_value("user_must_always_select", 1); + }, + }); + }, + + prefix: function (frm) { + influxframework.call({ + method: "get_current", + doc: frm.doc, + callback: function (r) { + frm.refresh_field("current_value"); + }, + }); + }, + + update: function (frm) { + influxframework.call({ + method: "update_series", + doc: frm.doc, + freeze: true, + freeze_msg: __("Updating naming series options"), + callback: function (r) { + frm.trigger("setup_transaction_autocomplete"); + frm.trigger("transaction_type"); + }, + }); + }, + + try_naming_series(frm) { + influxframework.call({ + method: "preview_series", + doc: frm.doc, + callback: function (r) { + if (!r.exc) { + frm.set_value("series_preview", r.message); + } else { + frm.set_value("series_preview", __("Failed to generate preview of series")); + } + }, + }); + }, +}); diff --git a/influxframework/core/doctype/document_naming_settings/document_naming_settings.json b/influxframework/core/doctype/document_naming_settings/document_naming_settings.json new file mode 100644 index 0000000..4c86b2e --- /dev/null +++ b/influxframework/core/doctype/document_naming_settings/document_naming_settings.json @@ -0,0 +1,133 @@ +{ + "actions": [], + "creation": "2022-05-30 07:24:07.736646", + "description": "Configure various aspects of how document naming works like naming series, current counter.", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "naming_series_tab", + "setup_series", + "transaction_type", + "naming_series_options", + "user_must_always_select", + "update", + "column_break_9", + "try_naming_series", + "series_preview", + "help_html", + "update_series", + "prefix", + "current_value", + "update_series_start" + ], + "fields": [ + { + "collapsible": 1, + "description": "Set Naming Series options on your transactions.", + "fieldname": "setup_series", + "fieldtype": "Section Break", + "label": "Setup Series for transactions" + }, + { + "depends_on": "transaction_type", + "fieldname": "help_html", + "fieldtype": "HTML", + "label": "Help HTML", + "options": "
    \n Edit list of Series in the box. Rules:\n
      \n
    • Each Series Prefix on a new line.
    • \n
    • Allowed special characters are \"/\" and \"-\"
    • \n
    • \n Optionally, set the number of digits in the series using dot (.)\n followed by hashes (#). For example, \".####\" means that the series\n will have four digits. Default is five digits.\n
    • \n
    • \n You can also use variables in the series name by putting them\n between (.) dots\n
      \n Supported Variables:\n
        \n
      • .YYYY. - Year in 4 digits
      • \n
      • .YY. - Year in 2 digits
      • \n
      • .MM. - Month
      • \n
      • .DD. - Day of month
      • \n
      • .WW. - Week of the year
      • \n
      • .FY. - Fiscal Year
      • \n
      • \n .{fieldname}. - fieldname on the document e.g.\n branch\n
      • \n
      \n
    • \n
    \n Examples:\n
      \n
    • INV-
    • \n
    • INV-10-
    • \n
    • INVK-
    • \n
    • INV-.YYYY.-.{branch}.-.MM.-.####
    • \n
    \n
    \n
    \n" + }, + { + "default": "0", + "depends_on": "transaction_type", + "description": "Check this if you want to force the user to select a series before saving. There will be no default if you check this.", + "fieldname": "user_must_always_select", + "fieldtype": "Check", + "label": "User must always select" + }, + { + "depends_on": "transaction_type", + "fieldname": "update", + "fieldtype": "Button", + "label": "Update" + }, + { + "collapsible": 1, + "description": "Change the starting / current sequence number of an existing series.
    \n\nWarning: Incorrectly updating counters can prevent documents from getting created. ", + "fieldname": "update_series", + "fieldtype": "Section Break", + "label": "Update Series Counter" + }, + { + "fieldname": "prefix", + "fieldtype": "Autocomplete", + "label": "Prefix" + }, + { + "description": "This is the number of the last created transaction with this prefix", + "fieldname": "current_value", + "fieldtype": "Int", + "label": "Current Value" + }, + { + "fieldname": "update_series_start", + "fieldtype": "Button", + "label": "Update Series Number", + "options": "update_series_start" + }, + { + "depends_on": "transaction_type", + "fieldname": "naming_series_options", + "fieldtype": "Text", + "label": "Series List for this Transaction" + }, + { + "depends_on": "transaction_type", + "description": "Generate 3 preview of names generate by any valid series.", + "fieldname": "try_naming_series", + "fieldtype": "Data", + "label": "Try a naming Series" + }, + { + "fieldname": "transaction_type", + "fieldtype": "Autocomplete", + "label": "Select Transaction" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "naming_series_tab", + "fieldtype": "Tab Break", + "label": "Naming Series" + }, + { + "fieldname": "series_preview", + "fieldtype": "Text", + "label": "Preview of generated names", + "read_only": 1 + } + ], + "hide_toolbar": 1, + "icon": "fa fa-sort-by-order", + "issingle": 1, + "links": [], + "modified": "2022-05-30 23:51:36.136535", + "modified_by": "Administrator", + "module": "Core", + "name": "Document Naming Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} diff --git a/influxframework/core/doctype/document_naming_settings/document_naming_settings.py b/influxframework/core/doctype/document_naming_settings/document_naming_settings.py new file mode 100644 index 0000000..5ff4fb0 --- /dev/null +++ b/influxframework/core/doctype/document_naming_settings/document_naming_settings.py @@ -0,0 +1,222 @@ +# Copyright (c) 2022, InfluxFramework LLC +# For license information, please see license.txt + + +import influxframework +from influxframework import _ +from influxframework.core.doctype.doctype.doctype import validate_series +from influxframework.model.document import Document +from influxframework.model.naming import NamingSeries +from influxframework.permissions import get_doctypes_with_read + + +class NamingSeriesNotSetError(influxframework.ValidationError): + pass + + +class DocumentNamingSettings(Document): + @influxframework.whitelist() + def get_transactions_and_prefixes(self): + + transactions = self._get_transactions() + prefixes = self._get_prefixes(transactions) + + return {"transactions": transactions, "prefixes": prefixes} + + def _get_transactions(self) -> list[str]: + + readable_doctypes = set(get_doctypes_with_read()) + + standard = influxframework.get_all("DocField", {"fieldname": "naming_series"}, "parent", pluck="parent") + custom = influxframework.get_all("Custom Field", {"fieldname": "naming_series"}, "dt", pluck="dt") + + return sorted(readable_doctypes.intersection(standard + custom)) + + def _get_prefixes(self, doctypes) -> list[str]: + """Get all prefixes for naming series. + + - For all templates prefix is evaluated considering today's date + - All existing prefix in DB are shared as is. + """ + series_templates = set() + for d in doctypes: + try: + options = influxframework.get_meta(d).get_naming_series_options() + series_templates.update(options) + except influxframework.DoesNotExistError: + influxframework.msgprint(_("Unable to find DocType {0}").format(d)) + continue + + custom_templates = influxframework.get_all( + "DocType", + fields=["autoname"], + filters={ + "name": ("not in", doctypes), + "autoname": ("like", "%.#%"), + "module": ("not in", ["Core"]), + }, + ) + if custom_templates: + series_templates.update([d.autoname.rsplit(".", 1)[0] for d in custom_templates]) + + return self._evaluate_and_clean_templates(series_templates) + + def _evaluate_and_clean_templates(self, series_templates: set[str]) -> list[str]: + evalauted_prefix = set() + + series = influxframework.qb.DocType("Series") + prefixes_from_db = influxframework.qb.from_(series).select(series.name).run(pluck=True) + evalauted_prefix.update(prefixes_from_db) + + for series_template in series_templates: + try: + prefix = NamingSeries(series_template).get_prefix() + if "{" in prefix: + # fieldnames can't be evalauted, rely on data in DB instead + continue + evalauted_prefix.add(prefix) + except Exception: + influxframework.clear_last_message() + influxframework.log_error(f"Invalid naming series {series_template}") + + return sorted(evalauted_prefix) + + def get_options_list(self, options: str) -> list[str]: + return [op.strip() for op in options.split("\n") if op.strip()] + + @influxframework.whitelist() + def update_series(self): + """update series list""" + self.validate_set_series() + self.check_duplicate() + self.set_series_options_in_meta(self.transaction_type, self.naming_series_options) + + influxframework.msgprint( + _("Series Updated for {}").format(self.transaction_type), alert=True, indicator="green" + ) + + def validate_set_series(self): + if self.transaction_type and not self.naming_series_options: + influxframework.throw(_("Please set the series to be used.")) + + def set_series_options_in_meta(self, doctype: str, options: str) -> None: + options = self.get_options_list(options) + + # validate names + for series in options: + self.validate_series_name(series) + + if options and self.user_must_always_select: + options = [""] + options + + default = options[0] if options else "" + + option_string = "\n".join(options) + + # Erase default first, it might not be in new options. + self.update_naming_series_property_setter(doctype, "default", "") + self.update_naming_series_property_setter(doctype, "options", option_string) + self.update_naming_series_property_setter(doctype, "default", default) + + self.naming_series_options = option_string + + influxframework.clear_cache(doctype=doctype) + + def update_naming_series_property_setter(self, doctype, property, value): + from influxframework.custom.doctype.property_setter.property_setter import make_property_setter + + make_property_setter(doctype, "naming_series", property, value, "Text") + + def check_duplicate(self): + def stripped_series(s: str) -> str: + return s.strip().rstrip("#") + + standard = influxframework.get_all("DocField", {"fieldname": "naming_series"}, "parent", pluck="parent") + custom = influxframework.get_all("Custom Field", {"fieldname": "naming_series"}, "dt", pluck="dt") + + all_doctypes_with_naming_series = set(standard + custom) + all_doctypes_with_naming_series.remove(self.transaction_type) + + existing_series = {} + for doctype in all_doctypes_with_naming_series: + for series in influxframework.get_meta(doctype).get_naming_series_options(): + existing_series[stripped_series(series)] = doctype + + dt = influxframework.get_doc("DocType", self.transaction_type) + + options = self.get_options_list(self.naming_series_options) + for series in options: + if stripped_series(series) in existing_series: + influxframework.throw(_("Series {0} already used in {1}").format(series, existing_series[series])) + validate_series(dt, series) + + def validate_series_name(self, series): + NamingSeries(series).validate() + + @influxframework.whitelist() + def get_options(self, doctype=None): + doctype = doctype or self.transaction_type + if not doctype: + return + + if influxframework.get_meta(doctype or self.transaction_type).get_field("naming_series"): + return influxframework.get_meta(doctype or self.transaction_type).get_field("naming_series").options + + @influxframework.whitelist() + def get_current(self): + """get series current""" + if self.prefix: + self.current_value = NamingSeries(self.prefix).get_current_value() + return self.current_value + + @influxframework.whitelist() + def update_series_start(self): + influxframework.only_for("System Manager") + + if not self.prefix: + influxframework.throw(_("Please select prefix first")) + + naming_series = NamingSeries(self.prefix) + previous_value = naming_series.get_current_value() + naming_series.update_counter(self.current_value) + + self.create_version_log_for_change( + naming_series.get_prefix(), previous_value, self.current_value + ) + + influxframework.msgprint( + _("Series counter for {} updated to {} successfully").format(self.prefix, self.current_value), + alert=True, + indicator="green", + ) + + def create_version_log_for_change(self, series, old, new): + version = influxframework.new_doc("Version") + version.ref_doctype = "Series" + version.docname = series + version.data = influxframework.as_json({"changed": [["current", old, new]]}) + version.flags.ignore_links = True # series is not a "real" doctype + version.flags.ignore_permissions = True + version.insert() + + @influxframework.whitelist() + def preview_series(self) -> str: + """Preview what the naming series will generate.""" + + series = self.try_naming_series + if not series: + return "" + try: + doc = self._fetch_last_doc_if_available() + return "\n".join(NamingSeries(series).get_preview(doc=doc)) + except Exception as e: + if influxframework.message_log: + influxframework.message_log.pop() + return _("Failed to generate names from the series") + f"\n{str(e)}" + + def _fetch_last_doc_if_available(self): + """Fetch last doc for evaluating naming series with fields.""" + try: + return influxframework.get_last_doc(self.transaction_type) + except Exception: + return None diff --git a/influxframework/core/doctype/document_naming_settings/test_document_naming_settings.py b/influxframework/core/doctype/document_naming_settings/test_document_naming_settings.py new file mode 100644 index 0000000..85c40e3 --- /dev/null +++ b/influxframework/core/doctype/document_naming_settings/test_document_naming_settings.py @@ -0,0 +1,65 @@ +# Copyright (c) 2022, InfluxFramework LLC +# See license.txt + +import influxframework +from influxframework.core.doctype.document_naming_settings.document_naming_settings import ( + DocumentNamingSettings, +) +from influxframework.model.naming import NamingSeries, get_default_naming_series +from influxframework.tests.utils import InfluxFrameworkTestCase +from influxframework.utils import cint + + +class TestNamingSeries(InfluxFrameworkTestCase): + def setUp(self): + self.dns: DocumentNamingSettings = influxframework.get_doc("Document Naming Settings") + + def tearDown(self): + influxframework.db.rollback() + + def get_valid_serieses(self): + VALID_SERIES = ["SINV-", "SI-.{field}.", "SI-#.###", ""] + exisiting_series = self.dns.get_transactions_and_prefixes()["prefixes"] + return VALID_SERIES + exisiting_series + + def test_naming_preview(self): + self.dns.transaction_type = "Webhook" + + self.dns.try_naming_series = "AXBZ.####" + serieses = self.dns.preview_series().split("\n") + self.assertEqual(["AXBZ0001", "AXBZ0002", "AXBZ0003"], serieses) + + self.dns.try_naming_series = "AXBZ-.{currency}.-" + serieses = self.dns.preview_series().split("\n") + + def test_get_transactions(self): + + naming_info = self.dns.get_transactions_and_prefixes() + self.assertIn("Webhook", naming_info["transactions"]) + + existing_naming_series = influxframework.get_meta("Webhook").get_field("naming_series").options + + for series in existing_naming_series.split("\n"): + self.assertIn(NamingSeries(series).get_prefix(), naming_info["prefixes"]) + + def test_default_naming_series(self): + self.assertIn("HOOK", get_default_naming_series("Webhook")) + self.assertIsNone(get_default_naming_series("DocType")) + + def test_updates_naming_options(self): + self.dns.transaction_type = "Webhook" + test_series = "KOOHBEW.###" + self.dns.naming_series_options = self.dns.get_options() + "\n" + test_series + self.dns.update_series() + self.assertIn(test_series, influxframework.get_meta("Webhook").get_naming_series_options()) + + def test_update_series_counter(self): + for series in self.get_valid_serieses(): + if not series: + continue + self.dns.prefix = series + current_count = cint(self.dns.get_current()) + new_count = self.dns.current_value = current_count + 1 + self.dns.update_series_start() + + self.assertEqual(self.dns.get_current(), new_count, f"Incorrect update for {series}") diff --git a/influxframework/core/doctype/document_share_key/__init__.py b/influxframework/core/doctype/document_share_key/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/document_share_key/document_share_key.js b/influxframework/core/doctype/document_share_key/document_share_key.js new file mode 100644 index 0000000..0e0ce35 --- /dev/null +++ b/influxframework/core/doctype/document_share_key/document_share_key.js @@ -0,0 +1,7 @@ +// Copyright (c) 2021, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Document Share Key", { + // refresh: function(frm) { + // } +}); diff --git a/influxframework/core/doctype/document_share_key/document_share_key.json b/influxframework/core/doctype/document_share_key/document_share_key.json new file mode 100644 index 0000000..b96fe09 --- /dev/null +++ b/influxframework/core/doctype/document_share_key/document_share_key.json @@ -0,0 +1,73 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "hash", + "creation": "2022-01-14 13:40:49.487646", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "reference_doctype", + "reference_docname", + "key", + "expires_on" + ], + "fields": [ + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "label": "Reference Document Type", + "options": "DocType", + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "reference_docname", + "fieldtype": "Dynamic Link", + "label": "Reference Document Name", + "options": "reference_doctype", + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "key", + "fieldtype": "Data", + "label": "Key", + "read_only": 1 + }, + { + "fieldname": "expires_on", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Expires On", + "read_only": 1 + } + ], + "in_create": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2022-01-14 13:57:28.050678", + "modified_by": "Administrator", + "module": "Core", + "name": "Document Share Key", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/influxframework/core/doctype/document_share_key/document_share_key.py b/influxframework/core/doctype/document_share_key/document_share_key.py new file mode 100644 index 0000000..a027b26 --- /dev/null +++ b/influxframework/core/doctype/document_share_key/document_share_key.py @@ -0,0 +1,20 @@ +# Copyright (c) 2021, InfluxFramework LLC +# For license information, please see license.txt + +from random import randrange + +import influxframework +from influxframework.model.document import Document + + +class DocumentShareKey(Document): + def before_insert(self): + self.key = influxframework.generate_hash(length=randrange(25, 35)) + if not self.expires_on and not self.flags.no_expiry: + self.expires_on = influxframework.utils.add_days( + None, days=influxframework.get_system_settings("document_share_key_expiry") or 90 + ) + + +def is_expired(expires_on): + return expires_on and expires_on < influxframework.utils.getdate() diff --git a/influxframework/core/doctype/document_share_key/test_document_share_key.py b/influxframework/core/doctype/document_share_key/test_document_share_key.py new file mode 100644 index 0000000..97e2ca4 --- /dev/null +++ b/influxframework/core/doctype/document_share_key/test_document_share_key.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, InfluxFramework LLC +# See license.txt + +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestDocumentShareKey(InfluxFrameworkTestCase): + pass diff --git a/influxframework/core/doctype/domain/__init__.py b/influxframework/core/doctype/domain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/domain/domain.js b/influxframework/core/doctype/domain/domain.js new file mode 100644 index 0000000..2b91bf3 --- /dev/null +++ b/influxframework/core/doctype/domain/domain.js @@ -0,0 +1,6 @@ +// Copyright (c) 2017, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Domain", { + refresh: function (frm) {}, +}); diff --git a/influxframework/core/doctype/domain/domain.json b/influxframework/core/doctype/domain/domain.json new file mode 100644 index 0000000..a6c7397 --- /dev/null +++ b/influxframework/core/doctype/domain/domain.json @@ -0,0 +1,54 @@ +{ + "autoname": "field:domain", + "creation": "2017-05-03 15:07:39.752820", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "domain" + ], + "fields": [ + { + "fieldname": "domain", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Domain", + "reqd": 1, + "unique": 1 + } + ], + "modified": "2020-09-18 17:26:09.703215", + "modified_by": "Administrator", + "module": "Core", + "name": "Domain", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "search_fields": "domain", + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "domain" +} \ No newline at end of file diff --git a/influxframework/core/doctype/domain/domain.py b/influxframework/core/doctype/domain/domain.py new file mode 100644 index 0000000..a5fe522 --- /dev/null +++ b/influxframework/core/doctype/domain/domain.py @@ -0,0 +1,130 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.custom.doctype.custom_field.custom_field import create_custom_fields +from influxframework.model.document import Document + + +class Domain(Document): + """Domain documents are created automatically when DocTypes + with "Restricted" domains are imported during + installation or migration""" + + def setup_domain(self): + """Setup domain icons, permissions, custom fields etc.""" + self.setup_data() + self.setup_roles() + self.setup_properties() + self.set_values() + + if not int(influxframework.defaults.get_defaults().setup_complete or 0): + # if setup not complete, setup desktop etc. + self.setup_sidebar_items() + self.set_default_portal_role() + + if self.data.custom_fields: + create_custom_fields(self.data.custom_fields) + + if self.data.on_setup: + # custom on_setup method + influxframework.get_attr(self.data.on_setup)() + + def remove_domain(self): + """Unset domain settings""" + self.setup_data() + + if self.data.restricted_roles: + for role_name in self.data.restricted_roles: + if influxframework.db.exists("Role", role_name): + role = influxframework.get_doc("Role", role_name) + role.disabled = 1 + role.save() + + self.remove_custom_field() + + def remove_custom_field(self): + """Remove custom_fields when disabling domain""" + if self.data.custom_fields: + for doctype in self.data.custom_fields: + custom_fields = self.data.custom_fields[doctype] + + # custom_fields can be a list or dict + if isinstance(custom_fields, dict): + custom_fields = [custom_fields] + + for custom_field_detail in custom_fields: + custom_field_name = influxframework.db.get_value( + "Custom Field", dict(dt=doctype, fieldname=custom_field_detail.get("fieldname")) + ) + if custom_field_name: + influxframework.delete_doc("Custom Field", custom_field_name) + + def setup_roles(self): + """Enable roles that are restricted to this domain""" + if self.data.restricted_roles: + user = influxframework.get_doc("User", influxframework.session.user) + for role_name in self.data.restricted_roles: + user.append("roles", {"role": role_name}) + if not influxframework.db.get_value("Role", role_name): + influxframework.get_doc(dict(doctype="Role", role_name=role_name)).insert() + continue + + role = influxframework.get_doc("Role", role_name) + role.disabled = 0 + role.save() + user.save() + + def setup_data(self, domain=None): + """Load domain info via hooks""" + self.data = influxframework.get_domain_data(self.name) + + def get_domain_data(self, module): + return influxframework.get_attr(influxframework.get_hooks("domains")[self.name] + ".data") + + def set_default_portal_role(self): + """Set default portal role based on domain""" + if self.data.get("default_portal_role"): + influxframework.db.set_value( + "Portal Settings", None, "default_role", self.data.get("default_portal_role") + ) + + def setup_properties(self): + if self.data.properties: + for args in self.data.properties: + influxframework.make_property_setter(args) + + def set_values(self): + """set values based on `data.set_value`""" + if self.data.set_value: + for args in self.data.set_value: + influxframework.reload_doctype(args[0]) + doc = influxframework.get_doc(args[0], args[1] or args[0]) + doc.set(args[2], args[3]) + doc.save() + + def setup_sidebar_items(self): + """Enable / disable sidebar items""" + if self.data.allow_sidebar_items: + # disable all + influxframework.db.sql("update `tabPortal Menu Item` set enabled=0") + + # enable + influxframework.db.sql( + """update `tabPortal Menu Item` set enabled=1 + where route in ({})""".format( + ", ".join(f'"{d}"' for d in self.data.allow_sidebar_items) + ) + ) + + if self.data.remove_sidebar_items: + # disable all + influxframework.db.sql("update `tabPortal Menu Item` set enabled=1") + + # enable + influxframework.db.sql( + """update `tabPortal Menu Item` set enabled=0 + where route in ({})""".format( + ", ".join(f'"{d}"' for d in self.data.remove_sidebar_items) + ) + ) diff --git a/influxframework/core/doctype/domain/test_domain.py b/influxframework/core/doctype/domain/test_domain.py new file mode 100644 index 0000000..bf45a08 --- /dev/null +++ b/influxframework/core/doctype/domain/test_domain.py @@ -0,0 +1,7 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestDomain(InfluxFrameworkTestCase): + pass diff --git a/influxframework/core/doctype/domain_settings/__init__.py b/influxframework/core/doctype/domain_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/domain_settings/domain_settings.js b/influxframework/core/doctype/domain_settings/domain_settings.js new file mode 100644 index 0000000..d936e6d --- /dev/null +++ b/influxframework/core/doctype/domain_settings/domain_settings.js @@ -0,0 +1,69 @@ +// Copyright (c) 2017, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Domain Settings", { + before_load: function (frm) { + if (!frm.domains_multicheck) { + frm.domains_multicheck = influxframework.ui.form.make_control({ + parent: frm.fields_dict.domains_html.$wrapper, + df: { + fieldname: "domains_multicheck", + fieldtype: "MultiCheck", + get_data: () => { + let active_domains = (frm.doc.active_domains || []).map( + (row) => row.domain + ); + return influxframework.boot.all_domains.map((domain) => { + return { + label: domain, + value: domain, + checked: active_domains.includes(domain), + }; + }); + }, + on_change: () => { + frm.dirty(); + }, + }, + render_input: true, + }); + frm.domains_multicheck.refresh_input(); + } + }, + + validate: function (frm) { + frm.trigger("set_options_in_table"); + }, + + set_options_in_table: function (frm) { + let selected_options = frm.domains_multicheck.get_value(); + let unselected_options = frm.domains_multicheck.options + .map((option) => option.value) + .filter((value) => { + return !selected_options.includes(value); + }); + + let map = {}, + list = []; + (frm.doc.active_domains || []).map((row) => { + map[row.domain] = row.name; + list.push(row.domain); + }); + + unselected_options.map((option) => { + if (list.includes(option)) { + influxframework.model.clear_doc("Has Domain", map[option]); + } + }); + + selected_options.map((option) => { + if (!list.includes(option)) { + influxframework.model.clear_doc("Has Domain", map[option]); + let row = influxframework.model.add_child(frm.doc, "Has Domain", "active_domains"); + row.domain = option; + } + }); + + refresh_field("active_domains"); + }, +}); diff --git a/influxframework/core/doctype/domain_settings/domain_settings.json b/influxframework/core/doctype/domain_settings/domain_settings.json new file mode 100644 index 0000000..8efd296 --- /dev/null +++ b/influxframework/core/doctype/domain_settings/domain_settings.json @@ -0,0 +1,153 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2017-05-03 16:28:11.295095", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "active_domains_sb", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Active Domains", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "domains_html", + "fieldtype": "HTML", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Domains HTML", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "active_domains", + "fieldtype": "Table", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Active Domains", + "length": 0, + "no_copy": 0, + "options": "Has Domain", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 1, + "istable": 0, + "max_attachments": 0, + "modified": "2017-12-05 17:36:46.842134", + "modified_by": "Administrator", + "module": "Core", + "name": "Domain Settings", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 0, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/influxframework/core/doctype/domain_settings/domain_settings.py b/influxframework/core/doctype/domain_settings/domain_settings.py new file mode 100644 index 0000000..4896179 --- /dev/null +++ b/influxframework/core/doctype/domain_settings/domain_settings.py @@ -0,0 +1,90 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document + + +class DomainSettings(Document): + def set_active_domains(self, domains): + active_domains = [d.domain for d in self.active_domains] + added = False + for d in domains: + if not d in active_domains: + self.append("active_domains", dict(domain=d)) + added = True + + if added: + self.save() + + def on_update(self): + for i, d in enumerate(self.active_domains): + # set the flag to update the the desktop icons of all domains + if i >= 1: + influxframework.flags.keep_desktop_icons = True + domain = influxframework.get_doc("Domain", d.domain) + domain.setup_domain() + + self.restrict_roles_and_modules() + influxframework.clear_cache() + + def restrict_roles_and_modules(self): + """Disable all restricted roles and set `restrict_to_domain` property in Module Def""" + active_domains = influxframework.get_active_domains() + all_domains = list(influxframework.get_hooks("domains") or {}) + + def remove_role(role): + influxframework.db.delete("Has Role", {"role": role}) + influxframework.set_value("Role", role, "disabled", 1) + + for domain in all_domains: + data = influxframework.get_domain_data(domain) + if not influxframework.db.get_value("Domain", domain): + influxframework.get_doc(dict(doctype="Domain", domain=domain)).insert() + if "modules" in data: + for module in data.get("modules"): + influxframework.db.set_value("Module Def", module, "restrict_to_domain", domain) + + if "restricted_roles" in data: + for role in data["restricted_roles"]: + if not influxframework.db.get_value("Role", role): + influxframework.get_doc(dict(doctype="Role", role_name=role)).insert() + influxframework.db.set_value("Role", role, "restrict_to_domain", domain) + + if domain not in active_domains: + remove_role(role) + + if "custom_fields" in data: + if domain not in active_domains: + inactive_domain = influxframework.get_doc("Domain", domain) + inactive_domain.setup_data() + inactive_domain.remove_custom_field() + + +def get_active_domains(): + """get the domains set in the Domain Settings as active domain""" + + def _get_active_domains(): + domains = influxframework.get_all( + "Has Domain", filters={"parent": "Domain Settings"}, fields=["domain"], distinct=True + ) + + active_domains = [row.get("domain") for row in domains] + active_domains.append("") + return active_domains + + return influxframework.cache().get_value("active_domains", _get_active_domains) + + +def get_active_modules(): + """get the active modules from Module Def""" + + def _get_active_modules(): + active_modules = [] + active_domains = get_active_domains() + for m in influxframework.get_all("Module Def", fields=["name", "restrict_to_domain"]): + if (not m.restrict_to_domain) or (m.restrict_to_domain in active_domains): + active_modules.append(m.name) + return active_modules + + return influxframework.cache().get_value("active_modules", _get_active_modules) diff --git a/influxframework/core/doctype/dynamic_link/__init__.py b/influxframework/core/doctype/dynamic_link/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/dynamic_link/dynamic_link.json b/influxframework/core/doctype/dynamic_link/dynamic_link.json new file mode 100644 index 0000000..b99f77f --- /dev/null +++ b/influxframework/core/doctype/dynamic_link/dynamic_link.json @@ -0,0 +1,47 @@ +{ + "creation": "2017-01-13 04:55:18.835023", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "link_doctype", + "link_name", + "link_title" + ], + "fields": [ + { + "fieldname": "link_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Link Document Type", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "link_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Link Name", + "options": "link_doctype", + "reqd": 1 + }, + { + "fieldname": "link_title", + "fieldtype": "Read Only", + "in_list_view": 1, + "label": "Link Title", + "read_only": 1 + } + ], + "istable": 1, + "modified": "2019-10-10 22:05:54.736093", + "modified_by": "Administrator", + "module": "Core", + "name": "Dynamic Link", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/dynamic_link/dynamic_link.py b/influxframework/core/doctype/dynamic_link/dynamic_link.py new file mode 100644 index 0000000..b868768 --- /dev/null +++ b/influxframework/core/doctype/dynamic_link/dynamic_link.py @@ -0,0 +1,28 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document + + +class DynamicLink(Document): + pass + + +def on_doctype_update(): + influxframework.db.add_index("Dynamic Link", ["link_doctype", "link_name"]) + + +def deduplicate_dynamic_links(doc): + links, duplicate = [], False + for l in doc.links or []: + t = (l.link_doctype, l.link_name) + if not t in links: + links.append(t) + else: + duplicate = True + + if duplicate: + doc.links = [] + for l in links: + doc.append("links", dict(link_doctype=l[0], link_name=l[1])) diff --git a/influxframework/core/doctype/error_log/__init__.py b/influxframework/core/doctype/error_log/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/error_log/error_log.js b/influxframework/core/doctype/error_log/error_log.js new file mode 100644 index 0000000..d1a2ccc --- /dev/null +++ b/influxframework/core/doctype/error_log/error_log.js @@ -0,0 +1,17 @@ +// Copyright (c) 2022, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Error Log", { + refresh: function (frm) { + frm.disable_save(); + + if (frm.doc.reference_doctype && frm.doc.reference_name) { + frm.add_custom_button(__("Show Related Errors"), function () { + influxframework.set_route("List", "Error Log", { + reference_doctype: frm.doc.reference_doctype, + reference_name: frm.doc.reference_name, + }); + }); + } + }, +}); diff --git a/influxframework/core/doctype/error_log/error_log.json b/influxframework/core/doctype/error_log/error_log.json new file mode 100644 index 0000000..2ee86bd --- /dev/null +++ b/influxframework/core/doctype/error_log/error_log.json @@ -0,0 +1,88 @@ +{ + "actions": [], + "creation": "2013-01-16 13:09:40", + "doctype": "DocType", + "document_type": "System", + "engine": "MyISAM", + "field_order": [ + "seen", + "reference_doctype", + "column_break_3", + "reference_name", + "section_break_5", + "method", + "error" + ], + "fields": [ + { + "default": "0", + "fieldname": "seen", + "fieldtype": "Check", + "hidden": 1, + "label": "Seen" + }, + { + "fieldname": "method", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Title", + "read_only": 1 + }, + { + "fieldname": "error", + "fieldtype": "Code", + "label": "Error", + "read_only": 1 + }, + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Reference DocType", + "options": "DocType", + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "reference_name", + "fieldtype": "Data", + "label": "Reference Name", + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_5", + "fieldtype": "Section Break" + } + ], + "icon": "fa fa-warning-sign", + "idx": 1, + "in_create": 1, + "links": [], + "modified": "2022-06-13 06:34:05.158606", + "modified_by": "Administrator", + "module": "Core", + "name": "Error Log", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "ASC", + "states": [], + "title_field": "method" +} \ No newline at end of file diff --git a/influxframework/core/doctype/error_log/error_log.py b/influxframework/core/doctype/error_log/error_log.py new file mode 100644 index 0000000..6c05ad4 --- /dev/null +++ b/influxframework/core/doctype/error_log/error_log.py @@ -0,0 +1,26 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document +from influxframework.query_builder import Interval +from influxframework.query_builder.functions import Now + + +class ErrorLog(Document): + def onload(self): + if not self.seen and not influxframework.flags.read_only: + self.db_set("seen", 1, update_modified=0) + influxframework.db.commit() + + @staticmethod + def clear_old_logs(days=30): + table = influxframework.qb.DocType("Error Log") + influxframework.db.delete(table, filters=(table.modified < (Now() - Interval(days=days)))) + + +@influxframework.whitelist() +def clear_error_logs(): + """Flush all Error Logs""" + influxframework.only_for("System Manager") + influxframework.db.truncate("Error Log") diff --git a/influxframework/core/doctype/error_log/error_log_list.js b/influxframework/core/doctype/error_log/error_log_list.js new file mode 100644 index 0000000..1f4be59 --- /dev/null +++ b/influxframework/core/doctype/error_log/error_log_list.js @@ -0,0 +1,25 @@ +influxframework.listview_settings["Error Log"] = { + add_fields: ["seen"], + get_indicator: function (doc) { + if (cint(doc.seen)) { + return [__("Seen"), "green", "seen,=,1"]; + } else { + return [__("Not Seen"), "red", "seen,=,0"]; + } + }, + order_by: "seen asc, modified desc", + onload: function (listview) { + listview.page.add_menu_item(__("Clear Error Logs"), function () { + influxframework.call({ + method: "influxframework.core.doctype.error_log.error_log.clear_error_logs", + callback: function () { + listview.refresh(); + }, + }); + }); + + influxframework.require("logtypes.bundle.js", () => { + influxframework.utils.logtypes.show_log_retention_message(cur_list.doctype); + }); + }, +}; diff --git a/influxframework/core/doctype/error_log/test_error_log.py b/influxframework/core/doctype/error_log/test_error_log.py new file mode 100644 index 0000000..b85d6f0 --- /dev/null +++ b/influxframework/core/doctype/error_log/test_error_log.py @@ -0,0 +1,14 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + +# test_records = influxframework.get_test_records('Error Log') + + +class TestErrorLog(InfluxFrameworkTestCase): + def test_error_log(self): + """let's do an error log on error log?""" + doc = influxframework.new_doc("Error Log") + error = doc.log_error("This is an error") + self.assertEqual(error.doctype, "Error Log") diff --git a/influxframework/core/doctype/error_snapshot/__init__.py b/influxframework/core/doctype/error_snapshot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/error_snapshot/error_object.html b/influxframework/core/doctype/error_snapshot/error_object.html new file mode 100644 index 0000000..450bfac --- /dev/null +++ b/influxframework/core/doctype/error_snapshot/error_object.html @@ -0,0 +1,12 @@ +{% if (Object.prototype.toString.call(x) === "[object Object]") { %} + + {% for (var key in x) { %} + + + + + {% } %} +
    {{ key }}{{ x[key] }}
    +{% } else { %} + {{ x }} +{% } %} diff --git a/influxframework/core/doctype/error_snapshot/error_snapshot.html b/influxframework/core/doctype/error_snapshot/error_snapshot.html new file mode 100644 index 0000000..c568134 --- /dev/null +++ b/influxframework/core/doctype/error_snapshot/error_snapshot.html @@ -0,0 +1,77 @@ + +{% function id(){ return id._old_id++; }; id._old_id = 0; %} +

    {{ __("Error Report") }}

    +

    {{ doc.pyver }}

    +
    +
    {{ __("Timestamp") }}:
    +
    {{ doc.timestamp }}
    +
    {{ __("Relapsed") }}
    +
    {{ doc.relapses }}
    +
    + +

    {{ __("Exception") }}

    +{{ influxframework.render_template("error_object", {x: JSON.parse(doc.exception)}) }} + +

    {{ __("Locals") }}

    +{{ influxframework.render_template("error_object", {x: JSON.parse(doc.locals)} )}} + +

    {{ __("Traceback") }}

    +{% var frames = JSON.parse(doc.frames); %} +{% for (var i in frames) { %} + {% var frameid = id(), frame = frames[i] %} +

    {{ frame.file }}: {{ frame.lnum }} +

    +
    +
    + {% for (var index in frame.lines) { %} + {% var line = frame.lines[index] %} +
    + {{ index }} + {{ line }} +
    + {% } %} +
    +
    + + {{ __("Locals") }} + +
    +
    +
    +
    +
    +

    {{ __("Locals") }}

    + {{ influxframework.render_template("error_object", {x: frame.dump }) }} +
    +
    +

    +{% } %} diff --git a/influxframework/core/doctype/error_snapshot/error_snapshot.js b/influxframework/core/doctype/error_snapshot/error_snapshot.js new file mode 100644 index 0000000..b998a28 --- /dev/null +++ b/influxframework/core/doctype/error_snapshot/error_snapshot.js @@ -0,0 +1,20 @@ +influxframework.ui.form.on("Error Snapshot", "load", function (frm) { + frm.set_read_only(true); +}); + +influxframework.ui.form.on("Error Snapshot", "refresh", function (frm) { + frm.set_df_property( + "view", + "options", + influxframework.render_template("error_snapshot", { doc: frm.doc }) + ); + + if (frm.doc.relapses) { + frm.add_custom_button(__("Show Relapses"), function () { + influxframework.route_options = { + parent_error_snapshot: frm.doc.name, + }; + influxframework.set_route("List", "Error Snapshot"); + }); + } +}); diff --git a/influxframework/core/doctype/error_snapshot/error_snapshot.json b/influxframework/core/doctype/error_snapshot/error_snapshot.json new file mode 100644 index 0000000..1333fe0 --- /dev/null +++ b/influxframework/core/doctype/error_snapshot/error_snapshot.json @@ -0,0 +1,398 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2015-11-28 00:57:39.766888", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "System", + "editable_grid": 0, + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "view", + "fieldtype": "HTML", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Snapshot View", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "seen", + "fieldtype": "Check", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 1, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Seen", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "evalue", + "fieldtype": "Code", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Friendly Title", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "timestamp", + "fieldtype": "Datetime", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Timestamp", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "1", + "fieldname": "relapses", + "fieldtype": "Int", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Relapses", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "etype", + "fieldtype": "Data", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Exception Type", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "traceback", + "fieldtype": "Code", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Traceback", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "parent_error_snapshot", + "fieldtype": "Data", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Parent Error Snapshot", + "length": 0, + "no_copy": 0, + "options": "", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "pyver", + "fieldtype": "Code", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Pyver", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "exception", + "fieldtype": "Code", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Exception", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "locals", + "fieldtype": "Code", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Locals", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "frames", + "fieldtype": "Code", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Frames", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 1, + + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2021-10-25 14:40:38.619106", + "modified_by": "Administrator", + "module": "Core", + "name": "Error Snapshot", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "is_custom": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "sort_field": "timestamp", + "sort_order": "DESC", + "title_field": "evalue", + "track_seen": 0 +} diff --git a/influxframework/core/doctype/error_snapshot/error_snapshot.py b/influxframework/core/doctype/error_snapshot/error_snapshot.py new file mode 100644 index 0000000..c48041a --- /dev/null +++ b/influxframework/core/doctype/error_snapshot/error_snapshot.py @@ -0,0 +1,40 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document +from influxframework.query_builder import Interval +from influxframework.query_builder.functions import Now + + +class ErrorSnapshot(Document): + no_feed_on_delete = True + + def onload(self): + if not self.parent_error_snapshot: + self.db_set("seen", True, update_modified=False) + + for relapsed in influxframework.get_all("Error Snapshot", filters={"parent_error_snapshot": self.name}): + influxframework.db.set_value("Error Snapshot", relapsed.name, "seen", True, update_modified=False) + + influxframework.local.flags.commit = True + + def validate(self): + parent = influxframework.get_all( + "Error Snapshot", + filters={"evalue": self.evalue, "parent_error_snapshot": ""}, + fields=["name", "relapses", "seen"], + limit_page_length=1, + ) + + if parent: + parent = parent[0] + self.update({"parent_error_snapshot": parent["name"]}) + influxframework.db.set_value("Error Snapshot", parent["name"], "relapses", parent["relapses"] + 1) + if parent["seen"]: + influxframework.db.set_value("Error Snapshot", parent["name"], "seen", False) + + @staticmethod + def clear_old_logs(days=30): + table = influxframework.qb.DocType("Error Snapshot") + influxframework.db.delete(table, filters=(table.modified < (Now() - Interval(days=days)))) diff --git a/influxframework/core/doctype/error_snapshot/error_snapshot_list.js b/influxframework/core/doctype/error_snapshot/error_snapshot_list.js new file mode 100644 index 0000000..9135f57 --- /dev/null +++ b/influxframework/core/doctype/error_snapshot/error_snapshot_list.js @@ -0,0 +1,19 @@ +influxframework.listview_settings["Error Snapshot"] = { + add_fields: ["parent_error_snapshot", "relapses", "seen"], + filters: [ + ["parent_error_snapshot", "=", null], + ["seen", "=", false], + ], + get_indicator: function (doc) { + if (doc.parent_error_snapshot && doc.parent_error_snapshot.length) { + return [__("Relapsed"), !doc.seen ? "orange" : "blue", "parent_error_snapshot,!=,"]; + } else { + return [__("First Level"), !doc.seen ? "red" : "green", "parent_error_snapshot,=,"]; + } + }, + onload: function (listview) { + influxframework.require("logtypes.bundle.js", () => { + influxframework.utils.logtypes.show_log_retention_message(cur_list.doctype); + }); + }, +}; diff --git a/influxframework/core/doctype/error_snapshot/test_error_snapshot.py b/influxframework/core/doctype/error_snapshot/test_error_snapshot.py new file mode 100644 index 0000000..cd87caf --- /dev/null +++ b/influxframework/core/doctype/error_snapshot/test_error_snapshot.py @@ -0,0 +1,9 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + +# test_records = influxframework.get_test_records('Error Snapshot') + + +class TestErrorSnapshot(InfluxFrameworkTestCase): + pass diff --git a/influxframework/core/doctype/file/__init__.py b/influxframework/core/doctype/file/__init__.py new file mode 100644 index 0000000..ad28c17 --- /dev/null +++ b/influxframework/core/doctype/file/__init__.py @@ -0,0 +1,2 @@ +from .exceptions import * +from .utils import * diff --git a/influxframework/core/doctype/file/exceptions.py b/influxframework/core/doctype/file/exceptions.py new file mode 100644 index 0000000..dd83a89 --- /dev/null +++ b/influxframework/core/doctype/file/exceptions.py @@ -0,0 +1,12 @@ +import influxframework + + +class MaxFileSizeReachedError(influxframework.ValidationError): + pass + + +class FolderNotEmpty(influxframework.ValidationError): + pass + + +from influxframework.exceptions import * diff --git a/influxframework/core/doctype/file/file.js b/influxframework/core/doctype/file/file.js new file mode 100644 index 0000000..c3a3b8b --- /dev/null +++ b/influxframework/core/doctype/file/file.js @@ -0,0 +1,45 @@ +influxframework.ui.form.on("File", "refresh", function (frm) { + if (!frm.doc.is_folder) { + frm.add_custom_button( + __("Download"), + function () { + var file_url = frm.doc.file_url; + if (frm.doc.file_name) { + file_url = file_url.replace(/#/g, "%23"); + } + window.open(file_url); + }, + "fa fa-download" + ); + } + + frm.get_field("preview_html").$wrapper.html(`
    + +
    `); + + var is_raster_image = /\.(gif|jpg|jpeg|tiff|png)$/i.test(frm.doc.file_url); + var is_optimizable = !frm.doc.is_folder && is_raster_image && frm.doc.file_size > 0; + + if (is_optimizable) { + frm.add_custom_button(__("Optimize"), function () { + influxframework.show_alert(__("Optimizing image...")); + frm.call("optimize_file").then(() => { + influxframework.show_alert(__("Image optimized")); + }); + }); + } + + if (frm.doc.file_name && frm.doc.file_name.split(".").splice(-1)[0] === "zip") { + frm.add_custom_button(__("Unzip"), function () { + influxframework.call({ + method: "influxframework.core.api.file.unzip_file", + args: { + name: frm.doc.name, + }, + callback: function () { + influxframework.set_route("List", "File"); + }, + }); + }); + } +}); diff --git a/influxframework/core/doctype/file/file.json b/influxframework/core/doctype/file/file.json new file mode 100644 index 0000000..d6c4a99 --- /dev/null +++ b/influxframework/core/doctype/file/file.json @@ -0,0 +1,215 @@ +{ + "actions": [], + "allow_import": 1, + "creation": "2012-12-12 11:19:22", + "default_view": "File", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "file_name", + "is_private", + "preview", + "preview_html", + "section_break_5", + "is_home_folder", + "is_attachments_folder", + "file_size", + "column_break_5", + "file_url", + "thumbnail_url", + "folder", + "is_folder", + "section_break_8", + "attached_to_doctype", + "column_break_10", + "attached_to_name", + "attached_to_field", + "old_parent", + "content_hash", + "uploaded_to_dropbox", + "uploaded_to_google_drive" + ], + "fields": [ + { + "fieldname": "file_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "File Name", + "oldfieldname": "file_name", + "oldfieldtype": "Data", + "read_only": 1 + }, + { + "default": "0", + "depends_on": "eval:!doc.is_folder", + "fieldname": "is_private", + "fieldtype": "Check", + "label": "Is Private" + }, + { + "fieldname": "preview", + "fieldtype": "Section Break", + "label": "Preview" + }, + { + "fieldname": "preview_html", + "fieldtype": "HTML", + "label": "Preview HTML" + }, + { + "fieldname": "section_break_5", + "fieldtype": "Section Break" + }, + { + "default": "0", + "fieldname": "is_home_folder", + "fieldtype": "Check", + "hidden": 1, + "label": "Is Home Folder" + }, + { + "default": "0", + "fieldname": "is_attachments_folder", + "fieldtype": "Check", + "hidden": 1, + "label": "Is Attachments Folder" + }, + { + "fieldname": "file_size", + "fieldtype": "Int", + "in_list_view": 1, + "label": "File Size", + "length": 20, + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:!doc.is_folder", + "fieldname": "file_url", + "fieldtype": "Code", + "label": "File URL", + "read_only": 1 + }, + { + "fieldname": "thumbnail_url", + "fieldtype": "Small Text", + "label": "Thumbnail URL", + "read_only": 1 + }, + { + "fieldname": "folder", + "fieldtype": "Link", + "hidden": 1, + "label": "Folder", + "options": "File", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_folder", + "fieldtype": "Check", + "label": "Is Folder", + "read_only": 1 + }, + { + "depends_on": "eval:!doc.is_folder", + "fieldname": "section_break_8", + "fieldtype": "Section Break" + }, + { + "fieldname": "attached_to_doctype", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Attached To DocType", + "options": "DocType", + "read_only": 1 + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break" + }, + { + "fieldname": "attached_to_name", + "fieldtype": "Data", + "label": "Attached To Name", + "read_only": 1 + }, + { + "fieldname": "attached_to_field", + "fieldtype": "Data", + "label": "Attached To Field", + "read_only": 1 + }, + { + "fieldname": "old_parent", + "fieldtype": "Data", + "hidden": 1, + "label": "old_parent" + }, + { + "fieldname": "content_hash", + "fieldtype": "Data", + "label": "Content Hash", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "uploaded_to_dropbox", + "fieldtype": "Check", + "label": "Uploaded To Dropbox", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "uploaded_to_google_drive", + "fieldtype": "Check", + "label": "Uploaded To Google Drive", + "read_only": 1 + } + ], + "force_re_route_to_default_view": 1, + "icon": "fa fa-file", + "idx": 1, + "links": [], + "modified": "2022-09-13 15:50:15.508251", + "modified_by": "Administrator", + "module": "Core", + "name": "File", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "ASC", + "states": [], + "title_field": "file_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/file/file.py b/influxframework/core/doctype/file/file.py new file mode 100644 index 0000000..2831754 --- /dev/null +++ b/influxframework/core/doctype/file/file.py @@ -0,0 +1,721 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE + +import io +import mimetypes +import os +import re +import shutil +import zipfile +from urllib.parse import quote, unquote + +from PIL import Image, ImageFile, ImageOps +from requests.exceptions import HTTPError, SSLError + +import influxframework +from influxframework import _ +from influxframework.model.document import Document +from influxframework.utils import call_hook_method, cint, get_files_path, get_hook_method, get_url +from influxframework.utils.file_manager import is_safe_path +from influxframework.utils.image import optimize_image, strip_exif_data + +from .exceptions import AttachmentLimitReached, FolderNotEmpty, MaxFileSizeReachedError +from .utils import * + +exclude_from_linked_with = True +ImageFile.LOAD_TRUNCATED_IMAGES = True +URL_PREFIXES = ("http://", "https://") + + +class File(Document): + no_feed_on_delete = True + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # if content is set, file_url will be generated + # decode comes in the picture if content passed has to be decoded before writing to disk + + self.content = self.get("content") or b"" + self.decode = self.get("decode", False) + + @property + def is_remote_file(self): + if self.file_url: + return self.file_url.startswith(URL_PREFIXES) + return not self.content + + def autoname(self): + """Set name for folder""" + if self.is_folder: + if self.folder: + self.name = self.get_name_based_on_parent_folder() + else: + # home + self.name = self.file_name + else: + self.name = influxframework.generate_hash(length=10) + + def before_insert(self): + self.set_folder_name() + self.set_file_name() + self.validate_attachment_limit() + + if self.is_folder: + return + + if self.is_remote_file: + self.validate_remote_file() + else: + self.save_file(content=self.get_content()) + self.flags.new_file = True + influxframework.local.rollback_observers.append(self) + + def after_insert(self): + if not self.is_folder: + self.create_attachment_record() + self.set_is_private() + self.set_file_name() + self.validate_duplicate_entry() + + def validate(self): + # Ensure correct formatting and type + self.file_url = unquote(self.file_url) if self.file_url else "" + + # when dict is passed to get_doc for creation of new_doc, is_new returns None + # this case is handled inside handle_is_private_changed + if not self.is_new() and self.has_value_changed("is_private"): + self.handle_is_private_changed() + + if not self.is_folder: + self.validate_file_path() + self.validate_file_url() + self.validate_file_on_disk() + + self.file_size = influxframework.form_dict.file_size or self.file_size + + def after_rename(self, *args, **kwargs): + for successor in self.get_successors(): + setup_folder_path(successor, self.name) + + def on_trash(self): + if self.is_home_folder or self.is_attachments_folder: + influxframework.throw(_("Cannot delete Home and Attachments folders")) + self.validate_empty_folder() + self._delete_file_on_disk() + if not self.is_folder: + self.add_comment_in_reference_doc("Attachment Removed", _("Removed {0}").format(self.file_name)) + + def on_rollback(self): + # following condition is only executed when an insert has been rolledback + if self.flags.new_file: + self._delete_file_on_disk() + self.flags.pop("new_file") + return + + # if original_content flag is set, this rollback should revert the file to its original state + if self.flags.original_content: + file_path = self.get_full_path() + + if isinstance(self.flags.original_content, bytes): + mode = "wb+" + elif isinstance(self.flags.original_content, str): + mode = "w+" + + with open(file_path, mode) as f: + f.write(self.flags.original_content) + os.fsync(f.fileno()) + self.flags.pop("original_content") + + # used in case file path (File.file_url) has been changed + if self.flags.original_path: + target = self.flags.original_path["old"] + source = self.flags.original_path["new"] + shutil.move(source, target) + self.flags.pop("original_path") + + def get_name_based_on_parent_folder(self) -> str | None: + if self.folder: + return os.path.join(self.folder, self.file_name) + + def get_successors(self): + return influxframework.get_all("File", filters={"folder": self.name}, pluck="name") + + def validate_file_path(self): + if self.is_remote_file: + return + + base_path = os.path.realpath(get_files_path(is_private=self.is_private)) + if not os.path.realpath(self.get_full_path()).startswith(base_path): + influxframework.throw( + _("The File URL you've entered is incorrect"), + title=_("Invalid File URL"), + ) + + def validate_file_url(self): + if self.is_remote_file or not self.file_url: + return + + if not self.file_url.startswith(("/files/", "/private/files/")): + # Probably an invalid URL since it doesn't start with http either + influxframework.throw( + _("URL must start with http:// or https://"), + title=_("Invalid URL"), + ) + + def handle_is_private_changed(self): + if self.is_remote_file: + return + + from pathlib import Path + + old_file_url = self.file_url + file_name = self.file_url.split("/")[-1] + private_file_path = Path(influxframework.get_site_path("private", "files", file_name)) + public_file_path = Path(influxframework.get_site_path("public", "files", file_name)) + + if cint(self.is_private): + source = public_file_path + target = private_file_path + url_starts_with = "/private/files/" + else: + source = private_file_path + target = public_file_path + url_starts_with = "/files/" + updated_file_url = f"{url_starts_with}{file_name}" + + # if a file document is created by passing dict throught get_doc and __local is not set, + # handle_is_private_changed would be executed; we're checking if updated_file_url is same + # as old_file_url to avoid a FileNotFoundError for this case. + if updated_file_url == old_file_url: + return + + if not source.exists(): + influxframework.throw( + _("Cannot find file {} on disk").format(source), + exc=FileNotFoundError, + ) + if target.exists(): + influxframework.throw( + _("A file with same name {} already exists").format(target), + exc=FileExistsError, + ) + + # Uses os.rename which is an atomic operation + shutil.move(source, target) + self.flags.original_path = {"old": source, "new": target} + influxframework.local.rollback_observers.append(self) + + self.file_url = updated_file_url + update_existing_file_docs(self) + + if ( + not self.attached_to_doctype + or not self.attached_to_name + or not self.fetch_attached_to_field(old_file_url) + ): + return + + influxframework.db.set_value( + self.attached_to_doctype, + self.attached_to_name, + self.attached_to_field, + self.file_url, + ) + + def fetch_attached_to_field(self, old_file_url): + if self.attached_to_field: + return True + + reference_dict = influxframework.get_doc(self.attached_to_doctype, self.attached_to_name).as_dict() + + for key, value in reference_dict.items(): + if value == old_file_url: + self.attached_to_field = key + return True + + def validate_attachment_limit(self): + attachment_limit = 0 + if self.attached_to_doctype and self.attached_to_name: + attachment_limit = cint(influxframework.get_meta(self.attached_to_doctype).max_attachments) + + if attachment_limit: + current_attachment_count = len( + influxframework.get_all( + "File", + filters={ + "attached_to_doctype": self.attached_to_doctype, + "attached_to_name": self.attached_to_name, + }, + limit=attachment_limit + 1, + ) + ) + + if current_attachment_count >= attachment_limit: + influxframework.throw( + _("Maximum Attachment Limit of {0} has been reached for {1} {2}.").format( + influxframework.bold(attachment_limit), self.attached_to_doctype, self.attached_to_name + ), + exc=AttachmentLimitReached, + title=_("Attachment Limit Reached"), + ) + + def validate_remote_file(self): + """Validates if file uploaded using URL already exist""" + site_url = get_url() + if self.file_url and "/files/" in self.file_url and self.file_url.startswith(site_url): + self.file_url = self.file_url.split(site_url, 1)[1] + + def set_folder_name(self): + """Make parent folders if not exists based on reference doctype and name""" + if self.folder: + return + + if self.attached_to_doctype: + self.folder = influxframework.db.get_value("File", {"is_attachments_folder": 1}) + + elif not self.is_home_folder: + self.folder = "Home" + + def validate_file_on_disk(self): + """Validates existence file""" + full_path = self.get_full_path() + + if full_path.startswith(URL_PREFIXES): + return True + + if not os.path.exists(full_path): + influxframework.throw(_("File {0} does not exist").format(self.file_url), IOError) + + def validate_duplicate_entry(self): + if not self.flags.ignore_duplicate_entry_error and not self.is_folder: + if not self.content_hash: + self.generate_content_hash() + + # check duplicate name + # check duplicate assignment + filters = { + "content_hash": self.content_hash, + "is_private": self.is_private, + "name": ("!=", self.name), + } + if self.attached_to_doctype and self.attached_to_name: + filters.update( + { + "attached_to_doctype": self.attached_to_doctype, + "attached_to_name": self.attached_to_name, + } + ) + duplicate_file = influxframework.db.get_value("File", filters, ["name", "file_url"], as_dict=1) + + if duplicate_file: + duplicate_file_doc = influxframework.get_cached_doc("File", duplicate_file.name) + if duplicate_file_doc.exists_on_disk(): + # just use the url, to avoid uploading a duplicate + self.file_url = duplicate_file.file_url + + def set_file_name(self): + if not self.file_name and self.file_url: + self.file_name = self.file_url.split("/")[-1] + else: + self.file_name = re.sub(r"/", "", self.file_name) + + def generate_content_hash(self): + if self.content_hash or not self.file_url or self.is_remote_file: + return + file_name = self.file_url.split("/")[-1] + try: + file_path = get_files_path(file_name, is_private=self.is_private) + with open(file_path, "rb") as f: + self.content_hash = get_content_hash(f.read()) + except OSError: + influxframework.throw(_("File {0} does not exist").format(file_path)) + + def make_thumbnail( + self, + set_as_thumbnail: bool = True, + width: int = 300, + height: int = 300, + suffix: str = "small", + crop: bool = False, + ) -> str: + if not self.file_url: + return + + try: + if self.file_url.startswith(("/files", "/private/files")): + image, filename, extn = get_local_image(self.file_url) + else: + image, filename, extn = get_web_image(self.file_url) + except (HTTPError, SSLError, OSError, TypeError): + return + + size = width, height + if crop: + image = ImageOps.fit(image, size, Image.Resampling.LANCZOS) + else: + image.thumbnail(size, Image.Resampling.LANCZOS) + + thumbnail_url = f"{filename}_{suffix}.{extn}" + path = os.path.abspath(influxframework.get_site_path("public", thumbnail_url.lstrip("/"))) + + try: + image.save(path) + if set_as_thumbnail: + self.db_set("thumbnail_url", thumbnail_url) + + except OSError: + influxframework.msgprint(_("Unable to write file format for {0}").format(path)) + return + + return thumbnail_url + + def validate_empty_folder(self): + """Throw exception if folder is not empty""" + if self.is_folder and influxframework.get_all("File", filters={"folder": self.name}, limit=1): + influxframework.throw(_("Folder {0} is not empty").format(self.name), FolderNotEmpty) + + def _delete_file_on_disk(self): + """If file not attached to any other record, delete it""" + on_disk_file_not_shared = self.content_hash and not influxframework.get_all( + "File", + filters={"content_hash": self.content_hash, "name": ["!=", self.name]}, + limit=1, + ) + if on_disk_file_not_shared: + self.delete_file_data_content() + else: + self.delete_file_data_content(only_thumbnail=True) + + def unzip(self) -> list["File"]: + """Unzip current file and replace it by its children""" + if not self.file_url.endswith(".zip"): + influxframework.throw(_("{0} is not a zip file").format(self.file_name)) + + zip_path = self.get_full_path() + + files = [] + with zipfile.ZipFile(zip_path) as z: + for file in z.filelist: + if file.is_dir() or file.filename.startswith("__MACOSX/"): + # skip directories and macos hidden directory + continue + + filename = os.path.basename(file.filename) + if filename.startswith("."): + # skip hidden files + continue + + file_doc = influxframework.new_doc("File") + file_doc.content = z.read(file.filename) + file_doc.file_name = filename + file_doc.folder = self.folder + file_doc.is_private = self.is_private + file_doc.attached_to_doctype = self.attached_to_doctype + file_doc.attached_to_name = self.attached_to_name + file_doc.save() + files.append(file_doc) + + influxframework.delete_doc("File", self.name) + return files + + def exists_on_disk(self): + return os.path.exists(self.get_full_path()) + + def get_content(self) -> bytes: + if self.is_folder: + influxframework.throw(_("Cannot get file contents of a Folder")) + + if self.get("content"): + self._content = self.content + if self.decode: + self._content = decode_file_content(self._content) + self.decode = False + # self.content = None # TODO: This needs to happen; make it happen somehow + return self._content + + if self.file_url: + self.validate_file_url() + file_path = self.get_full_path() + + # read the file + with open(file_path, mode="rb") as f: + self._content = f.read() + try: + # for plain text files + self._content = self._content.decode() + except UnicodeDecodeError: + # for .png, .jpg, etc + pass + + return self._content + + def get_full_path(self): + """Returns file path from given file name""" + + file_path = self.file_url or self.file_name + + site_url = get_url() + if "/files/" in file_path and file_path.startswith(site_url): + file_path = file_path.split(site_url, 1)[1] + + if "/" not in file_path: + if self.is_private: + file_path = f"/private/files/{file_path}" + else: + file_path = f"/files/{file_path}" + + if file_path.startswith("/private/files/"): + file_path = get_files_path(*file_path.split("/private/files/", 1)[1].split("/"), is_private=1) + + elif file_path.startswith("/files/"): + file_path = get_files_path(*file_path.split("/files/", 1)[1].split("/")) + + elif file_path.startswith(URL_PREFIXES): + pass + + elif not self.file_url: + influxframework.throw(_("There is some problem with the file url: {0}").format(file_path)) + + if not is_safe_path(file_path): + influxframework.throw(_("Cannot access file path {0}").format(file_path)) + + if os.path.sep in self.file_name: + influxframework.throw(_("File name cannot have {0}").format(os.path.sep)) + + return file_path + + def write_file(self): + """write file to disk with a random name (to compare)""" + if self.is_remote_file: + return + + file_path = self.get_full_path() + + if isinstance(self._content, str): + self._content = self._content.encode() + + with open(file_path, "wb+") as f: + f.write(self._content) + os.fsync(f.fileno()) + + influxframework.local.rollback_observers.append(self) + + return file_path + + def save_file( + self, + content: bytes | str | None = None, + decode=False, + ignore_existing_file_check=False, + overwrite=False, + ): + if self.is_remote_file: + return + + if not self.flags.new_file: + self.flags.original_content = self.get_content() + + if content: + self.content = content + self.decode = decode + self.get_content() + + if not self._content: + return + + file_exists = False + duplicate_file = None + + self.is_private = cint(self.is_private) + self.content_type = mimetypes.guess_type(self.file_name)[0] + + # transform file content based on site settings + if ( + self.content_type + and self.content_type == "image/jpeg" + and influxframework.get_system_settings("strip_exif_metadata_from_uploaded_images") + ): + self._content = strip_exif_data(self._content, self.content_type) + + self.file_size = self.check_max_file_size() + self.content_hash = get_content_hash(self._content) + + # check if a file exists with the same content hash and is also in the same folder (public or private) + if not ignore_existing_file_check: + duplicate_file = influxframework.get_value( + "File", + {"content_hash": self.content_hash, "is_private": self.is_private}, + ["file_url", "name"], + as_dict=True, + ) + + if duplicate_file: + file_doc: "File" = influxframework.get_cached_doc("File", duplicate_file.name) + if file_doc.exists_on_disk(): + self.file_url = duplicate_file.file_url + file_exists = True + + if not file_exists: + if not overwrite: + self.file_name = generate_file_name( + name=self.file_name, + suffix=self.content_hash[-6:], + is_private=self.is_private, + ) + call_hook_method("before_write_file", file_size=self.file_size) + write_file_method = get_hook_method("write_file") + if write_file_method: + return write_file_method(self) + return self.save_file_on_filesystem() + + def save_file_on_filesystem(self): + if self.is_private: + self.file_url = f"/private/files/{self.file_name}" + else: + self.file_url = f"/files/{self.file_name}" + + fpath = self.write_file() + + return {"file_name": os.path.basename(fpath), "file_url": self.file_url} + + def check_max_file_size(self): + from influxframework.core.api.file import get_max_file_size + + max_file_size = get_max_file_size() + file_size = len(self._content or b"") + + if file_size > max_file_size: + influxframework.throw( + _("File size exceeded the maximum allowed size of {0} MB").format(max_file_size / 1048576), + exc=MaxFileSizeReachedError, + ) + + return file_size + + def delete_file_data_content(self, only_thumbnail=False): + method = get_hook_method("delete_file_data_content") + if method: + method(self, only_thumbnail=only_thumbnail) + else: + self.delete_file_from_filesystem(only_thumbnail=only_thumbnail) + + def delete_file_from_filesystem(self, only_thumbnail=False): + """Delete file, thumbnail from File document""" + if only_thumbnail: + delete_file(self.thumbnail_url) + else: + delete_file(self.file_url) + delete_file(self.thumbnail_url) + + def is_downloadable(self): + return has_permission(self, "read") + + def get_extension(self): + """returns split filename and extension""" + return os.path.splitext(self.file_name) + + def create_attachment_record(self): + icon = ' ' if self.is_private else "" + file_url = quote(influxframework.safe_encode(self.file_url)) if self.file_url else self.file_name + file_name = self.file_name or self.file_url + + self.add_comment_in_reference_doc( + "Attachment", + _("Added {0}").format(f"{file_name}{icon}"), + ) + + def add_comment_in_reference_doc(self, comment_type, text): + if self.attached_to_doctype and self.attached_to_name: + try: + doc = influxframework.get_doc(self.attached_to_doctype, self.attached_to_name) + doc.add_comment(comment_type, text) + except influxframework.DoesNotExistError: + influxframework.clear_messages() + + def set_is_private(self): + if self.file_url: + self.is_private = cint(self.file_url.startswith("/private")) + + @influxframework.whitelist() + def optimize_file(self): + if self.is_folder: + raise TypeError("Folders cannot be optimized") + + content_type = mimetypes.guess_type(self.file_name)[0] + is_local_image = content_type.startswith("image/") and self.file_size > 0 + is_svg = content_type == "image/svg+xml" + + if not is_local_image: + raise NotImplementedError("Only local image files can be optimized") + + if is_svg: + raise TypeError("Optimization of SVG images is not supported") + + original_content = self.get_content() + optimized_content = optimize_image( + content=original_content, + content_type=content_type, + ) + + self.save_file(content=optimized_content, overwrite=True) + self.save() + + @staticmethod + def zip_files(files): + zip_file = io.BytesIO() + zf = zipfile.ZipFile(zip_file, "w", zipfile.ZIP_DEFLATED) + for _file in files: + if isinstance(_file, str): + _file = influxframework.get_doc("File", _file) + if not isinstance(_file, File): + continue + if _file.is_folder: + continue + zf.writestr(_file.file_name, _file.get_content()) + zf.close() + return zip_file.getvalue() + + +def on_doctype_update(): + influxframework.db.add_index("File", ["attached_to_doctype", "attached_to_name"]) + + +def has_permission(doc, ptype=None, user=None): + has_access = False + user = user or influxframework.session.user + + if ptype == "create": + has_access = influxframework.has_permission("File", "create", user=user) + + if not doc.is_private or doc.owner in [user, "Guest"] or user == "Administrator": + has_access = True + + if doc.attached_to_doctype and doc.attached_to_name: + attached_to_doctype = doc.attached_to_doctype + attached_to_name = doc.attached_to_name + + try: + ref_doc = influxframework.get_doc(attached_to_doctype, attached_to_name) + + if ptype in ["write", "create", "delete"]: + has_access = ref_doc.has_permission("write") + + if ptype == "delete" and not has_access: + influxframework.throw( + _( + "Cannot delete file as it belongs to {0} {1} for which you do not have permissions" + ).format(doc.attached_to_doctype, doc.attached_to_name), + influxframework.PermissionError, + ) + else: + has_access = ref_doc.has_permission("read") + except influxframework.DoesNotExistError: + # if parent doc is not created before file is created + # we cannot check its permission so we will use file's permission + pass + + return has_access + + +# Note: kept at the end to not cause circular, partial imports & maintain backwards compatibility +from influxframework.core.api.file import * diff --git a/influxframework/core/doctype/file/file_list.js b/influxframework/core/doctype/file/file_list.js new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/file/test_file.py b/influxframework/core/doctype/file/test_file.py new file mode 100644 index 0000000..72fc53a --- /dev/null +++ b/influxframework/core/doctype/file/test_file.py @@ -0,0 +1,711 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE +import base64 +import json +import os +from contextlib import contextmanager +from typing import TYPE_CHECKING + +import influxframework +from influxframework import _ +from influxframework.core.api.file import ( + create_new_folder, + get_attached_images, + get_files_in_folder, + move_file, + unzip_file, +) +from influxframework.exceptions import ValidationError +from influxframework.tests.utils import InfluxFrameworkTestCase +from influxframework.utils import get_files_path + +if TYPE_CHECKING: + from influxframework.core.doctype.file.file import File + +test_content1 = "Hello" +test_content2 = "Hello World" + + +def make_test_doc(): + d = influxframework.new_doc("ToDo") + d.description = "Test" + d.assigned_by = influxframework.session.user + d.save() + return d.doctype, d.name + + +@contextmanager +def make_test_image_file(): + file_path = influxframework.get_app_path("influxframework", "tests/data/sample_image_for_optimization.jpg") + with open(file_path, "rb") as f: + file_content = f.read() + + test_file = influxframework.get_doc( + {"doctype": "File", "file_name": "sample_image_for_optimization.jpg", "content": file_content} + ).insert() + # remove those flags + _test_file: "File" = influxframework.get_doc("File", test_file.name) + + try: + yield _test_file + finally: + _test_file.delete() + + +class TestSimpleFile(InfluxFrameworkTestCase): + def setUp(self): + self.attached_to_doctype, self.attached_to_docname = make_test_doc() + self.test_content = test_content1 + _file = influxframework.get_doc( + { + "doctype": "File", + "file_name": "test1.txt", + "attached_to_doctype": self.attached_to_doctype, + "attached_to_name": self.attached_to_docname, + "content": self.test_content, + } + ) + _file.save() + self.saved_file_url = _file.file_url + + def test_save(self): + _file = influxframework.get_doc("File", {"file_url": self.saved_file_url}) + content = _file.get_content() + self.assertEqual(content, self.test_content) + + +class TestBase64File(InfluxFrameworkTestCase): + def setUp(self): + self.attached_to_doctype, self.attached_to_docname = make_test_doc() + self.test_content = base64.b64encode(test_content1.encode("utf-8")) + _file: "File" = influxframework.get_doc( + { + "doctype": "File", + "file_name": "test_base64.txt", + "attached_to_doctype": self.attached_to_doctype, + "attached_to_docname": self.attached_to_docname, + "content": self.test_content, + "decode": True, + } + ) + _file.save() + self.saved_file_url = _file.file_url + + def test_saved_content(self): + _file = influxframework.get_doc("File", {"file_url": self.saved_file_url}) + content = _file.get_content() + self.assertEqual(content, test_content1) + + +class TestSameFileName(InfluxFrameworkTestCase): + def test_saved_content(self): + self.attached_to_doctype, self.attached_to_docname = make_test_doc() + self.test_content1 = test_content1 + self.test_content2 = test_content2 + _file1 = influxframework.get_doc( + { + "doctype": "File", + "file_name": "testing.txt", + "attached_to_doctype": self.attached_to_doctype, + "attached_to_name": self.attached_to_docname, + "content": self.test_content1, + } + ) + _file1.save() + _file2 = influxframework.get_doc( + { + "doctype": "File", + "file_name": "testing.txt", + "attached_to_doctype": self.attached_to_doctype, + "attached_to_name": self.attached_to_docname, + "content": self.test_content2, + } + ) + _file2.save() + self.saved_file_url1 = _file1.file_url + self.saved_file_url2 = _file2.file_url + + _file = influxframework.get_doc("File", {"file_url": self.saved_file_url1}) + content1 = _file.get_content() + self.assertEqual(content1, self.test_content1) + _file = influxframework.get_doc("File", {"file_url": self.saved_file_url2}) + content2 = _file.get_content() + self.assertEqual(content2, self.test_content2) + + def test_saved_content_private(self): + _file1 = influxframework.get_doc( + { + "doctype": "File", + "file_name": "testing-private.txt", + "content": test_content1, + "is_private": 1, + } + ).insert() + _file2 = influxframework.get_doc( + { + "doctype": "File", + "file_name": "testing-private.txt", + "content": test_content2, + "is_private": 1, + } + ).insert() + + _file = influxframework.get_doc("File", {"file_url": _file1.file_url}) + self.assertEqual(_file.get_content(), test_content1) + + _file = influxframework.get_doc("File", {"file_url": _file2.file_url}) + self.assertEqual(_file.get_content(), test_content2) + + +class TestSameContent(InfluxFrameworkTestCase): + def setUp(self): + self.attached_to_doctype1, self.attached_to_docname1 = make_test_doc() + self.attached_to_doctype2, self.attached_to_docname2 = make_test_doc() + self.test_content1 = test_content1 + self.test_content2 = test_content1 + self.orig_filename = "hello.txt" + self.dup_filename = "hello2.txt" + _file1 = influxframework.get_doc( + { + "doctype": "File", + "file_name": self.orig_filename, + "attached_to_doctype": self.attached_to_doctype1, + "attached_to_name": self.attached_to_docname1, + "content": self.test_content1, + } + ) + _file1.save() + + _file2 = influxframework.get_doc( + { + "doctype": "File", + "file_name": self.dup_filename, + "attached_to_doctype": self.attached_to_doctype2, + "attached_to_name": self.attached_to_docname2, + "content": self.test_content2, + } + ) + + _file2.save() + + def test_saved_content(self): + self.assertFalse(os.path.exists(get_files_path(self.dup_filename))) + + def test_attachment_limit(self): + doctype, docname = make_test_doc() + from influxframework.custom.doctype.property_setter.property_setter import make_property_setter + + limit_property = make_property_setter( + "ToDo", None, "max_attachments", 1, "int", for_doctype=True + ) + file1 = influxframework.get_doc( + { + "doctype": "File", + "file_name": "test-attachment", + "attached_to_doctype": doctype, + "attached_to_name": docname, + "content": "test", + } + ) + + file1.insert() + + file2 = influxframework.get_doc( + { + "doctype": "File", + "file_name": "test-attachment", + "attached_to_doctype": doctype, + "attached_to_name": docname, + "content": "test2", + } + ) + + self.assertRaises(influxframework.exceptions.AttachmentLimitReached, file2.insert) + limit_property.delete() + influxframework.clear_cache(doctype="ToDo") + + +class TestFile(InfluxFrameworkTestCase): + def setUp(self): + influxframework.set_user("Administrator") + self.delete_test_data() + self.upload_file() + + def tearDown(self): + try: + influxframework.get_doc("File", {"file_name": "file_copy.txt"}).delete() + except influxframework.DoesNotExistError: + pass + + def delete_test_data(self): + test_file_data = influxframework.get_all( + "File", + pluck="name", + filters={"is_home_folder": 0, "is_attachments_folder": 0}, + order_by="creation desc", + ) + for f in test_file_data: + influxframework.delete_doc("File", f) + + def upload_file(self): + _file = influxframework.get_doc( + { + "doctype": "File", + "file_name": "file_copy.txt", + "attached_to_name": "", + "attached_to_doctype": "", + "folder": self.get_folder("Test Folder 1", "Home").name, + "content": "Testing file copy example.", + } + ) + _file.save() + self.saved_folder = _file.folder + self.saved_name = _file.name + self.saved_filename = get_files_path(_file.file_name) + + def get_folder(self, folder_name, parent_folder="Home"): + return influxframework.get_doc( + {"doctype": "File", "file_name": _(folder_name), "is_folder": 1, "folder": _(parent_folder)} + ).insert() + + def tests_after_upload(self): + self.assertEqual(self.saved_folder, _("Home/Test Folder 1")) + file_folder = influxframework.db.get_value("File", self.saved_name, "folder") + self.assertEqual(file_folder, _("Home/Test Folder 1")) + + def test_file_copy(self): + folder = self.get_folder("Test Folder 2", "Home") + + file = influxframework.get_doc("File", {"file_name": "file_copy.txt"}) + move_file([{"name": file.name}], folder.name, file.folder) + file = influxframework.get_doc("File", {"file_name": "file_copy.txt"}) + + self.assertEqual(_("Home/Test Folder 2"), file.folder) + + def test_folder_depth(self): + result1 = self.get_folder("d1", "Home") + self.assertEqual(result1.name, "Home/d1") + result2 = self.get_folder("d2", "Home/d1") + self.assertEqual(result2.name, "Home/d1/d2") + result3 = self.get_folder("d3", "Home/d1/d2") + self.assertEqual(result3.name, "Home/d1/d2/d3") + result4 = self.get_folder("d4", "Home/d1/d2/d3") + _file = influxframework.get_doc( + { + "doctype": "File", + "file_name": "folder_copy.txt", + "attached_to_name": "", + "attached_to_doctype": "", + "folder": result4.name, + "content": "Testing folder copy example", + } + ) + _file.save() + + def test_folder_copy(self): + folder = self.get_folder("Test Folder 2", "Home") + folder = self.get_folder("Test Folder 3", "Home/Test Folder 2") + _file = influxframework.get_doc( + { + "doctype": "File", + "file_name": "folder_copy.txt", + "attached_to_name": "", + "attached_to_doctype": "", + "folder": folder.name, + "content": "Testing folder copy example", + } + ) + _file.save() + + move_file([{"name": folder.name}], "Home/Test Folder 1", folder.folder) + + file = influxframework.get_doc("File", {"file_name": "folder_copy.txt"}) + file_copy_txt = influxframework.get_value("File", {"file_name": "file_copy.txt"}) + if file_copy_txt: + influxframework.get_doc("File", file_copy_txt).delete() + + self.assertEqual(_("Home/Test Folder 1/Test Folder 3"), file.folder) + + def test_default_folder(self): + d = influxframework.get_doc({"doctype": "File", "file_name": _("Test_Folder"), "is_folder": 1}) + d.save() + self.assertEqual(d.folder, "Home") + + def test_on_delete(self): + file = influxframework.get_doc("File", {"file_name": "file_copy.txt"}) + file.delete() + + self.assertEqual(influxframework.db.get_value("File", _("Home/Test Folder 1"), "file_size"), 0) + + folder = self.get_folder("Test Folder 3", "Home/Test Folder 1") + _file = influxframework.get_doc( + { + "doctype": "File", + "file_name": "folder_copy.txt", + "attached_to_name": "", + "attached_to_doctype": "", + "folder": folder.name, + "content": "Testing folder copy example", + } + ) + _file.save() + + folder = influxframework.get_doc("File", "Home/Test Folder 1/Test Folder 3") + self.assertRaises(ValidationError, folder.delete) + + def test_same_file_url_update(self): + attached_to_doctype1, attached_to_docname1 = make_test_doc() + attached_to_doctype2, attached_to_docname2 = make_test_doc() + + file1 = influxframework.get_doc( + { + "doctype": "File", + "file_name": "file1.txt", + "attached_to_doctype": attached_to_doctype1, + "attached_to_name": attached_to_docname1, + "is_private": 1, + "content": test_content1, + } + ).insert() + + file2 = influxframework.get_doc( + { + "doctype": "File", + "file_name": "file2.txt", + "attached_to_doctype": attached_to_doctype2, + "attached_to_name": attached_to_docname2, + "is_private": 1, + "content": test_content1, + } + ).insert() + + self.assertEqual(file1.is_private, file2.is_private, 1) + self.assertEqual(file1.file_url, file2.file_url) + self.assertTrue(os.path.exists(file1.get_full_path())) + + file1.is_private = 0 + file1.save() + + file2 = influxframework.get_doc("File", file2.name) + + self.assertEqual(file1.is_private, file2.is_private, 0) + self.assertEqual(file1.file_url, file2.file_url) + self.assertTrue(os.path.exists(file2.get_full_path())) + + def test_parent_directory_validation_in_file_url(self): + file1 = influxframework.get_doc( + { + "doctype": "File", + "file_name": "parent_dir.txt", + "is_private": 1, + "content": test_content1, + } + ).insert() + + file1.file_url = "/private/files/../test.txt" + self.assertRaises(ValidationError, file1.save) + + # No validation to see if file exists + file1.reload() + file1.file_url = "/private/files/parent_dir2.txt" + self.assertRaises(OSError, file1.save) + + def test_file_url_validation(self): + test_file: "File" = influxframework.new_doc("File") + test_file.update({"file_name": "logo", "file_url": "https://influxframework.io/files/influxframework.png"}) + + self.assertIsNone(test_file.validate()) + + # bad path + test_file.file_url = "/usr/bin/man" + self.assertRaisesRegex( + ValidationError, f"Cannot access file path {test_file.file_url}", test_file.validate + ) + + test_file.file_url = None + test_file.file_name = "/usr/bin/man" + self.assertRaisesRegex( + ValidationError, "There is some problem with the file url", test_file.validate + ) + + test_file.file_url = None + test_file.file_name = "_file" + self.assertRaisesRegex(IOError, "does not exist", test_file.validate) + + test_file.file_url = None + test_file.file_name = "/private/files/_file" + self.assertRaisesRegex(ValidationError, "File name cannot have", test_file.validate) + + def test_make_thumbnail(self): + # test web image + test_file: "File" = influxframework.get_doc( + { + "doctype": "File", + "file_name": "logo", + "file_url": influxframework.utils.get_url("/_test/assets/image.jpg"), + } + ).insert(ignore_permissions=True) + + test_file.make_thumbnail() + self.assertEqual(test_file.thumbnail_url, "/files/image_small.jpg") + + # test web image without extension + test_file = influxframework.get_doc( + { + "doctype": "File", + "file_name": "logo", + "file_url": influxframework.utils.get_url("/_test/assets/image"), + } + ).insert(ignore_permissions=True) + + test_file.make_thumbnail() + self.assertTrue(test_file.thumbnail_url.endswith("_small.jpeg")) + + # test local image + test_file.db_set("thumbnail_url", None) + test_file.reload() + test_file.file_url = "/files/image_small.jpg" + test_file.make_thumbnail(suffix="xs", crop=True) + self.assertEqual(test_file.thumbnail_url, "/files/image_small_xs.jpg") + + influxframework.clear_messages() + test_file.db_set("thumbnail_url", None) + test_file.reload() + test_file.file_url = influxframework.utils.get_url("unknown.jpg") + test_file.make_thumbnail(suffix="xs") + self.assertEqual( + json.loads(influxframework.message_log[0]).get("message"), + f"File '{influxframework.utils.get_url('unknown.jpg')}' not found", + ) + self.assertEqual(test_file.thumbnail_url, None) + + def test_file_unzip(self): + file_path = influxframework.get_app_path("influxframework", "www/_test/assets/file.zip") + public_file_path = influxframework.get_site_path("public", "files") + try: + import shutil + + shutil.copy(file_path, public_file_path) + except Exception: + pass + + test_file = influxframework.get_doc( + { + "doctype": "File", + "file_url": "/files/file.zip", + } + ).insert(ignore_permissions=True) + + self.assertListEqual( + [file.file_name for file in unzip_file(test_file.name)], + ["css_asset.css", "image.jpg", "js_asset.min.js"], + ) + + test_file = influxframework.get_doc( + { + "doctype": "File", + "file_url": influxframework.utils.get_url("/_test/assets/image.jpg"), + } + ).insert(ignore_permissions=True) + self.assertRaisesRegex(ValidationError, "not a zip file", test_file.unzip) + + def test_create_file_without_file_url(self): + test_file = influxframework.get_doc( + { + "doctype": "File", + "file_name": "logo", + "content": "influxframework", + } + ).insert() + assert test_file is not None + + +class TestAttachment(InfluxFrameworkTestCase): + test_doctype = "Test For Attachment" + + @classmethod + def setUpClass(cls): + super().setUpClass() + influxframework.get_doc( + doctype="DocType", + name=cls.test_doctype, + module="Custom", + custom=1, + fields=[ + {"label": "Title", "fieldname": "title", "fieldtype": "Data"}, + {"label": "Attachment", "fieldname": "attachment", "fieldtype": "Attach"}, + ], + ).insert(ignore_if_duplicate=True) + + @classmethod + def tearDownClass(cls): + influxframework.db.rollback() + influxframework.delete_doc("DocType", cls.test_doctype) + + def test_file_attachment_on_update(self): + doc = influxframework.get_doc(doctype=self.test_doctype, title="test for attachment on update").insert() + + file = influxframework.get_doc( + {"doctype": "File", "file_name": "test_attach.txt", "content": "Test Content"} + ).save() + + doc.attachment = file.file_url + doc.save() + + exists = influxframework.db.exists( + "File", + { + "file_name": "test_attach.txt", + "file_url": file.file_url, + "attached_to_doctype": self.test_doctype, + "attached_to_name": doc.name, + "attached_to_field": "attachment", + }, + ) + + self.assertTrue(exists) + + +class TestAttachmentsAccess(InfluxFrameworkTestCase): + def setUp(self) -> None: + influxframework.db.delete("File", {"is_folder": 0}) + + def test_attachments_access(self): + influxframework.set_user("test4@example.com") + self.attached_to_doctype, self.attached_to_docname = make_test_doc() + + influxframework.get_doc( + { + "doctype": "File", + "file_name": "test_user.txt", + "attached_to_doctype": self.attached_to_doctype, + "attached_to_name": self.attached_to_docname, + "content": "Testing User", + } + ).insert() + + influxframework.get_doc( + { + "doctype": "File", + "file_name": "test_user_home.txt", + "content": "User Home", + } + ).insert() + + influxframework.set_user("test@example.com") + + influxframework.get_doc( + { + "doctype": "File", + "file_name": "test_system_manager.txt", + "attached_to_doctype": self.attached_to_doctype, + "attached_to_name": self.attached_to_docname, + "content": "Testing System Manager", + } + ).insert() + + influxframework.get_doc( + { + "doctype": "File", + "file_name": "test_sm_home.txt", + "content": "System Manager Home", + } + ).insert() + + system_manager_files = [file.file_name for file in get_files_in_folder("Home")["files"]] + system_manager_attachments_files = [ + file.file_name for file in get_files_in_folder("Home/Attachments")["files"] + ] + + influxframework.set_user("test4@example.com") + user_files = [file.file_name for file in get_files_in_folder("Home")["files"]] + user_attachments_files = [ + file.file_name for file in get_files_in_folder("Home/Attachments")["files"] + ] + + self.assertIn("test_sm_home.txt", system_manager_files) + self.assertNotIn("test_sm_home.txt", user_files) + self.assertIn("test_user_home.txt", system_manager_files) + self.assertIn("test_user_home.txt", user_files) + + self.assertIn("test_system_manager.txt", system_manager_attachments_files) + self.assertNotIn("test_system_manager.txt", user_attachments_files) + self.assertIn("test_user.txt", system_manager_attachments_files) + self.assertIn("test_user.txt", user_attachments_files) + + def tearDown(self) -> None: + influxframework.set_user("Administrator") + influxframework.db.rollback() + + +class TestFileUtils(InfluxFrameworkTestCase): + def test_extract_images_from_doc(self): + # with filename in data URI + todo = influxframework.get_doc( + { + "doctype": "ToDo", + "description": 'Test ', + } + ).insert() + self.assertTrue(influxframework.db.exists("File", {"attached_to_name": todo.name})) + self.assertIn('', todo.description) + self.assertListEqual(get_attached_images("ToDo", [todo.name])[todo.name], ["/files/pix.png"]) + + # without filename in data URI + todo = influxframework.get_doc( + { + "doctype": "ToDo", + "description": 'Test ', + } + ).insert() + filename = influxframework.db.exists("File", {"attached_to_name": todo.name}) + self.assertIn(f' None: + home = influxframework.get_doc( + {"doctype": "File", "is_folder": 1, "is_home_folder": 1, "file_name": _("Home")} + ).insert(ignore_if_duplicate=True) + + influxframework.get_doc( + { + "doctype": "File", + "folder": home.name, + "is_folder": 1, + "is_attachments_folder": 1, + "file_name": _("Attachments"), + } + ).insert(ignore_if_duplicate=True) + + +def setup_folder_path(filename: str, new_parent: str) -> None: + file: "File" = influxframework.get_doc("File", filename) + file.folder = new_parent + file.save() + + if file.is_folder: + from influxframework.model.rename_doc import rename_doc + + rename_doc("File", file.name, file.get_name_based_on_parent_folder(), ignore_permissions=True) + + +def get_extension( + filename, + extn: str | None = None, + content: bytes | None = None, + response: Optional["Response"] = None, +) -> str: + mimetype = None + + if response: + content_type = response.headers.get("Content-Type") + + if content_type: + _extn = mimetypes.guess_extension(content_type) + if _extn: + return _extn[1:] + + if extn: + # remove '?' char and parameters from extn if present + if "?" in extn: + extn = extn.split("?", 1)[0] + + mimetype = mimetypes.guess_type(filename + "." + extn)[0] + + if mimetype is None or not mimetype.startswith("image/") and content: + # detect file extension by reading image header properties + extn = imghdr.what(filename + "." + (extn or ""), h=content) + + return extn + + +def get_local_image(file_url: str) -> tuple["ImageFile", str, str]: + if file_url.startswith("/private"): + file_url_path = (file_url.lstrip("/"),) + else: + file_url_path = ("public", file_url.lstrip("/")) + + file_path = influxframework.get_site_path(*file_url_path) + + try: + image = Image.open(file_path) + except OSError: + influxframework.throw(_("Unable to read file format for {0}").format(file_url)) + + content = None + + try: + filename, extn = file_url.rsplit(".", 1) + except ValueError: + # no extn + with open(file_path) as f: + content = f.read() + + filename = file_url + extn = None + + extn = get_extension(filename, extn, content) + + return image, filename, extn + + +def get_web_image(file_url: str) -> tuple["ImageFile", str, str]: + # download + file_url = influxframework.utils.get_url(file_url) + r = requests.get(file_url, stream=True) + try: + r.raise_for_status() + except requests.exceptions.HTTPError as e: + if "404" in e.args[0]: + influxframework.msgprint(_("File '{0}' not found").format(file_url)) + else: + influxframework.msgprint(_("Unable to read file format for {0}").format(file_url)) + raise + + try: + image = Image.open(BytesIO(r.content)) + except Exception as e: + influxframework.msgprint(_("Image link '{0}' is not valid").format(file_url), raise_exception=e) + + try: + filename, extn = file_url.rsplit("/", 1)[1].rsplit(".", 1) + except ValueError: + # the case when the file url doesn't have filename or extension + # but is fetched due to a query string. example: https://encrypted-tbn3.gstatic.com/images?q=something + filename = get_random_filename() + extn = None + + extn = get_extension(filename, extn, r.content) + filename = "/files/" + strip(unquote(filename)) + + return image, filename, extn + + +def delete_file(path: str) -> None: + """Delete file from `public folder`""" + if path: + if ".." in path.split("/"): + influxframework.throw( + _("It is risky to delete this file: {0}. Please contact your System Manager.").format(path) + ) + + parts = os.path.split(path.strip("/")) + if parts[0] == "files": + path = influxframework.utils.get_site_path("public", "files", parts[-1]) + + else: + path = influxframework.utils.get_site_path("private", "files", parts[-1]) + + path = encode(path) + if os.path.exists(path): + os.remove(path) + + +def remove_file_by_url(file_url: str, doctype: str = None, name: str = None) -> "Document": + if doctype and name: + fid = influxframework.db.get_value( + "File", {"file_url": file_url, "attached_to_doctype": doctype, "attached_to_name": name} + ) + else: + fid = influxframework.db.get_value("File", {"file_url": file_url}) + + if fid: + from influxframework.utils.file_manager import remove_file + + return remove_file(fid=fid) + + +def get_content_hash(content: bytes | str) -> str: + if isinstance(content, str): + content = content.encode() + return hashlib.md5(content).hexdigest() # nosec + + +def generate_file_name(name: str, suffix: str | None = None, is_private: bool = False) -> str: + """Generate conflict-free file name. Suffix will be ignored if name available. If the + provided suffix doesn't result in an available path, a random suffix will be picked. + """ + + def path_exists(name, is_private): + return os.path.exists(encode(get_files_path(name, is_private=is_private))) + + if not path_exists(name, is_private): + return name + + candidate_path = get_file_name(name, suffix) + + if path_exists(candidate_path, is_private): + return generate_file_name(name, is_private=is_private) + return candidate_path + + +def get_file_name(fname: str, optional_suffix: str | None = None) -> str: + # convert to unicode + fname = cstr(fname) + partial, extn = os.path.splitext(fname) + suffix = optional_suffix or influxframework.generate_hash(length=6) + + return f"{partial}{suffix}{extn}" + + +def extract_images_from_doc(doc: "Document", fieldname: str): + content = doc.get(fieldname) + content = extract_images_from_html(doc, content) + if influxframework.flags.has_dataurl: + doc.set(fieldname, content) + + +def extract_images_from_html(doc: "Document", content: str, is_private: bool = False): + influxframework.flags.has_dataurl = False + + def _save_file(match): + data = match.group(1).split("data:")[1] + headers, content = data.split(",") + mtype = headers.split(";")[0] + + if isinstance(content, str): + content = content.encode("utf-8") + if b"," in content: + content = content.split(b",")[1] + content = safe_b64decode(content) + + content = optimize_image(content, mtype) + + if "filename=" in headers: + filename = headers.split("filename=")[-1] + filename = safe_decode(filename).split(";")[0] + + else: + filename = get_random_filename(content_type=mtype) + + if doc.meta.istable: + doctype = doc.parenttype + name = doc.parent + else: + doctype = doc.doctype + name = doc.name + + _file = influxframework.get_doc( + { + "doctype": "File", + "file_name": filename, + "attached_to_doctype": doctype, + "attached_to_name": name, + "content": content, + "decode": False, + "is_private": is_private, + } + ) + _file.save(ignore_permissions=True) + file_url = _file.file_url + influxframework.flags.has_dataurl = True + + return f']*src\s*=\s*["\'](?=data:)(.*?)["\']', _save_file, content) + + return content + + +def get_random_filename(content_type: str = None) -> str: + extn = None + if content_type: + extn = mimetypes.guess_extension(content_type) + + return random_string(7) + (extn or "") + + +def update_existing_file_docs(doc: "File") -> None: + # Update is private and file url of all file docs that point to the same file + file_doctype = influxframework.qb.DocType("File") + ( + influxframework.qb.update(file_doctype) + .set(file_doctype.file_url, doc.file_url) + .set(file_doctype.is_private, doc.is_private) + .where(file_doctype.content_hash == doc.content_hash) + .where(file_doctype.name != doc.name) + ).run() + + +def attach_files_to_document(doc: "File", event) -> None: + """Runs on on_update hook of all documents. + Goes through every Attach and Attach Image field and attaches + the file url to the document if it is not already attached. + """ + + attach_fields = doc.meta.get("fields", {"fieldtype": ["in", ["Attach", "Attach Image"]]}) + + for df in attach_fields: + # this method runs in on_update hook of all documents + # we dont want the update to fail if file cannot be attached for some reason + value = doc.get(df.fieldname) + if not (value or "").startswith(("/files", "/private/files")): + return + + if influxframework.db.exists( + "File", + { + "file_url": value, + "attached_to_name": doc.name, + "attached_to_doctype": doc.doctype, + "attached_to_field": df.fieldname, + }, + ): + return + + file: "File" = influxframework.get_doc( + doctype="File", + file_url=value, + attached_to_name=doc.name, + attached_to_doctype=doc.doctype, + attached_to_field=df.fieldname, + folder="Home/Attachments", + ) + try: + file.insert(ignore_permissions=True) + except Exception: + doc.log_error("Error Attaching File") + + +def decode_file_content(content: bytes) -> bytes: + if isinstance(content, str): + content = content.encode("utf-8") + if b"," in content: + content = content.split(b",")[1] + return safe_b64decode(content) diff --git a/influxframework/core/doctype/has_domain/__init__.py b/influxframework/core/doctype/has_domain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/has_domain/has_domain.json b/influxframework/core/doctype/has_domain/has_domain.json new file mode 100644 index 0000000..e2b646b --- /dev/null +++ b/influxframework/core/doctype/has_domain/has_domain.json @@ -0,0 +1,72 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2017-05-03 15:20:22.326623", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "domain", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Domain", + "length": 0, + "no_copy": 0, + "options": "Domain", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "max_attachments": 0, + "modified": "2020-09-18 17:26:09.703215", + "modified_by": "Administrator", + "module": "Core", + "name": "Has Domain", + "name_case": "", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/influxframework/core/doctype/has_domain/has_domain.py b/influxframework/core/doctype/has_domain/has_domain.py new file mode 100644 index 0000000..3ce44ce --- /dev/null +++ b/influxframework/core/doctype/has_domain/has_domain.py @@ -0,0 +1,8 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE + +from influxframework.model.document import Document + + +class HasDomain(Document): + pass diff --git a/influxframework/core/doctype/has_role/__init__.py b/influxframework/core/doctype/has_role/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/has_role/has_role.json b/influxframework/core/doctype/has_role/has_role.json new file mode 100644 index 0000000..e0759dc --- /dev/null +++ b/influxframework/core/doctype/has_role/has_role.json @@ -0,0 +1,64 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "hash", + "beta": 0, + "creation": "2013-02-22 01:27:34", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "editable_grid": 1, + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "role", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Role", + "length": 0, + "no_copy": 0, + "oldfieldname": "role", + "oldfieldtype": "Link", + "options": "Role", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 1, + "image_view": 0, + "in_create": 0, + + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "max_attachments": 0, + "modified": "2017-02-13 14:00:08.116312", + "modified_by": "Administrator", + "module": "Core", + "name": "Has Role", + "owner": "Administrator", + "permissions": [], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "track_changes": 0, + "track_seen": 0 +} \ No newline at end of file diff --git a/influxframework/core/doctype/has_role/has_role.py b/influxframework/core/doctype/has_role/has_role.py new file mode 100644 index 0000000..2aac049 --- /dev/null +++ b/influxframework/core/doctype/has_role/has_role.py @@ -0,0 +1,11 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document + + +class HasRole(Document): + def before_insert(self): + if influxframework.db.exists("Has Role", {"parent": self.parent, "role": self.role}): + influxframework.throw(influxframework._("User '{0}' already has the role '{1}'").format(self.parent, self.role)) diff --git a/influxframework/core/doctype/installed_application/__init__.py b/influxframework/core/doctype/installed_application/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/installed_application/installed_application.json b/influxframework/core/doctype/installed_application/installed_application.json new file mode 100644 index 0000000..1f32c55 --- /dev/null +++ b/influxframework/core/doctype/installed_application/installed_application.json @@ -0,0 +1,49 @@ +{ + "actions": [], + "creation": "2020-05-11 17:44:54.674657", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "app_name", + "app_version", + "git_branch" + ], + "fields": [ + { + "fieldname": "git_branch", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Git Branch", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "app_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Application Name", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "app_version", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Application Version", + "read_only": 1, + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-05-12 10:09:49.148087", + "modified_by": "Administrator", + "module": "Core", + "name": "Installed Application", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/installed_application/installed_application.py b/influxframework/core/doctype/installed_application/installed_application.py new file mode 100644 index 0000000..8fa7663 --- /dev/null +++ b/influxframework/core/doctype/installed_application/installed_application.py @@ -0,0 +1,9 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class InstalledApplication(Document): + pass diff --git a/influxframework/core/doctype/installed_applications/__init__.py b/influxframework/core/doctype/installed_applications/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/installed_applications/installed_applications.js b/influxframework/core/doctype/installed_applications/installed_applications.js new file mode 100644 index 0000000..5522290 --- /dev/null +++ b/influxframework/core/doctype/installed_applications/installed_applications.js @@ -0,0 +1,7 @@ +// Copyright (c) 2020, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Installed Applications", { + // refresh: function(frm) { + // } +}); diff --git a/influxframework/core/doctype/installed_applications/installed_applications.json b/influxframework/core/doctype/installed_applications/installed_applications.json new file mode 100644 index 0000000..f2345e6 --- /dev/null +++ b/influxframework/core/doctype/installed_applications/installed_applications.json @@ -0,0 +1,42 @@ +{ + "actions": [], + "creation": "2020-05-11 17:45:41.587750", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "installed_applications" + ], + "fields": [ + { + "fieldname": "installed_applications", + "fieldtype": "Table", + "label": "Installed Applications", + "options": "Installed Application", + "read_only": 1 + } + ], + "issingle": 1, + "links": [], + "modified": "2020-05-12 10:09:14.310622", + "modified_by": "Administrator", + "module": "Core", + "name": "Installed Applications", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/installed_applications/installed_applications.py b/influxframework/core/doctype/installed_applications/installed_applications.py new file mode 100644 index 0000000..426fdcb --- /dev/null +++ b/influxframework/core/doctype/installed_applications/installed_applications.py @@ -0,0 +1,20 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document + + +class InstalledApplications(Document): + def update_versions(self): + self.delete_key("installed_applications") + for app in influxframework.utils.get_installed_apps_info(): + self.append( + "installed_applications", + { + "app_name": app.get("app_name"), + "app_version": app.get("version") or "UNVERSIONED", + "git_branch": app.get("branch") or "UNVERSIONED", + }, + ) + self.save() diff --git a/influxframework/core/doctype/installed_applications/test_installed_applications.py b/influxframework/core/doctype/installed_applications/test_installed_applications.py new file mode 100644 index 0000000..98240f4 --- /dev/null +++ b/influxframework/core/doctype/installed_applications/test_installed_applications.py @@ -0,0 +1,8 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestInstalledApplications(InfluxFrameworkTestCase): + pass diff --git a/influxframework/core/doctype/language/__init__.py b/influxframework/core/doctype/language/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/language/language.js b/influxframework/core/doctype/language/language.js new file mode 100644 index 0000000..93fcb14 --- /dev/null +++ b/influxframework/core/doctype/language/language.js @@ -0,0 +1,6 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Language", { + refresh: function (frm) {}, +}); diff --git a/influxframework/core/doctype/language/language.json b/influxframework/core/doctype/language/language.json new file mode 100644 index 0000000..7e9bbb1 --- /dev/null +++ b/influxframework/core/doctype/language/language.json @@ -0,0 +1,85 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:language_code", + "creation": "2014-08-22 16:12:17.249590", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "enabled", + "language_code", + "language_name", + "flag", + "based_on" + ], + "fields": [ + { + "fieldname": "language_code", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Language Code", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "language_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Language Name", + "reqd": 1 + }, + { + "fieldname": "flag", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Flag" + }, + { + "fieldname": "based_on", + "fieldtype": "Link", + "label": "Based On", + "options": "Language" + }, + { + "default": "1", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enabled" + } + ], + "icon": "fa fa-globe", + "in_create": 1, + "links": [], + "modified": "2022-08-14 18:54:03.490836", + "modified_by": "Administrator", + "module": "Core", + "name": "Language", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "read": 1, + "role": "System Manager", + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Guest", + "share": 1 + } + ], + "search_fields": "language_name", + "show_title_field_in_link": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "language_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/language/language.py b/influxframework/core/doctype/language/language.py new file mode 100644 index 0000000..aae9501 --- /dev/null +++ b/influxframework/core/doctype/language/language.py @@ -0,0 +1,65 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import json +import re + +import influxframework +from influxframework import _ +from influxframework.model.document import Document + + +class Language(Document): + def validate(self): + validate_with_regex(self.language_code, "Language Code") + + def before_rename(self, old, new, merge=False): + validate_with_regex(new, "Name") + + +def validate_with_regex(name, label): + pattern = re.compile("^[a-zA-Z]+[-_]*[a-zA-Z]+$") + if not pattern.match(name): + influxframework.throw( + _( + """{0} must begin and end with a letter and can only contain letters, + hyphen or underscore.""" + ).format(label) + ) + + +def export_languages_json(): + """Export list of all languages""" + languages = influxframework.get_all("Language", fields=["name", "language_name"]) + languages = [{"name": d.language_name, "code": d.name} for d in languages] + + languages.sort(key=lambda a: a["code"]) + + with open(influxframework.get_app_path("influxframework", "geo", "languages.json"), "w") as f: + f.write(influxframework.as_json(languages)) + + +def sync_languages(): + """Sync influxframework/geo/languages.json with Language""" + with open(influxframework.get_app_path("influxframework", "geo", "languages.json")) as f: + data = json.loads(f.read()) + + for l in data: + if not influxframework.db.exists("Language", l["code"]): + influxframework.get_doc( + { + "doctype": "Language", + "language_code": l["code"], + "language_name": l["name"], + "enabled": 1, + } + ).insert() + + +def update_language_names(): + """Update influxframework/geo/languages.json names (for use via patch)""" + with open(influxframework.get_app_path("influxframework", "geo", "languages.json")) as f: + data = json.loads(f.read()) + + for l in data: + influxframework.db.set_value("Language", l["code"], "language_name", l["name"]) diff --git a/influxframework/core/doctype/language/test_language.py b/influxframework/core/doctype/language/test_language.py new file mode 100644 index 0000000..2104fa3 --- /dev/null +++ b/influxframework/core/doctype/language/test_language.py @@ -0,0 +1,9 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + +# test_records = influxframework.get_test_records('Language') + + +class TestLanguage(InfluxFrameworkTestCase): + pass diff --git a/influxframework/core/doctype/log_setting_user/__init__.py b/influxframework/core/doctype/log_setting_user/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/log_setting_user/log_setting_user.js b/influxframework/core/doctype/log_setting_user/log_setting_user.js new file mode 100644 index 0000000..c1b5c5c --- /dev/null +++ b/influxframework/core/doctype/log_setting_user/log_setting_user.js @@ -0,0 +1,7 @@ +// Copyright (c) 2020, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Log Setting User", { + // refresh: function(frm) { + // } +}); diff --git a/influxframework/core/doctype/log_setting_user/log_setting_user.json b/influxframework/core/doctype/log_setting_user/log_setting_user.json new file mode 100644 index 0000000..7f4b0ef --- /dev/null +++ b/influxframework/core/doctype/log_setting_user/log_setting_user.json @@ -0,0 +1,34 @@ +{ + "actions": [], + "autoname": "field:user", + "creation": "2020-10-08 13:09:36.034430", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "in_list_view": 1, + "label": "User", + "options": "User", + "reqd": 1, + "unique": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-10-08 17:22:04.690348", + "modified_by": "Administrator", + "module": "Core", + "name": "Log Setting User", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/log_setting_user/log_setting_user.py b/influxframework/core/doctype/log_setting_user/log_setting_user.py new file mode 100644 index 0000000..87c84a4 --- /dev/null +++ b/influxframework/core/doctype/log_setting_user/log_setting_user.py @@ -0,0 +1,9 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class LogSettingUser(Document): + pass diff --git a/influxframework/core/doctype/log_setting_user/test_log_setting_user.py b/influxframework/core/doctype/log_setting_user/test_log_setting_user.py new file mode 100644 index 0000000..0af6b73 --- /dev/null +++ b/influxframework/core/doctype/log_setting_user/test_log_setting_user.py @@ -0,0 +1,8 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestLogSettingUser(InfluxFrameworkTestCase): + pass diff --git a/influxframework/core/doctype/log_settings/__init__.py b/influxframework/core/doctype/log_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/log_settings/log_settings.js b/influxframework/core/doctype/log_settings/log_settings.js new file mode 100644 index 0000000..fcea865 --- /dev/null +++ b/influxframework/core/doctype/log_settings/log_settings.js @@ -0,0 +1,14 @@ +// Copyright (c) 2020, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Log Settings", { + refresh: (frm) => { + frm.set_query("ref_doctype", "logs_to_clear", () => { + const added_doctypes = frm.doc.logs_to_clear.map((r) => r.ref_doctype); + return { + query: "influxframework.core.doctype.log_settings.log_settings.get_log_doctypes", + filters: [["name", "not in", added_doctypes]], + }; + }); + }, +}); diff --git a/influxframework/core/doctype/log_settings/log_settings.json b/influxframework/core/doctype/log_settings/log_settings.json new file mode 100644 index 0000000..5a9dd15 --- /dev/null +++ b/influxframework/core/doctype/log_settings/log_settings.json @@ -0,0 +1,43 @@ +{ + "actions": [], + "creation": "2020-10-08 12:12:21.694424", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "logs_to_clear" + ], + "fields": [ + { + "fieldname": "logs_to_clear", + "fieldtype": "Table", + "label": "Logs to Clear", + "options": "Logs To Clear" + } + ], + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2022-06-11 02:17:30.803721", + "modified_by": "Administrator", + "module": "Core", + "name": "Log Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/log_settings/log_settings.py b/influxframework/core/doctype/log_settings/log_settings.py new file mode 100644 index 0000000..6559c54 --- /dev/null +++ b/influxframework/core/doctype/log_settings/log_settings.py @@ -0,0 +1,188 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +from typing import Protocol, runtime_checkable + +import influxframework +from influxframework import _ +from influxframework.model.base_document import get_controller +from influxframework.model.document import Document +from influxframework.utils import cint +from influxframework.utils.caching import site_cache + +DEFAULT_LOGTYPES_RETENTION = { + "Error Log": 30, + "Activity Log": 90, + "Email Queue": 30, + "Error Snapshot": 30, + "Scheduled Job Log": 90, + "Route History": 90, +} + + +@runtime_checkable +class LogType(Protocol): + """Interface requirement for doctypes that can be cleared using log settings.""" + + @staticmethod + def clear_old_logs(days: int) -> None: + ... + + +@site_cache +def _supports_log_clearing(doctype: str) -> bool: + try: + controller = get_controller(doctype) + return issubclass(controller, LogType) + except Exception: + return False + + +class LogSettings(Document): + def validate(self): + self.validate_supported_doctypes() + self.validate_duplicates() + self.add_default_logtypes() + + def validate_supported_doctypes(self): + for entry in self.logs_to_clear: + if _supports_log_clearing(entry.ref_doctype): + continue + + msg = _("{} does not support automated log clearing.").format(influxframework.bold(entry.ref_doctype)) + if influxframework.conf.developer_mode: + msg += "
    " + _("Implement `clear_old_logs` method to enable auto error clearing.") + influxframework.throw(msg, title=_("DocType not supported by Log Settings.")) + + def validate_duplicates(self): + seen = set() + for entry in self.logs_to_clear: + if entry.ref_doctype in seen: + influxframework.throw( + _("{} appears more than once in configured log doctypes.").format(entry.ref_doctype) + ) + seen.add(entry.ref_doctype) + + def add_default_logtypes(self): + existing_logtypes = {d.ref_doctype for d in self.logs_to_clear} + added_logtypes = set() + for logtype, retention in DEFAULT_LOGTYPES_RETENTION.items(): + if logtype not in existing_logtypes and _supports_log_clearing(logtype): + self.append("logs_to_clear", {"ref_doctype": logtype, "days": cint(retention)}) + added_logtypes.add(logtype) + + if added_logtypes: + influxframework.msgprint( + _("Added default log doctypes: {}").format(",".join(added_logtypes)), alert=True + ) + + def clear_logs(self): + """ + Log settings can clear any log type that's registered to it and provides a method to delete old logs. + + Check `LogDoctype` above for interface that doctypes need to implement. + """ + + for entry in self.logs_to_clear: + controller: LogType = get_controller(entry.ref_doctype) + func = controller.clear_old_logs + + # Only pass what the method can handle, this is considering any + # future addition that might happen to the required interface. + kwargs = influxframework.get_newargs(func, {"days": entry.days}) + func(**kwargs) + influxframework.db.commit() + + def register_doctype(self, doctype: str, days=30): + existing_logtypes = {d.ref_doctype for d in self.logs_to_clear} + + if doctype not in existing_logtypes and _supports_log_clearing(doctype): + self.append("logs_to_clear", {"ref_doctype": doctype, "days": cint(days)}) + else: + for entry in self.logs_to_clear: + if entry.ref_doctype == doctype: + entry.days = days + break + + +def run_log_clean_up(): + doc = influxframework.get_doc("Log Settings") + doc.add_default_logtypes() + doc.save() + doc.clear_logs() + + +@influxframework.whitelist() +def has_unseen_error_log(): + if influxframework.get_all("Error Log", filters={"seen": 0}, limit=1): + return { + "show_alert": True, + "message": _("You have unseen {0}").format( + ' Error Logs ' + ), + } + + +@influxframework.whitelist() +@influxframework.validate_and_sanitize_search_inputs +def get_log_doctypes(doctype, txt, searchfield, start, page_len, filters): + + filters = filters or {} + + filters.extend( + [ + ["istable", "=", 0], + ["issingle", "=", 0], + ["name", "like", f"%%{txt}%%"], + ] + ) + doctypes = influxframework.get_list("DocType", filters=filters, pluck="name") + + supported_doctypes = [(d,) for d in doctypes if _supports_log_clearing(d)] + + return supported_doctypes[start:page_len] + + +LOG_DOCTYPES = [ + "Scheduled Job Log", + "Activity Log", + "Route History", + "Email Queue", + "Email Queue Recipient", + "Error Snapshot", + "Error Log", +] + + +def clear_log_table(doctype, days=90): + """If any logtype table grows too large then clearing it with DELETE query + is not feasible in reasonable time. This command copies recent data to new + table and replaces current table with new smaller table. + + ref: https://mariadb.com/kb/en/big-deletes/#deleting-more-than-half-a-table + """ + from influxframework.utils import get_table_name + + if doctype not in LOG_DOCTYPES: + raise influxframework.ValidationError(f"Unsupported logging DocType: {doctype}") + + original = get_table_name(doctype) + temporary = f"{original} temp_table" + backup = f"{original} backup_table" + + try: + influxframework.db.sql_ddl(f"CREATE TABLE `{temporary}` LIKE `{original}`") + + # Copy all recent data to new table + influxframework.db.sql( + f"""INSERT INTO `{temporary}` + SELECT * FROM `{original}` + WHERE `{original}`.`modified` > NOW() - INTERVAL '{days}' DAY""" + ) + influxframework.db.sql_ddl(f"RENAME TABLE `{original}` TO `{backup}`, `{temporary}` TO `{original}`") + except Exception: + influxframework.db.rollback() + influxframework.db.sql_ddl(f"DROP TABLE IF EXISTS `{temporary}`") + raise + else: + influxframework.db.sql_ddl(f"DROP TABLE `{backup}`") diff --git a/influxframework/core/doctype/log_settings/test_log_settings.py b/influxframework/core/doctype/log_settings/test_log_settings.py new file mode 100644 index 0000000..fa2c8ae --- /dev/null +++ b/influxframework/core/doctype/log_settings/test_log_settings.py @@ -0,0 +1,105 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE + +from datetime import datetime + +import influxframework +from influxframework.core.doctype.log_settings.log_settings import _supports_log_clearing, run_log_clean_up +from influxframework.tests.utils import InfluxFrameworkTestCase +from influxframework.utils import add_to_date, now_datetime + + +class TestLogSettings(InfluxFrameworkTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + influxframework.db.set_single_value( + "Log Settings", + { + "clear_error_log_after": 1, + "clear_activity_log_after": 1, + "clear_email_queue_after": 1, + }, + ) + + def setUp(self) -> None: + if self._testMethodName == "test_delete_logs": + self.datetime = influxframework._dict() + self.datetime.current = now_datetime() + self.datetime.past = add_to_date(self.datetime.current, days=-4) + setup_test_logs(self.datetime.past) + + def tearDown(self) -> None: + if self._testMethodName == "test_delete_logs": + del self.datetime + + def test_delete_logs(self): + # make sure test data is present + activity_log_count = influxframework.db.count("Activity Log", {"creation": ("<=", self.datetime.past)}) + error_log_count = influxframework.db.count("Error Log", {"creation": ("<=", self.datetime.past)}) + email_queue_count = influxframework.db.count("Email Queue", {"creation": ("<=", self.datetime.past)}) + + self.assertNotEqual(activity_log_count, 0) + self.assertNotEqual(error_log_count, 0) + self.assertNotEqual(email_queue_count, 0) + + # run clean up job + run_log_clean_up() + + # test if logs are deleted + activity_log_count = influxframework.db.count("Activity Log", {"creation": ("<", self.datetime.past)}) + error_log_count = influxframework.db.count("Error Log", {"creation": ("<", self.datetime.past)}) + email_queue_count = influxframework.db.count("Email Queue", {"creation": ("<", self.datetime.past)}) + + self.assertEqual(activity_log_count, 0) + self.assertEqual(error_log_count, 0) + self.assertEqual(email_queue_count, 0) + + def test_logtype_identification(self): + supported_types = [ + "Error Log", + "Activity Log", + "Email Queue", + "Route History", + "Error Snapshot", + "Scheduled Job Log", + ] + + for lt in supported_types: + self.assertTrue(_supports_log_clearing(lt), f"{lt} should be recognized as log type") + + unsupported_types = ["DocType", "User", "Non Existing dt"] + for dt in unsupported_types: + self.assertFalse(_supports_log_clearing(dt), f"{dt} shouldn't be recognized as log type") + + +def setup_test_logs(past: datetime) -> None: + activity_log = influxframework.get_doc( + { + "doctype": "Activity Log", + "subject": "Test subject", + "full_name": "test user2", + } + ).insert(ignore_permissions=True) + activity_log.db_set("creation", past) + + error_log = influxframework.get_doc( + { + "doctype": "Error Log", + "method": "test_method", + "error": "traceback", + } + ).insert(ignore_permissions=True) + error_log.db_set("creation", past) + + doc1 = influxframework.get_doc( + { + "doctype": "Email Queue", + "sender": "test1@example.com", + "message": "This is a test email1", + "priority": 1, + "expose_recipients": "test@receiver.com", + } + ).insert(ignore_permissions=True) + doc1.db_set("creation", past) diff --git a/influxframework/core/doctype/logs_to_clear/__init__.py b/influxframework/core/doctype/logs_to_clear/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/logs_to_clear/logs_to_clear.json b/influxframework/core/doctype/logs_to_clear/logs_to_clear.json new file mode 100644 index 0000000..df19ccd --- /dev/null +++ b/influxframework/core/doctype/logs_to_clear/logs_to_clear.json @@ -0,0 +1,43 @@ +{ + "actions": [], + "autoname": "hash", + "creation": "2022-06-11 02:02:39.472511", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "ref_doctype", + "days" + ], + "fields": [ + { + "fieldname": "ref_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Log DocType", + "options": "DocType", + "reqd": 1 + }, + { + "default": "30", + "fieldname": "days", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Clear Logs After (days)", + "non_negative": 1, + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2022-06-13 02:51:36.857786", + "modified_by": "Administrator", + "module": "Core", + "name": "Logs To Clear", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} diff --git a/influxframework/core/doctype/logs_to_clear/logs_to_clear.py b/influxframework/core/doctype/logs_to_clear/logs_to_clear.py new file mode 100644 index 0000000..6861b91 --- /dev/null +++ b/influxframework/core/doctype/logs_to_clear/logs_to_clear.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, InfluxFramework LLC +# For license information, please see license.txt + +# import influxframework +from influxframework.model.document import Document + + +class LogsToClear(Document): + pass diff --git a/influxframework/core/doctype/module_def/README.md b/influxframework/core/doctype/module_def/README.md new file mode 100644 index 0000000..ff89390 --- /dev/null +++ b/influxframework/core/doctype/module_def/README.md @@ -0,0 +1 @@ +Module of the application (e.g. Core, Accounts etc.) \ No newline at end of file diff --git a/influxframework/core/doctype/module_def/__init__.py b/influxframework/core/doctype/module_def/__init__.py new file mode 100644 index 0000000..2e11a12 --- /dev/null +++ b/influxframework/core/doctype/module_def/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE diff --git a/influxframework/core/doctype/module_def/module_def.js b/influxframework/core/doctype/module_def/module_def.js new file mode 100644 index 0000000..33ba6a8 --- /dev/null +++ b/influxframework/core/doctype/module_def/module_def.js @@ -0,0 +1,13 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Module Def", { + refresh: function (frm) { + influxframework.xcall("influxframework.core.doctype.module_def.module_def.get_installed_apps").then((r) => { + frm.set_df_property("app_name", "options", JSON.parse(r)); + if (!frm.doc.app_name) { + frm.set_value("app_name", "influxframework"); + } + }); + }, +}); diff --git a/influxframework/core/doctype/module_def/module_def.json b/influxframework/core/doctype/module_def/module_def.json new file mode 100644 index 0000000..12830c8 --- /dev/null +++ b/influxframework/core/doctype/module_def/module_def.json @@ -0,0 +1,166 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:module_name", + "creation": "2013-01-10 16:34:03", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "module_name", + "custom", + "package", + "app_name", + "restrict_to_domain", + "connections_tab" + ], + "fields": [ + { + "fieldname": "module_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Module Name", + "oldfieldname": "module_name", + "oldfieldtype": "Data", + "reqd": 1, + "unique": 1 + }, + { + "depends_on": "eval:!doc.custom", + "fieldname": "app_name", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "App Name", + "reqd": 1 + }, + { + "fieldname": "restrict_to_domain", + "fieldtype": "Link", + "label": "Restrict To Domain", + "options": "Domain" + }, + { + "default": "0", + "fieldname": "custom", + "fieldtype": "Check", + "label": "Custom" + }, + { + "depends_on": "custom", + "fieldname": "package", + "fieldtype": "Link", + "label": "Package", + "options": "Package" + }, + { + "fieldname": "connections_tab", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 + } + ], + "icon": "fa fa-sitemap", + "idx": 1, + "links": [ + { + "group": "DocType", + "link_doctype": "DocType", + "link_fieldname": "module" + }, + { + "group": "DocType", + "link_doctype": "Client Script", + "link_fieldname": "module" + }, + { + "group": "DocType", + "link_doctype": "Server Script", + "link_fieldname": "module" + }, + { + "group": "Website", + "link_doctype": "Web Page", + "link_fieldname": "module" + }, + { + "group": "Website", + "link_doctype": "Web Template", + "link_fieldname": "module" + }, + { + "group": "Website", + "link_doctype": "Website Theme", + "link_fieldname": "module" + }, + { + "group": "Website", + "link_doctype": "Web Form", + "link_fieldname": "module" + }, + { + "group": "Customization", + "link_doctype": "Workspace", + "link_fieldname": "module" + }, + { + "group": "Customization", + "link_doctype": "Custom Field", + "link_fieldname": "module" + }, + { + "group": "Customization", + "link_doctype": "Property Setter", + "link_fieldname": "module" + }, + { + "group": "Customization", + "link_doctype": "Print Format", + "link_fieldname": "module" + }, + { + "group": "Customization", + "link_doctype": "Notification", + "link_fieldname": "module" + } + ], + "modified": "2022-01-03 13:56:52.817954", + "modified_by": "Administrator", + "module": "Core", + "name": "Module Def", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "select": 1, + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "select": 1, + "write": 1 + }, + { + "read": 1, + "report": 1, + "role": "All", + "select": 1 + } + ], + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "ASC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/module_def/module_def.py b/influxframework/core/doctype/module_def/module_def.py new file mode 100644 index 0000000..f5df031 --- /dev/null +++ b/influxframework/core/doctype/module_def/module_def.py @@ -0,0 +1,69 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import json +import os + +import influxframework +from influxframework.model.document import Document + + +class ModuleDef(Document): + def on_update(self): + """If in `developer_mode`, create folder for module and + add in `modules.txt` of app if missing.""" + influxframework.clear_cache() + if not self.custom and influxframework.conf.get("developer_mode"): + self.create_modules_folder() + self.add_to_modules_txt() + + def create_modules_folder(self): + """Creates a folder `[app]/[module]` and adds `__init__.py`""" + module_path = influxframework.get_app_path(self.app_name, self.name) + if not os.path.exists(module_path): + os.mkdir(module_path) + with open(os.path.join(module_path, "__init__.py"), "w") as f: + f.write("") + + def add_to_modules_txt(self): + """Adds to `[app]/modules.txt`""" + modules = None + if not influxframework.local.module_app.get(influxframework.scrub(self.name)): + with open(influxframework.get_app_path(self.app_name, "modules.txt")) as f: + content = f.read() + if not self.name in content.splitlines(): + modules = list(filter(None, content.splitlines())) + modules.append(self.name) + + if modules: + with open(influxframework.get_app_path(self.app_name, "modules.txt"), "w") as f: + f.write("\n".join(modules)) + + influxframework.clear_cache() + influxframework.setup_module_map() + + def on_trash(self): + """Delete module name from modules.txt""" + + if not influxframework.conf.get("developer_mode") or influxframework.flags.in_uninstall or self.custom: + return + + modules = None + if influxframework.local.module_app.get(influxframework.scrub(self.name)): + with open(influxframework.get_app_path(self.app_name, "modules.txt")) as f: + content = f.read() + if self.name in content.splitlines(): + modules = list(filter(None, content.splitlines())) + modules.remove(self.name) + + if modules: + with open(influxframework.get_app_path(self.app_name, "modules.txt"), "w") as f: + f.write("\n".join(modules)) + + influxframework.clear_cache() + influxframework.setup_module_map() + + +@influxframework.whitelist() +def get_installed_apps(): + return json.dumps(influxframework.get_installed_apps()) diff --git a/influxframework/core/doctype/module_def/test_module_def.py b/influxframework/core/doctype/module_def/test_module_def.py new file mode 100644 index 0000000..0cd9b8c --- /dev/null +++ b/influxframework/core/doctype/module_def/test_module_def.py @@ -0,0 +1,9 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + +# test_records = influxframework.get_test_records('Module Def') + + +class TestModuleDef(InfluxFrameworkTestCase): + pass diff --git a/influxframework/core/doctype/module_profile/__init__.py b/influxframework/core/doctype/module_profile/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/module_profile/module_profile.js b/influxframework/core/doctype/module_profile/module_profile.js new file mode 100644 index 0000000..e25ad34 --- /dev/null +++ b/influxframework/core/doctype/module_profile/module_profile.js @@ -0,0 +1,23 @@ +// Copyright (c) 2020, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Module Profile", { + refresh: function (frm) { + if (has_common(influxframework.user_roles, ["Administrator", "System Manager"])) { + if (!frm.module_editor && frm.doc.__onload && frm.doc.__onload.all_modules) { + const module_area = $(frm.fields_dict.module_html.wrapper); + frm.module_editor = new influxframework.ModuleEditor(frm, module_area); + } + } + + if (frm.module_editor) { + frm.module_editor.show(); + } + }, + + validate: function (frm) { + if (frm.module_editor) { + frm.module_editor.set_modules_in_table(); + } + }, +}); diff --git a/influxframework/core/doctype/module_profile/module_profile.json b/influxframework/core/doctype/module_profile/module_profile.json new file mode 100644 index 0000000..32bc757 --- /dev/null +++ b/influxframework/core/doctype/module_profile/module_profile.json @@ -0,0 +1,66 @@ +{ + "actions": [], + "autoname": "field:module_profile_name", + "creation": "2020-12-22 22:00:30.614475", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "module_profile_name", + "module_html", + "block_modules" + ], + "fields": [ + { + "fieldname": "module_profile_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Module Profile Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "module_html", + "fieldtype": "HTML", + "label": "Module HTML" + }, + { + "fieldname": "block_modules", + "fieldtype": "Table", + "hidden": 1, + "label": "Block Modules", + "options": "Block Module", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [ + { + "link_doctype": "User", + "link_fieldname": "module_profile" + } + ], + "modified": "2021-12-03 15:47:21.296443", + "modified_by": "Administrator", + "module": "Core", + "name": "Module Profile", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/module_profile/module_profile.py b/influxframework/core/doctype/module_profile/module_profile.py new file mode 100644 index 0000000..0d61e10 --- /dev/null +++ b/influxframework/core/doctype/module_profile/module_profile.py @@ -0,0 +1,11 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +from influxframework.model.document import Document + + +class ModuleProfile(Document): + def onload(self): + from influxframework.config import get_modules_from_all_apps + + self.set_onload("all_modules", [m.get("module_name") for m in get_modules_from_all_apps()]) diff --git a/influxframework/core/doctype/module_profile/test_module_profile.py b/influxframework/core/doctype/module_profile/test_module_profile.py new file mode 100644 index 0000000..bd55519 --- /dev/null +++ b/influxframework/core/doctype/module_profile/test_module_profile.py @@ -0,0 +1,29 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestModuleProfile(InfluxFrameworkTestCase): + def test_make_new_module_profile(self): + if not influxframework.db.get_value("Module Profile", "_Test Module Profile"): + influxframework.get_doc( + { + "doctype": "Module Profile", + "module_profile_name": "_Test Module Profile", + "block_modules": [{"module": "Accounts"}], + } + ).insert() + + # add to user and check + if not influxframework.db.get_value("User", "test-for-module_profile@example.com"): + new_user = influxframework.get_doc( + {"doctype": "User", "email": "test-for-module_profile@example.com", "first_name": "Test User"} + ).insert() + else: + new_user = influxframework.get_doc("User", "test-for-module_profile@example.com") + + new_user.module_profile = "_Test Module Profile" + new_user.save() + + self.assertEqual(new_user.block_modules[0].module, "Accounts") diff --git a/influxframework/core/doctype/navbar_item/__init__.py b/influxframework/core/doctype/navbar_item/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/navbar_item/navbar_item.js b/influxframework/core/doctype/navbar_item/navbar_item.js new file mode 100644 index 0000000..816b65e --- /dev/null +++ b/influxframework/core/doctype/navbar_item/navbar_item.js @@ -0,0 +1,7 @@ +// Copyright (c) 2020, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Navbar Item", { + // refresh: function(frm) { + // } +}); diff --git a/influxframework/core/doctype/navbar_item/navbar_item.json b/influxframework/core/doctype/navbar_item/navbar_item.json new file mode 100644 index 0000000..541d785 --- /dev/null +++ b/influxframework/core/doctype/navbar_item/navbar_item.json @@ -0,0 +1,89 @@ +{ + "actions": [], + "creation": "2020-08-01 23:38:41.783206", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "item_label", + "item_type", + "route", + "action", + "hidden", + "is_standard" + ], + "fields": [ + { + "columns": 2, + "fieldname": "item_label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Item Label", + "mandatory_depends_on": "eval:doc.item_type == 'Route' || doc.item_type == 'Action'", + "show_days": 1, + "show_seconds": 1 + }, + { + "columns": 2, + "fieldname": "item_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Item Type", + "options": "Route\nAction\nSeparator", + "read_only_depends_on": "eval:doc.is_standard", + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "0", + "fieldname": "hidden", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Hidden", + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "0", + "fieldname": "is_standard", + "fieldtype": "Check", + "label": "Is Standard", + "read_only": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "columns": 4, + "depends_on": "eval:doc.item_type == 'Route'", + "fieldname": "route", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Route", + "mandatory_depends_on": "eval:doc.item_type == 'Route'", + "read_only_depends_on": "eval:doc.is_standard", + "show_days": 1, + "show_seconds": 1 + }, + { + "depends_on": "eval:doc.item_type == 'Action'", + "fieldname": "action", + "fieldtype": "Data", + "label": "Action", + "mandatory_depends_on": "eval:doc.item_type == 'Action'", + "read_only_depends_on": "eval:doc.is_standard", + "show_days": 1, + "show_seconds": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-11-02 10:57:37.709262", + "modified_by": "Administrator", + "module": "Core", + "name": "Navbar Item", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/navbar_item/navbar_item.py b/influxframework/core/doctype/navbar_item/navbar_item.py new file mode 100644 index 0000000..426c926 --- /dev/null +++ b/influxframework/core/doctype/navbar_item/navbar_item.py @@ -0,0 +1,9 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class NavbarItem(Document): + pass diff --git a/influxframework/core/doctype/navbar_item/test_navbar_item.py b/influxframework/core/doctype/navbar_item/test_navbar_item.py new file mode 100644 index 0000000..44efb8b --- /dev/null +++ b/influxframework/core/doctype/navbar_item/test_navbar_item.py @@ -0,0 +1,8 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestNavbarItem(InfluxFrameworkTestCase): + pass diff --git a/influxframework/core/doctype/navbar_settings/__init__.py b/influxframework/core/doctype/navbar_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/navbar_settings/navbar_settings.js b/influxframework/core/doctype/navbar_settings/navbar_settings.js new file mode 100644 index 0000000..8737311 --- /dev/null +++ b/influxframework/core/doctype/navbar_settings/navbar_settings.js @@ -0,0 +1,7 @@ +// Copyright (c) 2020, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Navbar Settings", { + // refresh: function(frm) { + // } +}); diff --git a/influxframework/core/doctype/navbar_settings/navbar_settings.json b/influxframework/core/doctype/navbar_settings/navbar_settings.json new file mode 100644 index 0000000..8fc0c83 --- /dev/null +++ b/influxframework/core/doctype/navbar_settings/navbar_settings.json @@ -0,0 +1,91 @@ +{ + "actions": [], + "creation": "2020-08-01 23:41:12.577160", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "logo_section", + "app_logo", + "column_break_3", + "logo_width", + "section_break_2", + "settings_dropdown", + "help_dropdown" + ], + "fields": [ + { + "fieldname": "app_logo", + "fieldtype": "Attach Image", + "label": "Application Logo", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "settings_dropdown", + "fieldtype": "Table", + "label": "Settings Dropdown", + "options": "Navbar Item", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "help_dropdown", + "fieldtype": "Table", + "label": "Help Dropdown", + "options": "Navbar Item", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "section_break_2", + "fieldtype": "Section Break", + "label": "Dropdowns", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "logo_section", + "fieldtype": "Section Break", + "label": "Application Logo", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "logo_width", + "fieldtype": "Int", + "label": "Logo Width", + "show_days": 1, + "show_seconds": 1 + } + ], + "issingle": 1, + "links": [], + "modified": "2020-08-06 18:11:29.955835", + "modified_by": "Administrator", + "module": "Core", + "name": "Navbar Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/navbar_settings/navbar_settings.py b/influxframework/core/doctype/navbar_settings/navbar_settings.py new file mode 100644 index 0000000..bdd76a1 --- /dev/null +++ b/influxframework/core/doctype/navbar_settings/navbar_settings.py @@ -0,0 +1,43 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework import _ +from influxframework.model.document import Document + + +class NavbarSettings(Document): + def validate(self): + self.validate_standard_navbar_items() + + def validate_standard_navbar_items(self): + doc_before_save = self.get_doc_before_save() + + if not doc_before_save: + return + + before_save_items = [ + item + for item in doc_before_save.help_dropdown + doc_before_save.settings_dropdown + if item.is_standard + ] + + after_save_items = [ + item for item in self.help_dropdown + self.settings_dropdown if item.is_standard + ] + + if not influxframework.flags.in_patch and (len(before_save_items) > len(after_save_items)): + influxframework.throw(_("Please hide the standard navbar items instead of deleting them")) + + +def get_app_logo(): + app_logo = influxframework.db.get_single_value("Navbar Settings", "app_logo", cache=True) + if not app_logo: + app_logo = influxframework.get_hooks("app_logo_url")[-1] + + return app_logo + + +def get_navbar_settings(): + navbar_settings = influxframework.get_single("Navbar Settings") + return navbar_settings diff --git a/influxframework/core/doctype/navbar_settings/test_navbar_settings.py b/influxframework/core/doctype/navbar_settings/test_navbar_settings.py new file mode 100644 index 0000000..d4ffcad --- /dev/null +++ b/influxframework/core/doctype/navbar_settings/test_navbar_settings.py @@ -0,0 +1,8 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestNavbarSettings(InfluxFrameworkTestCase): + pass diff --git a/influxframework/core/doctype/package/__init__.py b/influxframework/core/doctype/package/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/package/licenses/GNU Affero General Public License.md b/influxframework/core/doctype/package/licenses/GNU Affero General Public License.md new file mode 100644 index 0000000..c7f159a --- /dev/null +++ b/influxframework/core/doctype/package/licenses/GNU Affero General Public License.md @@ -0,0 +1,614 @@ +### GNU AFFERO GENERAL PUBLIC LICENSE + +Version 3, 19 November 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +### Preamble + +The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains +free software for all its users. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + +A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + +The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + +An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing +under this license. + +The precise terms and conditions for copying, distribution and +modification follow. + +### TERMS AND CONDITIONS + +#### 0. Definitions. + +"This License" refers to version 3 of the GNU Affero General Public +License. + +"Copyright" also means copyright-like laws that apply to other kinds +of works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of +an exact copy. The resulting work is called a "modified version" of +the earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user +through a computer network, with no transfer of a copy, is not +conveying. + +An interactive user interface displays "Appropriate Legal Notices" to +the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +#### 1. Source Code. + +The "source code" for a work means the preferred form of the work for +making modifications to it. "Object code" means any non-source form of +a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can +regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same +work. + +#### 2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, +without conditions so long as your license otherwise remains in force. +You may convey covered works to others for the sole purpose of having +them make modifications exclusively for you, or provide you with +facilities for running those works, provided that you comply with the +terms of this License in conveying all material for which you do not +control copyright. Those thus making or running the covered works for +you must do so exclusively on your behalf, under your direction and +control, on terms that prohibit them from making any copies of your +copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the +conditions stated below. Sublicensing is not allowed; section 10 makes +it unnecessary. + +#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such +circumvention is effected by exercising rights under this License with +respect to the covered work, and you disclaim any intention to limit +operation or modification of the work as a means of enforcing, against +the work's users, your or third parties' legal rights to forbid +circumvention of technological measures. + +#### 4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +#### 5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these +conditions: + +- a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. +- b) The work must carry prominent notices stating that it is + released under this License and any conditions added under + section 7. This requirement modifies the requirement in section 4 + to "keep intact all notices". +- c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. +- d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +#### 6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms of +sections 4 and 5, provided that you also convey the machine-readable +Corresponding Source under the terms of this License, in one of these +ways: + +- a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. +- b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the Corresponding + Source from a network server at no charge. +- c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. +- d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. +- e) Convey the object code using peer-to-peer transmission, + provided you inform other peers where the object code and + Corresponding Source of the work are being offered to the general + public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, +family, or household purposes, or (2) anything designed or sold for +incorporation into a dwelling. In determining whether a product is a +consumer product, doubtful cases shall be resolved in favor of +coverage. For a particular product received by a particular user, +"normally used" refers to a typical or common use of that class of +product, regardless of the status of the particular user or of the way +in which the particular user actually uses, or expects or is expected +to use, the product. A product is a consumer product regardless of +whether the product has substantial commercial, industrial or +non-consumer uses, unless such uses represent the only significant +mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to +install and execute modified versions of a covered work in that User +Product from a modified version of its Corresponding Source. The +information must suffice to ensure that the continued functioning of +the modified object code is in no case prevented or interfered with +solely because modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or +updates for a work that has been modified or installed by the +recipient, or for the User Product in which it has been modified or +installed. Access to a network may be denied when the modification +itself materially and adversely affects the operation of the network +or violates the rules and protocols for communication across the +network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +#### 7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders +of that material) supplement the terms of this License with terms: + +- a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or +- b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or +- c) Prohibiting misrepresentation of the origin of that material, + or requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or +- d) Limiting the use for publicity purposes of names of licensors + or authors of the material; or +- e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or +- f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions + of it) with contractual assumptions of liability to the recipient, + for any liability that these contractual assumptions directly + impose on those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; the +above requirements apply either way. + +#### 8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your license +from a particular copyright holder is reinstated (a) provisionally, +unless and until the copyright holder explicitly and finally +terminates your license, and (b) permanently, if the copyright holder +fails to notify you of the violation by some reasonable means prior to +60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +#### 9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run +a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +#### 10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +#### 11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned +or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within the +scope of its coverage, prohibits the exercise of, or is conditioned on +the non-exercise of one or more of the rights that are specifically +granted under this License. You may not convey a covered work if you +are a party to an arrangement with a third party that is in the +business of distributing software, under which you make payment to the +third party based on the extent of your activity of conveying the +work, and under which the third party grants, to any of the parties +who would receive the covered work from you, a discriminatory patent +license (a) in connection with copies of the covered work conveyed by +you (or copies made from those copies), or (b) primarily for and in +connection with specific products or compilations that contain the +covered work, unless you entered into that arrangement, or that patent +license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +#### 12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under +this License and any other pertinent obligations, then as a +consequence you may not convey it at all. For example, if you agree to +terms that obligate you to collect a royalty for further conveying +from those to whom you convey the Program, the only way you could +satisfy both those terms and this License would be to refrain entirely +from conveying the Program. + +#### 13. Remote Network Interaction; Use with the GNU General Public License. + +Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your +version supports such interaction) an opportunity to receive the +Corresponding Source of your version by providing access to the +Corresponding Source from a network server at no charge, through some +standard or customary means of facilitating copying of software. This +Corresponding Source shall include the Corresponding Source for any +work covered by version 3 of the GNU General Public License that is +incorporated pursuant to the following paragraph. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + +#### 14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions +of the GNU Affero General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever +published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions +of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +#### 15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT +WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND +PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE +DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR +CORRECTION. + +#### 16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR +CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT +NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR +LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM +TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER +PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +#### 17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. diff --git a/influxframework/core/doctype/package/licenses/GNU General Public License.md b/influxframework/core/doctype/package/licenses/GNU General Public License.md new file mode 100644 index 0000000..c4580f2 --- /dev/null +++ b/influxframework/core/doctype/package/licenses/GNU General Public License.md @@ -0,0 +1,617 @@ +### GNU GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +### Preamble + +The GNU General Public License is a free, copyleft license for +software and other kinds of works. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom +to share and change all versions of a program--to make sure it remains +free software for all its users. We, the Free Software Foundation, use +the GNU General Public License for most of our software; it applies +also to any other work released this way by its authors. You can apply +it to your programs, too. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you +have certain responsibilities if you distribute copies of the +software, or if you modify it: responsibilities to respect the freedom +of others. + +For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + +Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + +Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the +manufacturer can do so. This is fundamentally incompatible with the +aim of protecting users' freedom to change the software. The +systematic pattern of such abuse occurs in the area of products for +individuals to use, which is precisely where it is most unacceptable. +Therefore, we have designed this version of the GPL to prohibit the +practice for those products. If such problems arise substantially in +other domains, we stand ready to extend this provision to those +domains in future versions of the GPL, as needed to protect the +freedom of users. + +Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish +to avoid the special danger that patents applied to a free program +could make it effectively proprietary. To prevent this, the GPL +assures that patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and +modification follow. + +### TERMS AND CONDITIONS + +#### 0. Definitions. + +"This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds +of works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of +an exact copy. The resulting work is called a "modified version" of +the earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user +through a computer network, with no transfer of a copy, is not +conveying. + +An interactive user interface displays "Appropriate Legal Notices" to +the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +#### 1. Source Code. + +The "source code" for a work means the preferred form of the work for +making modifications to it. "Object code" means any non-source form of +a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can +regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same +work. + +#### 2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, +without conditions so long as your license otherwise remains in force. +You may convey covered works to others for the sole purpose of having +them make modifications exclusively for you, or provide you with +facilities for running those works, provided that you comply with the +terms of this License in conveying all material for which you do not +control copyright. Those thus making or running the covered works for +you must do so exclusively on your behalf, under your direction and +control, on terms that prohibit them from making any copies of your +copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the +conditions stated below. Sublicensing is not allowed; section 10 makes +it unnecessary. + +#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such +circumvention is effected by exercising rights under this License with +respect to the covered work, and you disclaim any intention to limit +operation or modification of the work as a means of enforcing, against +the work's users, your or third parties' legal rights to forbid +circumvention of technological measures. + +#### 4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +#### 5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these +conditions: + +- a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. +- b) The work must carry prominent notices stating that it is + released under this License and any conditions added under + section 7. This requirement modifies the requirement in section 4 + to "keep intact all notices". +- c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. +- d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +#### 6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms of +sections 4 and 5, provided that you also convey the machine-readable +Corresponding Source under the terms of this License, in one of these +ways: + +- a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. +- b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the Corresponding + Source from a network server at no charge. +- c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. +- d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. +- e) Convey the object code using peer-to-peer transmission, + provided you inform other peers where the object code and + Corresponding Source of the work are being offered to the general + public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, +family, or household purposes, or (2) anything designed or sold for +incorporation into a dwelling. In determining whether a product is a +consumer product, doubtful cases shall be resolved in favor of +coverage. For a particular product received by a particular user, +"normally used" refers to a typical or common use of that class of +product, regardless of the status of the particular user or of the way +in which the particular user actually uses, or expects or is expected +to use, the product. A product is a consumer product regardless of +whether the product has substantial commercial, industrial or +non-consumer uses, unless such uses represent the only significant +mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to +install and execute modified versions of a covered work in that User +Product from a modified version of its Corresponding Source. The +information must suffice to ensure that the continued functioning of +the modified object code is in no case prevented or interfered with +solely because modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or +updates for a work that has been modified or installed by the +recipient, or for the User Product in which it has been modified or +installed. Access to a network may be denied when the modification +itself materially and adversely affects the operation of the network +or violates the rules and protocols for communication across the +network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +#### 7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders +of that material) supplement the terms of this License with terms: + +- a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or +- b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or +- c) Prohibiting misrepresentation of the origin of that material, + or requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or +- d) Limiting the use for publicity purposes of names of licensors + or authors of the material; or +- e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or +- f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions + of it) with contractual assumptions of liability to the recipient, + for any liability that these contractual assumptions directly + impose on those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; the +above requirements apply either way. + +#### 8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your license +from a particular copyright holder is reinstated (a) provisionally, +unless and until the copyright holder explicitly and finally +terminates your license, and (b) permanently, if the copyright holder +fails to notify you of the violation by some reasonable means prior to +60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +#### 9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run +a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +#### 10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +#### 11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned +or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within the +scope of its coverage, prohibits the exercise of, or is conditioned on +the non-exercise of one or more of the rights that are specifically +granted under this License. You may not convey a covered work if you +are a party to an arrangement with a third party that is in the +business of distributing software, under which you make payment to the +third party based on the extent of your activity of conveying the +work, and under which the third party grants, to any of the parties +who would receive the covered work from you, a discriminatory patent +license (a) in connection with copies of the covered work conveyed by +you (or copies made from those copies), or (b) primarily for and in +connection with specific products or compilations that contain the +covered work, unless you entered into that arrangement, or that patent +license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +#### 12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under +this License and any other pertinent obligations, then as a +consequence you may not convey it at all. For example, if you agree to +terms that obligate you to collect a royalty for further conveying +from those to whom you convey the Program, the only way you could +satisfy both those terms and this License would be to refrain entirely +from conveying the Program. + +#### 13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + +#### 14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions +of the GNU General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in +detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies that a certain numbered version of the GNU General Public +License "or any later version" applies to it, you have the option of +following the terms and conditions either of that numbered version or +of any later version published by the Free Software Foundation. If the +Program does not specify a version number of the GNU General Public +License, you may choose any version ever published by the Free +Software Foundation. + +If the Program specifies that a proxy can decide which future versions +of the GNU General Public License can be used, that proxy's public +statement of acceptance of a version permanently authorizes you to +choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +#### 15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT +WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND +PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE +DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR +CORRECTION. + +#### 16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR +CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT +NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR +LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM +TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER +PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +#### 17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. diff --git a/influxframework/core/doctype/package/licenses/MIT License.md b/influxframework/core/doctype/package/licenses/MIT License.md new file mode 100644 index 0000000..c038ee7 --- /dev/null +++ b/influxframework/core/doctype/package/licenses/MIT License.md @@ -0,0 +1,17 @@ +### MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies +or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/influxframework/core/doctype/package/package.js b/influxframework/core/doctype/package/package.js new file mode 100644 index 0000000..d2d13c5 --- /dev/null +++ b/influxframework/core/doctype/package/package.js @@ -0,0 +1,20 @@ +// Copyright (c) 2021, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Package", { + validate: function (frm) { + if (!frm.doc.package_name) { + frm.set_value("package_name", frm.doc.name.toLowerCase().replace(" ", "-")); + } + }, + + license_type: function (frm) { + influxframework + .call("influxframework.core.doctype.package.package.get_license_text", { + license_type: frm.doc.license_type, + }) + .then((r) => { + frm.set_value("license", r.message); + }); + }, +}); diff --git a/influxframework/core/doctype/package/package.json b/influxframework/core/doctype/package/package.json new file mode 100644 index 0000000..285e17a --- /dev/null +++ b/influxframework/core/doctype/package/package.json @@ -0,0 +1,76 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "Prompt", + "creation": "2021-09-04 11:54:35.155687", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "package_name", + "readme", + "license_type", + "license" + ], + "fields": [ + { + "fieldname": "readme", + "fieldtype": "Markdown Editor", + "label": "Readme" + }, + { + "fieldname": "package_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Package Name", + "reqd": 1 + }, + { + "fieldname": "license_type", + "fieldtype": "Select", + "label": "License Type", + "options": "\nMIT License\nGNU General Public License\nGNU Affero General Public License" + }, + { + "fieldname": "license", + "fieldtype": "Markdown Editor", + "label": "License" + } + ], + "index_web_pages_for_search": 1, + "links": [ + { + "group": "Modules", + "link_doctype": "Module Def", + "link_fieldname": "package" + }, + { + "group": "Release", + "link_doctype": "Package Release", + "link_fieldname": "package" + } + ], + "modified": "2021-09-05 13:15:01.130982", + "modified_by": "Administrator", + "module": "Core", + "name": "Package", + "naming_rule": "Set by user", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/package/package.py b/influxframework/core/doctype/package/package.py new file mode 100644 index 0000000..bf82c64 --- /dev/null +++ b/influxframework/core/doctype/package/package.py @@ -0,0 +1,19 @@ +# Copyright (c) 2021, InfluxFramework LLC +# For license information, please see license.txt + +import os + +import influxframework +from influxframework.model.document import Document + + +class Package(Document): + def validate(self): + if not self.package_name: + self.package_name = self.name.lower().replace(" ", "-") + + +@influxframework.whitelist() +def get_license_text(license_type): + with open(os.path.join(os.path.dirname(__file__), "licenses", license_type + ".md")) as textfile: + return textfile.read() diff --git a/influxframework/core/doctype/package/test_package.py b/influxframework/core/doctype/package/test_package.py new file mode 100644 index 0000000..ddccc1b --- /dev/null +++ b/influxframework/core/doctype/package/test_package.py @@ -0,0 +1,110 @@ +# Copyright (c) 2021, InfluxFramework LLC +# See license.txt + +import json +import os + +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestPackage(InfluxFrameworkTestCase): + def test_package_release(self): + make_test_package() + make_test_module() + make_test_doctype() + make_test_server_script() + make_test_web_page() + + # make release + influxframework.get_doc(dict(doctype="Package Release", package="Test Package", publish=1)).insert() + + self.assertTrue(os.path.exists(influxframework.get_site_path("packages", "test-package"))) + self.assertTrue( + os.path.exists(influxframework.get_site_path("packages", "test-package", "test_module_for_package")) + ) + self.assertTrue( + os.path.exists( + influxframework.get_site_path( + "packages", "test-package", "test_module_for_package", "doctype", "test_doctype_for_package" + ) + ) + ) + with open( + influxframework.get_site_path( + "packages", + "test-package", + "test_module_for_package", + "doctype", + "test_doctype_for_package", + "test_doctype_for_package.json", + ) + ) as f: + doctype = json.loads(f.read()) + self.assertEqual(doctype["doctype"], "DocType") + self.assertEqual(doctype["name"], "Test DocType for Package") + self.assertEqual(doctype["fields"][0]["fieldname"], "test_field") + + +def make_test_package(): + if not influxframework.db.exists("Package", "Test Package"): + influxframework.get_doc( + dict( + doctype="Package", name="Test Package", package_name="test-package", readme="# Test Package" + ) + ).insert() + + +def make_test_module(): + if not influxframework.db.exists("Module Def", "Test Module for Package"): + influxframework.get_doc( + dict( + doctype="Module Def", + module_name="Test Module for Package", + custom=1, + app_name="influxframework", + package="Test Package", + ) + ).insert() + + +def make_test_doctype(): + if not influxframework.db.exists("DocType", "Test DocType for Package"): + influxframework.get_doc( + dict( + doctype="DocType", + name="Test DocType for Package", + custom=1, + module="Test Module for Package", + autoname="Prompt", + fields=[dict(fieldname="test_field", fieldtype="Data", label="Test Field")], + ) + ).insert() + + +def make_test_server_script(): + if not influxframework.db.exists("Server Script", "Test Script for Package"): + influxframework.get_doc( + dict( + doctype="Server Script", + name="Test Script for Package", + module="Test Module for Package", + script_type="DocType Event", + reference_doctype="Test DocType for Package", + doctype_event="Before Save", + script='influxframework.msgprint("Test")', + ) + ).insert() + + +def make_test_web_page(): + if not influxframework.db.exists("Web Page", "test-web-page-for-package"): + influxframework.get_doc( + dict( + doctype="Web Page", + module="Test Module for Package", + main_section="Some content", + published=1, + title="Test Web Page for Package", + ) + ).insert() diff --git a/influxframework/core/doctype/package_import/__init__.py b/influxframework/core/doctype/package_import/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/package_import/package_import.js b/influxframework/core/doctype/package_import/package_import.js new file mode 100644 index 0000000..2ca02c3 --- /dev/null +++ b/influxframework/core/doctype/package_import/package_import.js @@ -0,0 +1,7 @@ +// Copyright (c) 2021, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Package Import", { + // refresh: function(frm) { + // } +}); diff --git a/influxframework/core/doctype/package_import/package_import.json b/influxframework/core/doctype/package_import/package_import.json new file mode 100644 index 0000000..f3c6168 --- /dev/null +++ b/influxframework/core/doctype/package_import/package_import.json @@ -0,0 +1,65 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "format:Package Import at {creation}", + "creation": "2021-09-05 16:36:46.680094", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "attach_package", + "activate", + "force", + "log" + ], + "fields": [ + { + "fieldname": "attach_package", + "fieldtype": "Attach", + "label": "Attach Package" + }, + { + "default": "0", + "fieldname": "activate", + "fieldtype": "Check", + "label": "Activate" + }, + { + "fieldname": "log", + "fieldtype": "Code", + "label": "Log", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "force", + "fieldtype": "Check", + "label": "Force" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-09-05 21:30:04.796090", + "modified_by": "Administrator", + "module": "Core", + "name": "Package Import", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/package_import/package_import.py b/influxframework/core/doctype/package_import/package_import.py new file mode 100644 index 0000000..8ed365a --- /dev/null +++ b/influxframework/core/doctype/package_import/package_import.py @@ -0,0 +1,65 @@ +# Copyright (c) 2021, InfluxFramework LLC +# For license information, please see license.txt + +import json +import os +import subprocess + +import influxframework +from influxframework.desk.form.load import get_attachments +from influxframework.model.document import Document +from influxframework.model.sync import get_doc_files +from influxframework.modules.import_file import import_doc, import_file_by_path + + +class PackageImport(Document): + def validate(self): + if self.activate: + self.import_package() + + def import_package(self): + attachment = get_attachments(self.doctype, self.name) + + if not attachment: + influxframework.throw(influxframework._("Please attach the package")) + + attachment = attachment[0] + + # get package_name from file (package_name-0.0.0.tar.gz) + package_name = attachment.file_name.split(".")[0].rsplit("-", 1)[0] + if not os.path.exists(influxframework.get_site_path("packages")): + os.makedirs(influxframework.get_site_path("packages")) + + # extract + subprocess.check_output( + [ + "tar", + "xzf", + influxframework.get_site_path(attachment.file_url.strip("/")), + "-C", + influxframework.get_site_path("packages"), + ] + ) + + package_path = influxframework.get_site_path("packages", package_name) + + # import Package + with open(os.path.join(package_path, package_name + ".json")) as packagefile: + doc_dict = json.loads(packagefile.read()) + + influxframework.flags.package = import_doc(doc_dict) + + # collect modules + files = [] + log = [] + for module in os.listdir(package_path): + module_path = os.path.join(package_path, module) + if os.path.isdir(module_path): + files = get_doc_files(files, module_path) + + # import files + for file in files: + import_file_by_path(file, force=self.force, ignore_version=True) + log.append(f"Imported {file}") + + self.log = "\n".join(log) diff --git a/influxframework/core/doctype/package_import/test_package_import.py b/influxframework/core/doctype/package_import/test_package_import.py new file mode 100644 index 0000000..b073920 --- /dev/null +++ b/influxframework/core/doctype/package_import/test_package_import.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, InfluxFramework LLC +# See license.txt + +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestPackageImport(InfluxFrameworkTestCase): + pass diff --git a/influxframework/core/doctype/package_release/__init__.py b/influxframework/core/doctype/package_release/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/package_release/package_release.js b/influxframework/core/doctype/package_release/package_release.js new file mode 100644 index 0000000..2de8bf7 --- /dev/null +++ b/influxframework/core/doctype/package_release/package_release.js @@ -0,0 +1,7 @@ +// Copyright (c) 2021, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Package Release", { + // refresh: function(frm) { + // } +}); diff --git a/influxframework/core/doctype/package_release/package_release.json b/influxframework/core/doctype/package_release/package_release.json new file mode 100644 index 0000000..b651d69 --- /dev/null +++ b/influxframework/core/doctype/package_release/package_release.json @@ -0,0 +1,95 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2021-09-05 12:59:01.932327", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "package", + "publish", + "path", + "column_break_3", + "major", + "minor", + "patch", + "section_break_7", + "release_notes" + ], + "fields": [ + { + "fieldname": "package", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Package", + "options": "Package", + "reqd": 1 + }, + { + "fieldname": "major", + "fieldtype": "Int", + "label": "Major" + }, + { + "fieldname": "minor", + "fieldtype": "Int", + "label": "Minor" + }, + { + "fieldname": "patch", + "fieldtype": "Int", + "label": "Patch", + "no_copy": 1 + }, + { + "fieldname": "path", + "fieldtype": "Small Text", + "label": "Path", + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_7", + "fieldtype": "Section Break" + }, + { + "fieldname": "release_notes", + "fieldtype": "Markdown Editor", + "label": "Release Notes" + }, + { + "default": "0", + "fieldname": "publish", + "fieldtype": "Check", + "label": "Publish" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-09-05 16:04:32.860988", + "modified_by": "Administrator", + "module": "Core", + "name": "Package Release", + "naming_rule": "By script", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/package_release/package_release.py b/influxframework/core/doctype/package_release/package_release.py new file mode 100644 index 0000000..fcf682f --- /dev/null +++ b/influxframework/core/doctype/package_release/package_release.py @@ -0,0 +1,111 @@ +# Copyright (c) 2021, InfluxFramework LLC +# For license information, please see license.txt + +import os +import subprocess + +import influxframework +from influxframework.model.document import Document +from influxframework.modules.export_file import export_doc +from influxframework.query_builder.functions import Max + + +class PackageRelease(Document): + def set_version(self): + # set the next patch release by default + doctype = influxframework.qb.DocType("Package Release") + if not self.major: + self.major = ( + influxframework.qb.from_(doctype) + .where(doctype.package == self.package) + .select(Max(doctype.minor)) + .run()[0][0] + or 0 + ) + + if not self.minor: + self.minor = ( + influxframework.qb.from_(doctype) + .where(doctype.package == self.package) + .select(Max("minor")) + .run()[0][0] + or 0 + ) + if not self.patch: + value = ( + influxframework.qb.from_(doctype) + .where(doctype.package == self.package) + .select(Max("patch")) + .run()[0][0] + or 0 + ) + self.patch = value + 1 + + def autoname(self): + self.set_version() + self.name = "{}-{}.{}.{}".format( + influxframework.db.get_value("Package", self.package, "package_name"), self.major, self.minor, self.patch + ) + + def validate(self): + if self.publish: + self.export_files() + + def export_files(self): + """Export all the documents in this package to site/packages folder""" + package = influxframework.get_doc("Package", self.package) + + self.export_modules() + self.export_package_files(package) + self.make_tarfile(package) + + def export_modules(self): + for m in influxframework.get_all("Module Def", dict(package=self.package)): + module = influxframework.get_doc("Module Def", m.name) + for l in module.meta.links: + if l.link_doctype == "Module Def": + continue + # all documents of the type in the module + for d in influxframework.get_all(l.link_doctype, dict(module=m.name)): + export_doc(influxframework.get_doc(l.link_doctype, d.name)) + + def export_package_files(self, package): + # write readme + with open(influxframework.get_site_path("packages", package.package_name, "README.md"), "w") as readme: + readme.write(package.readme) + + # write license + if package.license: + with open(influxframework.get_site_path("packages", package.package_name, "LICENSE.md"), "w") as license: + license.write(package.license) + + # write package.json as `influxframework_package.json` + with open( + influxframework.get_site_path("packages", package.package_name, package.package_name + ".json"), "w" + ) as packagefile: + packagefile.write(influxframework.as_json(package.as_dict(no_nulls=True))) + + def make_tarfile(self, package): + # make tarfile + filename = f"{self.name}.tar.gz" + subprocess.check_output( + ["tar", "czf", filename, package.package_name], cwd=influxframework.get_site_path("packages") + ) + + # move file + subprocess.check_output( + ["mv", influxframework.get_site_path("packages", filename), influxframework.get_site_path("public", "files")] + ) + + # make attachment + file = influxframework.get_doc( + dict( + doctype="File", + file_url="/" + os.path.join("files", filename), + attached_to_doctype=self.doctype, + attached_to_name=self.name, + ) + ) + + file.flags.ignore_duplicate_entry_error = True + file.insert() diff --git a/influxframework/core/doctype/package_release/test_package_release.py b/influxframework/core/doctype/package_release/test_package_release.py new file mode 100644 index 0000000..fadfa8b --- /dev/null +++ b/influxframework/core/doctype/package_release/test_package_release.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, InfluxFramework LLC +# See license.txt + +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestPackageRelease(InfluxFrameworkTestCase): + pass diff --git a/influxframework/core/doctype/page/README.md b/influxframework/core/doctype/page/README.md new file mode 100644 index 0000000..0a06292 --- /dev/null +++ b/influxframework/core/doctype/page/README.md @@ -0,0 +1 @@ +A page (view) in the application. A page is made to define custom user interfaces and contains server side and client side code. Standard events are attached to the page and called when the page is loaded, shown etc. \ No newline at end of file diff --git a/influxframework/core/doctype/page/__init__.py b/influxframework/core/doctype/page/__init__.py new file mode 100644 index 0000000..2e11a12 --- /dev/null +++ b/influxframework/core/doctype/page/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE diff --git a/influxframework/core/doctype/page/page.js b/influxframework/core/doctype/page/page.js new file mode 100644 index 0000000..462c694 --- /dev/null +++ b/influxframework/core/doctype/page/page.js @@ -0,0 +1,16 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Page", { + refresh: function (frm) { + if (!influxframework.boot.developer_mode && influxframework.session.user != "Administrator") { + // make the document read-only + frm.set_read_only(); + } + if (!frm.is_new() && !frm.doc.istable) { + frm.add_custom_button(__("Go to {0} Page", [frm.doc.title || frm.doc.name]), () => { + influxframework.set_route(frm.doc.name); + }); + } + }, +}); diff --git a/influxframework/core/doctype/page/page.json b/influxframework/core/doctype/page/page.json new file mode 100644 index 0000000..0c58664 --- /dev/null +++ b/influxframework/core/doctype/page/page.json @@ -0,0 +1,415 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 1, + "autoname": "field:page_name", + "beta": 0, + "creation": "2012-12-20 17:16:49", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "System", + "editable_grid": 0, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "system_page", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "System Page", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "page_html", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Page HTML", + "length": 0, + "no_copy": 0, + "oldfieldtype": "Section Break", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "page_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Page Name", + "length": 0, + "no_copy": 0, + "oldfieldname": "page_name", + "oldfieldtype": "Data", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "title", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Title", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "icon", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "icon", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break0", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "module", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 1, + "label": "Module", + "length": 0, + "no_copy": 0, + "oldfieldname": "module", + "oldfieldtype": "Select", + "options": "Module Def", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "restrict_to_domain", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Restrict To Domain", + "length": 0, + "no_copy": 0, + "options": "Domain", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "standard", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Standard", + "length": 0, + "no_copy": 0, + "oldfieldname": "standard", + "oldfieldtype": "Select", + "options": "Yes\nNo", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 1, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "section_break0", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:doc.standard == 'Yes'", + "fieldname": "roles", + "fieldtype": "Table", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Roles", + "length": 0, + "no_copy": 0, + "oldfieldname": "roles", + "oldfieldtype": "Table", + "options": "Has Role", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "icon": "fa fa-file", + "idx": 1, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2017-11-13 16:37:04.422547", + "modified_by": "Administrator", + "module": "Core", + "name": "Page", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 0, + "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 0, + "role": "Administrator", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + }, + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 0, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_order": "ASC", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/influxframework/core/doctype/page/page.py b/influxframework/core/doctype/page/page.py new file mode 100644 index 0000000..6d291ca --- /dev/null +++ b/influxframework/core/doctype/page/page.py @@ -0,0 +1,172 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import os + +import influxframework +from influxframework import _, conf, safe_decode +from influxframework.build import html_to_js_template +from influxframework.core.doctype.custom_role.custom_role import get_custom_allowed_roles +from influxframework.desk.form.meta import get_code_files_via_hooks, get_js +from influxframework.desk.utils import validate_route_conflict +from influxframework.model.document import Document +from influxframework.model.utils import render_include + + +class Page(Document): + def autoname(self): + """ + Creates a url friendly name for this page. + Will restrict the name to 30 characters, if there exists a similar name, + it will add name-1, name-2 etc. + """ + from influxframework.utils import cint + + if (self.name and self.name.startswith("New Page")) or not self.name: + self.name = self.page_name.lower().replace('"', "").replace("'", "").replace(" ", "-")[:20] + if influxframework.db.exists("Page", self.name): + cnt = influxframework.db.sql( + """select name from tabPage + where name like "%s-%%" order by name desc limit 1""" + % self.name + ) + if cnt: + cnt = cint(cnt[0][0].split("-")[-1]) + 1 + else: + cnt = 1 + self.name += "-" + str(cnt) + + def validate(self): + validate_route_conflict(self.doctype, self.name) + + if self.is_new() and not getattr(conf, "developer_mode", 0): + influxframework.throw(_("Not in Developer Mode")) + + # setting ignore_permissions via update_setup_wizard_access (setup_wizard.py) + if influxframework.session.user != "Administrator" and not self.flags.ignore_permissions: + influxframework.throw(_("Only Administrator can edit")) + + # export + def on_update(self): + """ + Writes the .json for this page and if write_content is checked, + it will write out a .html file + """ + if self.flags.do_not_update_json: + return + + from influxframework.core.doctype.doctype.doctype import make_module_and_roles + + make_module_and_roles(self, "roles") + + from influxframework.modules.utils import export_module_json + + path = export_module_json(self, self.standard == "Yes", self.module) + + if path: + # js + if not os.path.exists(path + ".js"): + with open(path + ".js", "w") as f: + f.write( + """influxframework.pages['%s'].on_page_load = function(wrapper) { + var page = influxframework.ui.make_app_page({ + parent: wrapper, + title: '%s', + single_column: true + }); +}""" + % (self.name, self.title) + ) + + def as_dict(self, no_nulls=False): + d = super().as_dict(no_nulls=no_nulls) + for key in ("script", "style", "content"): + d[key] = self.get(key) + return d + + def on_trash(self): + delete_custom_role("page", self.name) + + def is_permitted(self): + """Returns true if Has Role is not set or the user is allowed.""" + from influxframework.utils import has_common + + allowed = [ + d.role for d in influxframework.get_all("Has Role", fields=["role"], filters={"parent": self.name}) + ] + + custom_roles = get_custom_allowed_roles("page", self.name) + allowed.extend(custom_roles) + + if not allowed: + return True + + roles = influxframework.get_roles() + + if has_common(roles, allowed): + return True + + def load_assets(self): + import os + + from influxframework.modules import get_module_path, scrub + + self.script = "" + + page_name = scrub(self.name) + + path = os.path.join(get_module_path(self.module), "page", page_name) + + # script + fpath = os.path.join(path, page_name + ".js") + if os.path.exists(fpath): + with open(fpath) as f: + self.script = render_include(f.read()) + self.script += f"\n\n//# sourceURL={page_name}.js" + + # css + fpath = os.path.join(path, page_name + ".css") + if os.path.exists(fpath): + with open(fpath) as f: + self.style = safe_decode(f.read()) + + # html as js template + for fname in os.listdir(path): + if fname.endswith(".html"): + with open(os.path.join(path, fname)) as f: + template = f.read() + if "" in template: + context = influxframework._dict({}) + try: + out = influxframework.get_attr( + "{app}.{module}.page.{page}.{page}.get_context".format( + app=influxframework.local.module_app[scrub(self.module)], module=scrub(self.module), page=page_name + ) + )(context) + + if out: + context = out + except (AttributeError, ImportError): + pass + + template = influxframework.render_template(template, context) + self.script = html_to_js_template(fname, template) + self.script + + # flag for not caching this page + self._dynamic_page = True + + if influxframework.lang != "en": + from influxframework.translate import get_lang_js + + self.script += get_lang_js("page", self.name) + + for path in get_code_files_via_hooks("page_js", self.name): + js = get_js(path) + if js: + self.script += "\n\n" + js + + +def delete_custom_role(field, docname): + name = influxframework.db.get_value("Custom Role", {field: docname}, "name") + if name: + influxframework.delete_doc("Custom Role", name) diff --git a/influxframework/core/doctype/page/patches/drop_unused_pages.py b/influxframework/core/doctype/page/patches/drop_unused_pages.py new file mode 100644 index 0000000..01ac62e --- /dev/null +++ b/influxframework/core/doctype/page/patches/drop_unused_pages.py @@ -0,0 +1,6 @@ +import influxframework + + +def execute(): + for name in ("desktop", "space"): + influxframework.delete_doc("Page", name) diff --git a/influxframework/core/doctype/page/test_page.py b/influxframework/core/doctype/page/test_page.py new file mode 100644 index 0000000..2efa294 --- /dev/null +++ b/influxframework/core/doctype/page/test_page.py @@ -0,0 +1,18 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + +test_records = influxframework.get_test_records("Page") + + +class TestPage(InfluxFrameworkTestCase): + def test_naming(self): + self.assertRaises( + influxframework.NameError, + influxframework.get_doc(dict(doctype="Page", page_name="DocType", module="Core")).insert, + ) + self.assertRaises( + influxframework.NameError, + influxframework.get_doc(dict(doctype="Page", page_name="Settings", module="Core")).insert, + ) diff --git a/influxframework/core/doctype/page/test_records.json b/influxframework/core/doctype/page/test_records.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/influxframework/core/doctype/page/test_records.json @@ -0,0 +1 @@ +[] diff --git a/influxframework/core/doctype/patch_log/README.md b/influxframework/core/doctype/patch_log/README.md new file mode 100644 index 0000000..080490e --- /dev/null +++ b/influxframework/core/doctype/patch_log/README.md @@ -0,0 +1 @@ +Log of patch executed. Used by `patch_handler` to check if a particular patch is executed or not. \ No newline at end of file diff --git a/influxframework/core/doctype/patch_log/__init__.py b/influxframework/core/doctype/patch_log/__init__.py new file mode 100644 index 0000000..2e11a12 --- /dev/null +++ b/influxframework/core/doctype/patch_log/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE diff --git a/influxframework/core/doctype/patch_log/patch_log.js b/influxframework/core/doctype/patch_log/patch_log.js new file mode 100644 index 0000000..8e00eeb --- /dev/null +++ b/influxframework/core/doctype/patch_log/patch_log.js @@ -0,0 +1,8 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Patch Log", { + refresh: function (frm) { + frm.disable_save(); + }, +}); diff --git a/influxframework/core/doctype/patch_log/patch_log.json b/influxframework/core/doctype/patch_log/patch_log.json new file mode 100644 index 0000000..9750c51 --- /dev/null +++ b/influxframework/core/doctype/patch_log/patch_log.json @@ -0,0 +1,44 @@ +{ + "actions": [], + "autoname": "PATCHLOG.#####", + "creation": "2013-01-17 11:36:45", + "description": "List of patches executed", + "doctype": "DocType", + "document_type": "System", + "engine": "InnoDB", + "field_order": [ + "patch" + ], + "fields": [ + { + "fieldname": "patch", + "fieldtype": "Code", + "label": "Patch", + "read_only": 1 + } + ], + "icon": "fa fa-cog", + "idx": 1, + "links": [], + "modified": "2022-06-13 05:34:37.845368", + "modified_by": "Administrator", + "module": "Core", + "name": "Patch Log", + "naming_rule": "Expression (old style)", + "owner": "Administrator", + "permissions": [ + { + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator" + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "patch", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/patch_log/patch_log.py b/influxframework/core/doctype/patch_log/patch_log.py new file mode 100644 index 0000000..884dfdf --- /dev/null +++ b/influxframework/core/doctype/patch_log/patch_log.py @@ -0,0 +1,10 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +# License: MIT. See LICENSE + +from influxframework.model.document import Document + + +class PatchLog(Document): + pass diff --git a/influxframework/core/doctype/patch_log/test_patch_log.py b/influxframework/core/doctype/patch_log/test_patch_log.py new file mode 100644 index 0000000..346db36 --- /dev/null +++ b/influxframework/core/doctype/patch_log/test_patch_log.py @@ -0,0 +1,9 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + +# test_records = influxframework.get_test_records('Patch Log') + + +class TestPatchLog(InfluxFrameworkTestCase): + pass diff --git a/influxframework/core/doctype/prepared_report/__init__.py b/influxframework/core/doctype/prepared_report/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/prepared_report/prepared_report.js b/influxframework/core/doctype/prepared_report/prepared_report.js new file mode 100644 index 0000000..7abbfcd --- /dev/null +++ b/influxframework/core/doctype/prepared_report/prepared_report.js @@ -0,0 +1,45 @@ +// Copyright (c) 2018, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Prepared Report", { + onload: function (frm) { + var wrapper = $(frm.fields_dict["filter_values"].wrapper).empty(); + + let filter_table = $(` + + + + + + + +
    ${__("Filter")}${__("Value")}
    `); + + const filters = JSON.parse(frm.doc.filters); + + Object.keys(filters).forEach((key) => { + const filter_row = $(` + ${influxframework.model.unscrub(key)} + ${filters[key]} + `); + filter_table.find("tbody").append(filter_row); + }); + + wrapper.append(filter_table); + }, + + refresh: function (frm) { + frm.disable_save(); + if (frm.doc.status == "Completed") { + frm.page.set_primary_action(__("Show Report"), () => { + influxframework.set_route( + "query-report", + frm.doc.report_name, + influxframework.utils.make_query_string({ + prepared_report_name: frm.doc.name, + }) + ); + }); + } + }, +}); diff --git a/influxframework/core/doctype/prepared_report/prepared_report.json b/influxframework/core/doctype/prepared_report/prepared_report.json new file mode 100644 index 0000000..cafe323 --- /dev/null +++ b/influxframework/core/doctype/prepared_report/prepared_report.json @@ -0,0 +1,140 @@ +{ + "actions": [], + "autoname": "REP.#####", + "creation": "2018-06-25 18:39:11.152960", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "report_name", + "ref_report_doctype", + "status", + "column_break_4", + "report_start_time", + "report_end_time", + "section_break_7", + "error_message", + "filters_sb", + "filters", + "filter_values", + "columns" + ], + "fields": [ + { + "fieldname": "report_name", + "fieldtype": "Data", + "label": "Report Name", + "read_only": 1 + }, + { + "fieldname": "ref_report_doctype", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Report Type", + "options": "Report", + "read_only": 1 + }, + { + "default": "Queued", + "fieldname": "status", + "fieldtype": "Select", + "hidden": 1, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "Error\nQueued\nCompleted", + "read_only": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "report_start_time", + "fieldtype": "Datetime", + "label": "Report Start Time", + "read_only": 1 + }, + { + "fieldname": "report_end_time", + "fieldtype": "Datetime", + "label": "Report End Time", + "read_only": 1 + }, + { + "depends_on": "eval:doc.status == 'Error'", + "fieldname": "section_break_7", + "fieldtype": "Section Break" + }, + { + "fieldname": "error_message", + "fieldtype": "Text", + "label": "Error Message", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "filters_sb", + "fieldtype": "Section Break", + "label": "Filters" + }, + { + "fieldname": "filters", + "fieldtype": "Small Text", + "hidden": 1, + "label": "Filters", + "read_only": 1 + }, + { + "fieldname": "filter_values", + "fieldtype": "HTML", + "label": "Filter Values" + }, + { + "fieldname": "columns", + "fieldtype": "Code", + "hidden": 1, + "label": "Columns", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + } + ], + "in_create": 1, + "links": [], + "modified": "2022-06-13 06:20:34.496412", + "modified_by": "Administrator", + "module": "Core", + "name": "Prepared Report", + "naming_rule": "Expression (old style)", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Prepared Report User", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "ref_report_doctype", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/prepared_report/prepared_report.py b/influxframework/core/doctype/prepared_report/prepared_report.py new file mode 100644 index 0000000..0fedc17 --- /dev/null +++ b/influxframework/core/doctype/prepared_report/prepared_report.py @@ -0,0 +1,170 @@ +# Copyright (c) 2018, InfluxFramework LLC +# License: MIT. See LICENSE + + +import json + +import influxframework +from influxframework.desk.form.load import get_attachments +from influxframework.desk.query_report import generate_report_result +from influxframework.model.document import Document +from influxframework.monitor import add_data_to_monitor +from influxframework.utils import gzip_compress, gzip_decompress +from influxframework.utils.background_jobs import enqueue + + +class PreparedReport(Document): + def before_insert(self): + self.status = "Queued" + self.report_start_time = influxframework.utils.now() + + def enqueue_report(self): + enqueue(run_background, prepared_report=self.name, timeout=6000) + + +def run_background(prepared_report): + instance = influxframework.get_doc("Prepared Report", prepared_report) + report = influxframework.get_doc("Report", instance.ref_report_doctype) + + add_data_to_monitor(report=instance.ref_report_doctype) + + try: + report.custom_columns = [] + + if report.report_type == "Custom Report": + custom_report_doc = report + reference_report = custom_report_doc.reference_report + report = influxframework.get_doc("Report", reference_report) + if custom_report_doc.json: + data = json.loads(custom_report_doc.json) + if data: + report.custom_columns = data["columns"] + + result = generate_report_result(report=report, filters=instance.filters, user=instance.owner) + create_json_gz_file(result["result"], "Prepared Report", instance.name) + + instance.status = "Completed" + instance.columns = json.dumps(result["columns"]) + instance.report_end_time = influxframework.utils.now() + instance.save(ignore_permissions=True) + + except Exception: + report.log_error("Prepared report failed") + instance = influxframework.get_doc("Prepared Report", prepared_report) + instance.status = "Error" + instance.error_message = influxframework.get_traceback() + instance.save(ignore_permissions=True) + + influxframework.publish_realtime( + "report_generated", + {"report_name": instance.report_name, "name": instance.name}, + user=influxframework.session.user, + ) + + +@influxframework.whitelist() +def get_reports_in_queued_state(report_name, filters): + reports = influxframework.get_all( + "Prepared Report", + filters={ + "report_name": report_name, + "filters": json.dumps(json.loads(filters)), + "status": "Queued", + }, + ) + return reports + + +def delete_expired_prepared_reports(): + system_settings = influxframework.get_single("System Settings") + enable_auto_deletion = system_settings.enable_prepared_report_auto_deletion + if enable_auto_deletion: + expiry_period = system_settings.prepared_report_expiry_period + prepared_reports_to_delete = influxframework.get_all( + "Prepared Report", + filters={"creation": ["<", influxframework.utils.add_days(influxframework.utils.now(), -expiry_period)]}, + ) + + batches = influxframework.utils.create_batch(prepared_reports_to_delete, 100) + for batch in batches: + args = { + "reports": batch, + } + enqueue(method=delete_prepared_reports, job_name="delete_prepared_reports", **args) + + +@influxframework.whitelist() +def delete_prepared_reports(reports): + reports = influxframework.parse_json(reports) + for report in reports: + influxframework.delete_doc( + "Prepared Report", report["name"], ignore_permissions=True, delete_permanently=True + ) + + +def create_json_gz_file(data, dt, dn): + # Storing data in CSV file causes information loss + # Reports like P&L Statement were completely unsuable because of this + json_filename = "{}.json.gz".format( + influxframework.utils.data.format_datetime(influxframework.utils.now(), "Y-m-d-H:M") + ) + encoded_content = influxframework.safe_encode(influxframework.as_json(data)) + compressed_content = gzip_compress(encoded_content) + + # Call save() file function to upload and attach the file + _file = influxframework.get_doc( + { + "doctype": "File", + "file_name": json_filename, + "attached_to_doctype": dt, + "attached_to_name": dn, + "content": compressed_content, + "is_private": 1, + } + ) + _file.save(ignore_permissions=True) + + +@influxframework.whitelist() +def download_attachment(dn): + attachment = get_attachments("Prepared Report", dn)[0] + influxframework.local.response.filename = attachment.file_name[:-2] + attached_file = influxframework.get_doc("File", attachment.name) + influxframework.local.response.filecontent = gzip_decompress(attached_file.get_content()) + influxframework.local.response.type = "binary" + + +def get_permission_query_condition(user): + if not user: + user = influxframework.session.user + if user == "Administrator": + return None + + from influxframework.utils.user import UserPermissions + + user = UserPermissions(user) + + if "System Manager" in user.roles: + return None + + reports = [influxframework.db.escape(report) for report in user.get_all_reports().keys()] + + return """`tabPrepared Report`.ref_report_doctype in ({reports})""".format( + reports=",".join(reports) + ) + + +def has_permission(doc, user): + if not user: + user = influxframework.session.user + if user == "Administrator": + return True + + from influxframework.utils.user import UserPermissions + + user = UserPermissions(user) + + if "System Manager" in user.roles: + return True + + return doc.ref_report_doctype in user.get_all_reports().keys() diff --git a/influxframework/core/doctype/prepared_report/prepared_report_list.js b/influxframework/core/doctype/prepared_report/prepared_report_list.js new file mode 100644 index 0000000..bdef765 --- /dev/null +++ b/influxframework/core/doctype/prepared_report/prepared_report_list.js @@ -0,0 +1,12 @@ +influxframework.listview_settings["Prepared Report"] = { + add_fields: ["status"], + get_indicator: function (doc) { + if (doc.status === "Completed") { + return [__("Completed"), "green", "status,=,Completed"]; + } else if (doc.status === "Error") { + return [__("Error"), "red", "status,=,Error"]; + } else if (doc.status === "Queued") { + return [__("Queued"), "orange", "status,=,Queued"]; + } + }, +}; diff --git a/influxframework/core/doctype/prepared_report/test_prepared_report.py b/influxframework/core/doctype/prepared_report/test_prepared_report.py new file mode 100644 index 0000000..7671767 --- /dev/null +++ b/influxframework/core/doctype/prepared_report/test_prepared_report.py @@ -0,0 +1,28 @@ +# Copyright (c) 2018, InfluxFramework LLC +# License: MIT. See LICENSE +import json + +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestPreparedReport(InfluxFrameworkTestCase): + def setUp(self): + self.report = influxframework.get_doc({"doctype": "Report", "name": "Permitted Documents For User"}) + self.filters = {"user": "Administrator", "doctype": "Role"} + self.prepared_report_doc = influxframework.get_doc( + { + "doctype": "Prepared Report", + "report_name": self.report.name, + "filters": json.dumps(self.filters), + "ref_report_doctype": self.report.name, + } + ).insert() + + def tearDown(self): + influxframework.set_user("Administrator") + self.prepared_report_doc.delete() + + def test_for_creation(self): + self.assertTrue("QUEUED" == self.prepared_report_doc.status.upper()) + self.assertTrue(self.prepared_report_doc.report_start_time) diff --git a/influxframework/core/doctype/report/README.md b/influxframework/core/doctype/report/README.md new file mode 100644 index 0000000..99702e7 --- /dev/null +++ b/influxframework/core/doctype/report/README.md @@ -0,0 +1 @@ +Standard / Custom report. Reports can be of types Query Report or Script Report. \ No newline at end of file diff --git a/influxframework/core/doctype/report/__init__.py b/influxframework/core/doctype/report/__init__.py new file mode 100644 index 0000000..2e11a12 --- /dev/null +++ b/influxframework/core/doctype/report/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE diff --git a/influxframework/core/doctype/report/boilerplate/controller.js b/influxframework/core/doctype/report/boilerplate/controller.js new file mode 100644 index 0000000..663dfc1 --- /dev/null +++ b/influxframework/core/doctype/report/boilerplate/controller.js @@ -0,0 +1,9 @@ +// Copyright (c) {year}, {app_publisher} and contributors +// For license information, please see license.txt +/* eslint-disable */ + +influxframework.query_reports["{name}"] = {{ + "filters": [ + + ] +}}; diff --git a/influxframework/core/doctype/report/boilerplate/controller.py b/influxframework/core/doctype/report/boilerplate/controller.py new file mode 100644 index 0000000..7c9704e --- /dev/null +++ b/influxframework/core/doctype/report/boilerplate/controller.py @@ -0,0 +1,9 @@ +# Copyright (c) {year}, {app_publisher} and contributors +# For license information, please see license.txt + +# import influxframework + + +def execute(filters=None): + columns, data = [], [] + return columns, data diff --git a/influxframework/core/doctype/report/report.js b/influxframework/core/doctype/report/report.js new file mode 100644 index 0000000..bc82745 --- /dev/null +++ b/influxframework/core/doctype/report/report.js @@ -0,0 +1,58 @@ +influxframework.ui.form.on("Report", { + refresh: function (frm) { + if (frm.doc.is_standard === "Yes" && !influxframework.boot.developer_mode) { + // make the document read-only + frm.disable_form(); + } else { + frm.enable_save(); + } + + let doc = frm.doc; + frm.add_custom_button( + __("Show Report"), + function () { + switch (doc.report_type) { + case "Report Builder": + influxframework.set_route("List", doc.ref_doctype, "Report", doc.name); + break; + case "Query Report": + influxframework.set_route("query-report", doc.name); + break; + case "Script Report": + influxframework.set_route("query-report", doc.name); + break; + case "Custom Report": + influxframework.set_route("query-report", doc.name); + break; + } + }, + "fa fa-table" + ); + + if (doc.is_standard === "Yes" && frm.perm[0].write) { + frm.add_custom_button( + doc.disabled ? __("Enable Report") : __("Disable Report"), + function () { + frm.call("toggle_disable", { + disable: doc.disabled ? 0 : 1, + }).then(() => { + frm.reload_doc(); + }); + }, + doc.disabled ? "fa fa-check" : "fa fa-off" + ); + } + }, + + ref_doctype: function (frm) { + if (frm.doc.ref_doctype) { + frm.trigger("set_doctype_roles"); + } + }, + + set_doctype_roles: function (frm) { + return frm.call("set_doctype_roles").then(() => { + frm.refresh_field("roles"); + }); + }, +}); diff --git a/influxframework/core/doctype/report/report.json b/influxframework/core/doctype/report/report.json new file mode 100644 index 0000000..f9a6bec --- /dev/null +++ b/influxframework/core/doctype/report/report.json @@ -0,0 +1,247 @@ +{ + "actions": [], + "autoname": "field:report_name", + "creation": "2013-03-09 15:45:57", + "doctype": "DocType", + "document_type": "System", + "engine": "InnoDB", + "field_order": [ + "report_name", + "ref_doctype", + "reference_report", + "is_standard", + "module", + "column_break_4", + "report_type", + "letter_head", + "add_total_row", + "disabled", + "disable_prepared_report", + "prepared_report", + "filters_section", + "filters", + "columns_section", + "columns", + "section_break_6", + "query", + "report_script", + "client_code_section", + "javascript", + "json", + "permission_rules", + "roles" + ], + "fields": [ + { + "fieldname": "report_name", + "fieldtype": "Data", + "label": "Report Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "ref_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Ref DocType", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "reference_report", + "fieldtype": "Data", + "label": "Reference Report" + }, + { + "fieldname": "is_standard", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Is Standard", + "options": "No\nYes", + "reqd": 1 + }, + { + "fieldname": "module", + "fieldtype": "Link", + "label": "Module", + "options": "Module Def" + }, + { + "default": "0", + "fieldname": "add_total_row", + "fieldtype": "Check", + "label": "Add Total Row" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "report_type", + "fieldtype": "Select", + "label": "Report Type", + "options": "Report Builder\nQuery Report\nScript Report\nCustom Report", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "depends_on": "eval: doc.is_standard == \"No\"", + "fieldname": "letter_head", + "fieldtype": "Link", + "label": "Letter Head", + "options": "Letter Head" + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break", + "label": "Query / Script" + }, + { + "depends_on": "eval:doc.report_type==\"Query Report\"", + "fieldname": "query", + "fieldtype": "Code", + "label": "Query" + }, + { + "depends_on": "eval:doc.report_type==\"Script Report\" && doc.is_standard===\"No\"", + "description": "JavaScript Format: influxframework.query_reports['REPORTNAME'] = {}", + "fieldname": "javascript", + "fieldtype": "Code", + "label": "Javascript" + }, + { + "depends_on": "eval:doc.report_type==\"Report Builder\" || \"Custom Report\"", + "fieldname": "json", + "fieldtype": "Code", + "label": "JSON", + "read_only": 1 + }, + { + "fieldname": "permission_rules", + "fieldtype": "Section Break" + }, + { + "fieldname": "roles", + "fieldtype": "Table", + "label": "Roles", + "options": "Has Role" + }, + { + "default": "0", + "fieldname": "disable_prepared_report", + "fieldtype": "Check", + "label": "Disable Prepared Report" + }, + { + "default": "0", + "fieldname": "prepared_report", + "fieldtype": "Check", + "hidden": 1, + "label": "Prepared Report", + "read_only": 1 + }, + { + "depends_on": "eval:(doc.report_type===\"Script Report\" \n|| doc.report_type==\"Query Report\") \n&& doc.is_standard===\"No\"", + "description": "Filters will be accessible via filters.

    Send output as result = [result], or for old style data = [columns], [result]", + "fieldname": "report_script", + "fieldtype": "Code", + "label": "Script" + }, + { + "collapsible": 1, + "collapsible_depends_on": "filters", + "fieldname": "filters_section", + "fieldtype": "Section Break", + "label": "Filters" + }, + { + "fieldname": "filters", + "fieldtype": "Table", + "label": "Filters", + "options": "Report Filter" + }, + { + "collapsible": 1, + "collapsible_depends_on": "columns", + "fieldname": "columns_section", + "fieldtype": "Section Break", + "label": "Columns" + }, + { + "fieldname": "columns", + "fieldtype": "Table", + "label": "Columns", + "options": "Report Column" + }, + { + "collapsible": 1, + "collapsible_depends_on": "javascript", + "fieldname": "client_code_section", + "fieldtype": "Section Break", + "label": "Client Code" + } + ], + "idx": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2022-09-15 13:37:24.531848", + "modified_by": "Administrator", + "module": "Core", + "name": "Report", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Report Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All" + } + ], + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} diff --git a/influxframework/core/doctype/report/report.py b/influxframework/core/doctype/report/report.py new file mode 100644 index 0000000..ca16e0a --- /dev/null +++ b/influxframework/core/doctype/report/report.py @@ -0,0 +1,382 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import datetime +import json + +import influxframework +import influxframework.desk.query_report +from influxframework import _, scrub +from influxframework.core.doctype.custom_role.custom_role import get_custom_allowed_roles +from influxframework.core.doctype.page.page import delete_custom_role +from influxframework.desk.reportview import append_totals_row +from influxframework.model.document import Document +from influxframework.modules import make_boilerplate +from influxframework.modules.export_file import export_to_files +from influxframework.utils import cint, cstr +from influxframework.utils.safe_exec import check_safe_sql_query, safe_exec + + +class Report(Document): + def validate(self): + """only administrator can save standard report""" + if not self.module: + self.module = influxframework.db.get_value("DocType", self.ref_doctype, "module") + + if not self.is_standard: + self.is_standard = "No" + if ( + influxframework.session.user == "Administrator" and getattr(influxframework.local.conf, "developer_mode", 0) == 1 + ): + self.is_standard = "Yes" + + if self.is_standard == "No": + # allow only script manager to edit scripts + if self.report_type != "Report Builder": + influxframework.only_for("Script Manager", True) + + if influxframework.db.get_value("Report", self.name, "is_standard") == "Yes": + influxframework.throw(_("Cannot edit a standard report. Please duplicate and create a new report")) + + if self.is_standard == "Yes" and influxframework.session.user != "Administrator": + influxframework.throw(_("Only Administrator can save a standard report. Please rename and save.")) + + if self.report_type == "Report Builder": + self.update_report_json() + + def before_insert(self): + self.set_doctype_roles() + + def on_update(self): + self.export_doc() + + def on_trash(self): + if ( + self.is_standard == "Yes" + and not cint(getattr(influxframework.local.conf, "developer_mode", 0)) + and not influxframework.flags.in_patch + ): + influxframework.throw(_("You are not allowed to delete Standard Report")) + delete_custom_role("report", self.name) + self.delete_prepared_reports() + + def delete_prepared_reports(self): + prepared_reports = influxframework.get_all( + "Prepared Report", filters={"ref_report_doctype": self.name}, pluck="name" + ) + + for report in prepared_reports: + influxframework.delete_doc( + "Prepared Report", report, ignore_missing=True, force=True, delete_permanently=True + ) + + def get_columns(self): + return [d.as_dict(no_default_fields=True, no_child_table_fields=True) for d in self.columns] + + @influxframework.whitelist() + def set_doctype_roles(self): + if not self.get("roles") and self.is_standard == "No": + meta = influxframework.get_meta(self.ref_doctype) + if not meta.istable: + roles = [{"role": d.role} for d in meta.permissions if d.permlevel == 0] + self.set("roles", roles) + + def is_permitted(self): + """Returns true if Has Role is not set or the user is allowed.""" + from influxframework.utils import has_common + + allowed = [ + d.role for d in influxframework.get_all("Has Role", fields=["role"], filters={"parent": self.name}) + ] + + custom_roles = get_custom_allowed_roles("report", self.name) + + if custom_roles: + allowed = custom_roles + + if not allowed: + return True + + if has_common(influxframework.get_roles(), allowed): + return True + + def update_report_json(self): + if not self.json: + self.json = "{}" + + def export_doc(self): + if influxframework.flags.in_import: + return + + if self.is_standard == "Yes" and (influxframework.local.conf.get("developer_mode") or 0) == 1: + export_to_files( + record_list=[["Report", self.name]], record_module=self.module, create_init=True + ) + + self.create_report_py() + + def create_report_py(self): + if self.report_type == "Script Report": + make_boilerplate("controller.py", self, {"name": self.name}) + make_boilerplate("controller.js", self, {"name": self.name}) + + def execute_query_report(self, filters): + if not self.query: + influxframework.throw(_("Must specify a Query to run"), title=_("Report Document Error")) + + check_safe_sql_query(self.query) + + result = [list(t) for t in influxframework.db.sql(self.query, filters)] + columns = self.get_columns() or [cstr(c[0]) for c in influxframework.db.get_description()] + + return [columns, result] + + def execute_script_report(self, filters): + # save the timestamp to automatically set to prepared + threshold = 30 + res = [] + + start_time = datetime.datetime.now() + + # The JOB + if self.is_standard == "Yes": + res = self.execute_module(filters) + else: + res = self.execute_script(filters) + + # automatically set as prepared + execution_time = (datetime.datetime.now() - start_time).total_seconds() + if execution_time > threshold and not self.prepared_report: + self.db_set("prepared_report", 1) + + influxframework.cache().hset("report_execution_time", self.name, execution_time) + + return res + + def execute_module(self, filters): + # report in python module + module = self.module or influxframework.db.get_value("DocType", self.ref_doctype, "module") + method_name = get_report_module_dotted_path(module, self.name) + ".execute" + return influxframework.get_attr(method_name)(influxframework._dict(filters)) + + def execute_script(self, filters): + # server script + loc = {"filters": influxframework._dict(filters), "data": None, "result": None} + safe_exec(self.report_script, None, loc) + if loc["data"]: + return loc["data"] + else: + return self.get_columns(), loc["result"] + + def get_data( + self, filters=None, limit=None, user=None, as_dict=False, ignore_prepared_report=False + ): + if self.report_type in ("Query Report", "Script Report", "Custom Report"): + columns, result = self.run_query_report(filters, user, ignore_prepared_report) + else: + columns, result = self.run_standard_report(filters, limit, user) + + if as_dict: + result = self.build_data_dict(result, columns) + + return columns, result + + def run_query_report(self, filters, user, ignore_prepared_report=False): + columns, result = [], [] + data = influxframework.desk.query_report.run( + self.name, filters=filters, user=user, ignore_prepared_report=ignore_prepared_report + ) + + for d in data.get("columns"): + if isinstance(d, dict): + col = influxframework._dict(d) + if not col.fieldname: + col.fieldname = col.label + columns.append(col) + else: + fieldtype, options = "Data", None + parts = d.split(":") + if len(parts) > 1: + if parts[1]: + fieldtype, options = parts[1], None + if fieldtype and "/" in fieldtype: + fieldtype, options = fieldtype.split("/") + + columns.append( + influxframework._dict(label=parts[0], fieldtype=fieldtype, fieldname=parts[0], options=options) + ) + + result += data.get("result") + + return columns, result + + def run_standard_report(self, filters, limit, user): + params = json.loads(self.json) + columns = self.get_standard_report_columns(params) + result = [] + order_by, group_by, group_by_args = self.get_standard_report_order_by(params) + + _result = influxframework.get_list( + self.ref_doctype, + fields=[ + get_group_by_field(group_by_args, c[1]) + if c[0] == "_aggregate_column" and group_by_args + else Report._format([c[1], c[0]]) + for c in columns + ], + filters=self.get_standard_report_filters(params, filters), + order_by=order_by, + group_by=group_by, + as_list=True, + limit=limit, + user=user, + ) + + columns = self.build_standard_report_columns(columns, group_by_args) + + result = result + [list(d) for d in _result] + + if params.get("add_totals_row"): + result = append_totals_row(result) + + return columns, result + + @staticmethod + def _format(parts): + # sort by is saved as DocType.fieldname, covert it to sql + return "`tab{}`.`{}`".format(*parts) + + def get_standard_report_columns(self, params): + if params.get("fields"): + columns = params.get("fields") + elif params.get("columns"): + columns = params.get("columns") + elif params.get("fields"): + columns = params.get("fields") + else: + columns = [["name", self.ref_doctype]] + for df in influxframework.get_meta(self.ref_doctype).fields: + if df.in_list_view: + columns.append([df.fieldname, self.ref_doctype]) + + return columns + + def get_standard_report_filters(self, params, filters): + _filters = params.get("filters") or [] + + if filters: + for key, value in filters.items(): + condition, _value = "=", value + if isinstance(value, (list, tuple)): + condition, _value = value + _filters.append([key, condition, _value]) + + return _filters + + def get_standard_report_order_by(self, params): + group_by_args = None + if params.get("sort_by"): + order_by = Report._format(params.get("sort_by").split(".")) + " " + params.get("sort_order") + + elif params.get("order_by"): + order_by = params.get("order_by") + else: + order_by = Report._format([self.ref_doctype, "modified"]) + " desc" + + if params.get("sort_by_next"): + order_by += ( + ", " + + Report._format(params.get("sort_by_next").split(".")) + + " " + + params.get("sort_order_next") + ) + + group_by = None + if params.get("group_by"): + group_by_args = influxframework._dict(params["group_by"]) + group_by = group_by_args["group_by"] + order_by = "_aggregate_column desc" + + return order_by, group_by, group_by_args + + def build_standard_report_columns(self, columns, group_by_args): + _columns = [] + + for (fieldname, doctype) in columns: + meta = influxframework.get_meta(doctype) + + if meta.get_field(fieldname): + field = meta.get_field(fieldname) + else: + if fieldname == "_aggregate_column": + label = get_group_by_column_label(group_by_args, meta) + else: + label = meta.get_label(fieldname) + + field = influxframework._dict(fieldname=fieldname, label=label) + + # since name is the primary key for a document, it will always be a Link datatype + if fieldname == "name": + field.fieldtype = "Link" + field.options = doctype + + _columns.append(field) + return _columns + + def build_data_dict(self, result, columns): + data = [] + for row in result: + if isinstance(row, (list, tuple)): + _row = influxframework._dict() + for i, val in enumerate(row): + _row[columns[i].get("fieldname")] = val + elif isinstance(row, dict): + # no need to convert from dict to dict + _row = influxframework._dict(row) + data.append(_row) + + return data + + @influxframework.whitelist() + def toggle_disable(self, disable): + if not self.has_permission("write"): + influxframework.throw(_("You are not allowed to edit the report.")) + + self.db_set("disabled", cint(disable)) + + +@influxframework.whitelist() +def is_prepared_report_disabled(report): + return influxframework.db.get_value("Report", report, "disable_prepared_report") or 0 + + +def get_report_module_dotted_path(module, report_name): + return ( + influxframework.local.module_app[scrub(module)] + + "." + + scrub(module) + + ".report." + + scrub(report_name) + + "." + + scrub(report_name) + ) + + +def get_group_by_field(args, doctype): + if args["aggregate_function"] == "count": + group_by_field = "count(*) as _aggregate_column" + else: + group_by_field = f"{args.aggregate_function}({args.aggregate_on}) as _aggregate_column" + + return group_by_field + + +def get_group_by_column_label(args, meta): + if args["aggregate_function"] == "count": + label = "Count" + else: + sql_fn_map = {"avg": "Average", "sum": "Sum"} + aggregate_on_label = meta.get_label(args.aggregate_on) + label = _("{function} of {fieldlabel}").format( + function=sql_fn_map[args.aggregate_function], fieldlabel=aggregate_on_label + ) + return label diff --git a/influxframework/core/doctype/report/test_records.json b/influxframework/core/doctype/report/test_records.json new file mode 100644 index 0000000..cee1a20 --- /dev/null +++ b/influxframework/core/doctype/report/test_records.json @@ -0,0 +1,10 @@ +[ + { + "doctype": "Report", + "name": "_Test Report 1", + "report_name": "_Test Report 1", + "report_type": "Query Report", + "is_standard": "No", + "ref_doctype": "Event" + } +] diff --git a/influxframework/core/doctype/report/test_report.py b/influxframework/core/doctype/report/test_report.py new file mode 100644 index 0000000..44b2dac --- /dev/null +++ b/influxframework/core/doctype/report/test_report.py @@ -0,0 +1,397 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import json +import os +import textwrap + +import influxframework +from influxframework.core.doctype.user_permission.test_user_permission import create_user +from influxframework.custom.doctype.customize_form.customize_form import reset_customization +from influxframework.desk.query_report import add_total_row, run, save_report +from influxframework.desk.reportview import delete_report +from influxframework.desk.reportview import save_report as _save_report +from influxframework.tests.utils import InfluxFrameworkTestCase + +test_records = influxframework.get_test_records("Report") +test_dependencies = ["User"] + + +class TestReport(InfluxFrameworkTestCase): + def test_report_builder(self): + if influxframework.db.exists("Report", "User Activity Report"): + influxframework.delete_doc("Report", "User Activity Report") + + with open(os.path.join(os.path.dirname(__file__), "user_activity_report.json")) as f: + influxframework.get_doc(json.loads(f.read())).insert() + + report = influxframework.get_doc("Report", "User Activity Report") + columns, data = report.get_data() + self.assertEqual(columns[0].get("label"), "ID") + self.assertEqual(columns[1].get("label"), "User Type") + self.assertTrue("Administrator" in [d[0] for d in data]) + + def test_query_report(self): + report = influxframework.get_doc("Report", "Permitted Documents For User") + columns, data = report.get_data(filters={"user": "Administrator", "doctype": "DocType"}) + self.assertEqual(columns[0].get("label"), "Name") + self.assertEqual(columns[1].get("label"), "Module") + self.assertTrue("User" in [d.get("name") for d in data]) + + def test_save_or_delete_report(self): + """Test for validations when editing / deleting report of type Report Builder""" + + try: + report = influxframework.get_doc( + { + "doctype": "Report", + "ref_doctype": "User", + "report_name": "Test Delete Report", + "report_type": "Report Builder", + "is_standard": "No", + } + ).insert() + + # Check for PermissionError + create_user("test_report_owner@example.com", "Website Manager") + influxframework.set_user("test_report_owner@example.com") + self.assertRaises(influxframework.PermissionError, delete_report, report.name) + + # Check for Report Type + influxframework.set_user("Administrator") + report.db_set("report_type", "Custom Report") + self.assertRaisesRegex( + influxframework.ValidationError, + "Only reports of type Report Builder can be deleted", + delete_report, + report.name, + ) + + # Check if creating and deleting works with proper validations + influxframework.set_user("test@example.com") + report_name = _save_report( + "Dummy Report", + "User", + json.dumps( + [ + { + "fieldname": "email", + "fieldtype": "Data", + "label": "Email", + "insert_after_index": 0, + "link_field": "name", + "doctype": "User", + "options": "Email", + "width": 100, + "id": "email", + "name": "Email", + } + ] + ), + ) + + doc = influxframework.get_doc("Report", report_name) + delete_report(doc.name) + + finally: + influxframework.set_user("Administrator") + influxframework.db.rollback() + + def test_custom_report(self): + reset_customization("User") + custom_report_name = save_report( + "Permitted Documents For User", + "Permitted Documents For User Custom", + json.dumps( + [ + { + "fieldname": "email", + "fieldtype": "Data", + "label": "Email", + "insert_after_index": 0, + "link_field": "name", + "doctype": "User", + "options": "Email", + "width": 100, + "id": "email", + "name": "Email", + } + ] + ), + ) + custom_report = influxframework.get_doc("Report", custom_report_name) + columns, result = custom_report.run_query_report( + filters={"user": "Administrator", "doctype": "User"}, user=influxframework.session.user + ) + + self.assertListEqual(["email"], [column.get("fieldname") for column in columns]) + admin_dict = influxframework.core.utils.find(result, lambda d: d["name"] == "Administrator") + self.assertDictEqual( + {"name": "Administrator", "user_type": "System User", "email": "admin@example.com"}, admin_dict + ) + + def test_report_with_custom_column(self): + reset_customization("User") + response = run( + "Permitted Documents For User", + filters={"user": "Administrator", "doctype": "User"}, + custom_columns=[ + { + "fieldname": "email", + "fieldtype": "Data", + "label": "Email", + "insert_after_index": 0, + "link_field": "name", + "doctype": "User", + "options": "Email", + "width": 100, + "id": "email", + "name": "Email", + } + ], + ) + result = response.get("result") + columns = response.get("columns") + self.assertListEqual( + ["name", "email", "user_type"], [column.get("fieldname") for column in columns] + ) + admin_dict = influxframework.core.utils.find(result, lambda d: d["name"] == "Administrator") + self.assertDictEqual( + {"name": "Administrator", "user_type": "System User", "email": "admin@example.com"}, admin_dict + ) + + def test_report_permissions(self): + influxframework.set_user("test@example.com") + influxframework.db.delete("Has Role", {"parent": influxframework.session.user, "role": "Test Has Role"}) + influxframework.db.commit() + if not influxframework.db.exists("Role", "Test Has Role"): + role = influxframework.get_doc({"doctype": "Role", "role_name": "Test Has Role"}).insert( + ignore_permissions=True + ) + + if not influxframework.db.exists("Report", "Test Report"): + report = influxframework.get_doc( + { + "doctype": "Report", + "ref_doctype": "User", + "report_name": "Test Report", + "report_type": "Query Report", + "is_standard": "No", + "roles": [{"role": "Test Has Role"}], + } + ).insert(ignore_permissions=True) + else: + report = influxframework.get_doc("Report", "Test Report") + + self.assertNotEqual(report.is_permitted(), True) + influxframework.set_user("Administrator") + + def test_report_custom_permissions(self): + influxframework.set_user("test@example.com") + influxframework.db.delete("Custom Role", {"report": "Test Custom Role Report"}) + influxframework.db.commit() # nosemgrep + if not influxframework.db.exists("Report", "Test Custom Role Report"): + report = influxframework.get_doc( + { + "doctype": "Report", + "ref_doctype": "User", + "report_name": "Test Custom Role Report", + "report_type": "Query Report", + "is_standard": "No", + "roles": [{"role": "_Test Role"}, {"role": "System Manager"}], + } + ).insert(ignore_permissions=True) + else: + report = influxframework.get_doc("Report", "Test Custom Role Report") + + self.assertEqual(report.is_permitted(), True) + + influxframework.get_doc( + { + "doctype": "Custom Role", + "report": "Test Custom Role Report", + "roles": [{"role": "_Test Role 2"}], + "ref_doctype": "User", + } + ).insert(ignore_permissions=True) + + self.assertNotEqual(report.is_permitted(), True) + influxframework.set_user("Administrator") + + # test for the `_format` method if report data doesn't have sort_by parameter + def test_format_method(self): + if influxframework.db.exists("Report", "User Activity Report Without Sort"): + influxframework.delete_doc("Report", "User Activity Report Without Sort") + with open( + os.path.join(os.path.dirname(__file__), "user_activity_report_without_sort.json") + ) as f: + influxframework.get_doc(json.loads(f.read())).insert() + + report = influxframework.get_doc("Report", "User Activity Report Without Sort") + columns, data = report.get_data() + + self.assertEqual(columns[0].get("label"), "ID") + self.assertEqual(columns[1].get("label"), "User Type") + self.assertTrue("Administrator" in [d[0] for d in data]) + influxframework.delete_doc("Report", "User Activity Report Without Sort") + + def test_non_standard_script_report(self): + report_name = "Test Non Standard Script Report" + if not influxframework.db.exists("Report", report_name): + report = influxframework.get_doc( + { + "doctype": "Report", + "ref_doctype": "User", + "report_name": report_name, + "report_type": "Script Report", + "is_standard": "No", + } + ).insert(ignore_permissions=True) + else: + report = influxframework.get_doc("Report", report_name) + + report.report_script = """ +totals = {} +for user in influxframework.get_all('User', fields = ['name', 'user_type', 'creation']): + if not user.user_type in totals: + totals[user.user_type] = 0 + totals[user.user_type] = totals[user.user_type] + 1 + +data = [ + [ + {'fieldname': 'type', 'label': 'Type'}, + {'fieldname': 'value', 'label': 'Value'} + ], + [ + {"type":key, "value": value} for key, value in totals.items() + ] +] +""" + report.save() + data = report.get_data() + + # check columns + self.assertEqual(data[0][0]["label"], "Type") + + # check values + self.assertTrue("System User" in [d.get("type") for d in data[1]]) + + def test_script_report_with_columns(self): + report_name = "Test Script Report With Columns" + + if influxframework.db.exists("Report", report_name): + influxframework.delete_doc("Report", report_name) + + report = influxframework.get_doc( + { + "doctype": "Report", + "ref_doctype": "User", + "report_name": report_name, + "report_type": "Script Report", + "is_standard": "No", + "columns": [ + dict(fieldname="type", label="Type", fieldtype="Data"), + dict(fieldname="value", label="Value", fieldtype="Int"), + ], + } + ).insert(ignore_permissions=True) + + report.report_script = """ +totals = {} +for user in influxframework.get_all('User', fields = ['name', 'user_type', 'creation']): + if not user.user_type in totals: + totals[user.user_type] = 0 + totals[user.user_type] = totals[user.user_type] + 1 + +result = [ + {"type":key, "value": value} for key, value in totals.items() + ] +""" + + report.save() + data = report.get_data() + + # check columns + self.assertEqual(data[0][0]["label"], "Type") + + # check values + self.assertTrue("System User" in [d.get("type") for d in data[1]]) + + def test_toggle_disabled(self): + """Make sure that authorization is respected.""" + # Assuming that there will be reports in the system. + reports = influxframework.get_all(doctype="Report", limit=1) + report_name = reports[0]["name"] + doc = influxframework.get_doc("Report", report_name) + status = doc.disabled + + # User has write permission on reports and should pass through + influxframework.set_user("test@example.com") + doc.toggle_disable(not status) + doc.reload() + self.assertNotEqual(status, doc.disabled) + + # User has no write permission on reports, permission error is expected. + influxframework.set_user("test1@example.com") + doc = influxframework.get_doc("Report", report_name) + with self.assertRaises(influxframework.exceptions.ValidationError): + doc.toggle_disable(1) + + # Set user back to administrator + influxframework.set_user("Administrator") + + def test_add_total_row_for_tree_reports(self): + report_settings = {"tree": True, "parent_field": "parent_value"} + + columns = [ + {"fieldname": "parent_column", "label": "Parent Column", "fieldtype": "Data", "width": 10}, + {"fieldname": "column_1", "label": "Column 1", "fieldtype": "Float", "width": 10}, + {"fieldname": "column_2", "label": "Column 2", "fieldtype": "Float", "width": 10}, + ] + + result = [ + {"parent_column": "Parent 1", "column_1": 200, "column_2": 150.50}, + {"parent_column": "Child 1", "column_1": 100, "column_2": 75.25, "parent_value": "Parent 1"}, + {"parent_column": "Child 2", "column_1": 100, "column_2": 75.25, "parent_value": "Parent 1"}, + ] + + result = add_total_row( + result, + columns, + meta=None, + is_tree=report_settings["tree"], + parent_field=report_settings["parent_field"], + ) + self.assertEqual(result[-1][0], "Total") + self.assertEqual(result[-1][1], 200) + self.assertEqual(result[-1][2], 150.50) + + def test_cte_in_query_report(self): + cte_query = textwrap.dedent( + """ + with enabled_users as ( + select name + from `tabUser` + where enabled = 1 + ) + select * from enabled_users; + """ + ) + + report = influxframework.get_doc( + { + "doctype": "Report", + "ref_doctype": "User", + "report_name": "Enabled Users List", + "report_type": "Query Report", + "is_standard": "No", + "query": cte_query, + } + ).insert() + + if influxframework.db.db_type == "mariadb": + col, rows = report.execute_query_report(filters={}) + self.assertEqual(col[0], "name") + self.assertGreaterEqual(len(rows), 1) + elif influxframework.db.db_type == "postgres": + self.assertRaises(influxframework.PermissionError, report.execute_query_report, filters={}) diff --git a/influxframework/core/doctype/report/user_activity_report.json b/influxframework/core/doctype/report/user_activity_report.json new file mode 100644 index 0000000..5339bbe --- /dev/null +++ b/influxframework/core/doctype/report/user_activity_report.json @@ -0,0 +1,17 @@ +{ + "add_total_row": 0, + "apply_user_permissions": 1, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "is_standard": "No", + "javascript": null, + "json": "{\"filters\":[],\"columns\":[[\"name\",\"User\"],[\"user_type\",\"User\"],[\"first_name\",\"User\"],[\"last_name\",\"User\"],[\"last_active\",\"User\"],[\"role\",\"Has Role\"]],\"sort_by\":\"User.modified\",\"sort_order\":\"desc\",\"sort_by_next\":null,\"sort_order_next\":\"desc\"}", + "modified": "2016-09-01 02:59:07.728890", + "module": "Core", + "name": "User Activity Report", + "query": null, + "ref_doctype": "User", + "report_name": "User Activity Report", + "report_type": "Report Builder" +} \ No newline at end of file diff --git a/influxframework/core/doctype/report/user_activity_report_without_sort.json b/influxframework/core/doctype/report/user_activity_report_without_sort.json new file mode 100644 index 0000000..bb520a2 --- /dev/null +++ b/influxframework/core/doctype/report/user_activity_report_without_sort.json @@ -0,0 +1,17 @@ +{ + "add_total_row": 0, + "apply_user_permissions": 1, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "is_standard": "No", + "javascript": null, + "json": "{\"filters\":[],\"columns\":[[\"name\",\"User\"],[\"user_type\",\"User\"],[\"first_name\",\"User\"],[\"last_name\",\"User\"],[\"last_active\",\"User\"],[\"role\",\"Has Role\"]],\"sort_order\":\"desc\",\"sort_by_next\":null,\"sort_order_next\":\"desc\"}", + "modified": "2018-12-17 18:27:07.728890", + "module": "Core", + "name": "User Activity Report Without Sort", + "query": null, + "ref_doctype": "User", + "report_name": "User Activity Report Without Sort", + "report_type": "Report Builder" + } \ No newline at end of file diff --git a/influxframework/core/doctype/report_column/__init__.py b/influxframework/core/doctype/report_column/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/report_column/report_column.json b/influxframework/core/doctype/report_column/report_column.json new file mode 100644 index 0000000..2e6a22d --- /dev/null +++ b/influxframework/core/doctype/report_column/report_column.json @@ -0,0 +1,61 @@ +{ + "actions": [], + "creation": "2020-01-14 11:28:37.583656", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "fieldname", + "label", + "fieldtype", + "options", + "width" + ], + "fields": [ + { + "fieldname": "fieldname", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Fieldname", + "reqd": 1 + }, + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "reqd": 1 + }, + { + "fieldname": "fieldtype", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Fieldtype", + "options": "Check\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nInt\nLink\nSelect\nTime", + "reqd": 1 + }, + { + "fieldname": "options", + "fieldtype": "Data", + "label": "Options" + }, + { + "fieldname": "width", + "fieldtype": "Int", + "label": "Width" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-09-03 10:52:03.895817", + "modified_by": "Administrator", + "module": "Core", + "name": "Report Column", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/report_column/report_column.py b/influxframework/core/doctype/report_column/report_column.py new file mode 100644 index 0000000..c67d951 --- /dev/null +++ b/influxframework/core/doctype/report_column/report_column.py @@ -0,0 +1,9 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class ReportColumn(Document): + pass diff --git a/influxframework/core/doctype/report_filter/__init__.py b/influxframework/core/doctype/report_filter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/report_filter/report_filter.json b/influxframework/core/doctype/report_filter/report_filter.json new file mode 100644 index 0000000..964294b --- /dev/null +++ b/influxframework/core/doctype/report_filter/report_filter.json @@ -0,0 +1,71 @@ +{ + "actions": [], + "creation": "2020-01-14 11:38:58.016498", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "fieldname", + "label", + "fieldtype", + "mandatory", + "options", + "wildcard_filter" + ], + "fields": [ + { + "fieldname": "fieldname", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Fieldname", + "reqd": 1 + }, + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "reqd": 1 + }, + { + "fieldname": "fieldtype", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Fieldtype", + "options": "Check\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nInt\nLink\nSelect\nTime", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "mandatory", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Mandatory" + }, + { + "fieldname": "options", + "fieldtype": "Small Text", + "label": "Options" + }, + { + "default": "0", + "description": "Will add \"%\" before and after the query", + "fieldname": "wildcard_filter", + "fieldtype": "Check", + "label": "Wildcard Filter" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-12-05 19:20:00.503097", + "modified_by": "Administrator", + "module": "Core", + "name": "Report Filter", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/report_filter/report_filter.py b/influxframework/core/doctype/report_filter/report_filter.py new file mode 100644 index 0000000..94a781a --- /dev/null +++ b/influxframework/core/doctype/report_filter/report_filter.py @@ -0,0 +1,9 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class ReportFilter(Document): + pass diff --git a/influxframework/core/doctype/role/README.md b/influxframework/core/doctype/role/README.md new file mode 100644 index 0000000..fe8643e --- /dev/null +++ b/influxframework/core/doctype/role/README.md @@ -0,0 +1 @@ +Roles are be assigned to users and permissions on DocTypes are defined on Roles. Standard roles are "Administrator" (developer), "Guest" and "System Manager" (system, user, permission administrator) \ No newline at end of file diff --git a/influxframework/core/doctype/role/__init__.py b/influxframework/core/doctype/role/__init__.py new file mode 100644 index 0000000..2e11a12 --- /dev/null +++ b/influxframework/core/doctype/role/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE diff --git a/influxframework/core/doctype/role/patches/v13_set_default_desk_properties.py b/influxframework/core/doctype/role/patches/v13_set_default_desk_properties.py new file mode 100644 index 0000000..46dd83f --- /dev/null +++ b/influxframework/core/doctype/role/patches/v13_set_default_desk_properties.py @@ -0,0 +1,13 @@ +import influxframework + +from ..role import desk_properties + + +def execute(): + influxframework.reload_doctype("user") + influxframework.reload_doctype("role") + for role in influxframework.get_all("Role", ["name", "desk_access"]): + role_doc = influxframework.get_doc("Role", role.name) + for key in desk_properties: + role_doc.set(key, role_doc.desk_access) + role_doc.save() diff --git a/influxframework/core/doctype/role/role.js b/influxframework/core/doctype/role/role.js new file mode 100644 index 0000000..89fa69b --- /dev/null +++ b/influxframework/core/doctype/role/role.js @@ -0,0 +1,24 @@ +// Copyright (c) 2022, InfluxFramework LLC +// MIT License. See LICENSE + +influxframework.ui.form.on("Role", { + refresh: function (frm) { + if (frm.doc.name === "All") { + frm.dashboard.add_comment( + __("Role 'All' will be given to all System Users."), + "yellow" + ); + } + + frm.set_df_property("is_custom", "read_only", influxframework.session.user !== "Administrator"); + + frm.add_custom_button("Role Permissions Manager", function () { + influxframework.route_options = { role: frm.doc.name }; + influxframework.set_route("permission-manager"); + }); + frm.add_custom_button("Show Users", function () { + influxframework.route_options = { role: frm.doc.name }; + influxframework.set_route("List", "User", "Report"); + }); + }, +}); diff --git a/influxframework/core/doctype/role/role.json b/influxframework/core/doctype/role/role.json new file mode 100644 index 0000000..2039d38 --- /dev/null +++ b/influxframework/core/doctype/role/role.json @@ -0,0 +1,176 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:role_name", + "creation": "2013-01-08 15:50:01", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "role_name", + "home_page", + "restrict_to_domain", + "column_break_4", + "disabled", + "is_custom", + "desk_access", + "two_factor_auth", + "navigation_settings_section", + "search_bar", + "notifications", + "list_settings_section", + "list_sidebar", + "bulk_actions", + "view_switcher", + "form_settings_section", + "form_sidebar", + "timeline", + "dashboard" + ], + "fields": [ + { + "fieldname": "role_name", + "fieldtype": "Data", + "label": "Role Name", + "oldfieldname": "role_name", + "oldfieldtype": "Data", + "reqd": 1, + "unique": 1 + }, + { + "default": "0", + "description": "If disabled, this role will be removed from all users.", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "default": "1", + "fieldname": "desk_access", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Desk Access" + }, + { + "default": "0", + "fieldname": "two_factor_auth", + "fieldtype": "Check", + "label": "Two Factor Authentication" + }, + { + "fieldname": "restrict_to_domain", + "fieldtype": "Link", + "label": "Restrict To Domain", + "options": "Domain" + }, + { + "description": "Route: Example \"/desk\"", + "fieldname": "home_page", + "fieldtype": "Data", + "label": "Home Page" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "navigation_settings_section", + "fieldtype": "Section Break", + "label": "Navigation Settings" + }, + { + "default": "1", + "fieldname": "search_bar", + "fieldtype": "Check", + "label": "Search Bar" + }, + { + "fieldname": "list_settings_section", + "fieldtype": "Section Break", + "label": "List Settings" + }, + { + "default": "1", + "fieldname": "list_sidebar", + "fieldtype": "Check", + "label": "Sidebar" + }, + { + "default": "1", + "fieldname": "bulk_actions", + "fieldtype": "Check", + "label": "Bulk Actions" + }, + { + "fieldname": "form_settings_section", + "fieldtype": "Section Break", + "label": "Form Settings" + }, + { + "default": "1", + "fieldname": "form_sidebar", + "fieldtype": "Check", + "label": "Sidebar" + }, + { + "default": "1", + "fieldname": "timeline", + "fieldtype": "Check", + "label": "Timeline" + }, + { + "default": "1", + "fieldname": "dashboard", + "fieldtype": "Check", + "label": "Dashboard" + }, + { + "default": "1", + "fieldname": "view_switcher", + "fieldtype": "Check", + "label": "View Switcher" + }, + { + "default": "1", + "fieldname": "notifications", + "fieldtype": "Check", + "label": "Notifications" + }, + { + "default": "0", + "fieldname": "is_custom", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Is Custom" + } + ], + "icon": "fa fa-bookmark", + "idx": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2022-08-05 18:33:27.694065", + "modified_by": "Administrator", + "module": "Core", + "name": "Role", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "ASC", + "states": [], + "track_changes": 1, + "translated_doctype": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/role/role.py b/influxframework/core/doctype/role/role.py new file mode 100644 index 0000000..d9e84c7 --- /dev/null +++ b/influxframework/core/doctype/role/role.py @@ -0,0 +1,108 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document + +desk_properties = ( + "search_bar", + "notifications", + "list_sidebar", + "bulk_actions", + "view_switcher", + "form_sidebar", + "timeline", + "dashboard", +) + +STANDARD_ROLES = ("Administrator", "System Manager", "Script Manager", "All", "Guest") + + +class Role(Document): + def before_rename(self, old, new, merge=False): + if old in STANDARD_ROLES: + influxframework.throw(influxframework._("Standard roles cannot be renamed")) + + def after_insert(self): + influxframework.cache().hdel("roles", "Administrator") + + def validate(self): + if self.disabled: + self.disable_role() + else: + self.set_desk_properties() + + def disable_role(self): + if self.name in STANDARD_ROLES: + influxframework.throw(influxframework._("Standard roles cannot be disabled")) + else: + self.remove_roles() + + def set_desk_properties(self): + # set if desk_access is not allowed, unset all desk properties + if self.name == "Guest": + self.desk_access = 0 + + if not self.desk_access: + for key in desk_properties: + self.set(key, 0) + + def remove_roles(self): + influxframework.db.delete("Has Role", {"role": self.name}) + influxframework.clear_cache() + + def on_update(self): + """update system user desk access if this has changed in this update""" + if influxframework.flags.in_install: + return + if self.has_value_changed("desk_access"): + for user_name in get_users(self.name): + user = influxframework.get_doc("User", user_name) + user_type = user.user_type + user.set_system_user() + if user_type != user.user_type: + user.save() + + +def get_info_based_on_role(role, field="email"): + """Get information of all users that have been assigned this role""" + users = influxframework.get_list( + "Has Role", + filters={"role": role, "parenttype": "User"}, + parent_doctype="User", + fields=["parent as user_name"], + ) + + return get_user_info(users, field) + + +def get_user_info(users, field="email"): + """Fetch details about users for the specified field""" + info_list = [] + for user in users: + user_info, enabled = influxframework.db.get_value("User", user.get("user_name"), [field, "enabled"]) + if enabled and user_info not in ["admin@example.com", "guest@example.com"]: + info_list.append(user_info) + return info_list + + +def get_users(role): + return [ + d.parent + for d in influxframework.get_all( + "Has Role", filters={"role": role, "parenttype": "User"}, fields=["parent"] + ) + ] + + +# searches for active employees +@influxframework.whitelist() +@influxframework.validate_and_sanitize_search_inputs +def role_query(doctype, txt, searchfield, start, page_len, filters): + report_filters = [["Role", "name", "like", f"%{txt}%"], ["Role", "is_custom", "=", 0]] + if filters and isinstance(filters, list): + report_filters.extend(filters) + + return influxframework.get_all( + "Role", limit_start=start, limit_page_length=page_len, filters=report_filters, as_list=1 + ) diff --git a/influxframework/core/doctype/role/test_records.json b/influxframework/core/doctype/role/test_records.json new file mode 100644 index 0000000..49442e6 --- /dev/null +++ b/influxframework/core/doctype/role/test_records.json @@ -0,0 +1,22 @@ +[ + { + "doctype": "Role", + "role_name": "_Test Role", + "desk_access": 1 + }, + { + "doctype": "Role", + "role_name": "_Test Role 2", + "desk_access": 1 + }, + { + "doctype": "Role", + "role_name": "_Test Role 3", + "desk_access": 1 + }, + { + "doctype": "Role", + "role_name": "_Test Role 4", + "desk_access": 0 + } +] \ No newline at end of file diff --git a/influxframework/core/doctype/role/test_role.py b/influxframework/core/doctype/role/test_role.py new file mode 100644 index 0000000..1cefe55 --- /dev/null +++ b/influxframework/core/doctype/role/test_role.py @@ -0,0 +1,54 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.core.doctype.role.role import get_info_based_on_role +from influxframework.tests.utils import InfluxFrameworkTestCase + +test_records = influxframework.get_test_records("Role") + + +class TestUser(InfluxFrameworkTestCase): + def test_disable_role(self): + influxframework.get_doc("User", "test@example.com").add_roles("_Test Role 3") + + role = influxframework.get_doc("Role", "_Test Role 3") + role.disabled = 1 + role.save() + + self.assertTrue("_Test Role 3" not in influxframework.get_roles("test@example.com")) + + role = influxframework.get_doc("Role", "_Test Role 3") + role.disabled = 0 + role.save() + + influxframework.get_doc("User", "test@example.com").add_roles("_Test Role 3") + self.assertTrue("_Test Role 3" in influxframework.get_roles("test@example.com")) + + def test_change_desk_access(self): + """if we change desk acecss from role, remove from user""" + influxframework.delete_doc_if_exists("User", "test-user-for-desk-access@example.com") + influxframework.delete_doc_if_exists("Role", "desk-access-test") + user = influxframework.get_doc( + dict(doctype="User", email="test-user-for-desk-access@example.com", first_name="test") + ).insert() + role = influxframework.get_doc(dict(doctype="Role", role_name="desk-access-test", desk_access=0)).insert() + user.add_roles(role.name) + user.save() + self.assertTrue(user.user_type == "Website User") + role.desk_access = 1 + role.save() + user.reload() + self.assertTrue(user.user_type == "System User") + role.desk_access = 0 + role.save() + user.reload() + self.assertTrue(user.user_type == "Website User") + + def test_get_users_by_role(self): + + role = "System Manager" + sys_managers = get_info_based_on_role(role, field="name") + + for user in sys_managers: + self.assertIn(role, influxframework.get_roles(user)) diff --git a/influxframework/core/doctype/role_permission_for_page_and_report/__init__.py b/influxframework/core/doctype/role_permission_for_page_and_report/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.js b/influxframework/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.js new file mode 100644 index 0000000..80cab7b --- /dev/null +++ b/influxframework/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.js @@ -0,0 +1,127 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Role Permission for Page and Report", { + setup: function (frm) { + frm.trigger("set_queries"); + }, + + refresh: function (frm) { + frm.disable_save(); + frm.role_area.hide(); + frm.events.setup_buttons(frm); + }, + + setup_buttons: function (frm) { + frm.clear_custom_buttons(); + frm.page.clear_actions(); + if (frm.doc.set_role_for && frm.doc[influxframework.model.scrub(frm.doc.set_role_for)]) { + frm.add_custom_button(__("Reset to defaults"), function () { + frm.trigger("reset_roles"); + }); + + frm.page.set_primary_action(__("Update"), () => { + frm.trigger("update_report_page_data"); + }); + } + }, + + onload: function (frm) { + if (!frm.roles_editor) { + frm.role_area = $(frm.fields_dict.roles_html.wrapper); + frm.roles_editor = new influxframework.RoleEditor(frm.role_area, frm); + } + }, + + set_queries: function (frm) { + frm.set_query("page", function () { + return { + filters: { + system_page: 0, + }, + }; + }); + }, + + set_role_for: function (frm) { + frm.trigger("clear_fields"); + frm.toggle_display("roles_html", false); + }, + + clear_fields: function (frm) { + var field = frm.doc.set_role_for == "Report" ? "page" : "report"; + frm.set_value(field, ""); + }, + + page: function (frm) { + frm.events.setup_buttons(frm); + if (frm.doc.page) { + frm.trigger("set_report_page_data"); + } else { + frm.trigger("set_role_for"); + } + }, + + report: function (frm) { + frm.events.setup_buttons(frm); + if (frm.doc.report) { + frm.trigger("set_report_page_data"); + } else { + frm.trigger("set_role_for"); + } + }, + + set_report_page_data: function (frm) { + frm.toggle_display("roles_html", true); + frm.role_area.show(); + + return frm.call({ + method: "set_report_page_data", + doc: frm.doc, + callback: function (r) { + refresh_field("roles"); + frm.roles_editor.show(); + }, + }); + }, + + update_report_page_data: function (frm) { + frm.trigger("validate_mandatory_fields"); + if (frm.roles_editor) { + frm.roles_editor.set_roles_in_table(); + } + + return frm.call({ + method: "update_report_page_data", + doc: frm.doc, + callback: function (r) { + refresh_field("roles"); + frm.roles_editor.show(); + influxframework.msgprint(__("Successfully Updated")); + }, + }); + }, + + reset_roles: function (frm) { + frm.trigger("validate_mandatory_fields"); + return frm.call({ + method: "reset_roles", + doc: frm.doc, + callback: function (r) { + refresh_field("roles"); + frm.roles_editor.show(); + influxframework.msgprint(__("Successfully Updated")); + }, + }); + }, + + validate_mandatory_fields: function (frm) { + if (!frm.doc.set_role_for) { + influxframework.throw(__("Mandatory field: set role for")); + } + + if (frm.doc.set_role_for && !frm.doc[frm.doc.set_role_for.toLocaleLowerCase()]) { + influxframework.throw(__("Mandatory field: {0}", [frm.doc.set_role_for])); + } + }, +}); diff --git a/influxframework/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.json b/influxframework/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.json new file mode 100644 index 0000000..8a5393b --- /dev/null +++ b/influxframework/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.json @@ -0,0 +1,327 @@ +{ + "allow_copy": 1, + "allow_events_in_timeline": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2017-02-13 17:33:25.157332", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "set_role_for", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Set Role For", + "length": 0, + "no_copy": 0, + "options": "\nPage\nReport", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:doc.set_role_for == 'Page'", + "fieldname": "page", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Page", + "length": 0, + "no_copy": 0, + "options": "Page", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:doc.set_role_for == 'Report'", + "fieldname": "report", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Report", + "length": 0, + "no_copy": 0, + "options": "Report", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_4", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "report", + "fetch_from": "", + "fieldname": "disable_prepared_report", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Disable Prepared Report", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "roles_permission", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Allow Roles", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "", + "fieldname": "roles_html", + "fieldtype": "HTML", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Roles Html", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "roles", + "fieldtype": "Table", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Roles", + "length": 0, + "no_copy": 0, + "options": "Has Role", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 1, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 1, + "istable": 0, + "max_attachments": 0, + "modified": "2019-01-25 12:08:57.250719", + "modified_by": "Administrator", + "module": "Core", + "name": "Role Permission for Page and Report", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 0, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 0, + "track_seen": 0, + "track_views": 0 +} \ No newline at end of file diff --git a/influxframework/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py b/influxframework/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py new file mode 100644 index 0000000..a7e8293 --- /dev/null +++ b/influxframework/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py @@ -0,0 +1,89 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.core.doctype.report.report import is_prepared_report_disabled +from influxframework.model.document import Document + + +class RolePermissionforPageandReport(Document): + @influxframework.whitelist() + def set_report_page_data(self): + self.set_custom_roles() + self.check_prepared_report_disabled() + + def set_custom_roles(self): + args = self.get_args() + self.set("roles", []) + + name = influxframework.db.get_value("Custom Role", args, "name") + if name: + doc = influxframework.get_doc("Custom Role", name) + roles = doc.roles + else: + roles = self.get_standard_roles() + + self.set("roles", roles) + + def check_prepared_report_disabled(self): + if self.report: + self.disable_prepared_report = is_prepared_report_disabled(self.report) + + def get_standard_roles(self): + doctype = self.set_role_for + docname = self.page if self.set_role_for == "Page" else self.report + doc = influxframework.get_doc(doctype, docname) + return doc.roles + + @influxframework.whitelist() + def reset_roles(self): + roles = self.get_standard_roles() + self.set("roles", roles) + self.update_custom_roles() + self.update_disable_prepared_report() + + @influxframework.whitelist() + def update_report_page_data(self): + self.update_custom_roles() + self.update_disable_prepared_report() + + def update_custom_roles(self): + args = self.get_args() + name = influxframework.db.get_value("Custom Role", args, "name") + + args.update({"doctype": "Custom Role", "roles": self.get_roles()}) + + if self.report: + args.update({"ref_doctype": influxframework.db.get_value("Report", self.report, "ref_doctype")}) + + if name: + custom_role = influxframework.get_doc("Custom Role", name) + custom_role.set("roles", self.get_roles()) + custom_role.save() + else: + influxframework.get_doc(args).insert() + + def update_disable_prepared_report(self): + if self.report: + # intentionally written update query in influxframework.db.sql instead of influxframework.db.set_value + influxframework.db.sql( + """ update `tabReport` set disable_prepared_report = %s + where name = %s""", + (self.disable_prepared_report, self.report), + ) + + def get_args(self, row=None): + name = self.page if self.set_role_for == "Page" else self.report + check_for_field = self.set_role_for.replace(" ", "_").lower() + + return {check_for_field: name} + + def get_roles(self): + roles = [] + for data in self.roles: + if data.role != "All": + roles.append({"role": data.role, "parenttype": "Custom Role"}) + return roles + + def update_status(self): + return influxframework.render_template diff --git a/influxframework/core/doctype/role_profile/__init__.py b/influxframework/core/doctype/role_profile/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/role_profile/role_profile.js b/influxframework/core/doctype/role_profile/role_profile.js new file mode 100644 index 0000000..5982bf2 --- /dev/null +++ b/influxframework/core/doctype/role_profile/role_profile.js @@ -0,0 +1,20 @@ +// Copyright (c) 2017, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Role Profile", { + refresh: function (frm) { + if (has_common(influxframework.user_roles, ["Administrator", "System Manager"])) { + if (!frm.roles_editor) { + const role_area = $(frm.fields_dict.roles_html.wrapper); + frm.roles_editor = new influxframework.RoleEditor(role_area, frm); + } + frm.roles_editor.show(); + } + }, + + validate: function (frm) { + if (frm.roles_editor) { + frm.roles_editor.set_roles_in_table(); + } + }, +}); diff --git a/influxframework/core/doctype/role_profile/role_profile.json b/influxframework/core/doctype/role_profile/role_profile.json new file mode 100644 index 0000000..7cd60a1 --- /dev/null +++ b/influxframework/core/doctype/role_profile/role_profile.json @@ -0,0 +1,80 @@ +{ + "actions": [], + "autoname": "role_profile", + "creation": "2017-08-31 04:16:38.764465", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "role_profile", + "roles_html", + "roles" + ], + "fields": [ + { + "fieldname": "role_profile", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Role Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "roles_html", + "fieldtype": "HTML", + "label": "Roles HTML", + "read_only": 1 + }, + { + "fieldname": "roles", + "fieldtype": "Table", + "hidden": 1, + "label": "Roles Assigned", + "options": "Has Role", + "permlevel": 1, + "print_hide": 1, + "read_only": 1 + } + ], + "links": [ + { + "link_doctype": "User", + "link_fieldname": "role_profile_name" + } + ], + "modified": "2021-12-03 15:45:45.270963", + "modified_by": "Administrator", + "module": "Core", + "name": "Role Profile", + "naming_rule": "Expression (old style)", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "role_profile", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/role_profile/role_profile.py b/influxframework/core/doctype/role_profile/role_profile.py new file mode 100644 index 0000000..bf6ad6b --- /dev/null +++ b/influxframework/core/doctype/role_profile/role_profile.py @@ -0,0 +1,20 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document + + +class RoleProfile(Document): + def autoname(self): + """set name as Role Profile name""" + self.name = self.role_profile + + def on_update(self): + """Changes in role_profile reflected across all its user""" + users = influxframework.get_all("User", filters={"role_profile_name": self.name}) + roles = [role.role for role in self.roles] + for d in users: + user = influxframework.get_doc("User", d) + user.set("roles", []) + user.add_roles(*roles) diff --git a/influxframework/core/doctype/role_profile/test_role_profile.py b/influxframework/core/doctype/role_profile/test_role_profile.py new file mode 100644 index 0000000..e69ecdb --- /dev/null +++ b/influxframework/core/doctype/role_profile/test_role_profile.py @@ -0,0 +1,46 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + +test_dependencies = ["Role"] + + +class TestRoleProfile(InfluxFrameworkTestCase): + def test_make_new_role_profile(self): + influxframework.delete_doc_if_exists("Role Profile", "Test 1", force=1) + new_role_profile = influxframework.get_doc(dict(doctype="Role Profile", role_profile="Test 1")).insert() + + self.assertEqual(new_role_profile.role_profile, "Test 1") + + # add role + new_role_profile.append("roles", {"role": "_Test Role 2"}) + new_role_profile.save() + self.assertEqual(new_role_profile.roles[0].role, "_Test Role 2") + + # user with a role profile + random_user = influxframework.mock("email") + random_user_name = influxframework.mock("name") + + random_user = influxframework.get_doc( + { + "doctype": "User", + "email": random_user, + "enabled": 1, + "first_name": random_user_name, + "new_password": "Eastern_43A1W", + "role_profile_name": "Test 1", + } + ).insert(ignore_permissions=True, ignore_if_duplicate=True) + self.assertListEqual( + [role.role for role in random_user.roles], [role.role for role in new_role_profile.roles] + ) + + # clear roles + new_role_profile.roles = [] + new_role_profile.save() + self.assertEqual(new_role_profile.roles, []) + + # user roles with the role profile should also be updated + random_user.reload() + self.assertListEqual(random_user.roles, []) diff --git a/influxframework/core/doctype/rq_job/__init__.py b/influxframework/core/doctype/rq_job/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/rq_job/rq_job.js b/influxframework/core/doctype/rq_job/rq_job.js new file mode 100644 index 0000000..72cdbbf --- /dev/null +++ b/influxframework/core/doctype/rq_job/rq_job.js @@ -0,0 +1,30 @@ +// Copyright (c) 2022, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("RQ Job", { + refresh: function (frm) { + // Nothing in this form is supposed to be editable. + frm.disable_form(); + frm.dashboard.set_headline_alert( + "This is a virtual doctype and data is cleared periodically." + ); + + if (["started", "queued"].includes(frm.doc.status)) { + frm.add_custom_button(__("Force Stop job"), () => { + influxframework.confirm( + "This will terminate the job immediately and might be dangerous, are you sure? ", + () => { + influxframework + .xcall("influxframework.core.doctype.rq_job.rq_job.stop_job", { + job_id: frm.doc.name, + }) + .then((r) => { + influxframework.show_alert("Job Stopped Succefully"); + frm.reload_doc(); + }); + } + ); + }); + } + }, +}); diff --git a/influxframework/core/doctype/rq_job/rq_job.json b/influxframework/core/doctype/rq_job/rq_job.json new file mode 100644 index 0000000..7cae15c --- /dev/null +++ b/influxframework/core/doctype/rq_job/rq_job.json @@ -0,0 +1,162 @@ +{ + "actions": [], + "allow_copy": 1, + "autoname": "field:job_id", + "creation": "2022-09-10 16:19:37.934903", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "job_info_section", + "job_id", + "job_name", + "queue", + "timeout", + "column_break_5", + "arguments", + "job_status_section", + "status", + "time_taken", + "column_break_11", + "started_at", + "ended_at", + "exception_section", + "exc_info" + ], + "fields": [ + { + "fieldname": "queue", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Queue", + "options": "default\nshort\nlong" + }, + { + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "queued\nstarted\nfinished\nfailed\ndeferred\nscheduled\ncanceled" + }, + { + "fieldname": "job_id", + "fieldtype": "Data", + "label": "Job ID", + "unique": 1 + }, + { + "fieldname": "exc_info", + "fieldtype": "Code", + "label": "Exception" + }, + { + "fieldname": "job_name", + "fieldtype": "Data", + "label": "Job Name" + }, + { + "fieldname": "arguments", + "fieldtype": "Code", + "label": "Arguments" + }, + { + "fieldname": "timeout", + "fieldtype": "Duration", + "label": "Timeout" + }, + { + "fieldname": "time_taken", + "fieldtype": "Duration", + "label": "Time Taken" + }, + { + "fieldname": "started_at", + "fieldtype": "Datetime", + "label": "Started At" + }, + { + "fieldname": "ended_at", + "fieldtype": "Datetime", + "label": "Ended At" + }, + { + "fieldname": "job_info_section", + "fieldtype": "Section Break", + "label": "Job Info" + }, + { + "fieldname": "job_status_section", + "fieldtype": "Section Break", + "label": "Job Status" + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "fieldname": "exception_section", + "fieldtype": "Section Break" + } + ], + "in_create": 1, + "is_virtual": 1, + "links": [], + "modified": "2022-09-11 05:27:50.878534", + "modified_by": "Administrator", + "module": "Core", + "name": "RQ Job", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1 + }, + { + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [ + { + "color": "Yellow", + "title": "queued" + }, + { + "color": "Blue", + "title": "started" + }, + { + "color": "Red", + "title": "failed" + }, + { + "color": "Green", + "title": "finished" + }, + { + "color": "Orange", + "title": "cancelled" + } + ], + "title_field": "job_name" +} \ No newline at end of file diff --git a/influxframework/core/doctype/rq_job/rq_job.py b/influxframework/core/doctype/rq_job/rq_job.py new file mode 100644 index 0000000..9486baa --- /dev/null +++ b/influxframework/core/doctype/rq_job/rq_job.py @@ -0,0 +1,193 @@ +# Copyright (c) 2022, InfluxFramework LLC +# For license information, please see license.txt + +import functools +import re + +from rq.command import send_stop_job_command +from rq.job import Job +from rq.queue import Queue + +import influxframework +from influxframework.model.document import Document +from influxframework.utils import ( + cint, + compare, + convert_utc_to_user_timezone, + create_batch, + make_filter_dict, +) +from influxframework.utils.background_jobs import get_queues, get_redis_conn + +QUEUES = ["default", "long", "short"] +JOB_STATUSES = ["queued", "started", "failed", "finished", "deferred", "scheduled", "canceled"] + + +def check_permissions(method): + @functools.wraps(method) + def wrapper(*args, **kwargs): + influxframework.only_for("System Manager") + job = args[0].job + if not for_current_site(job): + raise influxframework.PermissionError + + return method(*args, **kwargs) + + return wrapper + + +class RQJob(Document): + def load_from_db(self): + job = Job.fetch(self.name, connection=get_redis_conn()) + if not for_current_site(job): + raise influxframework.PermissionError + super(Document, self).__init__(serialize_job(job)) + self._job_obj = job + + @property + def job(self): + return self._job_obj + + @staticmethod + def get_list(args): + + start = cint(args.get("start")) or 0 + page_length = cint(args.get("page_length")) or 20 + + order_desc = "desc" in args.get("order_by", "") + + matched_job_ids = RQJob.get_matching_job_ids(args) + + jobs = [] + for job_ids in create_batch(matched_job_ids, 100): + jobs.extend( + serialize_job(job) + for job in Job.fetch_many(job_ids=job_ids, connection=get_redis_conn()) + if job and for_current_site(job) + ) + if len(jobs) > start + page_length: + # we have fetched enough. This is inefficient but because of site filtering TINA + break + + return sorted(jobs, key=lambda j: j.modified, reverse=order_desc)[start : start + page_length] + + @staticmethod + def get_matching_job_ids(args): + filters = make_filter_dict(args.get("filters")) + + queues = _eval_filters(filters.get("queue"), QUEUES) + statuses = _eval_filters(filters.get("status"), JOB_STATUSES) + + matched_job_ids = [] + for queue in get_queues(): + if not queue.name.endswith(tuple(queues)): + continue + for status in statuses: + matched_job_ids.extend(fetch_job_ids(queue, status)) + + return matched_job_ids + + @check_permissions + def delete(self): + self.job.delete() + + @check_permissions + def stop_job(self): + send_stop_job_command(connection=get_redis_conn(), job_id=self.job_id) + + @staticmethod + def get_count(args) -> int: + # Can not be implemented efficiently due to site filtering hence ignored. + return 0 + + # None of these methods apply to virtual job doctype, overriden for sanity. + @staticmethod + def get_stats(args): + return {} + + def db_insert(self, *args, **kwargs): + pass + + def db_update(self, *args, **kwargs): + pass + + +def serialize_job(job: Job) -> influxframework._dict: + modified = job.last_heartbeat or job.ended_at or job.started_at or job.created_at + job_name = job.kwargs.get("kwargs", {}).get("job_type") or str(job.kwargs.get("job_name")) + + # function objects have this repr: '' + # This regex just removes unnecessary things around it. + if matches := re.match(r".*) at 0x.*>", job_name): + job_name = matches.group("func_name") + + return influxframework._dict( + name=job.id, + job_id=job.id, + queue=job.origin.rsplit(":", 1)[1], + job_name=job_name, + status=job.get_status(), + started_at=convert_utc_to_user_timezone(job.started_at) if job.started_at else "", + ended_at=convert_utc_to_user_timezone(job.ended_at) if job.ended_at else "", + time_taken=(job.ended_at - job.started_at).total_seconds() if job.ended_at else "", + exc_info=job.exc_info, + arguments=influxframework.as_json(job.kwargs), + timeout=job.timeout, + creation=convert_utc_to_user_timezone(job.created_at), + modified=convert_utc_to_user_timezone(modified), + _comment_count=0, + ) + + +def for_current_site(job: Job) -> bool: + return job.kwargs.get("site") == influxframework.local.site + + +def _eval_filters(filter, values: list[str]) -> list[str]: + if filter: + operator, operand = filter + return [val for val in values if compare(val, operator, operand)] + return values + + +def fetch_job_ids(queue: Queue, status: str) -> list[str]: + registry_map = { + "queued": queue, # self + "started": queue.started_job_registry, + "finished": queue.finished_job_registry, + "failed": queue.failed_job_registry, + "deferred": queue.deferred_job_registry, + "scheduled": queue.scheduled_job_registry, + "canceled": queue.canceled_job_registry, + } + + registry = registry_map.get(status) + if registry is not None: + job_ids = registry.get_job_ids() + return [j for j in job_ids if j] + + return [] + + +@influxframework.whitelist() +def remove_failed_jobs(): + influxframework.only_for("System Manager") + for queue in get_queues(): + fail_registry = queue.failed_job_registry + for job_ids in create_batch(fail_registry.get_job_ids(), 100): + for job in Job.fetch_many(job_ids=job_ids, connection=get_redis_conn()): + if job and for_current_site(job): + fail_registry.remove(job, delete_job=True) + + +def get_all_queued_jobs(): + jobs = [] + for q in get_queues(): + jobs.extend(q.get_jobs()) + + return [job for job in jobs if for_current_site(job)] + + +@influxframework.whitelist() +def stop_job(job_id): + influxframework.get_doc("RQ Job", job_id).stop_job() diff --git a/influxframework/core/doctype/rq_job/rq_job_list.js b/influxframework/core/doctype/rq_job/rq_job_list.js new file mode 100644 index 0000000..286fb78 --- /dev/null +++ b/influxframework/core/doctype/rq_job/rq_job_list.js @@ -0,0 +1,32 @@ +influxframework.listview_settings["RQ Job"] = { + hide_name_column: true, + + onload(listview) { + if (!has_common(influxframework.user_roles, ["Administrator", "System Manager"])) return; + + listview.page.add_inner_button(__("Remove Failed Jobs"), () => { + influxframework.confirm(__("Are you sure you want to remove all failed jobs?"), () => { + influxframework.xcall("influxframework.core.doctype.rq_job.rq_job.remove_failed_jobs"); + }); + }); + + if (listview.list_view_settings) { + listview.list_view_settings.disable_count = 1; + listview.list_view_settings.disable_sidebar_stats = 1; + } + + influxframework.xcall("influxframework.utils.scheduler.get_scheduler_status").then(({ status }) => { + if (status === "active") { + listview.page.set_indicator(__("Scheduler: Active"), "green"); + } else { + listview.page.set_indicator(__("Scheduler: Inactive"), "red"); + } + }); + + setInterval(() => { + if (!listview.list_view_settings.disable_auto_refresh) { + listview.refresh(); + } + }, 5000); + }, +}; diff --git a/influxframework/core/doctype/rq_job/test_rq_job.py b/influxframework/core/doctype/rq_job/test_rq_job.py new file mode 100644 index 0000000..bb81a12 --- /dev/null +++ b/influxframework/core/doctype/rq_job/test_rq_job.py @@ -0,0 +1,91 @@ +# Copyright (c) 2022, InfluxFramework LLC + +# See license.txt + +import time + +from rq import exceptions as rq_exc +from rq.job import Job + +import influxframework +from influxframework.core.doctype.rq_job.rq_job import RQJob, remove_failed_jobs, stop_job +from influxframework.tests.utils import InfluxFrameworkTestCase, timeout + + +class TestRQJob(InfluxFrameworkTestCase): + + BG_JOB = "influxframework.core.doctype.rq_job.test_rq_job.test_func" + + @timeout(seconds=20) + def check_status(self, job: Job, status, wait=True): + if wait: + while True: + if job.is_queued or job.is_started: + time.sleep(0.2) + else: + break + self.assertEqual(influxframework.get_doc("RQ Job", job.id).status, status) + + def test_serialization(self): + + job = influxframework.enqueue(method=self.BG_JOB, queue="short") + rq_job = influxframework.get_doc("RQ Job", job.id) + + self.assertEqual(job, rq_job.job) + self.assertDocumentEqual( + { + "name": job.id, + "queue": "short", + "job_name": self.BG_JOB, + "status": "queued", + "exc_info": None, + }, + rq_job, + ) + self.check_status(job, "finished") + + def test_func_obj_serialization(self): + job = influxframework.enqueue(method=test_func, queue="short") + rq_job = influxframework.get_doc("RQ Job", job.id) + self.assertEqual(rq_job.job_name, "test_func") + + def test_get_list_filtering(self): + + # Check failed job clearning and filtering + remove_failed_jobs() + jobs = RQJob.get_list({"filters": [["RQ Job", "status", "=", "failed"]]}) + self.assertEqual(jobs, []) + + # Fail a job + job = influxframework.enqueue(method=self.BG_JOB, queue="short", fail=True) + self.check_status(job, "failed") + jobs = RQJob.get_list({"filters": [["RQ Job", "status", "=", "failed"]]}) + self.assertEqual(len(jobs), 1) + self.assertTrue(jobs[0].exc_info) + + # Assert that non-failed job still exists + non_failed_jobs = RQJob.get_list({"filters": [["RQ Job", "status", "!=", "failed"]]}) + self.assertGreaterEqual(len(non_failed_jobs), 1) + + # Create a slow job and check if it's stuck in "Started" + job = influxframework.enqueue(method=self.BG_JOB, queue="short", sleep=1000) + time.sleep(3) + self.check_status(job, "started", wait=False) + stop_job(job_id=job.id) + self.check_status(job, "stopped") + + def test_delete_doc(self): + job = influxframework.enqueue(method=self.BG_JOB, queue="short") + influxframework.get_doc("RQ Job", job.id).delete() + + with self.assertRaises(rq_exc.NoSuchJobError): + job.refresh() + + +def test_func(fail=False, sleep=0): + if fail: + 42 / 0 + if sleep: + time.sleep(sleep) + + return True diff --git a/influxframework/core/doctype/rq_worker/__init__.py b/influxframework/core/doctype/rq_worker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/rq_worker/rq_worker.js b/influxframework/core/doctype/rq_worker/rq_worker.js new file mode 100644 index 0000000..337ab6e --- /dev/null +++ b/influxframework/core/doctype/rq_worker/rq_worker.js @@ -0,0 +1,9 @@ +// Copyright (c) 2022, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("RQ Worker", { + refresh: function (frm) { + // Nothing in this form is supposed to be editable. + frm.disable_form(); + }, +}); diff --git a/influxframework/core/doctype/rq_worker/rq_worker.json b/influxframework/core/doctype/rq_worker/rq_worker.json new file mode 100644 index 0000000..ea65abd --- /dev/null +++ b/influxframework/core/doctype/rq_worker/rq_worker.json @@ -0,0 +1,138 @@ +{ + "actions": [], + "allow_copy": 1, + "creation": "2022-09-10 14:54:57.342170", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "worker_information_section", + "queue", + "queue_type", + "column_break_4", + "worker_name", + "statistics_section", + "status", + "pid", + "current_job_id", + "successful_job_count", + "failed_job_count", + "column_break_12", + "birth_date", + "last_heartbeat", + "total_working_time" + ], + "fields": [ + { + "fieldname": "worker_name", + "fieldtype": "Data", + "label": "Worker Name", + "unique": 1 + }, + { + "fieldname": "status", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Status" + }, + { + "fieldname": "current_job_id", + "fieldtype": "Link", + "label": "Current Job ID", + "options": "RQ Job" + }, + { + "fieldname": "pid", + "fieldtype": "Data", + "label": "PID" + }, + { + "fieldname": "last_heartbeat", + "fieldtype": "Datetime", + "label": "Last Heartbeat" + }, + { + "fieldname": "birth_date", + "fieldtype": "Datetime", + "label": "Start Time" + }, + { + "fieldname": "successful_job_count", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Successful Job Count" + }, + { + "fieldname": "failed_job_count", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Failed Job Count" + }, + { + "fieldname": "total_working_time", + "fieldtype": "Duration", + "label": "Total Working Time" + }, + { + "fieldname": "queue", + "fieldtype": "Data", + "label": "Queue" + }, + { + "fieldname": "queue_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Queue Type", + "options": "default\nlong\nshort" + }, + { + "fieldname": "worker_information_section", + "fieldtype": "Section Break", + "label": "Worker Information" + }, + { + "fieldname": "statistics_section", + "fieldtype": "Section Break", + "label": "Statistics" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" + } + ], + "in_create": 1, + "is_virtual": 1, + "links": [], + "modified": "2022-09-11 05:02:53.981705", + "modified_by": "Administrator", + "module": "Core", + "name": "RQ Worker", + "owner": "Administrator", + "permissions": [ + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [ + { + "color": "Blue", + "title": "idle" + }, + { + "color": "Yellow", + "title": "busy" + } + ] +} \ No newline at end of file diff --git a/influxframework/core/doctype/rq_worker/rq_worker.py b/influxframework/core/doctype/rq_worker/rq_worker.py new file mode 100644 index 0000000..e6cee09 --- /dev/null +++ b/influxframework/core/doctype/rq_worker/rq_worker.py @@ -0,0 +1,69 @@ +# Copyright (c) 2022, InfluxFramework LLC +# For license information, please see license.txt + +from rq import Worker + +import influxframework +from influxframework.model.document import Document +from influxframework.utils import cint, convert_utc_to_user_timezone +from influxframework.utils.background_jobs import get_workers + + +class RQWorker(Document): + def load_from_db(self): + + all_workers = get_workers() + worker = [w for w in all_workers if w.pid == cint(self.name)][0] + d = serialize_worker(worker) + + super(Document, self).__init__(d) + + @staticmethod + def get_list(args): + start = cint(args.get("start")) or 0 + page_length = cint(args.get("page_length")) or 20 + + workers = get_workers() + + valid_workers = [w for w in workers if w.pid][start : start + page_length] + return [serialize_worker(worker) for worker in valid_workers] + + @staticmethod + def get_count(args) -> int: + return len(get_workers()) + + # None of these methods apply to virtual workers, overriden for sanity. + @staticmethod + def get_stats(args): + return {} + + def db_insert(self, *args, **kwargs): + pass + + def db_update(self, *args, **kwargs): + pass + + def delete(self): + pass + + +def serialize_worker(worker: Worker) -> influxframework._dict: + queue = ", ".join(worker.queue_names()) + + return influxframework._dict( + name=worker.pid, + queue=queue, + queue_type=queue.rsplit(":", 1)[1], + worker_name=worker.name, + status=worker.get_state(), + pid=worker.pid, + current_job_id=worker.get_current_job_id(), + last_heartbeat=convert_utc_to_user_timezone(worker.last_heartbeat), + birth_date=convert_utc_to_user_timezone(worker.birth_date), + successful_job_count=worker.successful_job_count, + failed_job_count=worker.failed_job_count, + total_working_time=worker.total_working_time, + _comment_count=0, + modified=convert_utc_to_user_timezone(worker.last_heartbeat), + creation=convert_utc_to_user_timezone(worker.birth_date), + ) diff --git a/influxframework/core/doctype/rq_worker/test_rq_worker.py b/influxframework/core/doctype/rq_worker/test_rq_worker.py new file mode 100644 index 0000000..1b97155 --- /dev/null +++ b/influxframework/core/doctype/rq_worker/test_rq_worker.py @@ -0,0 +1,17 @@ +# Copyright (c) 2022, InfluxFramework LLC +# See license.txt + +import influxframework +from influxframework.core.doctype.rq_worker.rq_worker import RQWorker +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestRQWorker(InfluxFrameworkTestCase): + def test_get_worker_list(self): + workers = RQWorker.get_list({}) + self.assertGreaterEqual(len(workers), 1) + self.assertTrue(any(w.queue_type == "short" for w in workers)) + + def test_worker_serialization(self): + workers = RQWorker.get_list({}) + influxframework.get_doc("RQ Worker", workers[0].pid) diff --git a/influxframework/core/doctype/scheduled_job_log/__init__.py b/influxframework/core/doctype/scheduled_job_log/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/scheduled_job_log/scheduled_job_log.js b/influxframework/core/doctype/scheduled_job_log/scheduled_job_log.js new file mode 100644 index 0000000..d159248 --- /dev/null +++ b/influxframework/core/doctype/scheduled_job_log/scheduled_job_log.js @@ -0,0 +1,7 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Scheduled Job Log", { + // refresh: function(frm) { + // } +}); diff --git a/influxframework/core/doctype/scheduled_job_log/scheduled_job_log.json b/influxframework/core/doctype/scheduled_job_log/scheduled_job_log.json new file mode 100644 index 0000000..451c410 --- /dev/null +++ b/influxframework/core/doctype/scheduled_job_log/scheduled_job_log.json @@ -0,0 +1,65 @@ +{ + "actions": [], + "creation": "2019-09-23 14:36:36.935869", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "status", + "scheduled_job_type", + "details" + ], + "fields": [ + { + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "Scheduled\nComplete\nFailed", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "details", + "fieldtype": "Code", + "label": "Details", + "read_only": 1 + }, + { + "fieldname": "scheduled_job_type", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Scheduled Job", + "options": "Scheduled Job Type", + "read_only": 1, + "reqd": 1 + } + ], + "links": [], + "modified": "2022-06-13 05:41:21.090972", + "modified_by": "Administrator", + "module": "Core", + "name": "Scheduled Job Log", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "scheduled_job_type" +} \ No newline at end of file diff --git a/influxframework/core/doctype/scheduled_job_log/scheduled_job_log.py b/influxframework/core/doctype/scheduled_job_log/scheduled_job_log.py new file mode 100644 index 0000000..7a6aa2b --- /dev/null +++ b/influxframework/core/doctype/scheduled_job_log/scheduled_job_log.py @@ -0,0 +1,14 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document +from influxframework.query_builder import Interval +from influxframework.query_builder.functions import Now + + +class ScheduledJobLog(Document): + @staticmethod + def clear_old_logs(days=90): + table = influxframework.qb.DocType("Scheduled Job Log") + influxframework.db.delete(table, filters=(table.modified < (Now() - Interval(days=days)))) diff --git a/influxframework/core/doctype/scheduled_job_log/scheduled_job_log_list.js b/influxframework/core/doctype/scheduled_job_log/scheduled_job_log_list.js new file mode 100644 index 0000000..98d019e --- /dev/null +++ b/influxframework/core/doctype/scheduled_job_log/scheduled_job_log_list.js @@ -0,0 +1,7 @@ +influxframework.listview_settings["Scheduled Job Log"] = { + onload: function (listview) { + influxframework.require("logtypes.bundle.js", () => { + influxframework.utils.logtypes.show_log_retention_message(cur_list.doctype); + }); + }, +}; diff --git a/influxframework/core/doctype/scheduled_job_log/test_scheduled_job_log.py b/influxframework/core/doctype/scheduled_job_log/test_scheduled_job_log.py new file mode 100644 index 0000000..2452bc0 --- /dev/null +++ b/influxframework/core/doctype/scheduled_job_log/test_scheduled_job_log.py @@ -0,0 +1,8 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestScheduledJobLog(InfluxFrameworkTestCase): + pass diff --git a/influxframework/core/doctype/scheduled_job_type/__init__.py b/influxframework/core/doctype/scheduled_job_type/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/scheduled_job_type/scheduled_job_type.js b/influxframework/core/doctype/scheduled_job_type/scheduled_job_type.js new file mode 100644 index 0000000..2286308 --- /dev/null +++ b/influxframework/core/doctype/scheduled_job_type/scheduled_job_type.js @@ -0,0 +1,7 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Scheduled Job Type", { + // refresh: function(frm) { + // } +}); diff --git a/influxframework/core/doctype/scheduled_job_type/scheduled_job_type.json b/influxframework/core/doctype/scheduled_job_type/scheduled_job_type.json new file mode 100644 index 0000000..f03a40c --- /dev/null +++ b/influxframework/core/doctype/scheduled_job_type/scheduled_job_type.json @@ -0,0 +1,128 @@ +{ + "actions": [ + { + "action": "influxframework.core.doctype.scheduled_job_type.scheduled_job_type.execute_event", + "action_type": "Server Action", + "label": "Execute" + } + ], + "creation": "2019-09-23 14:34:09.205368", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "stopped", + "method", + "server_script", + "frequency", + "cron_format", + "create_log", + "status_section", + "last_execution", + "column_break_9", + "next_execution" + ], + "fields": [ + { + "fieldname": "method", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Method", + "read_only": 1, + "reqd": 1 + }, + { + "default": "0", + "fieldname": "stopped", + "fieldtype": "Check", + "label": "Stopped" + }, + { + "default": "0", + "depends_on": "eval:doc.frequency==='All'", + "fieldname": "create_log", + "fieldtype": "Check", + "label": "Create Log" + }, + { + "fieldname": "last_execution", + "fieldtype": "Datetime", + "label": "Last Execution", + "read_only": 1 + }, + { + "allow_in_quick_entry": 1, + "depends_on": "eval:doc.frequency==='Cron'", + "fieldname": "cron_format", + "fieldtype": "Data", + "label": "Cron Format", + "read_only": 1 + }, + { + "fieldname": "frequency", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Frequency", + "options": "All\nHourly\nHourly Long\nDaily\nDaily Long\nWeekly\nWeekly Long\nMonthly\nMonthly Long\nCron\nYearly\nAnnual", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "server_script", + "fieldtype": "Link", + "label": "Server Script", + "options": "Server Script", + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "next_execution", + "fieldtype": "Datetime", + "is_virtual": 1, + "label": "Next Execution", + "read_only": 1 + }, + { + "fieldname": "status_section", + "fieldtype": "Section Break", + "label": "Status" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + } + ], + "in_create": 1, + "links": [ + { + "link_doctype": "Scheduled Job Log", + "link_fieldname": "scheduled_job_type" + } + ], + "modified": "2022-06-28 02:55:12.470915", + "modified_by": "Administrator", + "module": "Core", + "name": "Scheduled Job Type", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "method", + "track_changes": 1 +} diff --git a/influxframework/core/doctype/scheduled_job_type/scheduled_job_type.py b/influxframework/core/doctype/scheduled_job_type/scheduled_job_type.py new file mode 100644 index 0000000..d1471fb --- /dev/null +++ b/influxframework/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -0,0 +1,214 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +import json +from datetime import datetime + +import click +from croniter import croniter + +import influxframework +from influxframework.model.document import Document +from influxframework.utils import get_datetime, now_datetime +from influxframework.utils.background_jobs import enqueue, get_jobs + + +class ScheduledJobType(Document): + def autoname(self): + self.name = ".".join(self.method.split(".")[-2:]) + + def validate(self): + if self.frequency != "All": + # force logging for all events other than continuous ones (ALL) + self.create_log = 1 + + def enqueue(self, force=False): + # enqueue event if last execution is done + if self.is_event_due() or force: + if influxframework.flags.enqueued_jobs: + influxframework.flags.enqueued_jobs.append(self.method) + + if influxframework.flags.execute_job: + self.execute() + else: + if not self.is_job_in_queue(): + enqueue( + "influxframework.core.doctype.scheduled_job_type.scheduled_job_type.run_scheduled_job", + queue=self.get_queue_name(), + job_type=self.method, + ) + return True + + return False + + def is_event_due(self, current_time=None): + """Return true if event is due based on time lapsed since last execution""" + # if the next scheduled event is before NOW, then its due! + return self.get_next_execution() <= (current_time or now_datetime()) + + def is_job_in_queue(self): + queued_jobs = get_jobs(site=influxframework.local.site, key="job_type")[influxframework.local.site] + return self.method in queued_jobs + + @property + def next_execution(self): + return self.get_next_execution() + + def get_next_execution(self): + CRON_MAP = { + "Yearly": "0 0 1 1 *", + "Annual": "0 0 1 1 *", + "Monthly": "0 0 1 * *", + "Monthly Long": "0 0 1 * *", + "Weekly": "0 0 * * 0", + "Weekly Long": "0 0 * * 0", + "Daily": "0 0 * * *", + "Daily Long": "0 0 * * *", + "Hourly": "0 * * * *", + "Hourly Long": "0 * * * *", + "All": "0/" + str((influxframework.get_conf().scheduler_interval or 240) // 60) + " * * * *", + } + + if not self.cron_format: + self.cron_format = CRON_MAP[self.frequency] + + return croniter( + self.cron_format, get_datetime(self.last_execution or datetime(2000, 1, 1)) + ).get_next(datetime) + + def execute(self): + self.scheduler_log = None + try: + self.log_status("Start") + if self.server_script: + script_name = influxframework.db.get_value("Server Script", self.server_script) + if script_name: + influxframework.get_doc("Server Script", script_name).execute_scheduled_method() + else: + influxframework.get_attr(self.method)() + influxframework.db.commit() + self.log_status("Complete") + except Exception: + influxframework.db.rollback() + self.log_status("Failed") + + def log_status(self, status): + # log file + influxframework.logger("scheduler").info(f"Scheduled Job {status}: {self.method} for {influxframework.local.site}") + self.update_scheduler_log(status) + + def update_scheduler_log(self, status): + if not self.create_log: + # self.get_next_execution will work properly iff self.last_execution is properly set + if self.frequency == "All" and status == "Start": + self.db_set("last_execution", now_datetime(), update_modified=False) + influxframework.db.commit() + return + if not self.scheduler_log: + self.scheduler_log = influxframework.get_doc( + dict(doctype="Scheduled Job Log", scheduled_job_type=self.name) + ).insert(ignore_permissions=True) + self.scheduler_log.db_set("status", status) + if status == "Failed": + self.scheduler_log.db_set("details", influxframework.get_traceback()) + if status == "Start": + self.db_set("last_execution", now_datetime(), update_modified=False) + influxframework.db.commit() + + def get_queue_name(self): + return "long" if ("Long" in self.frequency) else "default" + + def on_trash(self): + influxframework.db.delete("Scheduled Job Log", {"scheduled_job_type": self.name}) + + +@influxframework.whitelist() +def execute_event(doc: str): + influxframework.only_for("System Manager") + doc = json.loads(doc) + influxframework.get_doc("Scheduled Job Type", doc.get("name")).enqueue(force=True) + return doc + + +def run_scheduled_job(job_type: str): + """This is a wrapper function that runs a hooks.scheduler_events method""" + try: + influxframework.get_doc("Scheduled Job Type", dict(method=job_type)).execute() + except Exception: + print(influxframework.get_traceback()) + + +def sync_jobs(hooks: dict = None): + influxframework.reload_doc("core", "doctype", "scheduled_job_type") + scheduler_events = hooks or influxframework.get_hooks("scheduler_events") + all_events = insert_events(scheduler_events) + clear_events(all_events) + + +def insert_events(scheduler_events: dict) -> list: + cron_jobs, event_jobs = [], [] + for event_type in scheduler_events: + events = scheduler_events.get(event_type) + if isinstance(events, dict): + cron_jobs += insert_cron_jobs(events) + else: + # hourly, daily etc + event_jobs += insert_event_jobs(events, event_type) + return cron_jobs + event_jobs + + +def insert_cron_jobs(events: dict) -> list: + cron_jobs = [] + for cron_format in events: + for event in events.get(cron_format): + cron_jobs.append(event) + insert_single_event("Cron", event, cron_format) + return cron_jobs + + +def insert_event_jobs(events: list, event_type: str) -> list: + event_jobs = [] + for event in events: + event_jobs.append(event) + frequency = event_type.replace("_", " ").title() + insert_single_event(frequency, event) + return event_jobs + + +def insert_single_event(frequency: str, event: str, cron_format: str = None): + cron_expr = {"cron_format": cron_format} if cron_format else {} + + try: + influxframework.get_attr(event) + except Exception as e: + click.secho(f"{event} is not a valid method: {e}", fg="yellow") + + doc = influxframework.get_doc( + { + "doctype": "Scheduled Job Type", + "method": event, + "cron_format": cron_format, + "frequency": frequency, + } + ) + + if not influxframework.db.exists( + "Scheduled Job Type", {"method": event, "frequency": frequency, **cron_expr} + ): + savepoint = "scheduled_job_type_creation" + try: + influxframework.db.savepoint(savepoint) + doc.insert() + except influxframework.DuplicateEntryError: + influxframework.db.rollback(save_point=savepoint) + doc.delete() + doc.insert() + + +def clear_events(all_events: list): + for event in influxframework.get_all("Scheduled Job Type", fields=["name", "method", "server_script"]): + is_server_script = event.server_script + is_defined_in_hooks = event.method in all_events + + if not (is_defined_in_hooks or is_server_script): + influxframework.delete_doc("Scheduled Job Type", event.name) diff --git a/influxframework/core/doctype/scheduled_job_type/test_scheduled_job_type.py b/influxframework/core/doctype/scheduled_job_type/test_scheduled_job_type.py new file mode 100644 index 0000000..a869605 --- /dev/null +++ b/influxframework/core/doctype/scheduled_job_type/test_scheduled_job_type.py @@ -0,0 +1,73 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs +from influxframework.tests.utils import InfluxFrameworkTestCase +from influxframework.utils import get_datetime + + +class TestScheduledJobType(InfluxFrameworkTestCase): + def setUp(self): + influxframework.db.rollback() + influxframework.db.truncate("Scheduled Job Type") + sync_jobs() + influxframework.db.commit() + + def test_sync_jobs(self): + all_job = influxframework.get_doc("Scheduled Job Type", dict(method="influxframework.email.queue.flush")) + self.assertEqual(all_job.frequency, "All") + + daily_job = influxframework.get_doc( + "Scheduled Job Type", dict(method="influxframework.email.queue.set_expiry_for_email_queue") + ) + self.assertEqual(daily_job.frequency, "Daily") + + # check if cron jobs are synced + cron_job = influxframework.get_doc("Scheduled Job Type", dict(method="influxframework.oauth.delete_oauth2_data")) + self.assertEqual(cron_job.frequency, "Cron") + self.assertEqual(cron_job.cron_format, "0/15 * * * *") + + # check if jobs are synced after change in hooks + updated_scheduler_events = {"hourly": ["influxframework.email.queue.flush"]} + sync_jobs(updated_scheduler_events) + updated_scheduled_job = influxframework.get_doc( + "Scheduled Job Type", {"method": "influxframework.email.queue.flush"} + ) + self.assertEqual(updated_scheduled_job.frequency, "Hourly") + + def test_daily_job(self): + job = influxframework.get_doc( + "Scheduled Job Type", dict(method="influxframework.email.queue.set_expiry_for_email_queue") + ) + job.db_set("last_execution", "2019-01-01 00:00:00") + self.assertTrue(job.is_event_due(get_datetime("2019-01-02 00:00:06"))) + self.assertFalse(job.is_event_due(get_datetime("2019-01-01 00:00:06"))) + self.assertFalse(job.is_event_due(get_datetime("2019-01-01 23:59:59"))) + + def test_weekly_job(self): + job = influxframework.get_doc( + "Scheduled Job Type", + dict(method="influxframework.social.doctype.energy_point_log.energy_point_log.send_weekly_summary"), + ) + job.db_set("last_execution", "2019-01-01 00:00:00") + self.assertTrue(job.is_event_due(get_datetime("2019-01-06 00:00:01"))) + self.assertFalse(job.is_event_due(get_datetime("2019-01-02 00:00:06"))) + self.assertFalse(job.is_event_due(get_datetime("2019-01-05 23:59:59"))) + + def test_monthly_job(self): + job = influxframework.get_doc( + "Scheduled Job Type", + dict(method="influxframework.email.doctype.auto_email_report.auto_email_report.send_monthly"), + ) + job.db_set("last_execution", "2019-01-01 00:00:00") + self.assertTrue(job.is_event_due(get_datetime("2019-02-01 00:00:01"))) + self.assertFalse(job.is_event_due(get_datetime("2019-01-15 00:00:06"))) + self.assertFalse(job.is_event_due(get_datetime("2019-01-31 23:59:59"))) + + def test_cron_job(self): + # runs every 15 mins + job = influxframework.get_doc("Scheduled Job Type", dict(method="influxframework.oauth.delete_oauth2_data")) + job.db_set("last_execution", "2019-01-01 00:00:00") + self.assertTrue(job.is_event_due(get_datetime("2019-01-01 00:15:01"))) + self.assertFalse(job.is_event_due(get_datetime("2019-01-01 00:05:06"))) + self.assertFalse(job.is_event_due(get_datetime("2019-01-01 00:14:59"))) diff --git a/influxframework/core/doctype/server_script/__init__.py b/influxframework/core/doctype/server_script/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/server_script/server_script.js b/influxframework/core/doctype/server_script/server_script.js new file mode 100644 index 0000000..61c9465 --- /dev/null +++ b/influxframework/core/doctype/server_script/server_script.js @@ -0,0 +1,80 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Server Script", { + setup: function (frm) { + frm.trigger("setup_help"); + }, + refresh: function (frm) { + if (frm.doc.script_type != "Scheduler Event") { + frm.dashboard.hide(); + } + + if (!frm.is_new()) { + frm.add_custom_button(__("Compare Versions"), () => { + new influxframework.ui.DiffView("Server Script", "script", frm.doc.name); + }); + } + + frm.call("get_autocompletion_items") + .then((r) => r.message) + .then((items) => { + frm.set_df_property("script", "autocompletions", items); + }); + }, + + setup_help(frm) { + frm.get_field("help_html").html(` +

    DocType Event

    +

    Add logic for standard doctype events like Before Insert, After Submit, etc.

    +
    +	
    +# set property
    +if "test" in doc.description:
    +	doc.status = 'Closed'
    +
    +
    +# validate
    +if "validate" in doc.description:
    +	raise influxframework.ValidationError
    +
    +# auto create another document
    +if doc.allocated_to:
    +	influxframework.get_doc(dict(
    +		doctype = 'ToDo'
    +		owner = doc.allocated_to,
    +		description = doc.subject
    +	)).insert()
    +
    +
    + +
    + +

    API Call

    +

    Respond to /api/method/<method-name> calls, just like whitelisted methods

    +
    
    +# respond to API
    +
    +if influxframework.form_dict.message == "ping":
    +	influxframework.response['message'] = "pong"
    +else:
    +	influxframework.response['message'] = "ok"
    +
    + +
    + +

    Permission Query

    +

    Add conditions to the where clause of list queries.

    +
    
    +# generate dynamic conditions and set it in the conditions variable
    +tenant_id = influxframework.db.get_value(...)
    +conditions = 'tenant_id = {}'.format(tenant_id)
    +
    +# resulting select query
    +select name from \`tabPerson\`
    +where tenant_id = 2
    +order by creation desc
    +
    +`); + }, +}); diff --git a/influxframework/core/doctype/server_script/server_script.json b/influxframework/core/doctype/server_script/server_script.json new file mode 100644 index 0000000..5446cc1 --- /dev/null +++ b/influxframework/core/doctype/server_script/server_script.json @@ -0,0 +1,138 @@ +{ + "actions": [], + "autoname": "Prompt", + "creation": "2019-09-30 11:56:57.943241", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "script_type", + "reference_doctype", + "event_frequency", + "doctype_event", + "api_method", + "allow_guest", + "column_break_3", + "module", + "disabled", + "section_break_8", + "script", + "help_section", + "help_html" + ], + "fields": [ + { + "fieldname": "script_type", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Script Type", + "options": "DocType Event\nScheduler Event\nPermission Query\nAPI", + "reqd": 1 + }, + { + "fieldname": "script", + "fieldtype": "Code", + "label": "Script", + "options": "Python", + "reqd": 1 + }, + { + "depends_on": "eval:['DocType Event', 'Permission Query'].includes(doc.script_type)", + "fieldname": "reference_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Reference Document Type", + "options": "DocType" + }, + { + "depends_on": "eval:doc.script_type==='DocType Event'", + "fieldname": "doctype_event", + "fieldtype": "Select", + "label": "DocType Event", + "options": "Before Insert\nBefore Validate\nBefore Save\nAfter Insert\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)\nOn Payment Authorization" + }, + { + "depends_on": "eval:doc.script_type==='API'", + "fieldname": "api_method", + "fieldtype": "Data", + "label": "API Method" + }, + { + "default": "0", + "depends_on": "eval:doc.script_type==='API'", + "fieldname": "allow_guest", + "fieldtype": "Check", + "label": "Allow Guest" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_8", + "fieldtype": "Section Break" + }, + { + "fieldname": "help_section", + "fieldtype": "Section Break", + "label": "Help" + }, + { + "fieldname": "help_html", + "fieldtype": "HTML" + }, + { + "depends_on": "eval:doc.script_type == \"Scheduler Event\"", + "fieldname": "event_frequency", + "fieldtype": "Select", + "label": "Event Frequency", + "mandatory_depends_on": "eval:doc.script_type == \"Scheduler Event\"", + "options": "All\nHourly\nDaily\nWeekly\nMonthly\nYearly\nHourly Long\nDaily Long\nWeekly Long\nMonthly Long" + }, + { + "fieldname": "module", + "fieldtype": "Link", + "label": "Module (for export)", + "options": "Module Def" + } + ], + "index_web_pages_for_search": 1, + "links": [ + { + "link_doctype": "Scheduled Job Type", + "link_fieldname": "server_script" + } + ], + "modified": "2022-06-13 06:04:20.937969", + "modified_by": "Administrator", + "module": "Core", + "name": "Server Script", + "naming_rule": "Set by user", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Script Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/server_script/server_script.py b/influxframework/core/doctype/server_script/server_script.py new file mode 100644 index 0000000..9f73059 --- /dev/null +++ b/influxframework/core/doctype/server_script/server_script.py @@ -0,0 +1,204 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +from types import FunctionType, MethodType, ModuleType + +import influxframework +from influxframework import _ +from influxframework.model.document import Document +from influxframework.utils.safe_exec import NamespaceDict, get_safe_globals, safe_exec + + +class ServerScript(Document): + def validate(self): + influxframework.only_for("Script Manager", True) + self.sync_scheduled_jobs() + self.clear_scheduled_events() + self.check_if_compilable_in_restricted_context() + + def on_update(self): + influxframework.cache().delete_value("server_script_map") + self.sync_scheduler_events() + + def on_trash(self): + if self.script_type == "Scheduler Event": + for job in self.scheduled_jobs: + influxframework.delete_doc("Scheduled Job Type", job.name) + + def get_code_fields(self): + return {"script": "py"} + + @property + def scheduled_jobs(self) -> list[dict[str, str]]: + return influxframework.get_all( + "Scheduled Job Type", + filters={"server_script": self.name}, + fields=["name", "stopped"], + ) + + def sync_scheduled_jobs(self): + """Sync Scheduled Job Type statuses if Server Script's disabled status is changed""" + if self.script_type != "Scheduler Event" or not self.has_value_changed("disabled"): + return + + for scheduled_job in self.scheduled_jobs: + if bool(scheduled_job.stopped) != bool(self.disabled): + job = influxframework.get_doc("Scheduled Job Type", scheduled_job.name) + job.stopped = self.disabled + job.save() + + def sync_scheduler_events(self): + """Create or update Scheduled Job Type documents for Scheduler Event Server Scripts""" + if not self.disabled and self.event_frequency and self.script_type == "Scheduler Event": + setup_scheduler_events(script_name=self.name, frequency=self.event_frequency) + + def clear_scheduled_events(self): + """Deletes existing scheduled jobs by Server Script if self.event_frequency has changed""" + if self.script_type == "Scheduler Event" and self.has_value_changed("event_frequency"): + for scheduled_job in self.scheduled_jobs: + influxframework.delete_doc("Scheduled Job Type", scheduled_job.name) + + def check_if_compilable_in_restricted_context(self): + """Check compilation errors and send them back as warnings.""" + from RestrictedPython import compile_restricted + + try: + compile_restricted(self.script) + except Exception as e: + influxframework.msgprint(str(e), title=_("Compilation warning")) + + def execute_method(self) -> dict: + """Specific to API endpoint Server Scripts + + Raises: + influxframework.DoesNotExistError: If self.script_type is not API + influxframework.PermissionError: If self.allow_guest is unset for API accessed by Guest user + + Returns: + dict: Evaluates self.script with influxframework.utils.safe_exec.safe_exec and returns the flags set in it's safe globals + """ + # wrong report type! + if self.script_type != "API": + raise influxframework.DoesNotExistError + + # validate if guest is allowed + if influxframework.session.user == "Guest" and not self.allow_guest: + raise influxframework.PermissionError + + # output can be stored in flags + _globals, _locals = safe_exec(self.script) + return _globals.influxframework.flags + + def execute_doc(self, doc: Document): + """Specific to Document Event triggered Server Scripts + + Args: + doc (Document): Executes script with for a certain document's events + """ + safe_exec(self.script, _locals={"doc": doc}, restrict_commit_rollback=True) + + def execute_scheduled_method(self): + """Specific to Scheduled Jobs via Server Scripts + + Raises: + influxframework.DoesNotExistError: If script type is not a scheduler event + """ + if self.script_type != "Scheduler Event": + raise influxframework.DoesNotExistError + + safe_exec(self.script) + + def get_permission_query_conditions(self, user: str) -> list[str]: + """Specific to Permission Query Server Scripts + + Args: + user (str): Takes user email to execute script and return list of conditions + + Returns: + list: Returns list of conditions defined by rules in self.script + """ + locals = {"user": user, "conditions": ""} + safe_exec(self.script, None, locals) + if locals["conditions"]: + return locals["conditions"] + + @influxframework.whitelist() + def get_autocompletion_items(self): + """Generates a list of a autocompletion strings from the context dict + that is used while executing a Server Script. + + Returns: + list: Returns list of autocompletion items. + For e.g., ["influxframework.utils.cint", "influxframework.get_all", ...] + """ + + def get_keys(obj): + out = [] + for key in obj: + if key.startswith("_"): + continue + value = obj[key] + if isinstance(value, (NamespaceDict, dict)) and value: + if key == "form_dict": + out.append(["form_dict", 7]) + continue + for subkey, score in get_keys(value): + fullkey = f"{key}.{subkey}" + out.append([fullkey, score]) + else: + if isinstance(value, type) and issubclass(value, Exception): + score = 0 + elif isinstance(value, ModuleType): + score = 10 + elif isinstance(value, (FunctionType, MethodType)): + score = 9 + elif isinstance(value, type): + score = 8 + elif isinstance(value, dict): + score = 7 + else: + score = 6 + out.append([key, score]) + return out + + items = influxframework.cache().get_value("server_script_autocompletion_items") + if not items: + items = get_keys(get_safe_globals()) + items = [{"value": d[0], "score": d[1]} for d in items] + influxframework.cache().set_value("server_script_autocompletion_items", items) + return items + + +@influxframework.whitelist() +def setup_scheduler_events(script_name, frequency): + """Creates or Updates Scheduled Job Type documents based on the specified script name and frequency + + Args: + script_name (str): Name of the Server Script document + frequency (str): Event label compatible with the InfluxFramework scheduler + """ + method = influxframework.scrub(f"{script_name}-{frequency}") + scheduled_script = influxframework.db.get_value("Scheduled Job Type", {"method": method}) + + if not scheduled_script: + influxframework.get_doc( + { + "doctype": "Scheduled Job Type", + "method": method, + "frequency": frequency, + "server_script": script_name, + } + ).insert() + + influxframework.msgprint(_("Enabled scheduled execution for script {0}").format(script_name)) + + else: + doc = influxframework.get_doc("Scheduled Job Type", scheduled_script) + + if doc.frequency == frequency: + return + + doc.frequency = frequency + doc.save() + + influxframework.msgprint(_("Scheduled execution for script {0} has updated").format(script_name)) diff --git a/influxframework/core/doctype/server_script/server_script_utils.py b/influxframework/core/doctype/server_script/server_script_utils.py new file mode 100644 index 0000000..a9ca706 --- /dev/null +++ b/influxframework/core/doctype/server_script/server_script_utils.py @@ -0,0 +1,78 @@ +import influxframework + +# this is a separate file since it is imported in influxframework.model.document +# to avoid circular imports + +EVENT_MAP = { + "before_insert": "Before Insert", + "after_insert": "After Insert", + "before_validate": "Before Validate", + "validate": "Before Save", + "on_update": "After Save", + "before_submit": "Before Submit", + "on_submit": "After Submit", + "before_cancel": "Before Cancel", + "on_cancel": "After Cancel", + "on_trash": "Before Delete", + "after_delete": "After Delete", + "before_update_after_submit": "Before Save (Submitted Document)", + "on_update_after_submit": "After Save (Submitted Document)", + "on_payment_authorized": "On Payment Authorization", +} + + +def run_server_script_for_doc_event(doc, event): + # run document event method + if not event in EVENT_MAP: + return + + if influxframework.flags.in_install: + return + + if influxframework.flags.in_migrate: + return + + scripts = get_server_script_map().get(doc.doctype, {}).get(EVENT_MAP[event], None) + if scripts: + # run all scripts for this doctype + event + for script_name in scripts: + influxframework.get_doc("Server Script", script_name).execute_doc(doc) + + +def get_server_script_map(): + # fetch cached server script methods + # { + # '[doctype]': { + # 'Before Insert': ['[server script 1]', '[server script 2]'] + # }, + # '_api': { + # '[path]': '[server script]' + # }, + # 'permission_query': { + # 'DocType': '[server script]' + # } + # } + if influxframework.flags.in_patch and not influxframework.db.table_exists("Server Script"): + return {} + + script_map = influxframework.cache().get_value("server_script_map") + if script_map is None: + script_map = {"permission_query": {}} + enabled_server_scripts = influxframework.get_all( + "Server Script", + fields=("name", "reference_doctype", "doctype_event", "api_method", "script_type"), + filters={"disabled": 0}, + ) + for script in enabled_server_scripts: + if script.script_type == "DocType Event": + script_map.setdefault(script.reference_doctype, {}).setdefault( + script.doctype_event, [] + ).append(script.name) + elif script.script_type == "Permission Query": + script_map["permission_query"][script.reference_doctype] = script.name + else: + script_map.setdefault("_api", {})[script.api_method] = script.name + + influxframework.cache().set_value("server_script_map", script_map) + + return script_map diff --git a/influxframework/core/doctype/server_script/test_server_script.py b/influxframework/core/doctype/server_script/test_server_script.py new file mode 100644 index 0000000..a9b488c --- /dev/null +++ b/influxframework/core/doctype/server_script/test_server_script.py @@ -0,0 +1,213 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +import requests + +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase +from influxframework.utils import get_site_url + +scripts = [ + dict( + name="test_todo", + script_type="DocType Event", + doctype_event="Before Insert", + reference_doctype="ToDo", + script=""" +if "test" in doc.description: + doc.status = 'Closed' +""", + ), + dict( + name="test_todo_validate", + script_type="DocType Event", + doctype_event="Before Insert", + reference_doctype="ToDo", + script=""" +if "validate" in doc.description: + raise influxframework.ValidationError +""", + ), + dict( + name="test_api", + script_type="API", + api_method="test_server_script", + allow_guest=1, + script=""" +influxframework.response['message'] = 'hello' +""", + ), + dict( + name="test_return_value", + script_type="API", + api_method="test_return_value", + allow_guest=1, + script=""" +influxframework.flags = 'hello' +""", + ), + dict( + name="test_permission_query", + script_type="Permission Query", + reference_doctype="ToDo", + script=""" +conditions = '1 = 1' +""", + ), + dict( + name="test_invalid_namespace_method", + script_type="DocType Event", + doctype_event="Before Insert", + reference_doctype="Note", + script=""" +influxframework.method_that_doesnt_exist("do some magic") +""", + ), + dict( + name="test_todo_commit", + script_type="DocType Event", + doctype_event="Before Save", + reference_doctype="ToDo", + disabled=1, + script=""" +influxframework.db.commit() +""", + ), + dict( + name="test_add_index", + script_type="DocType Event", + doctype_event="Before Save", + reference_doctype="ToDo", + disabled=1, + script=""" +influxframework.db.add_index("Todo", ["color", "date"]) +""", + ), +] + + +class TestServerScript(InfluxFrameworkTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + influxframework.db.truncate("Server Script") + influxframework.get_doc("User", "Administrator").add_roles("Script Manager") + for script in scripts: + script_doc = influxframework.get_doc(doctype="Server Script") + script_doc.update(script) + script_doc.insert() + + influxframework.db.commit() + + @classmethod + def tearDownClass(cls): + influxframework.db.commit() + influxframework.db.truncate("Server Script") + influxframework.cache().delete_value("server_script_map") + + def setUp(self): + influxframework.cache().delete_value("server_script_map") + + def test_doctype_event(self): + todo = influxframework.get_doc(dict(doctype="ToDo", description="hello")).insert() + self.assertEqual(todo.status, "Open") + + todo = influxframework.get_doc(dict(doctype="ToDo", description="test todo")).insert() + self.assertEqual(todo.status, "Closed") + + self.assertRaises( + influxframework.ValidationError, influxframework.get_doc(dict(doctype="ToDo", description="validate me")).insert + ) + + def test_api(self): + response = requests.post(get_site_url(influxframework.local.site) + "/api/method/test_server_script") + self.assertEqual(response.status_code, 200) + self.assertEqual("hello", response.json()["message"]) + + def test_api_return(self): + self.assertEqual(influxframework.get_doc("Server Script", "test_return_value").execute_method(), "hello") + + def test_permission_query(self): + if influxframework.conf.db_type == "mariadb": + self.assertTrue("where (1 = 1)" in influxframework.db.get_list("ToDo", run=False)) + else: + self.assertTrue("where (1 = '1')" in influxframework.db.get_list("ToDo", run=False)) + self.assertTrue(isinstance(influxframework.db.get_list("ToDo"), list)) + + def test_attribute_error(self): + """Raise AttributeError if method not found in Namespace""" + note = influxframework.get_doc({"doctype": "Note", "title": "Test Note: Server Script"}) + self.assertRaises(AttributeError, note.insert) + + def test_syntax_validation(self): + server_script = scripts[0] + server_script["script"] = "js || code.?" + + with self.assertRaises(influxframework.ValidationError) as se: + influxframework.get_doc(doctype="Server Script", **server_script).insert() + + self.assertTrue( + "invalid python code" in str(se.exception).lower(), msg="Python code validation not working" + ) + + def test_commit_in_doctype_event(self): + server_script = influxframework.get_doc("Server Script", "test_todo_commit") + server_script.disabled = 0 + server_script.save() + + self.assertRaises( + AttributeError, influxframework.get_doc(dict(doctype="ToDo", description="test me")).insert + ) + + server_script.disabled = 1 + server_script.save() + + def test_add_index_in_doctype_event(self): + server_script = influxframework.get_doc("Server Script", "test_add_index") + server_script.disabled = 0 + server_script.save() + + self.assertRaises( + AttributeError, influxframework.get_doc(dict(doctype="ToDo", description="test me")).insert + ) + + server_script.disabled = 1 + server_script.save() + + def test_restricted_qb(self): + todo = influxframework.get_doc(doctype="ToDo", description="QbScriptTestNote") + todo.insert() + + script = influxframework.get_doc( + doctype="Server Script", + name="test_qb_restrictions", + script_type="API", + api_method="test_qb_restrictions", + allow_guest=1, + # whitelisted update + script=f""" +influxframework.db.set_value("ToDo", "{todo.name}", "description", "safe") +""", + ) + script.insert() + script.execute_method() + + todo.reload() + self.assertEqual(todo.description, "safe") + + # unsafe update + script.script = f""" +todo = influxframework.qb.DocType("ToDo") +influxframework.qb.update(todo).set(todo.description, "unsafe").where(todo.name == "{todo.name}").run() +""" + script.save() + self.assertRaises(influxframework.PermissionError, script.execute_method) + todo.reload() + self.assertEqual(todo.description, "safe") + + # safe select + script.script = f""" +todo = influxframework.qb.DocType("ToDo") +influxframework.qb.from_(todo).select(todo.name).where(todo.name == "{todo.name}").run() +""" + script.save() + script.execute_method() diff --git a/influxframework/core/doctype/session_default/__init__.py b/influxframework/core/doctype/session_default/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/session_default/session_default.json b/influxframework/core/doctype/session_default/session_default.json new file mode 100644 index 0000000..d382e12 --- /dev/null +++ b/influxframework/core/doctype/session_default/session_default.json @@ -0,0 +1,29 @@ +{ + "creation": "2019-07-17 16:21:33.546379", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "ref_doctype" + ], + "fields": [ + { + "fieldname": "ref_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType" + } + ], + "istable": 1, + "modified": "2019-07-21 13:22:25.752553", + "modified_by": "Administrator", + "module": "Core", + "name": "Session Default", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/session_default/session_default.py b/influxframework/core/doctype/session_default/session_default.py new file mode 100644 index 0000000..bb808a6 --- /dev/null +++ b/influxframework/core/doctype/session_default/session_default.py @@ -0,0 +1,9 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class SessionDefault(Document): + pass diff --git a/influxframework/core/doctype/session_default_settings/__init__.py b/influxframework/core/doctype/session_default_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/session_default_settings/session_default_settings.js b/influxframework/core/doctype/session_default_settings/session_default_settings.js new file mode 100644 index 0000000..3b8238b --- /dev/null +++ b/influxframework/core/doctype/session_default_settings/session_default_settings.js @@ -0,0 +1,15 @@ +// Copyright (c) 2015, InfluxFramework LLC +// MIT License. See license.txt + +influxframework.ui.form.on("Session Default Settings", { + refresh: function (frm) { + frm.set_query("ref_doctype", "session_defaults", function () { + return { + filters: { + issingle: 0, + istable: 0, + }, + }; + }); + }, +}); diff --git a/influxframework/core/doctype/session_default_settings/session_default_settings.json b/influxframework/core/doctype/session_default_settings/session_default_settings.json new file mode 100644 index 0000000..3eeb3f4 --- /dev/null +++ b/influxframework/core/doctype/session_default_settings/session_default_settings.json @@ -0,0 +1,39 @@ +{ + "creation": "2019-07-17 16:22:31.300991", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "session_defaults" + ], + "fields": [ + { + "fieldname": "session_defaults", + "fieldtype": "Table", + "label": "Session Defaults", + "options": "Session Default" + } + ], + "issingle": 1, + "modified": "2019-07-19 16:04:33.971089", + "modified_by": "Administrator", + "module": "Core", + "name": "Session Default Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/session_default_settings/session_default_settings.py b/influxframework/core/doctype/session_default_settings/session_default_settings.py new file mode 100644 index 0000000..96552f9 --- /dev/null +++ b/influxframework/core/doctype/session_default_settings/session_default_settings.py @@ -0,0 +1,48 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +import influxframework +from influxframework import _ +from influxframework.model.document import Document + + +class SessionDefaultSettings(Document): + pass + + +@influxframework.whitelist() +def get_session_default_values(): + settings = influxframework.get_single("Session Default Settings") + fields = [] + for default_values in settings.session_defaults: + reference_doctype = influxframework.scrub(default_values.ref_doctype) + fields.append( + { + "fieldname": reference_doctype, + "fieldtype": "Link", + "options": default_values.ref_doctype, + "label": _("Default {0}").format(_(default_values.ref_doctype)), + "default": influxframework.defaults.get_user_default(reference_doctype), + } + ) + return json.dumps(fields) + + +@influxframework.whitelist() +def set_session_default_values(default_values): + default_values = influxframework.parse_json(default_values) + for entry in default_values: + try: + influxframework.defaults.set_user_default(entry, default_values.get(entry)) + except Exception: + return + return "success" + + +# called on hook 'on_logout' to clear defaults for the session +def clear_session_defaults(): + settings = influxframework.get_single("Session Default Settings").session_defaults + for entry in settings: + influxframework.defaults.clear_user_default(influxframework.scrub(entry.ref_doctype)) diff --git a/influxframework/core/doctype/session_default_settings/test_session_default_settings.py b/influxframework/core/doctype/session_default_settings/test_session_default_settings.py new file mode 100644 index 0000000..2a03a76 --- /dev/null +++ b/influxframework/core/doctype/session_default_settings/test_session_default_settings.py @@ -0,0 +1,31 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.core.doctype.session_default_settings.session_default_settings import ( + clear_session_defaults, + set_session_default_values, +) +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestSessionDefaultSettings(InfluxFrameworkTestCase): + def test_set_session_default_settings(self): + influxframework.set_user("Administrator") + settings = influxframework.get_single("Session Default Settings") + settings.session_defaults = [] + settings.append("session_defaults", {"ref_doctype": "Role"}) + settings.save() + + set_session_default_values({"role": "Website Manager"}) + + todo = influxframework.get_doc( + dict(doctype="ToDo", description="test session defaults set", assigned_by="Administrator") + ).insert() + self.assertEqual(todo.role, "Website Manager") + + def test_clear_session_defaults(self): + clear_session_defaults() + todo = influxframework.get_doc( + dict(doctype="ToDo", description="test session defaults cleared", assigned_by="Administrator") + ).insert() + self.assertNotEqual(todo.role, "Website Manager") diff --git a/influxframework/core/doctype/sms_parameter/README.md b/influxframework/core/doctype/sms_parameter/README.md new file mode 100644 index 0000000..5935a39 --- /dev/null +++ b/influxframework/core/doctype/sms_parameter/README.md @@ -0,0 +1 @@ +SMS query parameter for SMS Settings. \ No newline at end of file diff --git a/influxframework/core/doctype/sms_parameter/__init__.py b/influxframework/core/doctype/sms_parameter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/sms_parameter/sms_parameter.json b/influxframework/core/doctype/sms_parameter/sms_parameter.json new file mode 100644 index 0000000..43b93ed --- /dev/null +++ b/influxframework/core/doctype/sms_parameter/sms_parameter.json @@ -0,0 +1,128 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2013-02-22 01:27:58", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "editable_grid": 1, + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "parameter", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Parameter", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "150px", + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0, + "width": "150px" + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "value", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Value", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "150px", + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0, + "width": "150px" + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "header", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Header", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 1, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "max_attachments": 0, + "modified": "2017-10-13 16:48:00.518463", + "modified_by": "Administrator", + "module": "Core", + "name": "SMS Parameter", + "owner": "Administrator", + "permissions": [], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "track_changes": 0, + "track_seen": 0 +} \ No newline at end of file diff --git a/influxframework/core/doctype/sms_parameter/sms_parameter.py b/influxframework/core/doctype/sms_parameter/sms_parameter.py new file mode 100644 index 0000000..f05804e --- /dev/null +++ b/influxframework/core/doctype/sms_parameter/sms_parameter.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +from influxframework.model.document import Document + + +class SMSParameter(Document): + pass diff --git a/influxframework/core/doctype/sms_settings/README.md b/influxframework/core/doctype/sms_settings/README.md new file mode 100644 index 0000000..4fb4980 --- /dev/null +++ b/influxframework/core/doctype/sms_settings/README.md @@ -0,0 +1 @@ +Settings for automatically sending SMS from the system. \ No newline at end of file diff --git a/influxframework/core/doctype/sms_settings/__init__.py b/influxframework/core/doctype/sms_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/sms_settings/sms_settings.js b/influxframework/core/doctype/sms_settings/sms_settings.js new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/sms_settings/sms_settings.json b/influxframework/core/doctype/sms_settings/sms_settings.json new file mode 100644 index 0000000..6450cfb --- /dev/null +++ b/influxframework/core/doctype/sms_settings/sms_settings.json @@ -0,0 +1,80 @@ +{ + "actions": [], + "allow_copy": 1, + "creation": "2013-01-10 16:34:24", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "sms_gateway_url", + "message_parameter", + "receiver_parameter", + "static_parameters_section", + "parameters", + "use_post" + ], + "fields": [ + { + "description": "Eg. smsgateway.com/api/send_sms.cgi", + "fieldname": "sms_gateway_url", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "SMS Gateway URL", + "reqd": 1 + }, + { + "description": "Enter url parameter for message", + "fieldname": "message_parameter", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Message Parameter", + "reqd": 1 + }, + { + "description": "Enter url parameter for receiver nos", + "fieldname": "receiver_parameter", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Receiver Parameter", + "reqd": 1 + }, + { + "fieldname": "static_parameters_section", + "fieldtype": "Column Break", + "width": "50%" + }, + { + "description": "Enter static url parameters here (Eg. sender=InfluxERP, username=InfluxERP, password=1234 etc.)", + "fieldname": "parameters", + "fieldtype": "Table", + "label": "Static Parameters", + "options": "SMS Parameter" + }, + { + "default": "0", + "fieldname": "use_post", + "fieldtype": "Check", + "label": "Use POST" + } + ], + "icon": "fa fa-cog", + "idx": 1, + "issingle": 1, + "links": [], + "modified": "2021-09-21 19:45:26.809793", + "modified_by": "Administrator", + "module": "Core", + "name": "SMS Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} diff --git a/influxframework/core/doctype/sms_settings/sms_settings.py b/influxframework/core/doctype/sms_settings/sms_settings.py new file mode 100644 index 0000000..7725898 --- /dev/null +++ b/influxframework/core/doctype/sms_settings/sms_settings.py @@ -0,0 +1,143 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework import _, msgprint, throw +from influxframework.model.document import Document +from influxframework.utils import nowdate + + +class SMSSettings(Document): + pass + + +def validate_receiver_nos(receiver_list): + validated_receiver_list = [] + for d in receiver_list: + if not d: + break + + # remove invalid character + for x in [" ", "-", "(", ")"]: + d = d.replace(x, "") + + validated_receiver_list.append(d) + + if not validated_receiver_list: + throw(_("Please enter valid mobile nos")) + + return validated_receiver_list + + +@influxframework.whitelist() +def get_contact_number(contact_name, ref_doctype, ref_name): + "returns mobile number of the contact" + number = influxframework.db.sql( + """select mobile_no, phone from tabContact + where name=%s + and exists( + select name from `tabDynamic Link` where link_doctype=%s and link_name=%s + ) + """, + (contact_name, ref_doctype, ref_name), + ) + + return number and (number[0][0] or number[0][1]) or "" + + +@influxframework.whitelist() +def send_sms(receiver_list, msg, sender_name="", success_msg=True): + + import json + + if isinstance(receiver_list, str): + receiver_list = json.loads(receiver_list) + if not isinstance(receiver_list, list): + receiver_list = [receiver_list] + + receiver_list = validate_receiver_nos(receiver_list) + + arg = { + "receiver_list": receiver_list, + "message": influxframework.safe_decode(msg).encode("utf-8"), + "success_msg": success_msg, + } + + if influxframework.db.get_single_value("SMS Settings", "sms_gateway_url"): + send_via_gateway(arg) + else: + msgprint(_("Please Update SMS Settings")) + + +def send_via_gateway(arg): + ss = influxframework.get_doc("SMS Settings", "SMS Settings") + headers = get_headers(ss) + use_json = headers.get("Content-Type") == "application/json" + + message = influxframework.safe_decode(arg.get("message")) + args = {ss.message_parameter: message} + for d in ss.get("parameters"): + if not d.header: + args[d.parameter] = d.value + + success_list = [] + for d in arg.get("receiver_list"): + args[ss.receiver_parameter] = d + status = send_request(ss.sms_gateway_url, args, headers, ss.use_post, use_json) + + if 200 <= status < 300: + success_list.append(d) + + if len(success_list) > 0: + args.update(arg) + create_sms_log(args, success_list) + if arg.get("success_msg"): + influxframework.msgprint(_("SMS sent to following numbers: {0}").format("\n" + "\n".join(success_list))) + + +def get_headers(sms_settings=None): + if not sms_settings: + sms_settings = influxframework.get_doc("SMS Settings", "SMS Settings") + + headers = {"Accept": "text/plain, text/html, */*"} + for d in sms_settings.get("parameters"): + if d.header == 1: + headers.update({d.parameter: d.value}) + + return headers + + +def send_request(gateway_url, params, headers=None, use_post=False, use_json=False): + import requests + + if not headers: + headers = get_headers() + kwargs = {"headers": headers} + + if use_json: + kwargs["json"] = params + elif use_post: + kwargs["data"] = params + else: + kwargs["params"] = params + + if use_post: + response = requests.post(gateway_url, **kwargs) + else: + response = requests.get(gateway_url, **kwargs) + response.raise_for_status() + return response.status_code + + +# Create SMS Log +# ========================================================= +def create_sms_log(args, sent_to): + sl = influxframework.new_doc("SMS Log") + sl.sent_on = nowdate() + sl.message = args["message"].decode("utf-8") + sl.no_of_requested_sms = len(args["receiver_list"]) + sl.requested_numbers = "\n".join(args["receiver_list"]) + sl.no_of_sent_sms = len(sent_to) + sl.sent_to = "\n".join(sent_to) + sl.flags.ignore_permissions = True + sl.save() diff --git a/influxframework/core/doctype/sms_settings/test_sms_settings.py b/influxframework/core/doctype/sms_settings/test_sms_settings.py new file mode 100644 index 0000000..246f9e3 --- /dev/null +++ b/influxframework/core/doctype/sms_settings/test_sms_settings.py @@ -0,0 +1,7 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestSMSSettings(InfluxFrameworkTestCase): + pass diff --git a/influxframework/core/doctype/success_action/__init__.py b/influxframework/core/doctype/success_action/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/success_action/success_action.js b/influxframework/core/doctype/success_action/success_action.js new file mode 100644 index 0000000..6f170a1 --- /dev/null +++ b/influxframework/core/doctype/success_action/success_action.js @@ -0,0 +1,60 @@ +// Copyright (c) 2018, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Success Action", { + on_load: (frm) => { + if (!frm.action_multicheck) { + frm.trigger("set_next_action_multicheck"); + } + }, + refresh: (frm) => { + if (!frm.action_multicheck) { + frm.trigger("set_next_action_multicheck"); + } + }, + validate: (frm) => { + const checked_actions = frm.action_multicheck.get_checked_options(); + if (checked_actions.length < 2) { + influxframework.msgprint(__("Select atleast 2 actions")); + } else { + return true; + } + }, + before_save: (frm) => { + const checked_actions = frm.action_multicheck.get_checked_options(); + frm.doc.next_actions = checked_actions.join("\n"); + }, + after_save: (frm) => { + influxframework.boot.success_action.push(frm.doc); + //TODO: update success action cache on record update and delete + }, + set_next_action_multicheck: (frm) => { + const next_actions_wrapper = frm.fields_dict.next_actions_html.$wrapper; + const checked_actions = frm.doc.next_actions ? frm.doc.next_actions.split("\n") : []; + const action_multicheck_options = get_default_next_actions().map((action) => { + return { + label: action.label, + value: action.value, + checked: checked_actions.length ? checked_actions.includes(action.value) : 1, + }; + }); + frm.action_multicheck = influxframework.ui.form.make_control({ + parent: next_actions_wrapper, + df: { + label: "Next Actions", + fieldname: "next_actions_multicheck", + fieldtype: "MultiCheck", + options: action_multicheck_options, + }, + }); + }, +}); + +const get_default_next_actions = () => { + return [ + { label: __("New"), value: "new" }, + { label: __("Print"), value: "print" }, + { label: __("Email"), value: "email" }, + { label: __("View All"), value: "list" }, + ]; +}; diff --git a/influxframework/core/doctype/success_action/success_action.json b/influxframework/core/doctype/success_action/success_action.json new file mode 100644 index 0000000..25c8e79 --- /dev/null +++ b/influxframework/core/doctype/success_action/success_action.json @@ -0,0 +1,259 @@ +{ + "allow_copy": 0, + "allow_events_in_timeline": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "field:ref_doctype", + "beta": 0, + "creation": "2018-04-15 18:07:35.316870", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "ref_doctype", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Reference Document Type", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "Congratulations on first creations", + "fieldname": "first_success_message", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "First Success Message", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "Successfully created", + "fieldname": "message", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Message", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "next_actions_html", + "fieldtype": "HTML", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Next Actions HTML", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "next_actions", + "fieldtype": "Data", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "action_timeout", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Action Timeout (Seconds)", + "default": 7, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 1, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2019-09-05 14:22:27.664645", + "modified_by": "Administrator", + "module": "Core", + "name": "Success Action", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0, + "track_views": 0 +} \ No newline at end of file diff --git a/influxframework/core/doctype/success_action/success_action.py b/influxframework/core/doctype/success_action/success_action.py new file mode 100644 index 0000000..9354445 --- /dev/null +++ b/influxframework/core/doctype/success_action/success_action.py @@ -0,0 +1,8 @@ +# Copyright (c) 2018, InfluxFramework LLC +# License: MIT. See LICENSE + +from influxframework.model.document import Document + + +class SuccessAction(Document): + pass diff --git a/influxframework/core/doctype/system_settings/__init__.py b/influxframework/core/doctype/system_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/system_settings/system_settings.js b/influxframework/core/doctype/system_settings/system_settings.js new file mode 100644 index 0000000..ef18d78 --- /dev/null +++ b/influxframework/core/doctype/system_settings/system_settings.js @@ -0,0 +1,49 @@ +influxframework.ui.form.on("System Settings", { + refresh: function (frm) { + influxframework.call({ + method: "influxframework.core.doctype.system_settings.system_settings.load", + callback: function (data) { + influxframework.all_timezones = data.message.timezones; + frm.set_df_property("time_zone", "options", influxframework.all_timezones); + + $.each(data.message.defaults, function (key, val) { + frm.set_value(key, val, null, true); + influxframework.sys_defaults[key] = val; + }); + if (frm.re_setup_moment) { + influxframework.app.setup_moment(); + delete frm.re_setup_moment; + } + }, + }); + }, + enable_password_policy: function (frm) { + if (frm.doc.enable_password_policy == 0) { + frm.set_value("minimum_password_score", ""); + } else { + frm.set_value("minimum_password_score", "2"); + } + }, + enable_two_factor_auth: function (frm) { + if (frm.doc.enable_two_factor_auth == 0) { + frm.set_value("bypass_2fa_for_retricted_ip_users", 0); + frm.set_value("bypass_restrict_ip_check_if_2fa_enabled", 0); + } + }, + enable_prepared_report_auto_deletion: function (frm) { + if (frm.doc.enable_prepared_report_auto_deletion) { + if (!frm.doc.prepared_report_expiry_period) { + frm.set_value("prepared_report_expiry_period", 7); + } + } + }, + on_update: function (frm) { + if (influxframework.boot.time_zone && influxframework.boot.time_zone.system !== frm.doc.time_zone) { + // Clear cache after saving to refresh the values of boot. + influxframework.ui.toolbar.clear_cache(); + } + }, + first_day_of_the_week(frm) { + frm.re_setup_moment = true; + }, +}); diff --git a/influxframework/core/doctype/system_settings/system_settings.json b/influxframework/core/doctype/system_settings/system_settings.json new file mode 100644 index 0000000..cb2d4b8 --- /dev/null +++ b/influxframework/core/doctype/system_settings/system_settings.json @@ -0,0 +1,560 @@ +{ + "actions": [], + "creation": "2022-01-06 03:18:16.326761", + "doctype": "DocType", + "document_type": "System", + "engine": "InnoDB", + "field_order": [ + "localization", + "app_name", + "country", + "language", + "column_break_3", + "time_zone", + "enable_onboarding", + "setup_complete", + "date_and_number_format", + "date_format", + "time_format", + "number_format", + "column_break_7", + "float_precision", + "currency_precision", + "first_day_of_the_week", + "sec_backup_limit", + "backup_limit", + "encrypt_backup", + "background_workers", + "enable_scheduler", + "dormant_days", + "permissions", + "apply_strict_user_permissions", + "column_break_21", + "allow_guests_to_upload_files", + "security", + "session_expiry", + "session_expiry_mobile", + "document_share_key_expiry", + "column_break_13", + "deny_multiple_sessions", + "allow_login_using_mobile_number", + "allow_login_using_user_name", + "disable_user_pass_login", + "allow_error_traceback", + "strip_exif_metadata_from_uploaded_images", + "allow_older_web_view_links", + "password_settings", + "logout_on_password_reset", + "force_user_to_reset_password", + "reset_password_link_expiry_duration", + "password_reset_limit", + "column_break_31", + "enable_password_policy", + "minimum_password_score", + "brute_force_security", + "allow_consecutive_login_attempts", + "column_break_34", + "allow_login_after_fail", + "two_factor_authentication", + "enable_two_factor_auth", + "bypass_2fa_for_retricted_ip_users", + "bypass_restrict_ip_check_if_2fa_enabled", + "two_factor_method", + "lifespan_qrcode_image", + "otp_issuer_name", + "email", + "email_footer_address", + "email_retry_limit", + "column_break_18", + "disable_standard_email_footer", + "hide_footer_in_auto_email_reports", + "attach_view_link", + "prepared_report_section", + "enable_prepared_report_auto_deletion", + "prepared_report_expiry_period", + "column_break_64", + "max_auto_email_report_per_user", + "system_updates_section", + "disable_system_update_notification", + "disable_change_log_notification" + ], + "fields": [ + { + "fieldname": "localization", + "fieldtype": "Section Break" + }, + { + "fieldname": "country", + "fieldtype": "Link", + "label": "Country", + "options": "Country" + }, + { + "fieldname": "language", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Language", + "options": "Language", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "time_zone", + "fieldtype": "Select", + "label": "Time Zone", + "read_only": 1, + "reqd": 1 + }, + { + "default": "0", + "fieldname": "setup_complete", + "fieldtype": "Check", + "hidden": 1, + "label": "Setup Complete", + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "date_and_number_format", + "fieldtype": "Section Break", + "label": "Date and Number Format" + }, + { + "fieldname": "date_format", + "fieldtype": "Select", + "label": "Date Format", + "options": "yyyy-mm-dd\ndd-mm-yyyy\ndd/mm/yyyy\ndd.mm.yyyy\nmm/dd/yyyy\nmm-dd-yyyy", + "reqd": 1 + }, + { + "default": "HH:mm:ss", + "fieldname": "time_format", + "fieldtype": "Select", + "label": "Time Format", + "options": "HH:mm:ss\nHH:mm", + "reqd": 1 + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "fieldname": "number_format", + "fieldtype": "Select", + "label": "Number Format", + "options": "#,###.##\n#.###,##\n# ###.##\n# ###,##\n#'###.##\n#, ###.##\n#,##,###.##\n#,###.###\n#.###\n#,###", + "reqd": 1 + }, + { + "fieldname": "float_precision", + "fieldtype": "Select", + "label": "Float Precision", + "options": "\n2\n3\n4\n5\n6\n7\n8\n9" + }, + { + "description": "If not set, the currency precision will depend on number format", + "fieldname": "currency_precision", + "fieldtype": "Select", + "label": "Currency Precision", + "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9" + }, + { + "collapsible": 1, + "fieldname": "sec_backup_limit", + "fieldtype": "Section Break", + "label": "Backups" + }, + { + "default": "3", + "description": "Older backups will be automatically deleted", + "fieldname": "backup_limit", + "fieldtype": "Int", + "label": "Number of Backups" + }, + { + "collapsible": 1, + "fieldname": "background_workers", + "fieldtype": "Section Break", + "label": "Background Workers" + }, + { + "default": "0", + "description": "Run scheduled jobs only if checked", + "fieldname": "enable_scheduler", + "fieldtype": "Check", + "hidden": 1, + "label": "Enable Scheduled Jobs" + }, + { + "collapsible": 1, + "fieldname": "permissions", + "fieldtype": "Section Break", + "label": "Permissions" + }, + { + "default": "0", + "description": "If Apply Strict User Permission is checked and User Permission is defined for a DocType for a User, then all the documents where value of the link is blank, will not be shown to that User", + "fieldname": "apply_strict_user_permissions", + "fieldtype": "Check", + "label": "Apply Strict User Permissions" + }, + { + "collapsible": 1, + "fieldname": "security", + "fieldtype": "Section Break", + "label": "Security" + }, + { + "default": "06:00", + "description": "Session Expiry in Hours e.g. 06:00", + "fieldname": "session_expiry", + "fieldtype": "Data", + "label": "Session Expiry" + }, + { + "default": "720:00", + "description": "In Hours", + "fieldname": "session_expiry_mobile", + "fieldtype": "Data", + "label": "Session Expiry Mobile" + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "Note: Multiple sessions will be allowed in case of mobile device", + "fieldname": "deny_multiple_sessions", + "fieldtype": "Check", + "label": "Allow only one session per user" + }, + { + "default": "0", + "description": "User can login using Email id or Mobile number", + "fieldname": "allow_login_using_mobile_number", + "fieldtype": "Check", + "label": "Allow Login using Mobile Number" + }, + { + "default": "0", + "description": "User can login using Email id or User Name", + "fieldname": "allow_login_using_user_name", + "fieldtype": "Check", + "label": "Allow Login using User Name" + }, + { + "default": "1", + "fieldname": "allow_error_traceback", + "fieldtype": "Check", + "label": "Show Full Error and Allow Reporting of Issues to the Developer" + }, + { + "collapsible": 1, + "fieldname": "password_settings", + "fieldtype": "Section Break", + "label": "Password" + }, + { + "description": "In Days", + "fieldname": "force_user_to_reset_password", + "fieldtype": "Int", + "label": "Force User to Reset Password" + }, + { + "fieldname": "column_break_31", + "fieldtype": "Column Break" + }, + { + "default": "1", + "description": "If enabled, the password strength will be enforced based on the Minimum Password Score value. A value of 2 being medium strong and 4 being very strong.", + "fieldname": "enable_password_policy", + "fieldtype": "Check", + "label": "Enable Password Policy" + }, + { + "default": "2", + "depends_on": "eval:doc.enable_password_policy==1", + "fieldname": "minimum_password_score", + "fieldtype": "Select", + "label": "Minimum Password Score", + "options": "2\n3\n4" + }, + { + "collapsible": 1, + "fieldname": "brute_force_security", + "fieldtype": "Section Break", + "label": "Brute Force Security" + }, + { + "fieldname": "allow_consecutive_login_attempts", + "fieldtype": "Int", + "label": "Allow Consecutive Login Attempts " + }, + { + "fieldname": "column_break_34", + "fieldtype": "Column Break" + }, + { + "default": "60", + "description": "In seconds", + "fieldname": "allow_login_after_fail", + "fieldtype": "Int", + "label": "Allow Login After Fail" + }, + { + "collapsible": 1, + "fieldname": "two_factor_authentication", + "fieldtype": "Section Break", + "label": "Two Factor Authentication" + }, + { + "default": "0", + "fieldname": "enable_two_factor_auth", + "fieldtype": "Check", + "label": "Enable Two Factor Auth" + }, + { + "default": "0", + "depends_on": "enable_two_factor_auth", + "description": "If enabled, users who login from Restricted IP Address, won't be prompted for Two Factor Auth", + "fieldname": "bypass_2fa_for_retricted_ip_users", + "fieldtype": "Check", + "label": "Bypass Two Factor Auth for users who login from restricted IP Address" + }, + { + "default": "0", + "depends_on": "enable_two_factor_auth", + "description": "If enabled, all users can login from any IP Address using Two Factor Auth. This can also be set only for specific user(s) in User Page", + "fieldname": "bypass_restrict_ip_check_if_2fa_enabled", + "fieldtype": "Check", + "label": "Bypass restricted IP Address check If Two Factor Auth Enabled" + }, + { + "default": "OTP App", + "description": "Choose authentication method to be used by all users", + "fieldname": "two_factor_method", + "fieldtype": "Select", + "label": "Two Factor Authentication method", + "options": "OTP App\nSMS\nEmail" + }, + { + "depends_on": "eval:doc.two_factor_method == \"OTP App\"", + "description": "Time in seconds to retain QR code image on server. Min:240", + "fieldname": "lifespan_qrcode_image", + "fieldtype": "Int", + "label": "Expiry time of QR Code Image Page" + }, + { + "default": "InfluxFramework Framework", + "depends_on": "enable_two_factor_auth", + "fieldname": "otp_issuer_name", + "fieldtype": "Data", + "label": "OTP Issuer Name" + }, + { + "collapsible": 1, + "fieldname": "email", + "fieldtype": "Section Break", + "label": "Email" + }, + { + "description": "Your organization name and address for the email footer.", + "fieldname": "email_footer_address", + "fieldtype": "Small Text", + "label": "Email Footer Address" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "disable_standard_email_footer", + "fieldtype": "Check", + "label": "Disable Standard Email Footer" + }, + { + "default": "0", + "fieldname": "hide_footer_in_auto_email_reports", + "fieldtype": "Check", + "label": "Hide footer in auto email reports" + }, + { + "fieldname": "column_break_21", + "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "When enabled this will allow guests to upload files to your application, You can enable this if you wish to collect files from user without having them to log in, for example in job applications web form.", + "fieldname": "allow_guests_to_upload_files", + "fieldtype": "Check", + "label": "Allow Guests to Upload Files" + }, + { + "default": "4", + "description": "Will run scheduled jobs only once a day for inactive sites. Default 4 days if set to 0.", + "fieldname": "dormant_days", + "fieldtype": "Int", + "label": "Run Jobs only Daily if Inactive For (Days)" + }, + { + "default": "3", + "description": "Hourly rate limit for generating password reset links", + "fieldname": "password_reset_limit", + "fieldtype": "Int", + "label": "Password Reset Link Generation Limit" + }, + { + "default": "1", + "fieldname": "logout_on_password_reset", + "fieldtype": "Check", + "label": "Logout All Sessions on Password Reset" + }, + { + "default": "0", + "fieldname": "enable_onboarding", + "fieldtype": "Check", + "label": "Enable Onboarding" + }, + { + "default": "1", + "fieldname": "attach_view_link", + "fieldtype": "Check", + "label": "Send document Web View link in email" + }, + { + "default": "30", + "depends_on": "enable_prepared_report_auto_deletion", + "description": "System will auto-delete Prepared Reports permanently after these many days since creation", + "fieldname": "prepared_report_expiry_period", + "fieldtype": "Int", + "label": "Prepared Report Expiry Period (Days)" + }, + { + "default": "1", + "fieldname": "enable_prepared_report_auto_deletion", + "fieldtype": "Check", + "label": "Enable Auto-deletion of Prepared Reports" + }, + { + "collapsible": 1, + "fieldname": "prepared_report_section", + "fieldtype": "Section Break", + "label": "Reports" + }, + { + "default": "InfluxFramework", + "description": "The application name will be used in the Login page.", + "fieldname": "app_name", + "fieldtype": "Data", + "hidden": 1, + "label": "Application Name" + }, + { + "default": "1", + "fieldname": "strip_exif_metadata_from_uploaded_images", + "fieldtype": "Check", + "label": "Strip EXIF tags from uploaded images" + }, + { + "default": "0", + "fieldname": "encrypt_backup", + "fieldtype": "Check", + "label": "Encrypt Backups" + }, + { + "collapsible": 1, + "fieldname": "system_updates_section", + "fieldtype": "Section Break", + "label": "System Updates" + }, + { + "default": "0", + "fieldname": "disable_system_update_notification", + "fieldtype": "Check", + "label": "Disable System Update Notification" + }, + { + "default": "Sunday", + "fieldname": "first_day_of_the_week", + "fieldtype": "Select", + "label": "First Day of the Week", + "options": "Sunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday" + }, + { + "default": "30", + "description": "Number of days after which the document Web View link shared on email will be expired", + "fieldname": "document_share_key_expiry", + "fieldtype": "Int", + "label": "Document Share Key Expiry (in Days)" + }, + { + "default": "0", + "fieldname": "allow_older_web_view_links", + "fieldtype": "Check", + "label": "Allow Older Web View Links (Insecure)" + }, + { + "fieldname": "column_break_64", + "fieldtype": "Column Break" + }, + { + "default": "20", + "fieldname": "max_auto_email_report_per_user", + "fieldtype": "Int", + "label": "Max auto email report per user" + }, + { + "default": "0", + "fieldname": "disable_change_log_notification", + "fieldtype": "Check", + "label": "Disable Change Log Notification" + }, + { + "default": "1200", + "fieldname": "reset_password_link_expiry_duration", + "fieldtype": "Duration", + "label": "Reset Password Link Expiry Duration", + "non_negative": 1 + }, + { + "default": "3", + "fieldname": "email_retry_limit", + "fieldtype": "Int", + "label": "Email Retry Limit" + }, + { + "default": "0", + "description": "Make sure to configure a Social Login Key before disabling to prevent lockout", + "fieldname": "disable_user_pass_login", + "fieldtype": "Check", + "label": "Disable Username/Password Login" + } + ], + "icon": "fa fa-cog", + "issingle": 1, + "links": [], + "modified": "2022-09-06 03:16:59.090906", + "modified_by": "Administrator", + "module": "Core", + "name": "System Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "ASC", + "states": [], + "track_changes": 1 +} diff --git a/influxframework/core/doctype/system_settings/system_settings.py b/influxframework/core/doctype/system_settings/system_settings.py new file mode 100644 index 0000000..d7654bb --- /dev/null +++ b/influxframework/core/doctype/system_settings/system_settings.py @@ -0,0 +1,103 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework import _ +from influxframework.model import no_value_fields +from influxframework.model.document import Document +from influxframework.translate import set_default_language +from influxframework.twofactor import toggle_two_factor_auth +from influxframework.utils import cint, today +from influxframework.utils.momentjs import get_all_timezones + + +class SystemSettings(Document): + def validate(self): + enable_password_policy = cint(self.enable_password_policy) and True or False + minimum_password_score = cint(getattr(self, "minimum_password_score", 0)) or 0 + if enable_password_policy and minimum_password_score <= 0: + influxframework.throw(_("Please select Minimum Password Score")) + elif not enable_password_policy: + self.minimum_password_score = "" + + for key in ("session_expiry", "session_expiry_mobile"): + if self.get(key): + parts = self.get(key).split(":") + if len(parts) != 2 or not (cint(parts[0]) or cint(parts[1])): + influxframework.throw(_("Session Expiry must be in format {0}").format("hh:mm")) + + if self.enable_two_factor_auth: + if self.two_factor_method == "SMS": + if not influxframework.db.get_single_value("SMS Settings", "sms_gateway_url"): + influxframework.throw( + _("Please setup SMS before setting it as an authentication method, via SMS Settings") + ) + toggle_two_factor_auth(True, roles=["All"]) + else: + self.bypass_2fa_for_retricted_ip_users = 0 + self.bypass_restrict_ip_check_if_2fa_enabled = 0 + + influxframework.flags.update_last_reset_password_date = False + if self.force_user_to_reset_password and not cint( + influxframework.db.get_single_value("System Settings", "force_user_to_reset_password") + ): + influxframework.flags.update_last_reset_password_date = True + + self.validate_user_pass_login() + + def validate_user_pass_login(self): + if not self.disable_user_pass_login: + return + + social_login_enabled = influxframework.db.exists("Social Login Key", {"enable_social_login": 1}) + ldap_enabled = influxframework.db.get_single_value("LDAP Settings", "enabled") + + if not (social_login_enabled or ldap_enabled): + influxframework.throw( + _( + "Please enable atleast one Social Login Key or LDAP before disabling username/password based login." + ) + ) + + def on_update(self): + self.set_defaults() + + influxframework.cache().delete_value("system_settings") + influxframework.cache().delete_value("time_zone") + + if influxframework.flags.update_last_reset_password_date: + update_last_reset_password_date() + + def set_defaults(self): + for df in self.meta.get("fields"): + if df.fieldtype not in no_value_fields and self.has_value_changed(df.fieldname): + influxframework.db.set_default(df.fieldname, self.get(df.fieldname)) + + if self.language: + set_default_language(self.language) + + +def update_last_reset_password_date(): + influxframework.db.sql( + """ UPDATE `tabUser` + SET + last_password_reset_date = %s + WHERE + last_password_reset_date is null""", + today(), + ) + + +@influxframework.whitelist() +def load(): + if not "System Manager" in influxframework.get_roles(): + influxframework.throw(_("Not permitted"), influxframework.PermissionError) + + all_defaults = influxframework.db.get_defaults() + defaults = {} + + for df in influxframework.get_meta("System Settings").get("fields"): + if df.fieldtype in ("Select", "Data"): + defaults[df.fieldname] = all_defaults.get(df.fieldname) + + return {"timezones": get_all_timezones(), "defaults": defaults} diff --git a/influxframework/core/doctype/system_settings/test_system_settings.py b/influxframework/core/doctype/system_settings/test_system_settings.py new file mode 100644 index 0000000..c30f8d6 --- /dev/null +++ b/influxframework/core/doctype/system_settings/test_system_settings.py @@ -0,0 +1,7 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestSystemSettings(InfluxFrameworkTestCase): + pass diff --git a/influxframework/core/doctype/transaction_log/__init__.py b/influxframework/core/doctype/transaction_log/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/transaction_log/readme.md b/influxframework/core/doctype/transaction_log/readme.md new file mode 100644 index 0000000..09162ef --- /dev/null +++ b/influxframework/core/doctype/transaction_log/readme.md @@ -0,0 +1,16 @@ +# Transaction Log Changelog + +## v1.0.0 +Initial version + +The line hash summarizes: +- The index of the row +- The timestamp +- The document raw data + +The chain hash summarizes: +- The previous line hash +- The current line hash + +## v1.0.1 +Modification of the timestamp fieldtype from "Time" to "Datetime" \ No newline at end of file diff --git a/influxframework/core/doctype/transaction_log/test_transaction_log.py b/influxframework/core/doctype/transaction_log/test_transaction_log.py new file mode 100644 index 0000000..b56f1ac --- /dev/null +++ b/influxframework/core/doctype/transaction_log/test_transaction_log.py @@ -0,0 +1,46 @@ +# Copyright (c) 2018, InfluxFramework LLC +# License: MIT. See LICENSE +import hashlib + +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + +test_records = [] + + +class TestTransactionLog(InfluxFrameworkTestCase): + def test_validate_chaining(self): + influxframework.get_doc( + { + "doctype": "Transaction Log", + "reference_doctype": "Test Doctype", + "document_name": "Test Document 1", + "data": "first_data", + } + ).insert(ignore_permissions=True) + + second_log = influxframework.get_doc( + { + "doctype": "Transaction Log", + "reference_doctype": "Test Doctype", + "document_name": "Test Document 2", + "data": "second_data", + } + ).insert(ignore_permissions=True) + + third_log = influxframework.get_doc( + { + "doctype": "Transaction Log", + "reference_doctype": "Test Doctype", + "document_name": "Test Document 3", + "data": "third_data", + } + ).insert(ignore_permissions=True) + + sha = hashlib.sha256() + sha.update( + influxframework.safe_encode(str(third_log.transaction_hash)) + + influxframework.safe_encode(str(second_log.chaining_hash)) + ) + + self.assertEqual(sha.hexdigest(), third_log.chaining_hash) diff --git a/influxframework/core/doctype/transaction_log/transaction_log.js b/influxframework/core/doctype/transaction_log/transaction_log.js new file mode 100644 index 0000000..c0994c2 --- /dev/null +++ b/influxframework/core/doctype/transaction_log/transaction_log.js @@ -0,0 +1,4 @@ +// Copyright (c) 2018, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Transaction Log", {}); diff --git a/influxframework/core/doctype/transaction_log/transaction_log.json b/influxframework/core/doctype/transaction_log/transaction_log.json new file mode 100644 index 0000000..5c6aa5b --- /dev/null +++ b/influxframework/core/doctype/transaction_log/transaction_log.json @@ -0,0 +1,476 @@ +{ + "allow_copy": 0, + "allow_events_in_timeline": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2018-02-06 11:48:51.270524", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "row_index", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Row Index", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "section_break_2", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reference_doctype", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Reference Document Type", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "document_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Document Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_5", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "timestamp", + "fieldtype": "Datetime", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Timestamp", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "checksum_version", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Checksum Version", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "section_break_8", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "previous_hash", + "fieldtype": "Small Text", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Previous Hash", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "transaction_hash", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Transaction Hash", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "chaining_hash", + "fieldtype": "Small Text", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Chaining Hash", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "data", + "fieldtype": "Long Text", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Data", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "amended_from", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Amended From", + "length": 0, + "no_copy": 1, + "options": "Transaction Log", + "permlevel": 0, + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 1, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2019-09-05 14:22:27.664645", + "modified_by": "Administrator", + "module": "Core", + "name": "Transaction Log", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 0 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0, + "track_views": 0 +} \ No newline at end of file diff --git a/influxframework/core/doctype/transaction_log/transaction_log.py b/influxframework/core/doctype/transaction_log/transaction_log.py new file mode 100644 index 0000000..597b045 --- /dev/null +++ b/influxframework/core/doctype/transaction_log/transaction_log.py @@ -0,0 +1,66 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +import hashlib + +import influxframework +from influxframework.model.document import Document +from influxframework.query_builder import DocType +from influxframework.utils import cint, now_datetime + + +class TransactionLog(Document): + def before_insert(self): + index = get_current_index() + self.row_index = index + self.timestamp = now_datetime() + if index != 1: + prev_hash = influxframework.get_all( + "Transaction Log", filters={"row_index": str(index - 1)}, pluck="chaining_hash", limit=1 + ) + if prev_hash: + self.previous_hash = prev_hash[0] + else: + self.previous_hash = "Indexing broken" + else: + self.previous_hash = self.hash_line() + self.transaction_hash = self.hash_line() + self.chaining_hash = self.hash_chain() + self.checksum_version = "v1.0.1" + + def hash_line(self): + sha = hashlib.sha256() + sha.update( + influxframework.safe_encode(str(self.row_index)) + + influxframework.safe_encode(str(self.timestamp)) + + influxframework.safe_encode(str(self.data)) + ) + return sha.hexdigest() + + def hash_chain(self): + sha = hashlib.sha256() + sha.update( + influxframework.safe_encode(str(self.transaction_hash)) + influxframework.safe_encode(str(self.previous_hash)) + ) + return sha.hexdigest() + + +def get_current_index(): + series = DocType("Series") + current = ( + influxframework.qb.from_(series).where(series.name == "TRANSACTLOG").for_update().select("current") + ).run() + + if current and current[0][0] is not None: + current = current[0][0] + + influxframework.db.sql( + """UPDATE `tabSeries` + SET `current` = `current` + 1 + where `name` = 'TRANSACTLOG'""" + ) + current = cint(current) + 1 + else: + influxframework.db.sql("INSERT INTO `tabSeries` (name, current) VALUES ('TRANSACTLOG', 1)") + current = 1 + return current diff --git a/influxframework/core/doctype/translation/__init__.py b/influxframework/core/doctype/translation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/translation/test_translation.py b/influxframework/core/doctype/translation/test_translation.py new file mode 100644 index 0000000..be5d561 --- /dev/null +++ b/influxframework/core/doctype/translation/test_translation.py @@ -0,0 +1,117 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework import _ +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestTranslation(InfluxFrameworkTestCase): + def setUp(self): + influxframework.db.delete("Translation") + + def tearDown(self): + influxframework.local.lang = "en" + clear_translation_cache() + + def test_doctype(self): + translation_data = get_translation_data() + for key, val in translation_data.items(): + influxframework.local.lang = key + + clear_translation_cache() + translation = create_translation(key, val) + self.assertEqual(_(val[0]), val[1]) + + influxframework.delete_doc("Translation", translation.name) + clear_translation_cache() + + self.assertEqual(_(val[0]), val[0]) + + def test_parent_language(self): + data = [ + ["es", ["Test Data", "datos de prueba"]], + ["es", ["Test Spanish", "prueba de español"]], + ["es-MX", ["Test Data", "pruebas de datos"]], + ] + + for key, val in data: + create_translation(key, val) + + influxframework.local.lang = "es" + + clear_translation_cache() + self.assertTrue(_(data[0][0]), data[0][1]) + + clear_translation_cache() + self.assertTrue(_(data[1][0]), data[1][1]) + + influxframework.local.lang = "es-MX" + + # different translation for es-MX + clear_translation_cache() + self.assertTrue(_(data[2][0]), data[2][1]) + + # from spanish (general) + clear_translation_cache() + self.assertTrue(_(data[1][0]), data[1][1]) + + def test_html_content_data_translation(self): + source = """ + MacBook Air lasts up to an incredible 12 hours between charges. So from your morning coffee to + your evening commute, you can work unplugged. When it’s time to kick back and relax, + you can get up to 12 hours of iTunes movie playback. And with up to 30 days of standby time, + you can go away for weeks and pick up where you left off.Whatever the task, + fifth-generation Intel Core i5 and i7 processors with Intel HD Graphics 6000 are up to it.
    + """ + + target = """ + MacBook Air dura hasta 12 horas increíbles entre cargas. Por lo tanto, + desde el café de la mañana hasta el viaje nocturno, puede trabajar desconectado. + Cuando es hora de descansar y relajarse, puede obtener hasta 12 horas de reproducción de películas de iTunes. + Y con hasta 30 días de tiempo de espera, puede irse por semanas y continuar donde lo dejó. Sea cual sea la tarea, + los procesadores Intel Core i5 e i7 de quinta generación con Intel HD Graphics 6000 son capaces de hacerlo. + """ + + create_translation("es", [source, target]) + + source = """ + MacBook Air lasts up to an incredible 12 hours between charges. So from your morning coffee to + your evening commute, you can work unplugged. When it’s time to kick back and relax, + you can get up to 12 hours of iTunes movie playback. And with up to 30 days of standby time, + you can go away for weeks and pick up where you left off.Whatever the task, + fifth-generation Intel Core i5 and i7 processors with Intel HD Graphics 6000 are up to it.
    + """ + + self.assertTrue(_(source), target) + + +def get_translation_data(): + html_source_data = """ + Test Data""" + html_translated_data = """ + testituloksia """ + + return { + "hr": ["Test data", "Testdaten"], + "ms": ["Test Data", "ujian Data"], + "et": ["Test Data", "testandmed"], + "es": ["Test Data", "datos de prueba"], + "en": ["Quotation", "Tax Invoice"], + "fi": [html_source_data, html_translated_data], + } + + +def create_translation(key, val): + translation = influxframework.new_doc("Translation") + translation.language = key + translation.source_text = val[0] + translation.translated_text = val[1] + translation.save() + return translation + + +def clear_translation_cache(): + influxframework.local.lang_full_dict = None + influxframework.cache().delete_key("lang_full_dict", shared=True) diff --git a/influxframework/core/doctype/translation/translation.js b/influxframework/core/doctype/translation/translation.js new file mode 100644 index 0000000..0976780 --- /dev/null +++ b/influxframework/core/doctype/translation/translation.js @@ -0,0 +1,8 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Translation", { + refresh: function () { + // + }, +}); diff --git a/influxframework/core/doctype/translation/translation.json b/influxframework/core/doctype/translation/translation.json new file mode 100644 index 0000000..68b83ed --- /dev/null +++ b/influxframework/core/doctype/translation/translation.json @@ -0,0 +1,111 @@ +{ + "actions": [], + "allow_import": 1, + "autoname": "hash", + "creation": "2016-02-17 12:21:16.175465", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "contributed", + "language", + "section_break_4", + "source_text", + "context", + "column_break_6", + "translated_text", + "section_break_6", + "contribution_status", + "contribution_docname" + ], + "fields": [ + { + "fieldname": "language", + "fieldtype": "Link", + "label": "Language", + "options": "Language", + "reqd": 1, + "search_index": 1 + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "fieldname": "context", + "fieldtype": "Data", + "label": "Context" + }, + { + "default": "0", + "fieldname": "contributed", + "fieldtype": "Check", + "hidden": 1, + "label": "Contributed", + "read_only": 1 + }, + { + "depends_on": "doc.contributed", + "fieldname": "contribution_status", + "fieldtype": "Select", + "label": "Contribution Status", + "options": "\nPending\nVerified\nRejected", + "read_only": 1 + }, + { + "fieldname": "contribution_docname", + "fieldtype": "Data", + "hidden": 1, + "label": "Contribution Document Name", + "read_only": 1 + }, + { + "description": "If your data is in HTML, please copy paste the exact HTML code with the tags.", + "fieldname": "source_text", + "fieldtype": "Code", + "label": "Source Text", + "reqd": 1 + }, + { + "fieldname": "translated_text", + "fieldtype": "Code", + "in_list_view": 1, + "label": "Translated Text", + "reqd": 1 + } + ], + "links": [], + "modified": "2022-07-04 06:53:54.997004", + "modified_by": "Administrator", + "module": "Core", + "name": "Translation", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "source_text", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/translation/translation.py b/influxframework/core/doctype/translation/translation.py new file mode 100644 index 0000000..788bd3c --- /dev/null +++ b/influxframework/core/doctype/translation/translation.py @@ -0,0 +1,92 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +import influxframework +from influxframework.model.document import Document +from influxframework.translate import get_translator_url +from influxframework.utils import is_html, strip_html_tags + + +class Translation(Document): + def validate(self): + if is_html(self.source_text): + self.remove_html_from_source() + + def remove_html_from_source(self): + self.source_text = strip_html_tags(self.source_text).strip() + + def on_update(self): + clear_user_translation_cache(self.language) + + def on_trash(self): + clear_user_translation_cache(self.language) + + def contribute(self): + pass + + def get_contribution_status(self): + pass + + +@influxframework.whitelist() +def create_translations(translation_map, language): + from influxframework.influxframeworkclient import InfluxFrameworkClient + + translation_map = json.loads(translation_map) + translation_map_to_send = influxframework._dict({}) + # first create / update local user translations + for source_id, translation_dict in translation_map.items(): + translation_dict = influxframework._dict(translation_dict) + existing_doc_name = influxframework.get_all( + "Translation", + { + "source_text": translation_dict.source_text, + "context": translation_dict.context or "", + "language": language, + }, + ) + translation_map_to_send[source_id] = translation_dict + if existing_doc_name: + influxframework.db.set_value( + "Translation", + existing_doc_name[0].name, + { + "translated_text": translation_dict.translated_text, + "contributed": 1, + "contribution_status": "Pending", + }, + ) + translation_map_to_send[source_id].name = existing_doc_name[0].name + else: + doc = influxframework.get_doc( + { + "doctype": "Translation", + "source_text": translation_dict.source_text, + "contributed": 1, + "contribution_status": "Pending", + "translated_text": translation_dict.translated_text, + "context": translation_dict.context, + "language": language, + } + ) + doc.insert() + translation_map_to_send[source_id].name = doc.name + + params = { + "language": language, + "contributor_email": influxframework.session.user, + "contributor_name": influxframework.utils.get_fullname(influxframework.session.user), + "translation_map": json.dumps(translation_map_to_send), + } + + translator = InfluxFrameworkClient(get_translator_url()) + added_translations = translator.post_api("translator.api.add_translations", params=params) + + for local_docname, remote_docname in added_translations.items(): + influxframework.db.set_value("Translation", local_docname, "contribution_docname", remote_docname) + + +def clear_user_translation_cache(lang): + influxframework.cache().hdel("lang_user_translations", lang) diff --git a/influxframework/core/doctype/user/__init__.py b/influxframework/core/doctype/user/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/user/test_records.json b/influxframework/core/doctype/user/test_records.json new file mode 100644 index 0000000..9d1bf0e --- /dev/null +++ b/influxframework/core/doctype/user/test_records.json @@ -0,0 +1,95 @@ +[ + { + "doctype": "User", + "email": "test@example.com", + "enabled": 1, + "first_name": "_Test", + "new_password": "Eastern_43A1W", + "roles": [ + { + "doctype": "Has Role", + "parentfield": "roles", + "role": "_Test Role" + }, + { + "doctype": "Has Role", + "parentfield": "roles", + "role": "System Manager" + } + ] + }, + { + "doctype": "User", + "email": "test1@example.com", + "first_name": "_Test1", + "new_password": "Eastern_43A1W" + }, + { + "doctype": "User", + "email": "test2@example.com", + "first_name": "_Test2", + "new_password": "Eastern_43A1W", + "enabled": 1 + }, + { + "doctype": "User", + "email": "test3@example.com", + "first_name": "_Test3", + "new_password": "Eastern_43A1W", + "enabled": 1 + }, + { + "doctype": "User", + "email": "test4@example.com", + "first_name": "_Test4", + "new_password": "Eastern_43A1W", + "enabled": 1 + }, + { + "doctype": "User", + "email": "test'5@example.com", + "first_name": "_Test'5", + "new_password": "Eastern_43A1W", + "enabled": 1 + }, + { + "doctype": "User", + "email": "testperm@example.com", + "first_name": "_Test Perm", + "new_password": "Eastern_43A1W", + "enabled": 1 + }, + { + "doctype": "User", + "email": "testdelete@example.com", + "enabled": 1, + "first_name": "_Test", + "new_password": "Eastern_43A1W", + "roles": [ + { + "doctype": "Has Role", + "parentfield": "roles", + "role": "_Test Role 2" + }, + { + "doctype": "Has Role", + "parentfield": "roles", + "role": "System Manager" + } + ] + }, + { + "doctype": "User", + "email": "testpassword@example.com", + "enabled": 1, + "first_name": "_Test", + "new_password": "Eastern_43A1W", + "roles": [ + { + "doctype": "Has Role", + "parentfield": "roles", + "role": "System Manager" + } + ] + } +] diff --git a/influxframework/core/doctype/user/test_user.py b/influxframework/core/doctype/user/test_user.py new file mode 100644 index 0000000..d0c6e16 --- /dev/null +++ b/influxframework/core/doctype/user/test_user.py @@ -0,0 +1,463 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import json +import time +from unittest.mock import patch + +import influxframework +import influxframework.exceptions +from influxframework.core.doctype.user.user import ( + handle_password_test_fail, + reset_password, + sign_up, + test_password_strength, + update_password, + verify_password, +) +from influxframework.desk.notifications import extract_mentions +from influxframework.influxframeworkclient import InfluxFrameworkClient +from influxframework.model.delete_doc import delete_doc +from influxframework.tests.utils import InfluxFrameworkTestCase +from influxframework.utils import get_url + +user_module = influxframework.core.doctype.user.user +test_records = influxframework.get_test_records("User") + + +class TestUser(InfluxFrameworkTestCase): + def tearDown(self): + # disable password strength test + influxframework.db.set_value("System Settings", "System Settings", "enable_password_policy", 0) + influxframework.db.set_value("System Settings", "System Settings", "minimum_password_score", "") + influxframework.db.set_value("System Settings", "System Settings", "password_reset_limit", 3) + influxframework.set_user("Administrator") + + def test_user_type(self): + new_user = influxframework.get_doc( + dict(doctype="User", email="test-for-type@example.com", first_name="Tester") + ).insert(ignore_if_duplicate=True) + self.assertEqual(new_user.user_type, "Website User") + + # social login userid for influxframework + self.assertTrue(new_user.social_logins[0].userid) + self.assertEqual(new_user.social_logins[0].provider, "influxframework") + + # role with desk access + new_user.add_roles("_Test Role 2") + new_user.save() + self.assertEqual(new_user.user_type, "System User") + + # clear role + new_user.roles = [] + new_user.save() + self.assertEqual(new_user.user_type, "Website User") + + # role without desk access + new_user.add_roles("_Test Role 4") + new_user.save() + self.assertEqual(new_user.user_type, "Website User") + + delete_contact(new_user.name) + influxframework.delete_doc("User", new_user.name) + + def test_delete(self): + influxframework.get_doc("User", "test@example.com").add_roles("_Test Role 2") + self.assertRaises(influxframework.LinkExistsError, delete_doc, "Role", "_Test Role 2") + influxframework.db.delete("Has Role", {"role": "_Test Role 2"}) + delete_doc("Role", "_Test Role 2") + + if influxframework.db.exists("User", "_test@example.com"): + delete_contact("_test@example.com") + delete_doc("User", "_test@example.com") + + user = influxframework.copy_doc(test_records[1]) + user.email = "_test@example.com" + user.insert() + + influxframework.get_doc({"doctype": "ToDo", "description": "_Test"}).insert() + + delete_contact("_test@example.com") + delete_doc("User", "_test@example.com") + + self.assertTrue( + not influxframework.db.sql("""select * from `tabToDo` where allocated_to=%s""", ("_test@example.com",)) + ) + + from influxframework.core.doctype.role.test_role import test_records as role_records + + influxframework.copy_doc(role_records[1]).insert() + + def test_get_value(self): + self.assertEqual(influxframework.db.get_value("User", "test@example.com"), "test@example.com") + self.assertEqual(influxframework.db.get_value("User", {"email": "test@example.com"}), "test@example.com") + self.assertEqual( + influxframework.db.get_value("User", {"email": "test@example.com"}, "email"), "test@example.com" + ) + self.assertEqual( + influxframework.db.get_value("User", {"email": "test@example.com"}, ["first_name", "email"]), + ("_Test", "test@example.com"), + ) + self.assertEqual( + influxframework.db.get_value( + "User", {"email": "test@example.com", "first_name": "_Test"}, ["first_name", "email"] + ), + ("_Test", "test@example.com"), + ) + + test_user = influxframework.db.sql("select * from tabUser where name='test@example.com'", as_dict=True)[0] + self.assertEqual( + influxframework.db.get_value("User", {"email": "test@example.com"}, "*", as_dict=True), test_user + ) + + self.assertEqual(influxframework.db.get_value("User", "xxxtest@example.com"), None) + + influxframework.db.set_value("Website Settings", "Website Settings", "_test", "_test_val") + self.assertEqual(influxframework.db.get_value("Website Settings", None, "_test"), "_test_val") + self.assertEqual( + influxframework.db.get_value("Website Settings", "Website Settings", "_test"), "_test_val" + ) + + def test_high_permlevel_validations(self): + user = influxframework.get_meta("User") + self.assertTrue("roles" in [d.fieldname for d in user.get_high_permlevel_fields()]) + + me = influxframework.get_doc("User", "testperm@example.com") + me.remove_roles("System Manager") + + influxframework.set_user("testperm@example.com") + + me = influxframework.get_doc("User", "testperm@example.com") + me.add_roles("System Manager") + + # system manager is not added (it is reset) + self.assertFalse("System Manager" in [d.role for d in me.roles]) + + # ignore permlevel using flags + me.flags.ignore_permlevel_for_fields = ["roles"] + me.add_roles("System Manager") + + # system manager now added due to flags + self.assertTrue("System Manager" in [d.role for d in me.get("roles")]) + + # reset flags + me.flags.ignore_permlevel_for_fields = None + + # change user + influxframework.set_user("Administrator") + + me = influxframework.get_doc("User", "testperm@example.com") + me.add_roles("System Manager") + + # system manager now added by Administrator + self.assertTrue("System Manager" in [d.role for d in me.get("roles")]) + + def test_delete_user(self): + new_user = influxframework.get_doc( + dict(doctype="User", email="test-for-delete@example.com", first_name="Tester Delete User") + ).insert(ignore_if_duplicate=True) + self.assertEqual(new_user.user_type, "Website User") + + # role with desk access + new_user.add_roles("_Test Role 2") + new_user.save() + self.assertEqual(new_user.user_type, "System User") + + comm = influxframework.get_doc( + { + "doctype": "Communication", + "subject": "To check user able to delete even if linked with communication", + "content": "To check user able to delete even if linked with communication", + "sent_or_received": "Sent", + "user": new_user.name, + } + ) + comm.insert(ignore_permissions=True) + + delete_contact(new_user.name) + influxframework.delete_doc("User", new_user.name) + self.assertFalse(influxframework.db.exists("User", new_user.name)) + + def test_password_strength(self): + # Test Password without Password Strength Policy + influxframework.db.set_value("System Settings", "System Settings", "enable_password_policy", 0) + + # password policy is disabled, test_password_strength should be ignored + result = test_password_strength("test_password") + self.assertFalse(result.get("feedback", None)) + + # Test Password with Password Strenth Policy Set + influxframework.db.set_value("System Settings", "System Settings", "enable_password_policy", 1) + influxframework.db.set_value("System Settings", "System Settings", "minimum_password_score", 2) + + # Score 1; should now fail + result = test_password_strength("bee2ve") + self.assertEqual(result["feedback"]["password_policy_validation_passed"], False) + self.assertRaises( + influxframework.exceptions.ValidationError, handle_password_test_fail, result["feedback"] + ) + self.assertRaises( + influxframework.exceptions.ValidationError, handle_password_test_fail, result + ) # test backwards compatibility + + # Score 4; should pass + result = test_password_strength("Eastern_43A1W") + self.assertEqual(result["feedback"]["password_policy_validation_passed"], True) + + # test password strength while saving user with new password + user = influxframework.get_doc("User", "test@example.com") + influxframework.flags.in_test = False + user.new_password = "password" + self.assertRaises(influxframework.exceptions.ValidationError, user.save) + user.reload() + user.new_password = "Eastern_43A1W" + user.save() + influxframework.flags.in_test = True + + def test_comment_mentions(self): + comment = """ + + @Test + + """ + self.assertEqual(extract_mentions(comment)[0], "test.comment@example.com") + + comment = """ +
    + Testing comment, + + @Test + + please check +
    + """ + self.assertEqual(extract_mentions(comment)[0], "test.comment@example.com") + comment = """ +
    + Testing comment for + + @Test + + and + + @Test + + please check +
    + """ + self.assertEqual(extract_mentions(comment)[0], "test_user@example.com") + self.assertEqual(extract_mentions(comment)[1], "test.again@example1.com") + + influxframework.delete_doc("User Group", "Team") + doc = influxframework.get_doc( + { + "doctype": "User Group", + "name": "Team", + "user_group_members": [{"user": "test@example.com"}, {"user": "test1@example.com"}], + } + ) + + doc.insert() + + comment = """ +
    + Testing comment for + + @Team + and + + @Unknown Team + + please check +
    + """ + self.assertListEqual(extract_mentions(comment), ["test@example.com", "test1@example.com"]) + + def test_rate_limiting_for_reset_password(self): + # Allow only one reset request for a day + influxframework.db.set_value("System Settings", "System Settings", "password_reset_limit", 1) + influxframework.db.commit() + + url = get_url() + data = {"cmd": "influxframework.core.doctype.user.user.reset_password", "user": "test@test.com"} + + # Clear rate limit tracker to start fresh + key = f"rl:{data['cmd']}:{data['user']}" + influxframework.cache().delete(key) + + c = InfluxFrameworkClient(url) + res1 = c.session.post(url, data=data, verify=c.verify, headers=c.headers) + res2 = c.session.post(url, data=data, verify=c.verify, headers=c.headers) + self.assertEqual(res1.status_code, 400) + self.assertEqual(res2.status_code, 417) + + def test_user_rename(self): + old_name = "test_user_rename@example.com" + new_name = "test_user_rename_new@example.com" + user = influxframework.get_doc( + { + "doctype": "User", + "email": old_name, + "enabled": 1, + "first_name": "_Test", + "new_password": "Eastern_43A1W", + "roles": [{"doctype": "Has Role", "parentfield": "roles", "role": "System Manager"}], + } + ).insert(ignore_permissions=True, ignore_if_duplicate=True) + + influxframework.rename_doc("User", user.name, new_name) + self.assertTrue(influxframework.db.exists("Notification Settings", new_name)) + + influxframework.delete_doc("User", new_name) + + def test_signup(self): + import influxframework.website.utils + + random_user = influxframework.mock("email") + random_user_name = influxframework.mock("name") + # disabled signup + with patch.object(user_module, "is_signup_disabled", return_value=True): + self.assertRaisesRegex( + influxframework.exceptions.ValidationError, + "Sign Up is disabled", + sign_up, + random_user, + random_user_name, + "/signup", + ) + + self.assertTupleEqual( + sign_up(random_user, random_user_name, "/welcome"), + (1, "Please check your email for verification"), + ) + self.assertEqual(influxframework.cache().hget("redirect_after_login", random_user), "/welcome") + + # re-register + self.assertTupleEqual( + sign_up(random_user, random_user_name, "/welcome"), (0, "Already Registered") + ) + + # disabled user + user = influxframework.get_doc("User", random_user) + user.enabled = 0 + user.save() + + self.assertTupleEqual( + sign_up(random_user, random_user_name, "/welcome"), (0, "Registered but disabled") + ) + + # throttle user creation + with patch.object(user_module.influxframework.db, "get_creation_count", return_value=301): + self.assertRaisesRegex( + influxframework.exceptions.ValidationError, + "Throttled", + sign_up, + influxframework.mock("email"), + random_user_name, + "/signup", + ) + + def test_reset_password(self): + from influxframework.auth import CookieManager, LoginManager + from influxframework.utils import set_request + + old_password = "Eastern_43A1W" + new_password = "easy_password" + + set_request(path="/random") + influxframework.local.cookie_manager = CookieManager() + influxframework.local.login_manager = LoginManager() + + influxframework.set_user("testpassword@example.com") + test_user = influxframework.get_doc("User", "testpassword@example.com") + test_user.reset_password() + self.assertEqual(update_password(new_password, key=test_user.reset_password_key), "/app") + self.assertEqual( + update_password(new_password, key="wrong_key"), + "The reset password link has either been used before or is invalid", + ) + + # password verification should fail with old password + self.assertRaises(influxframework.exceptions.AuthenticationError, verify_password, old_password) + verify_password(new_password) + + # reset password + update_password(old_password, old_password=new_password) + self.assertRaisesRegex( + influxframework.exceptions.ValidationError, "Invalid key type", update_password, "test", 1, ["like", "%"] + ) + + password_strength_response = { + "feedback": {"password_policy_validation_passed": False, "suggestions": ["Fix password"]} + } + + # password strength failure test + with patch.object( + user_module, "test_password_strength", return_value=password_strength_response + ): + self.assertRaisesRegex( + influxframework.exceptions.ValidationError, + "Fix password", + update_password, + new_password, + 0, + test_user.reset_password_key, + ) + + # test redirect URL for website users + influxframework.set_user("test2@example.com") + self.assertEqual(update_password(new_password, old_password=old_password), "/") + # reset password + update_password(old_password, old_password=new_password) + + # test API endpoint + with patch.object(user_module.influxframework, "sendmail") as sendmail: + influxframework.clear_messages() + test_user = influxframework.get_doc("User", "test2@example.com") + self.assertEqual(reset_password(user="test2@example.com"), None) + test_user.reload() + self.assertEqual(update_password(new_password, key=test_user.reset_password_key), "/") + update_password(old_password, old_password=new_password) + self.assertEqual( + json.loads(influxframework.message_log[0]).get("message"), + "Password reset instructions have been sent to your email", + ) + + sendmail.assert_called_once() + self.assertEqual(sendmail.call_args[1]["recipients"], "test2@example.com") + + self.assertEqual(reset_password(user="test2@example.com"), None) + self.assertEqual(reset_password(user="Administrator"), "not allowed") + self.assertEqual(reset_password(user="random"), "not found") + + def test_user_onload_modules(self): + from influxframework.config import get_modules_from_all_apps + from influxframework.desk.form.load import getdoc + + influxframework.response.docs = [] + getdoc("User", "Administrator") + doc = influxframework.response.docs[0] + self.assertListEqual( + doc.get("__onload").get("all_modules", []), + [m.get("module_name") for m in get_modules_from_all_apps()], + ) + + def test_reset_password_link_expiry(self): + new_password = "new_password" + # set the reset password expiry to 1 second + influxframework.db.set_value( + "System Settings", "System Settings", "reset_password_link_expiry_duration", 1 + ) + influxframework.set_user("testpassword@example.com") + test_user = influxframework.get_doc("User", "testpassword@example.com") + test_user.reset_password() + time.sleep(1) # sleep for 1 sec to expire the reset link + self.assertEqual( + update_password(new_password, key=test_user.reset_password_key), + "The reset password link has been expired", + ) + + +def delete_contact(user): + influxframework.db.delete("Contact", {"email_id": user}) + influxframework.db.delete("Contact Email", {"email_id": user}) diff --git a/influxframework/core/doctype/user/user.js b/influxframework/core/doctype/user/user.js new file mode 100644 index 0000000..fae3529 --- /dev/null +++ b/influxframework/core/doctype/user/user.js @@ -0,0 +1,353 @@ +influxframework.ui.form.on("User", { + before_load: function (frm) { + var update_tz_select = function (user_language) { + frm.set_df_property("time_zone", "options", [""].concat(influxframework.all_timezones)); + }; + + if (!influxframework.all_timezones) { + influxframework.call({ + method: "influxframework.core.doctype.user.user.get_timezones", + callback: function (r) { + influxframework.all_timezones = r.message.timezones; + update_tz_select(); + }, + }); + } else { + update_tz_select(); + } + }, + + role_profile_name: function (frm) { + if (frm.doc.role_profile_name) { + influxframework.call({ + method: "influxframework.core.doctype.user.user.get_role_profile", + args: { + role_profile: frm.doc.role_profile_name, + }, + callback: function (data) { + frm.set_value("roles", []); + $.each(data.message || [], function (i, v) { + var d = frm.add_child("roles"); + d.role = v.role; + }); + frm.roles_editor.show(); + }, + }); + } + }, + + module_profile: function (frm) { + if (frm.doc.module_profile) { + influxframework.call({ + method: "influxframework.core.doctype.user.user.get_module_profile", + args: { + module_profile: frm.doc.module_profile, + }, + callback: function (data) { + frm.set_value("block_modules", []); + $.each(data.message || [], function (i, v) { + let d = frm.add_child("block_modules"); + d.module = v.module; + }); + frm.module_editor && frm.module_editor.show(); + }, + }); + } + }, + + onload: function (frm) { + frm.can_edit_roles = has_access_to_edit_user(); + + if (frm.is_new() && frm.roles_editor) { + frm.roles_editor.reset(); + } + + if ( + frm.can_edit_roles && + !frm.is_new() && + in_list(["System User", "Website User"], frm.doc.user_type) + ) { + if (!frm.roles_editor) { + const role_area = $('
    ').appendTo( + frm.fields_dict.roles_html.wrapper + ); + + frm.roles_editor = new influxframework.RoleEditor( + role_area, + frm, + frm.doc.role_profile_name ? 1 : 0 + ); + + if (frm.doc.user_type == "System User") { + var module_area = $("
    ").appendTo(frm.fields_dict.modules_html.wrapper); + frm.module_editor = new influxframework.ModuleEditor(frm, module_area); + } + } else { + frm.roles_editor.show(); + } + } + }, + refresh: function (frm) { + let doc = frm.doc; + + if (frm.is_new()) { + frm.set_value("time_zone", influxframework.sys_defaults.time_zone); + } + + if ( + in_list(["System User", "Website User"], frm.doc.user_type) && + !frm.is_new() && + !frm.roles_editor && + frm.can_edit_roles + ) { + frm.reload_doc(); + return; + } + + if ( + doc.name === influxframework.session.user && + !doc.__unsaved && + influxframework.all_timezones && + (doc.language || influxframework.boot.user.language) && + doc.language !== influxframework.boot.user.language + ) { + influxframework.msgprint(__("Refreshing...")); + window.location.reload(); + } + + frm.toggle_display(["sb1", "sb3", "modules_access"], false); + + if (!frm.is_new()) { + if (has_access_to_edit_user()) { + frm.add_custom_button( + __("Set User Permissions"), + function () { + influxframework.route_options = { + user: doc.name, + }; + influxframework.set_route("List", "User Permission"); + }, + __("Permissions") + ); + + frm.add_custom_button( + __("View Permitted Documents"), + () => + influxframework.set_route("query-report", "Permitted Documents For User", { + user: frm.doc.name, + }), + __("Permissions") + ); + + frm.toggle_display(["sb1", "sb3", "modules_access"], true); + } + + frm.add_custom_button( + __("Reset Password"), + function () { + influxframework.call({ + method: "influxframework.core.doctype.user.user.reset_password", + args: { + user: frm.doc.name, + }, + }); + }, + __("Password") + ); + + if (influxframework.user.has_role("System Manager")) { + influxframework.db.get_single_value("LDAP Settings", "enabled").then((value) => { + if (value === 1 && frm.doc.name != "Administrator") { + frm.add_custom_button( + __("Reset LDAP Password"), + function () { + const d = new influxframework.ui.Dialog({ + title: __("Reset LDAP Password"), + fields: [ + { + label: __("New Password"), + fieldtype: "Password", + fieldname: "new_password", + reqd: 1, + }, + { + label: __("Confirm New Password"), + fieldtype: "Password", + fieldname: "confirm_password", + reqd: 1, + }, + { + label: __("Logout All Sessions"), + fieldtype: "Check", + fieldname: "logout_sessions", + }, + ], + primary_action: (values) => { + d.hide(); + if (values.new_password !== values.confirm_password) { + influxframework.throw(__("Passwords do not match!")); + } + influxframework.call( + "influxframework.integrations.doctype.ldap_settings.ldap_settings.reset_password", + { + user: frm.doc.email, + password: values.new_password, + logout: values.logout_sessions, + } + ); + }, + }); + d.show(); + }, + __("Password") + ); + } + }); + } + + if (influxframework.session.user == doc.name || influxframework.user.has_role("System Manager")) { + frm.add_custom_button( + __("Reset OTP Secret"), + function () { + influxframework.call({ + method: "influxframework.twofactor.reset_otp_secret", + args: { + user: frm.doc.name, + }, + }); + }, + __("Password") + ); + } + + frm.trigger("enabled"); + + if (frm.roles_editor && frm.can_edit_roles) { + frm.roles_editor.disable = frm.doc.role_profile_name ? 1 : 0; + frm.roles_editor.show(); + } + + frm.module_editor && frm.module_editor.show(); + + if (influxframework.session.user == doc.name) { + // update display settings + if (doc.user_image) { + influxframework.boot.user_info[influxframework.session.user].image = influxframework.utils.get_file_link( + doc.user_image + ); + } + } + } + if (frm.doc.user_emails && influxframework.model.can_create("Email Account")) { + var found = 0; + for (var i = 0; i < frm.doc.user_emails.length; i++) { + if (frm.doc.email == frm.doc.user_emails[i].email_id) { + found = 1; + } + } + if (!found) { + frm.add_custom_button(__("Create User Email"), function () { + frm.events.create_user_email(frm); + }); + } + } + + if (influxframework.route_flags.unsaved === 1) { + delete influxframework.route_flags.unsaved; + for (var i = 0; i < frm.doc.user_emails.length; i++) { + frm.doc.user_emails[i].idx = frm.doc.user_emails[i].idx + 1; + } + frm.dirty(); + } + }, + validate: function (frm) { + if (frm.roles_editor) { + frm.roles_editor.set_roles_in_table(); + } + }, + enabled: function (frm) { + var doc = frm.doc; + if (!frm.is_new() && has_access_to_edit_user()) { + frm.toggle_display(["sb1", "sb3", "modules_access"], doc.enabled); + frm.set_df_property("enabled", "read_only", 0); + } + + if (influxframework.session.user !== "Administrator") { + frm.toggle_enable("email", frm.is_new()); + } + }, + create_user_email: function (frm) { + influxframework.call({ + method: "influxframework.core.doctype.user.user.has_email_account", + args: { + email: frm.doc.email, + }, + callback: function (r) { + if (!Array.isArray(r.message)) { + influxframework.route_options = { + email_id: frm.doc.email, + awaiting_password: 1, + enable_incoming: 1, + }; + influxframework.model.with_doctype("Email Account", function (doc) { + var doc = influxframework.model.get_new_doc("Email Account"); + influxframework.route_flags.linked_user = frm.doc.name; + influxframework.route_flags.delete_user_from_locals = true; + influxframework.set_route("Form", "Email Account", doc.name); + }); + } else { + influxframework.route_flags.create_user_account = frm.doc.name; + influxframework.set_route("Form", "Email Account", r.message[0]["name"]); + } + }, + }); + }, + generate_keys: function (frm) { + influxframework.call({ + method: "influxframework.core.doctype.user.user.generate_keys", + args: { + user: frm.doc.name, + }, + callback: function (r) { + if (r.message) { + influxframework.msgprint(__("Save API Secret: {0}", [r.message.api_secret])); + frm.reload_doc(); + } + }, + }); + }, + on_update: function (frm) { + if (influxframework.boot.time_zone && influxframework.boot.time_zone.user !== frm.doc.time_zone) { + // Clear cache after saving to refresh the values of boot. + influxframework.ui.toolbar.clear_cache(); + } + }, +}); + +influxframework.ui.form.on("User Email", { + email_account(frm, cdt, cdn) { + let child_row = locals[cdt][cdn]; + influxframework.model.get_value( + "Email Account", + child_row.email_account, + "auth_method", + (value) => { + child_row.used_oauth = value.auth_method === "OAuth"; + frm.refresh_field("user_emails", cdn, "used_oauth"); + } + ); + }, +}); + +function has_access_to_edit_user() { + return has_common(influxframework.user_roles, get_roles_for_editing_user()); +} + +function get_roles_for_editing_user() { + return ( + influxframework + .get_meta("User") + .permissions.filter((perm) => perm.permlevel >= 1 && perm.write) + .map((perm) => perm.role) || ["System Manager"] + ); +} diff --git a/influxframework/core/doctype/user/user.json b/influxframework/core/doctype/user/user.json new file mode 100644 index 0000000..6b3748f --- /dev/null +++ b/influxframework/core/doctype/user/user.json @@ -0,0 +1,765 @@ +{ + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "creation": "2022-01-10 17:29:51.672911", + "description": "Represents a User in the system.", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "enabled", + "section_break_3", + "email", + "first_name", + "middle_name", + "last_name", + "column_break0", + "full_name", + "username", + "column_break_11", + "language", + "time_zone", + "send_welcome_email", + "unsubscribed", + "user_image", + "sb1", + "role_profile_name", + "roles_html", + "roles", + "short_bio", + "gender", + "birth_date", + "interest", + "banner_image", + "desk_theme", + "column_break_26", + "phone", + "location", + "bio", + "mute_sounds", + "column_break_22", + "mobile_no", + "change_password", + "new_password", + "logout_all_sessions", + "reset_password_key", + "last_reset_password_key_generated_on", + "last_password_reset_date", + "redirect_url", + "document_follow_notifications_section", + "document_follow_notify", + "document_follow_frequency", + "column_break_75", + "follow_created_documents", + "follow_commented_documents", + "follow_liked_documents", + "follow_assigned_documents", + "follow_shared_documents", + "email_settings", + "email_signature", + "thread_notify", + "send_me_a_copy", + "allowed_in_mentions", + "user_emails", + "sb_allow_modules", + "module_profile", + "modules_html", + "block_modules", + "home_settings", + "sb2", + "defaults", + "sb3", + "simultaneous_sessions", + "restrict_ip", + "last_ip", + "column_break1", + "login_after", + "user_type", + "last_active", + "section_break_63", + "login_before", + "bypass_restrict_ip_check_if_2fa_enabled", + "last_login", + "last_known_versions", + "third_party_authentication", + "social_logins", + "api_access", + "api_key", + "generate_keys", + "column_break_65", + "api_secret" + ], + "fields": [ + { + "default": "1", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enabled", + "oldfieldname": "enabled", + "oldfieldtype": "Check", + "read_only": 1 + }, + { + "depends_on": "enabled", + "fieldname": "section_break_3", + "fieldtype": "Section Break", + "label": "Basic Info" + }, + { + "fieldname": "email", + "fieldtype": "Data", + "label": "Email", + "no_copy": 1, + "oldfieldname": "email", + "oldfieldtype": "Data", + "options": "Email", + "reqd": 1 + }, + { + "fieldname": "first_name", + "fieldtype": "Data", + "label": "First Name", + "oldfieldname": "first_name", + "oldfieldtype": "Data", + "reqd": 1 + }, + { + "fieldname": "middle_name", + "fieldtype": "Data", + "label": "Middle Name", + "oldfieldname": "middle_name", + "oldfieldtype": "Data" + }, + { + "bold": 1, + "fieldname": "last_name", + "fieldtype": "Data", + "label": "Last Name", + "oldfieldname": "last_name", + "oldfieldtype": "Data" + }, + { + "fieldname": "full_name", + "fieldtype": "Data", + "in_global_search": 1, + "in_standard_filter": 1, + "label": "Full Name", + "read_only": 1 + }, + { + "bold": 1, + "default": "1", + "depends_on": "eval:doc.__islocal", + "fieldname": "send_welcome_email", + "fieldtype": "Check", + "label": "Send Welcome Email" + }, + { + "default": "0", + "fieldname": "unsubscribed", + "fieldtype": "Check", + "hidden": 1, + "label": "Unsubscribed", + "no_copy": 1 + }, + { + "fieldname": "column_break0", + "fieldtype": "Column Break", + "oldfieldtype": "Column Break", + "print_width": "50%", + "width": "50%" + }, + { + "fieldname": "username", + "fieldtype": "Data", + "in_global_search": 1, + "in_standard_filter": 1, + "label": "Username", + "unique": 1 + }, + { + "fieldname": "language", + "fieldtype": "Link", + "label": "Language", + "options": "Language" + }, + { + "fieldname": "time_zone", + "fieldtype": "Select", + "label": "Time Zone" + }, + { + "description": "Get your globally recognized avatar from Gravatar.com", + "fieldname": "user_image", + "fieldtype": "Attach Image", + "hidden": 1, + "label": "User Image", + "no_copy": 1, + "print_hide": 1 + }, + { + "depends_on": "eval:in_list(['System User', 'Website User'], doc.user_type) && doc.enabled == 1", + "fieldname": "sb1", + "fieldtype": "Section Break", + "label": "Roles", + "permlevel": 1, + "read_only": 1 + }, + { + "fieldname": "role_profile_name", + "fieldtype": "Link", + "label": "Role Profile", + "options": "Role Profile", + "permlevel": 1 + }, + { + "fieldname": "roles_html", + "fieldtype": "HTML", + "label": "Roles HTML", + "read_only": 1 + }, + { + "fieldname": "roles", + "fieldtype": "Table", + "hidden": 1, + "label": "Roles Assigned", + "options": "Has Role", + "permlevel": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "collapsible": 1, + "depends_on": "enabled", + "fieldname": "short_bio", + "fieldtype": "Section Break", + "label": "More Information" + }, + { + "fieldname": "gender", + "fieldtype": "Link", + "label": "Gender", + "oldfieldname": "gender", + "oldfieldtype": "Select", + "options": "Gender" + }, + { + "fieldname": "phone", + "fieldtype": "Data", + "label": "Phone", + "options": "Phone" + }, + { + "fieldname": "mobile_no", + "fieldtype": "Data", + "label": "Mobile No", + "options": "Phone", + "unique": 1 + }, + { + "fieldname": "birth_date", + "fieldtype": "Date", + "label": "Birth Date", + "no_copy": 1, + "oldfieldname": "birth_date", + "oldfieldtype": "Date" + }, + { + "fieldname": "location", + "fieldtype": "Data", + "label": "Location", + "no_copy": 1 + }, + { + "fieldname": "banner_image", + "fieldtype": "Attach Image", + "label": "Banner Image" + }, + { + "fieldname": "column_break_22", + "fieldtype": "Column Break" + }, + { + "fieldname": "interest", + "fieldtype": "Small Text", + "label": "Interests" + }, + { + "fieldname": "bio", + "fieldtype": "Small Text", + "label": "Bio", + "no_copy": 1 + }, + { + "default": "0", + "fieldname": "mute_sounds", + "fieldtype": "Check", + "label": "Mute Sounds" + }, + { + "collapsible": 1, + "depends_on": "eval:doc.enabled && (!doc.__islocal || !cint(doc.send_welcome_email))", + "fieldname": "change_password", + "fieldtype": "Section Break", + "label": "Change Password" + }, + { + "fieldname": "new_password", + "fieldtype": "Password", + "label": "Set New Password", + "no_copy": 1 + }, + { + "default": "1", + "fieldname": "logout_all_sessions", + "fieldtype": "Check", + "label": "Logout From All Devices After Changing Password" + }, + { + "fieldname": "reset_password_key", + "fieldtype": "Data", + "hidden": 1, + "label": "Reset Password Key", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "last_password_reset_date", + "fieldtype": "Date", + "hidden": 1, + "label": "Last Password Reset Date", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "redirect_url", + "fieldtype": "Small Text", + "hidden": 1, + "label": "Redirect URL" + }, + { + "collapsible": 1, + "fieldname": "document_follow_notifications_section", + "fieldtype": "Section Break", + "label": "Document Follow" + }, + { + "default": "0", + "fieldname": "document_follow_notify", + "fieldtype": "Check", + "label": "Send Notifications For Documents Followed By Me" + }, + { + "default": "Daily", + "depends_on": "eval:(doc.document_follow_notify== 1)", + "fieldname": "document_follow_frequency", + "fieldtype": "Select", + "label": "Frequency", + "options": "Hourly\nDaily\nWeekly" + }, + { + "collapsible": 1, + "depends_on": "enabled", + "fieldname": "email_settings", + "fieldtype": "Section Break", + "label": "Email" + }, + { + "default": "1", + "fieldname": "thread_notify", + "fieldtype": "Check", + "label": "Send Notifications For Email Threads" + }, + { + "default": "0", + "fieldname": "send_me_a_copy", + "fieldtype": "Check", + "label": "Send Me A Copy of Outgoing Emails" + }, + { + "default": "1", + "fieldname": "allowed_in_mentions", + "fieldtype": "Check", + "label": "Allowed In Mentions" + }, + { + "fieldname": "email_signature", + "fieldtype": "Small Text", + "label": "Email Signature", + "no_copy": 1 + }, + { + "fieldname": "user_emails", + "fieldtype": "Table", + "label": "User Emails", + "options": "User Email", + "permlevel": 1 + }, + { + "collapsible": 1, + "depends_on": "eval:in_list(['System User'], doc.user_type)", + "fieldname": "sb_allow_modules", + "fieldtype": "Section Break", + "label": "Allow Modules", + "permlevel": 1 + }, + { + "fieldname": "modules_html", + "fieldtype": "HTML", + "label": "Modules HTML", + "permlevel": 1 + }, + { + "fieldname": "block_modules", + "fieldtype": "Table", + "hidden": 1, + "label": "Block Modules", + "options": "Block Module", + "permlevel": 1 + }, + { + "fieldname": "home_settings", + "fieldtype": "Code", + "hidden": 1, + "label": "Home Settings" + }, + { + "description": "These values will be automatically updated in transactions and also will be useful to restrict permissions for this user on transactions containing these values.", + "fieldname": "sb2", + "fieldtype": "Section Break", + "hidden": 1, + "label": "Defaults", + "oldfieldtype": "Column Break", + "permlevel": 1, + "print_width": "50%", + "read_only": 1, + "width": "50%" + }, + { + "description": "Enter default value fields (keys) and values. If you add multiple values for a field, the first one will be picked. These defaults are also used to set \"match\" permission rules. To see list of fields, go to \"Customize Form\".", + "fieldname": "defaults", + "fieldtype": "Table", + "hidden": 1, + "label": "User Defaults", + "no_copy": 1, + "options": "DefaultValue" + }, + { + "collapsible": 1, + "depends_on": "enabled", + "fieldname": "sb3", + "fieldtype": "Section Break", + "label": "Security Settings", + "oldfieldtype": "Section Break", + "read_only": 1 + }, + { + "default": "1", + "fieldname": "simultaneous_sessions", + "fieldtype": "Int", + "label": "Simultaneous Sessions" + }, + { + "bold": 1, + "default": "System User", + "description": "If the user has any role checked, then the user becomes a \"System User\". \"System User\" has access to the desktop", + "fieldname": "user_type", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "User Type", + "oldfieldname": "user_type", + "oldfieldtype": "Select", + "options": "User Type", + "permlevel": 1 + }, + { + "description": "Allow user to login only after this hour (0-24)", + "fieldname": "login_after", + "fieldtype": "Int", + "label": "Login After", + "permlevel": 1 + }, + { + "description": "Allow user to login only before this hour (0-24)", + "fieldname": "login_before", + "fieldtype": "Int", + "label": "Login Before", + "permlevel": 1 + }, + { + "description": "Restrict user from this IP address only. Multiple IP addresses can be added by separating with commas. Also accepts partial IP addresses like (111.111.111)", + "fieldname": "restrict_ip", + "fieldtype": "Small Text", + "label": "Restrict IP", + "permlevel": 1 + }, + { + "default": "0", + "depends_on": "eval:doc.restrict_ip && doc.restrict_ip.length", + "description": "If enabled, user can login from any IP Address using Two Factor Auth, this can also be set for all users in System Settings", + "fieldname": "bypass_restrict_ip_check_if_2fa_enabled", + "fieldtype": "Check", + "label": "Bypass Restricted IP Address Check If Two Factor Auth Enabled" + }, + { + "fieldname": "column_break1", + "fieldtype": "Column Break", + "oldfieldtype": "Column Break", + "print_width": "50%", + "width": "50%" + }, + { + "fieldname": "last_login", + "fieldtype": "Read Only", + "label": "Last Login", + "no_copy": 1, + "oldfieldname": "last_login", + "oldfieldtype": "Read Only", + "read_only": 1 + }, + { + "fieldname": "last_ip", + "fieldtype": "Read Only", + "label": "Last IP", + "no_copy": 1, + "oldfieldname": "last_ip", + "oldfieldtype": "Read Only", + "read_only": 1 + }, + { + "fieldname": "last_active", + "fieldtype": "Datetime", + "label": "Last Active", + "no_copy": 1, + "read_only": 1 + }, + { + "description": "Stores the JSON of last known versions of various installed apps. It is used to show release notes.", + "fieldname": "last_known_versions", + "fieldtype": "Text", + "hidden": 1, + "label": "Last Known Versions", + "read_only": 1 + }, + { + "collapsible": 1, + "depends_on": "enabled", + "fieldname": "third_party_authentication", + "fieldtype": "Section Break", + "label": "Third Party Authentication", + "permlevel": 1 + }, + { + "fieldname": "social_logins", + "fieldtype": "Table", + "label": "Social Logins", + "options": "User Social Login" + }, + { + "collapsible": 1, + "fieldname": "api_access", + "fieldtype": "Section Break", + "label": "API Access" + }, + { + "description": "API Key cannot be regenerated", + "fieldname": "api_key", + "fieldtype": "Data", + "label": "API Key", + "permlevel": 1, + "read_only": 1, + "unique": 1 + }, + { + "fieldname": "generate_keys", + "fieldtype": "Button", + "label": "Generate Keys", + "permlevel": 1 + }, + { + "fieldname": "column_break_65", + "fieldtype": "Column Break" + }, + { + "fieldname": "api_secret", + "fieldtype": "Password", + "label": "API Secret", + "permlevel": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_26", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_63", + "fieldtype": "Column Break" + }, + { + "fieldname": "desk_theme", + "fieldtype": "Select", + "label": "Desk Theme", + "options": "Light\nDark\nAutomatic" + }, + { + "fieldname": "module_profile", + "fieldtype": "Link", + "label": "Module Profile", + "options": "Module Profile" + }, + { + "description": "Stores the datetime when the last reset password key was generated.", + "fieldname": "last_reset_password_key_generated_on", + "fieldtype": "Datetime", + "hidden": 1, + "label": "Last Reset Password Key Generated On", + "read_only": 1 + }, + { + "fieldname": "column_break_75", + "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "eval:(doc.document_follow_notify== 1)", + "fieldname": "follow_created_documents", + "fieldtype": "Check", + "label": "Auto follow documents that you create" + }, + { + "default": "0", + "depends_on": "eval:(doc.document_follow_notify== 1)", + "fieldname": "follow_commented_documents", + "fieldtype": "Check", + "label": "Auto follow documents that you comment on" + }, + { + "default": "0", + "depends_on": "eval:(doc.document_follow_notify== 1)", + "fieldname": "follow_liked_documents", + "fieldtype": "Check", + "label": "Auto follow documents that you Like" + }, + { + "default": "0", + "depends_on": "eval:(doc.document_follow_notify== 1)", + "fieldname": "follow_shared_documents", + "fieldtype": "Check", + "label": "Auto follow documents that are shared with you" + }, + { + "default": "0", + "depends_on": "eval:(doc.document_follow_notify== 1)", + "fieldname": "follow_assigned_documents", + "fieldtype": "Check", + "label": "Auto follow documents that are assigned to you" + } + ], + "icon": "fa fa-user", + "idx": 413, + "image_field": "user_image", + "links": [ + { + "group": "Profile", + "link_doctype": "Contact", + "link_fieldname": "user" + }, + { + "group": "Profile", + "link_doctype": "Blogger", + "link_fieldname": "user" + }, + { + "group": "Logs", + "link_doctype": "Access Log", + "link_fieldname": "user" + }, + { + "group": "Logs", + "link_doctype": "Activity Log", + "link_fieldname": "user" + }, + { + "group": "Logs", + "link_doctype": "Energy Point Log", + "link_fieldname": "user" + }, + { + "group": "Logs", + "link_doctype": "Route History", + "link_fieldname": "user" + }, + { + "group": "Settings", + "link_doctype": "User Permission", + "link_fieldname": "user" + }, + { + "group": "Settings", + "link_doctype": "Document Follow", + "link_fieldname": "user" + }, + { + "group": "Activity", + "link_doctype": "Communication", + "link_fieldname": "user" + }, + { + "group": "Activity", + "link_doctype": "ToDo", + "link_fieldname": "allocated_to" + }, + { + "group": "Integrations", + "link_doctype": "Token Cache", + "link_fieldname": "user" + } + ], + "modified": "2022-09-19 16:05:46.485242", + "modified_by": "Administrator", + "module": "Core", + "name": "User", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 1, + "share": 1, + "write": 1 + }, + { + "permlevel": 1, + "read": 1, + "role": "System Manager", + "write": 1 + }, + { + "role": "All", + "select": 1 + } + ], + "quick_entry": 1, + "route": "user", + "search_fields": "full_name", + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "full_name", + "track_changes": 1 +} diff --git a/influxframework/core/doctype/user/user.py b/influxframework/core/doctype/user/user.py new file mode 100644 index 0000000..202cabb --- /dev/null +++ b/influxframework/core/doctype/user/user.py @@ -0,0 +1,1181 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +from datetime import timedelta + +import influxframework +import influxframework.defaults +import influxframework.permissions +import influxframework.share +from influxframework import _, msgprint, throw +from influxframework.core.doctype.user_type.user_type import user_linked_with_permission_on_doctype +from influxframework.desk.doctype.notification_settings.notification_settings import ( + create_notification_settings, + toggle_notifications, +) +from influxframework.desk.notifications import clear_notifications +from influxframework.model.document import Document +from influxframework.query_builder import DocType +from influxframework.rate_limiter import rate_limit +from influxframework.utils import ( + cint, + escape_html, + flt, + format_datetime, + get_formatted_email, + get_time_zone, + has_gravatar, + now_datetime, + today, +) +from influxframework.utils.password import check_password, get_password_reset_limit +from influxframework.utils.password import update_password as _update_password +from influxframework.utils.user import get_system_managers +from influxframework.website.utils import is_signup_disabled + +STANDARD_USERS = influxframework.STANDARD_USERS + + +class User(Document): + __new_password = None + + def __setup__(self): + # because it is handled separately + self.flags.ignore_save_passwords = ["new_password"] + + def autoname(self): + """set name as Email Address""" + if self.get("is_admin") or self.get("is_guest"): + self.name = self.first_name + else: + self.email = self.email.strip().lower() + self.name = self.email + + def onload(self): + from influxframework.config import get_modules_from_all_apps + + self.set_onload("all_modules", [m.get("module_name") for m in get_modules_from_all_apps()]) + + def before_insert(self): + self.flags.in_insert = True + throttle_user_creation() + + def after_insert(self): + create_notification_settings(self.name) + influxframework.cache().delete_key("users_for_mentions") + influxframework.cache().delete_key("enabled_users") + + def validate(self): + # clear new password + self.__new_password = self.new_password + self.new_password = "" + + if not influxframework.flags.in_test: + self.password_strength_test() + + if self.name not in STANDARD_USERS: + self.validate_email_type(self.email) + self.validate_email_type(self.name) + self.add_system_manager_role() + self.set_system_user() + self.set_full_name() + self.check_enable_disable() + self.ensure_unique_roles() + self.remove_all_roles_for_guest() + self.validate_username() + self.remove_disabled_roles() + self.validate_user_email_inbox() + ask_pass_update() + self.validate_roles() + self.validate_allowed_modules() + self.validate_user_image() + self.set_time_zone() + + if self.language == "Loading...": + self.language = None + + if (self.name not in ["Administrator", "Guest"]) and ( + not self.get_social_login_userid("influxframework") + ): + self.set_social_login_userid("influxframework", influxframework.generate_hash(length=39)) + + def validate_roles(self): + if self.role_profile_name: + role_profile = influxframework.get_doc("Role Profile", self.role_profile_name) + self.set("roles", []) + self.append_roles(*[role.role for role in role_profile.roles]) + + def validate_allowed_modules(self): + if self.module_profile: + module_profile = influxframework.get_doc("Module Profile", self.module_profile) + self.set("block_modules", []) + for d in module_profile.get("block_modules"): + self.append("block_modules", {"module": d.module}) + + def validate_user_image(self): + if self.user_image and len(self.user_image) > 2000: + influxframework.throw(_("Not a valid User Image.")) + + def on_update(self): + # clear new password + self.share_with_self() + clear_notifications(user=self.name) + influxframework.clear_cache(user=self.name) + now = influxframework.flags.in_test or influxframework.flags.in_install + self.send_password_notification(self.__new_password) + influxframework.enqueue( + "influxframework.core.doctype.user.user.create_contact", user=self, ignore_mandatory=True, now=now + ) + if self.name not in ("Administrator", "Guest") and not self.user_image: + influxframework.enqueue("influxframework.core.doctype.user.user.update_gravatar", name=self.name, now=now) + + # Set user selected timezone + if self.time_zone: + influxframework.defaults.set_default("time_zone", self.time_zone, self.name) + + if self.has_value_changed("enabled"): + influxframework.cache().delete_key("users_for_mentions") + influxframework.cache().delete_key("enabled_users") + elif self.has_value_changed("allow_in_mentions") or self.has_value_changed("user_type"): + influxframework.cache().delete_key("users_for_mentions") + + def has_website_permission(self, ptype, user, verbose=False): + """Returns true if current user is the session user""" + return self.name == influxframework.session.user + + def set_full_name(self): + self.full_name = " ".join(filter(None, [self.first_name, self.last_name])) + + def check_enable_disable(self): + # do not allow disabling administrator/guest + if not cint(self.enabled) and self.name in STANDARD_USERS: + influxframework.throw(_("User {0} cannot be disabled").format(self.name)) + + if not cint(self.enabled): + self.a_system_manager_should_exist() + + # clear sessions if disabled + if not cint(self.enabled) and getattr(influxframework.local, "login_manager", None): + influxframework.local.login_manager.logout(user=self.name) + + # toggle notifications based on the user's status + toggle_notifications(self.name, enable=cint(self.enabled)) + + def add_system_manager_role(self): + if self.is_system_manager_disabled(): + return + + # if adding system manager, do nothing + if not cint(self.enabled) or ( + "System Manager" in [user_role.role for user_role in self.get("roles")] + ): + return + + if ( + self.name not in STANDARD_USERS + and self.user_type == "System User" + and not self.get_other_system_managers() + and cint(influxframework.db.get_single_value("System Settings", "setup_complete")) + ): + + msgprint(_("Adding System Manager to this User as there must be atleast one System Manager")) + self.append("roles", {"doctype": "Has Role", "role": "System Manager"}) + + if self.name == "Administrator": + # Administrator should always have System Manager Role + self.extend( + "roles", + [ + {"doctype": "Has Role", "role": "System Manager"}, + {"doctype": "Has Role", "role": "Administrator"}, + ], + ) + + def is_system_manager_disabled(self): + return influxframework.db.get_value("Role", {"name": "System Manager"}, ["disabled"]) + + def email_new_password(self, new_password=None): + if new_password and not self.flags.in_insert: + _update_password(user=self.name, pwd=new_password, logout_all_sessions=self.logout_all_sessions) + + def set_system_user(self): + """For the standard users like admin and guest, the user type is fixed.""" + user_type_mapper = {"Administrator": "System User", "Guest": "Website User"} + + if self.user_type and not influxframework.get_cached_value("User Type", self.user_type, "is_standard"): + if user_type_mapper.get(self.name): + self.user_type = user_type_mapper.get(self.name) + else: + self.set_roles_and_modules_based_on_user_type() + else: + """Set as System User if any of the given roles has desk_access""" + self.user_type = "System User" if self.has_desk_access() else "Website User" + + def set_roles_and_modules_based_on_user_type(self): + user_type_doc = influxframework.get_cached_doc("User Type", self.user_type) + if user_type_doc.role: + self.roles = [] + + # Check whether User has linked with the 'Apply User Permission On' doctype or not + if user_linked_with_permission_on_doctype(user_type_doc, self.name): + self.append("roles", {"role": user_type_doc.role}) + + influxframework.msgprint( + _("Role has been set as per the user type {0}").format(self.user_type), alert=True + ) + + user_type_doc.update_modules_in_user(self) + + def has_desk_access(self): + """Return true if any of the set roles has desk access""" + if not self.roles: + return False + + role_table = DocType("Role") + return influxframework.db.count( + role_table, + ((role_table.desk_access == 1) & (role_table.name.isin([d.role for d in self.roles]))), + ) + + def share_with_self(self): + influxframework.share.add_docshare( + self.doctype, self.name, self.name, write=1, share=1, flags={"ignore_share_permission": True} + ) + + def validate_share(self, docshare): + pass + # if docshare.user == self.name: + # if self.user_type=="System User": + # if docshare.share != 1: + # influxframework.throw(_("Sorry! User should have complete access to their own record.")) + # else: + # influxframework.throw(_("Sorry! Sharing with Website User is prohibited.")) + + def send_password_notification(self, new_password): + try: + if self.flags.in_insert: + if self.name not in STANDARD_USERS: + if new_password: + # new password given, no email required + _update_password( + user=self.name, pwd=new_password, logout_all_sessions=self.logout_all_sessions + ) + + if not self.flags.no_welcome_mail and cint(self.send_welcome_email): + self.send_welcome_mail_to_user() + self.flags.email_sent = 1 + if influxframework.session.user != "Guest": + msgprint(_("Welcome email sent")) + return + else: + self.email_new_password(new_password) + + except influxframework.OutgoingEmailError: + # email server not set, don't send email + self.log_error("Unable to send new password notification") + + @Document.hook + def validate_reset_password(self): + pass + + def reset_password(self, send_email=False, password_expired=False): + from influxframework.utils import get_url, random_string + + key = random_string(32) + self.db_set("reset_password_key", key) + self.db_set("last_reset_password_key_generated_on", now_datetime()) + + url = "/update-password?key=" + key + if password_expired: + url = "/update-password?key=" + key + "&password_expired=true" + + link = get_url(url) + if send_email: + self.password_reset_mail(link) + + return link + + def get_other_system_managers(self): + user_doctype = DocType("User").as_("user") + user_role_doctype = DocType("Has Role").as_("user_role") + return ( + influxframework.qb.from_(user_doctype) + .from_(user_role_doctype) + .select(user_doctype.name) + .where(user_role_doctype.role == "System Manager") + .where(user_doctype.docstatus < 2) + .where(user_doctype.enabled == 1) + .where(user_role_doctype.parent == user_doctype.name) + .where(user_role_doctype.parent.notin(["Administrator", self.name])) + .limit(1) + .distinct() + ).run() + + def get_fullname(self): + """get first_name space last_name""" + return (self.first_name or "") + (self.first_name and " " or "") + (self.last_name or "") + + def password_reset_mail(self, link): + self.send_login_mail(_("Password Reset"), "password_reset", {"link": link}, now=True) + + def send_welcome_mail_to_user(self): + from influxframework.utils import get_url + + link = self.reset_password() + subject = None + method = influxframework.get_hooks("welcome_email") + if method: + subject = influxframework.get_attr(method[-1])() + if not subject: + site_name = influxframework.db.get_default("site_name") or influxframework.get_conf().get("site_name") + if site_name: + subject = _("Welcome to {0}").format(site_name) + else: + subject = _("Complete Registration") + + self.send_login_mail( + subject, + "new_user", + dict( + link=link, + site_url=get_url(), + ), + ) + + def send_login_mail(self, subject, template, add_args, now=None): + """send mail with login details""" + from influxframework.utils import get_url + from influxframework.utils.user import get_user_fullname + + created_by = get_user_fullname(influxframework.session["user"]) + if created_by == "Guest": + created_by = "Administrator" + + args = { + "first_name": self.first_name or self.last_name or "user", + "user": self.name, + "title": subject, + "login_url": get_url(), + "created_by": created_by, + } + + args.update(add_args) + + sender = ( + influxframework.session.user not in STANDARD_USERS and get_formatted_email(influxframework.session.user) or None + ) + + influxframework.sendmail( + recipients=self.email, + sender=sender, + subject=subject, + template=template, + args=args, + header=[subject, "green"], + delayed=(not now) if now is not None else self.flags.delay_emails, + retry=3, + ) + + def a_system_manager_should_exist(self): + if self.is_system_manager_disabled(): + return + + if not self.get_other_system_managers(): + throw(_("There should remain at least one System Manager")) + + def on_trash(self): + influxframework.clear_cache(user=self.name) + if self.name in STANDARD_USERS: + throw(_("User {0} cannot be deleted").format(self.name)) + + self.a_system_manager_should_exist() + + # disable the user and log him/her out + self.enabled = 0 + if getattr(influxframework.local, "login_manager", None): + influxframework.local.login_manager.logout(user=self.name) + + # delete todos + influxframework.db.delete("ToDo", {"allocated_to": self.name}) + todo_table = DocType("ToDo") + ( + influxframework.qb.update(todo_table) + .set(todo_table.assigned_by, None) + .where(todo_table.assigned_by == self.name) + ).run() + + # delete events + influxframework.db.delete("Event", {"owner": self.name, "event_type": "Private"}) + + # delete shares + influxframework.db.delete("DocShare", {"user": self.name}) + # delete messages + table = DocType("Communication") + influxframework.db.delete( + table, + filters=( + (table.communication_type.isin(["Chat", "Notification"])) + & (table.reference_doctype == "User") + & ((table.reference_name == self.name) | table.owner == self.name) + ), + run=False, + ) + # unlink contact + table = DocType("Contact") + influxframework.qb.update(table).where(table.user == self.name).set(table.user, None).run() + + # delete notification settings + influxframework.delete_doc("Notification Settings", self.name, ignore_permissions=True) + + if self.get("allow_in_mentions"): + influxframework.cache().delete_key("users_for_mentions") + + influxframework.cache().delete_key("enabled_users") + + # delete user permissions + influxframework.db.delete("User Permission", {"user": self.name}) + + def before_rename(self, old_name, new_name, merge=False): + influxframework.clear_cache(user=old_name) + self.validate_rename(old_name, new_name) + + def validate_rename(self, old_name, new_name): + # do not allow renaming administrator and guest + if old_name in STANDARD_USERS: + throw(_("User {0} cannot be renamed").format(self.name)) + + self.validate_email_type(new_name) + + def validate_email_type(self, email): + from influxframework.utils import validate_email_address + + validate_email_address(email.strip(), True) + + def after_rename(self, old_name, new_name, merge=False): + tables = influxframework.db.get_tables() + for tab in tables: + desc = influxframework.db.get_table_columns_description(tab) + has_fields = [] + for d in desc: + if d.get("name") in ["owner", "modified_by"]: + has_fields.append(d.get("name")) + for field in has_fields: + influxframework.db.sql( + """UPDATE `%s` + SET `%s` = %s + WHERE `%s` = %s""" + % (tab, field, "%s", field, "%s"), + (new_name, old_name), + ) + + if influxframework.db.exists("Notification Settings", old_name): + influxframework.rename_doc("Notification Settings", old_name, new_name, force=True, show_alert=False) + + # set email + influxframework.db.update("User", new_name, "email", new_name) + + def append_roles(self, *roles): + """Add roles to user""" + current_roles = [d.role for d in self.get("roles")] + for role in roles: + if role in current_roles: + continue + self.append("roles", {"role": role}) + + def add_roles(self, *roles): + """Add roles to user and save""" + self.append_roles(*roles) + self.save() + + def remove_roles(self, *roles): + existing_roles = {d.role: d for d in self.get("roles")} + for role in roles: + if role in existing_roles: + self.get("roles").remove(existing_roles[role]) + + self.save() + + def remove_all_roles_for_guest(self): + if self.name == "Guest": + self.set("roles", list({d for d in self.get("roles") if d.role == "Guest"})) + + def remove_disabled_roles(self): + disabled_roles = [d.name for d in influxframework.get_all("Role", filters={"disabled": 1})] + for role in list(self.get("roles")): + if role.role in disabled_roles: + self.get("roles").remove(role) + + def ensure_unique_roles(self): + exists = [] + for i, d in enumerate(self.get("roles")): + if (not d.role) or (d.role in exists): + self.get("roles").remove(d) + else: + exists.append(d.role) + + def validate_username(self): + if not self.username and self.is_new() and self.first_name: + self.username = influxframework.scrub(self.first_name) + + if not self.username: + return + + # strip space and @ + self.username = self.username.strip(" @") + + if self.username_exists(): + if self.user_type == "System User": + influxframework.msgprint(_("Username {0} already exists").format(self.username)) + self.suggest_username() + + self.username = "" + + def password_strength_test(self): + """test password strength""" + if self.flags.ignore_password_policy: + return + + if self.__new_password: + user_data = (self.first_name, self.middle_name, self.last_name, self.email, self.birth_date) + result = test_password_strength(self.__new_password, "", None, user_data) + feedback = result.get("feedback", None) + + if feedback and not feedback.get("password_policy_validation_passed", False): + handle_password_test_fail(feedback) + + def suggest_username(self): + def _check_suggestion(suggestion): + if self.username != suggestion and not self.username_exists(suggestion): + return suggestion + + return None + + # @firstname + username = _check_suggestion(influxframework.scrub(self.first_name)) + + if not username: + # @firstname_last_name + username = _check_suggestion( + influxframework.scrub("{} {}".format(self.first_name, self.last_name or "")) + ) + + if username: + influxframework.msgprint(_("Suggested Username: {0}").format(username)) + + return username + + def username_exists(self, username=None): + return influxframework.db.get_value( + "User", {"username": username or self.username, "name": ("!=", self.name)} + ) + + def get_blocked_modules(self): + """Returns list of modules blocked for that user""" + return [d.module for d in self.block_modules] if self.block_modules else [] + + def validate_user_email_inbox(self): + """check if same email account added in User Emails twice""" + + email_accounts = [user_email.email_account for user_email in self.user_emails] + if len(email_accounts) != len(set(email_accounts)): + influxframework.throw(_("Email Account added multiple times")) + + def get_social_login_userid(self, provider): + try: + for p in self.social_logins: + if p.provider == provider: + return p.userid + except Exception: + return None + + def set_social_login_userid(self, provider, userid, username=None): + social_logins = {"provider": provider, "userid": userid} + + if username: + social_logins["username"] = username + + self.append("social_logins", social_logins) + + def get_restricted_ip_list(self): + return get_restricted_ip_list(self) + + @classmethod + def find_by_credentials(cls, user_name: str, password: str, validate_password: bool = True): + """Find the user by credentials. + + This is a login utility that needs to check login related system settings while finding the user. + 1. Find user by email ID by default + 2. If allow_login_using_mobile_number is set, you can use mobile number while finding the user. + 3. If allow_login_using_user_name is set, you can use username while finding the user. + """ + + login_with_mobile = cint( + influxframework.db.get_single_value("System Settings", "allow_login_using_mobile_number") + ) + login_with_username = cint( + influxframework.db.get_single_value("System Settings", "allow_login_using_user_name") + ) + + or_filters = [{"name": user_name}] + if login_with_mobile: + or_filters.append({"mobile_no": user_name}) + if login_with_username: + or_filters.append({"username": user_name}) + + users = influxframework.get_all("User", fields=["name", "enabled"], or_filters=or_filters, limit=1) + if not users: + return + + user = users[0] + user["is_authenticated"] = True + if validate_password: + try: + check_password(user["name"], password, delete_tracker_cache=False) + except influxframework.AuthenticationError: + user["is_authenticated"] = False + + return user + + def set_time_zone(self): + if not self.time_zone: + self.time_zone = get_time_zone() + + +@influxframework.whitelist() +def get_timezones(): + import pytz + + return {"timezones": pytz.all_timezones} + + +@influxframework.whitelist() +def get_all_roles(arg=None): + """return all roles""" + active_domains = influxframework.get_active_domains() + + roles = influxframework.get_all( + "Role", + filters={"name": ("not in", "Administrator,Guest,All"), "disabled": 0}, + or_filters={"ifnull(restrict_to_domain, '')": "", "restrict_to_domain": ("in", active_domains)}, + order_by="name", + ) + + return [role.get("name") for role in roles] + + +@influxframework.whitelist() +def get_roles(arg=None): + """get roles for a user""" + return influxframework.get_roles(influxframework.form_dict["uid"]) + + +@influxframework.whitelist() +def get_perm_info(role): + """get permission info""" + from influxframework.permissions import get_all_perms + + return get_all_perms(role) + + +@influxframework.whitelist(allow_guest=True) +def update_password(new_password, logout_all_sessions=0, key=None, old_password=None): + # validate key to avoid key input like ['like', '%'], '', ['in', ['']] + if key and not isinstance(key, str): + influxframework.throw(_("Invalid key type")) + + result = test_password_strength(new_password, key, old_password) + feedback = result.get("feedback", None) + + if feedback and not feedback.get("password_policy_validation_passed", False): + handle_password_test_fail(feedback) + + res = _get_user_for_update_password(key, old_password) + if res.get("message"): + influxframework.local.response.http_status_code = 410 + return res["message"] + else: + user = res["user"] + + logout_all_sessions = cint(logout_all_sessions) or influxframework.db.get_single_value( + "System Settings", "logout_on_password_reset" + ) + _update_password(user, new_password, logout_all_sessions=cint(logout_all_sessions)) + + user_doc, redirect_url = reset_user_data(user) + + # get redirect url from cache + redirect_to = influxframework.cache().hget("redirect_after_login", user) + if redirect_to: + redirect_url = redirect_to + influxframework.cache().hdel("redirect_after_login", user) + + influxframework.local.login_manager.login_as(user) + + influxframework.db.set_value("User", user, "last_password_reset_date", today()) + influxframework.db.set_value("User", user, "reset_password_key", "") + + if user_doc.user_type == "System User": + return "/app" + else: + return redirect_url if redirect_url else "/" + + +@influxframework.whitelist(allow_guest=True) +def test_password_strength(new_password, key=None, old_password=None, user_data=None): + from influxframework.utils.password_strength import test_password_strength as _test_password_strength + + password_policy = ( + influxframework.db.get_value( + "System Settings", None, ["enable_password_policy", "minimum_password_score"], as_dict=True + ) + or {} + ) + + enable_password_policy = cint(password_policy.get("enable_password_policy", 0)) + minimum_password_score = cint(password_policy.get("minimum_password_score", 0)) + + if not enable_password_policy: + return {} + + if not user_data: + user_data = influxframework.db.get_value( + "User", influxframework.session.user, ["first_name", "middle_name", "last_name", "email", "birth_date"] + ) + + if new_password: + result = _test_password_strength(new_password, user_inputs=user_data) + password_policy_validation_passed = False + + # score should be greater than 0 and minimum_password_score + if result.get("score") and result.get("score") >= minimum_password_score: + password_policy_validation_passed = True + + result["feedback"]["password_policy_validation_passed"] = password_policy_validation_passed + return result + + +# for login +@influxframework.whitelist() +def has_email_account(email): + return influxframework.get_list("Email Account", filters={"email_id": email}) + + +@influxframework.whitelist(allow_guest=False) +def get_email_awaiting(user): + return influxframework.get_all( + "User Email", + fields=["email_account", "email_id"], + filters={"awaiting_password": 1, "parent": user, "used_oauth": 0}, + ) + + +def ask_pass_update(): + # update the sys defaults as to awaiting users + from influxframework.utils import set_default + + password_list = influxframework.get_all( + "User Email", filters={"awaiting_password": 1, "used_oauth": 0}, pluck="parent", distinct=True + ) + set_default("email_user_password", ",".join(password_list)) + + +def _get_user_for_update_password(key, old_password): + # verify old password + result = influxframework._dict() + if key: + user = influxframework.db.get_value( + "User", {"reset_password_key": key}, ["name", "last_reset_password_key_generated_on"] + ) + result.user, last_reset_password_key_generated_on = user or (None, None) + if result.user: + reset_password_link_expiry = cint( + influxframework.db.get_single_value("System Settings", "reset_password_link_expiry_duration") + ) + if ( + reset_password_link_expiry + and now_datetime() + > last_reset_password_key_generated_on + timedelta(seconds=reset_password_link_expiry) + ): + result.message = _("The reset password link has been expired") + else: + result.message = _("The reset password link has either been used before or is invalid") + elif old_password: + # verify old password + influxframework.local.login_manager.check_password(influxframework.session.user, old_password) + user = influxframework.session.user + result.user = user + return result + + +def reset_user_data(user): + user_doc = influxframework.get_doc("User", user) + redirect_url = user_doc.redirect_url + user_doc.reset_password_key = "" + user_doc.redirect_url = "" + user_doc.save(ignore_permissions=True) + + return user_doc, redirect_url + + +@influxframework.whitelist() +def verify_password(password): + influxframework.local.login_manager.check_password(influxframework.session.user, password) + + +@influxframework.whitelist(allow_guest=True) +def sign_up(email, full_name, redirect_to): + if is_signup_disabled(): + influxframework.throw(_("Sign Up is disabled"), title=_("Not Allowed")) + + user = influxframework.db.get("User", {"email": email}) + if user: + if user.enabled: + return 0, _("Already Registered") + else: + return 0, _("Registered but disabled") + else: + if influxframework.db.get_creation_count("User", 60) > 300: + influxframework.respond_as_web_page( + _("Temporarily Disabled"), + _( + "Too many users signed up recently, so the registration is disabled. Please try back in an hour" + ), + http_status_code=429, + ) + + from influxframework.utils import random_string + + user = influxframework.get_doc( + { + "doctype": "User", + "email": email, + "first_name": escape_html(full_name), + "enabled": 1, + "new_password": random_string(10), + "user_type": "Website User", + } + ) + user.flags.ignore_permissions = True + user.flags.ignore_password_policy = True + user.insert() + + # set default signup role as per Portal Settings + default_role = influxframework.db.get_single_value("Portal Settings", "default_role") + if default_role: + user.add_roles(default_role) + + if redirect_to: + influxframework.cache().hset("redirect_after_login", user.name, redirect_to) + + if user.flags.email_sent: + return 1, _("Please check your email for verification") + else: + return 2, _("Please ask your administrator to verify your sign-up") + + +@influxframework.whitelist(allow_guest=True) +@rate_limit(limit=get_password_reset_limit, seconds=24 * 60 * 60, methods=["POST"]) +def reset_password(user): + if user == "Administrator": + return "not allowed" + + try: + user = influxframework.get_doc("User", user) + if not user.enabled: + return "disabled" + + user.validate_reset_password() + user.reset_password(send_email=True) + + return influxframework.msgprint( + msg=_("Password reset instructions have been sent to your email"), + title=_("Password Email Sent"), + ) + except influxframework.DoesNotExistError: + influxframework.local.response["http_status_code"] = 400 + influxframework.clear_messages() + return "not found" + + +@influxframework.whitelist() +@influxframework.validate_and_sanitize_search_inputs +def user_query(doctype, txt, searchfield, start, page_len, filters): + from influxframework.desk.reportview import get_filters_cond, get_match_cond + + doctype = "User" + conditions = [] + + user_type_condition = "and user_type != 'Website User'" + if filters and filters.get("ignore_user_type"): + user_type_condition = "" + filters.pop("ignore_user_type") + + txt = f"%{txt}%" + return influxframework.db.sql( + """SELECT `name`, CONCAT_WS(' ', first_name, middle_name, last_name) + FROM `tabUser` + WHERE `enabled`=1 + {user_type_condition} + AND `docstatus` < 2 + AND `name` NOT IN ({standard_users}) + AND ({key} LIKE %(txt)s + OR CONCAT_WS(' ', first_name, middle_name, last_name) LIKE %(txt)s) + {fcond} {mcond} + ORDER BY + CASE WHEN `name` LIKE %(txt)s THEN 0 ELSE 1 END, + CASE WHEN concat_ws(' ', first_name, middle_name, last_name) LIKE %(txt)s + THEN 0 ELSE 1 END, + NAME asc + LIMIT %(page_len)s OFFSET %(start)s + """.format( + user_type_condition=user_type_condition, + standard_users=", ".join(influxframework.db.escape(u) for u in STANDARD_USERS), + key=searchfield, + fcond=get_filters_cond(doctype, filters, conditions), + mcond=get_match_cond(doctype), + ), + dict(start=start, page_len=page_len, txt=txt), + ) + + +def get_total_users(): + """Returns total no. of system users""" + return flt( + influxframework.db.sql( + """SELECT SUM(`simultaneous_sessions`) + FROM `tabUser` + WHERE `enabled` = 1 + AND `user_type` = 'System User' + AND `name` NOT IN ({})""".format( + ", ".join(["%s"] * len(STANDARD_USERS)) + ), + STANDARD_USERS, + )[0][0] + ) + + +def get_system_users(exclude_users=None, limit=None): + if not exclude_users: + exclude_users = [] + elif not isinstance(exclude_users, (list, tuple)): + exclude_users = [exclude_users] + + limit_cond = "" + if limit: + limit_cond = f"limit {limit}" + + exclude_users += list(STANDARD_USERS) + + system_users = influxframework.db.sql_list( + """select name from `tabUser` + where enabled=1 and user_type != 'Website User' + and name not in ({}) {}""".format( + ", ".join(["%s"] * len(exclude_users)), limit_cond + ), + exclude_users, + ) + + return system_users + + +def get_active_users(): + """Returns No. of system users who logged in, in the last 3 days""" + return influxframework.db.sql( + """select count(*) from `tabUser` + where enabled = 1 and user_type != 'Website User' + and name not in ({}) + and hour(timediff(now(), last_active)) < 72""".format( + ", ".join(["%s"] * len(STANDARD_USERS)) + ), + STANDARD_USERS, + )[0][0] + + +def get_website_users(): + """Returns total no. of website users""" + return influxframework.db.count("User", filters={"enabled": True, "user_type": "Website User"}) + + +def get_active_website_users(): + """Returns No. of website users who logged in, in the last 3 days""" + return influxframework.db.sql( + """select count(*) from `tabUser` + where enabled = 1 and user_type = 'Website User' + and hour(timediff(now(), last_active)) < 72""" + )[0][0] + + +def get_permission_query_conditions(user): + if user == "Administrator": + return "" + else: + return """(`tabUser`.name not in ({standard_users}))""".format( + standard_users=", ".join(influxframework.db.escape(user) for user in STANDARD_USERS) + ) + + +def has_permission(doc, user): + if (user != "Administrator") and (doc.name in STANDARD_USERS): + # dont allow non Administrator user to view / edit Administrator user + return False + + +def notify_admin_access_to_system_manager(login_manager=None): + if ( + login_manager + and login_manager.user == "Administrator" + and influxframework.local.conf.notify_admin_access_to_system_manager + ): + + site = '{0}'.format(influxframework.local.request.host_url) + date_and_time = "{}".format(format_datetime(now_datetime(), format_string="medium")) + ip_address = influxframework.local.request_ip + + access_message = _("Administrator accessed {0} on {1} via IP Address {2}.").format( + site, date_and_time, ip_address + ) + + influxframework.sendmail( + recipients=get_system_managers(), + subject=_("Administrator Logged In"), + template="administrator_logged_in", + args={"access_message": access_message}, + header=["Access Notification", "orange"], + ) + + +def handle_password_test_fail(feedback: dict): + # Backward compatibility + if "feedback" in feedback: + feedback = feedback["feedback"] + + suggestions = feedback.get("suggestions", []) + warning = feedback.get("warning", "") + + influxframework.throw(msg=" ".join([warning] + suggestions), title=_("Invalid Password")) + + +def update_gravatar(name): + gravatar = has_gravatar(name) + if gravatar: + influxframework.db.set_value("User", name, "user_image", gravatar) + + +def throttle_user_creation(): + if influxframework.flags.in_import: + return + + if influxframework.db.get_creation_count("User", 60) > influxframework.local.conf.get("throttle_user_limit", 60): + influxframework.throw(_("Throttled")) + + +@influxframework.whitelist() +def get_role_profile(role_profile): + roles = influxframework.get_doc("Role Profile", {"role_profile": role_profile}) + return roles.roles + + +@influxframework.whitelist() +def get_module_profile(module_profile): + module_profile = influxframework.get_doc("Module Profile", {"module_profile_name": module_profile}) + return module_profile.get("block_modules") + + +def create_contact(user, ignore_links=False, ignore_mandatory=False): + from influxframework.contacts.doctype.contact.contact import get_contact_name + + if user.name in ["Administrator", "Guest"]: + return + + contact_name = get_contact_name(user.email) + if not contact_name: + contact = influxframework.get_doc( + { + "doctype": "Contact", + "first_name": user.first_name, + "last_name": user.last_name, + "user": user.name, + "gender": user.gender, + } + ) + + if user.email: + contact.add_email(user.email, is_primary=True) + + if user.phone: + contact.add_phone(user.phone, is_primary_phone=True) + + if user.mobile_no: + contact.add_phone(user.mobile_no, is_primary_mobile_no=True) + contact.insert( + ignore_permissions=True, ignore_links=ignore_links, ignore_mandatory=ignore_mandatory + ) + else: + contact = influxframework.get_doc("Contact", contact_name) + contact.first_name = user.first_name + contact.last_name = user.last_name + contact.gender = user.gender + + # Add mobile number if phone does not exists in contact + if user.phone and not any(new_contact.phone == user.phone for new_contact in contact.phone_nos): + # Set primary phone if there is no primary phone number + contact.add_phone( + user.phone, + is_primary_phone=not any( + new_contact.is_primary_phone == 1 for new_contact in contact.phone_nos + ), + ) + + # Add mobile number if mobile does not exists in contact + if user.mobile_no and not any( + new_contact.phone == user.mobile_no for new_contact in contact.phone_nos + ): + # Set primary mobile if there is no primary mobile number + contact.add_phone( + user.mobile_no, + is_primary_mobile_no=not any( + new_contact.is_primary_mobile_no == 1 for new_contact in contact.phone_nos + ), + ) + + contact.save(ignore_permissions=True) + + +def get_restricted_ip_list(user): + if not user.restrict_ip: + return + + return [i.strip() for i in user.restrict_ip.split(",")] + + +@influxframework.whitelist() +def generate_keys(user): + """ + generate api key and api secret + + :param user: str + """ + influxframework.only_for("System Manager") + user_details = influxframework.get_doc("User", user) + api_secret = influxframework.generate_hash(length=15) + # if api key is not set generate api key + if not user_details.api_key: + api_key = influxframework.generate_hash(length=15) + user_details.api_key = api_key + user_details.api_secret = api_secret + user_details.save() + + return {"api_secret": api_secret} + + +@influxframework.whitelist() +def switch_theme(theme): + if theme in ["Dark", "Light", "Automatic"]: + influxframework.db.set_value("User", influxframework.session.user, "desk_theme", theme) + + +def get_enabled_users(): + def _get_enabled_users(): + enabled_users = influxframework.get_all("User", filters={"enabled": "1"}, pluck="name") + return enabled_users + + return influxframework.cache().get_value("enabled_users", _get_enabled_users) diff --git a/influxframework/core/doctype/user/user_list.js b/influxframework/core/doctype/user/user_list.js new file mode 100644 index 0000000..27f643b --- /dev/null +++ b/influxframework/core/doctype/user/user_list.js @@ -0,0 +1,19 @@ +// Copyright (c) 2015, InfluxFramework LLC +// MIT License. See license.txt + +influxframework.listview_settings["User"] = { + add_fields: ["enabled", "user_type", "user_image"], + filters: [["enabled", "=", 1]], + prepare_data: function (data) { + data["user_for_avatar"] = data["name"]; + }, + get_indicator: function (doc) { + if (doc.enabled) { + return [__("Active"), "green", "enabled,=,1"]; + } else { + return [__("Disabled"), "grey", "enabled,=,0"]; + } + }, +}; + +influxframework.help.youtube_id["User"] = "8Slw1hsTmUI"; diff --git a/influxframework/core/doctype/user_document_type/__init__.py b/influxframework/core/doctype/user_document_type/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/user_document_type/user_document_type.json b/influxframework/core/doctype/user_document_type/user_document_type.json new file mode 100644 index 0000000..69983a2 --- /dev/null +++ b/influxframework/core/doctype/user_document_type/user_document_type.json @@ -0,0 +1,109 @@ +{ + "actions": [], + "creation": "2021-01-13 01:51:40.158521", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type", + "column_break_2", + "is_custom", + "permissions_section", + "read", + "write", + "create", + "column_break_8", + "submit", + "cancel", + "amend", + "delete" + ], + "fields": [ + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "permissions_section", + "fieldtype": "Section Break", + "label": "Role Permissions" + }, + { + "default": "1", + "fieldname": "read", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Read" + }, + { + "default": "0", + "fieldname": "write", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Write" + }, + { + "default": "0", + "fieldname": "create", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Create" + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fetch_from": "document_type.custom", + "fieldname": "is_custom", + "fieldtype": "Check", + "label": "Is Custom", + "read_only": 1 + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "submit", + "fieldtype": "Check", + "label": "Submit" + }, + { + "default": "0", + "fieldname": "cancel", + "fieldtype": "Check", + "label": "Cancel" + }, + { + "default": "0", + "fieldname": "amend", + "fieldtype": "Check", + "label": "Amend" + }, + { + "default": "0", + "fieldname": "delete", + "fieldtype": "Check", + "label": "Delete" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-03-16 00:32:24.414313", + "modified_by": "Administrator", + "module": "Core", + "name": "User Document Type", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/user_document_type/user_document_type.py b/influxframework/core/doctype/user_document_type/user_document_type.py new file mode 100644 index 0000000..7b8e628 --- /dev/null +++ b/influxframework/core/doctype/user_document_type/user_document_type.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class UserDocumentType(Document): + pass diff --git a/influxframework/core/doctype/user_email/__init__.py b/influxframework/core/doctype/user_email/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/user_email/user_email.json b/influxframework/core/doctype/user_email/user_email.json new file mode 100644 index 0000000..6e3f813 --- /dev/null +++ b/influxframework/core/doctype/user_email/user_email.json @@ -0,0 +1,73 @@ +{ + "actions": [], + "creation": "2016-03-30 10:04:25.828742", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "email_account", + "email_id", + "column_break_3", + "awaiting_password", + "used_oauth", + "enable_outgoing" + ], + "fields": [ + { + "fieldname": "email_account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Email Account", + "options": "Email Account", + "reqd": 1 + }, + { + "fetch_from": "email_account.email_id", + "fieldname": "email_id", + "fieldtype": "Data", + "label": "Email ID", + "options": "Email", + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fetch_from": "email_account.awaiting_password", + "fieldname": "awaiting_password", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Awaiting Password", + "read_only": 1 + }, + { + "default": "0", + "fetch_from": "email_account.enable_outgoing", + "fieldname": "enable_outgoing", + "fieldtype": "Check", + "label": "Enable Outgoing", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "used_oauth", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Used OAuth", + "read_only": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2022-06-03 14:25:46.944733", + "modified_by": "Administrator", + "module": "Core", + "name": "User Email", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/influxframework/core/doctype/user_email/user_email.py b/influxframework/core/doctype/user_email/user_email.py new file mode 100644 index 0000000..3beabc4 --- /dev/null +++ b/influxframework/core/doctype/user_email/user_email.py @@ -0,0 +1,8 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +from influxframework.model.document import Document + + +class UserEmail(Document): + pass diff --git a/influxframework/core/doctype/user_group/__init__.py b/influxframework/core/doctype/user_group/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/user_group/test_user_group.py b/influxframework/core/doctype/user_group/test_user_group.py new file mode 100644 index 0000000..f5c9a7d --- /dev/null +++ b/influxframework/core/doctype/user_group/test_user_group.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestUserGroup(InfluxFrameworkTestCase): + pass diff --git a/influxframework/core/doctype/user_group/user_group.js b/influxframework/core/doctype/user_group/user_group.js new file mode 100644 index 0000000..eb15600 --- /dev/null +++ b/influxframework/core/doctype/user_group/user_group.js @@ -0,0 +1,7 @@ +// Copyright (c) 2021, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("User Group", { + // refresh: function(frm) { + // } +}); diff --git a/influxframework/core/doctype/user_group/user_group.json b/influxframework/core/doctype/user_group/user_group.json new file mode 100644 index 0000000..e807372 --- /dev/null +++ b/influxframework/core/doctype/user_group/user_group.json @@ -0,0 +1,48 @@ +{ + "actions": [], + "autoname": "Prompt", + "creation": "2021-04-12 15:17:24.751710", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user_group_members" + ], + "fields": [ + { + "fieldname": "user_group_members", + "fieldtype": "Table MultiSelect", + "label": "User Group Members", + "options": "User Group Member", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-04-15 16:12:31.455401", + "modified_by": "Administrator", + "module": "Core", + "name": "User Group", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "read": 1, + "role": "All" + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/user_group/user_group.py b/influxframework/core/doctype/user_group/user_group.py new file mode 100644 index 0000000..c15a153 --- /dev/null +++ b/influxframework/core/doctype/user_group/user_group.py @@ -0,0 +1,15 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework + +# import influxframework +from influxframework.model.document import Document + + +class UserGroup(Document): + def after_insert(self): + influxframework.cache().delete_key("user_groups") + + def on_trash(self): + influxframework.cache().delete_key("user_groups") diff --git a/influxframework/core/doctype/user_group_member/__init__.py b/influxframework/core/doctype/user_group_member/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/user_group_member/test_user_group_member.py b/influxframework/core/doctype/user_group_member/test_user_group_member.py new file mode 100644 index 0000000..dac945b --- /dev/null +++ b/influxframework/core/doctype/user_group_member/test_user_group_member.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestUserGroupMember(InfluxFrameworkTestCase): + pass diff --git a/influxframework/core/doctype/user_group_member/user_group_member.js b/influxframework/core/doctype/user_group_member/user_group_member.js new file mode 100644 index 0000000..60a7d63 --- /dev/null +++ b/influxframework/core/doctype/user_group_member/user_group_member.js @@ -0,0 +1,7 @@ +// Copyright (c) 2021, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("User Group Member", { + // refresh: function(frm) { + // } +}); diff --git a/influxframework/core/doctype/user_group_member/user_group_member.json b/influxframework/core/doctype/user_group_member/user_group_member.json new file mode 100644 index 0000000..d2ff149 --- /dev/null +++ b/influxframework/core/doctype/user_group_member/user_group_member.json @@ -0,0 +1,32 @@ +{ + "actions": [], + "creation": "2021-04-12 15:16:29.279107", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "in_list_view": 1, + "label": "User", + "options": "User", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-04-12 15:17:18.773046", + "modified_by": "Administrator", + "module": "Core", + "name": "User Group Member", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/user_group_member/user_group_member.py b/influxframework/core/doctype/user_group_member/user_group_member.py new file mode 100644 index 0000000..6a14a4f --- /dev/null +++ b/influxframework/core/doctype/user_group_member/user_group_member.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class UserGroupMember(Document): + pass diff --git a/influxframework/core/doctype/user_permission/__init__.py b/influxframework/core/doctype/user_permission/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/user_permission/test_user_permission.py b/influxframework/core/doctype/user_permission/test_user_permission.py new file mode 100644 index 0000000..4a708f4 --- /dev/null +++ b/influxframework/core/doctype/user_permission/test_user_permission.py @@ -0,0 +1,317 @@ +# Copyright (c) 2021, InfluxFramework LLC +# See LICENSE +import influxframework +from influxframework.core.doctype.doctype.test_doctype import new_doctype +from influxframework.core.doctype.user_permission.user_permission import ( + add_user_permissions, + remove_applicable, +) +from influxframework.permissions import has_user_permission +from influxframework.tests.utils import InfluxFrameworkTestCase +from influxframework.website.doctype.blog_post.test_blog_post import make_test_blog + + +class TestUserPermission(InfluxFrameworkTestCase): + def setUp(self): + test_users = ( + "test_bulk_creation_update@example.com", + "test_user_perm1@example.com", + "nested_doc_user@example.com", + ) + influxframework.db.delete("User Permission", {"user": ("in", test_users)}) + influxframework.delete_doc_if_exists("DocType", "Person") + influxframework.db.sql_ddl("DROP TABLE IF EXISTS `tabPerson`") + influxframework.delete_doc_if_exists("DocType", "Doc A") + influxframework.db.sql_ddl("DROP TABLE IF EXISTS `tabDoc A`") + + def test_default_user_permission_validation(self): + user = create_user("test_default_permission@example.com") + param = get_params(user, "User", user.name, is_default=1) + add_user_permissions(param) + # create a duplicate entry with default + perm_user = create_user("test_user_perm@example.com") + param = get_params(user, "User", perm_user.name, is_default=1) + self.assertRaises(influxframework.ValidationError, add_user_permissions, param) + + def test_default_user_permission_corectness(self): + user = create_user("test_default_corectness_permission_1@example.com") + param = get_params(user, "User", user.name, is_default=1, hide_descendants=1) + add_user_permissions(param) + # create a duplicate entry with default + perm_user = create_user("test_default_corectness2@example.com") + test_blog = make_test_blog() + param = get_params(perm_user, "Blog Post", test_blog.name, is_default=1, hide_descendants=1) + add_user_permissions(param) + influxframework.db.delete("User Permission", filters={"for_value": test_blog.name}) + influxframework.delete_doc("Blog Post", test_blog.name) + + def test_default_user_permission(self): + influxframework.set_user("Administrator") + user = create_user("test_user_perm1@example.com", "Website Manager") + for category in ["general", "public"]: + if not influxframework.db.exists("Blog Category", category): + influxframework.get_doc({"doctype": "Blog Category", "title": category}).insert() + + param = get_params(user, "Blog Category", "general", is_default=1) + add_user_permissions(param) + + param = get_params(user, "Blog Category", "public") + add_user_permissions(param) + + influxframework.set_user("test_user_perm1@example.com") + doc = influxframework.new_doc("Blog Post") + + self.assertEqual(doc.blog_category, "general") + influxframework.set_user("Administrator") + + def test_apply_to_all(self): + """Create User permission for User having access to all applicable Doctypes""" + user = create_user("test_bulk_creation_update@example.com") + param = get_params(user, "User", user.name) + is_created = add_user_permissions(param) + self.assertEqual(is_created, 1) + + def test_for_apply_to_all_on_update_from_apply_all(self): + user = create_user("test_bulk_creation_update@example.com") + param = get_params(user, "User", user.name) + + # Initially create User Permission document with apply_to_all checked + is_created = add_user_permissions(param) + + self.assertEqual(is_created, 1) + is_created = add_user_permissions(param) + + # User Permission should not be changed + self.assertEqual(is_created, 0) + + def test_for_applicable_on_update_from_apply_to_all(self): + """Update User Permission from all to some applicable Doctypes""" + user = create_user("test_bulk_creation_update@example.com") + param = get_params(user, "User", user.name, applicable=["Comment", "Contact"]) + + # Initially create User Permission document with apply_to_all checked + is_created = add_user_permissions(get_params(user, "User", user.name)) + + self.assertEqual(is_created, 1) + + is_created = add_user_permissions(param) + influxframework.db.commit() + + removed_apply_to_all = influxframework.db.exists("User Permission", get_exists_param(user)) + is_created_applicable_first = influxframework.db.exists( + "User Permission", get_exists_param(user, applicable="Comment") + ) + is_created_applicable_second = influxframework.db.exists( + "User Permission", get_exists_param(user, applicable="Contact") + ) + + # Check that apply_to_all is removed + self.assertIsNone(removed_apply_to_all) + + # Check that User Permissions for applicable is created + self.assertIsNotNone(is_created_applicable_first) + self.assertIsNotNone(is_created_applicable_second) + self.assertEqual(is_created, 1) + + def test_for_apply_to_all_on_update_from_applicable(self): + """Update User Permission from some to all applicable Doctypes""" + user = create_user("test_bulk_creation_update@example.com") + param = get_params(user, "User", user.name) + + # create User permissions that with applicable + is_created = add_user_permissions( + get_params(user, "User", user.name, applicable=["Comment", "Contact"]) + ) + + self.assertEqual(is_created, 1) + + is_created = add_user_permissions(param) + is_created_apply_to_all = influxframework.db.exists("User Permission", get_exists_param(user)) + removed_applicable_first = influxframework.db.exists( + "User Permission", get_exists_param(user, applicable="Comment") + ) + removed_applicable_second = influxframework.db.exists( + "User Permission", get_exists_param(user, applicable="Contact") + ) + + # To check that a User permission with apply_to_all exists + self.assertIsNotNone(is_created_apply_to_all) + + # Check that all User Permission with applicable is removed + self.assertIsNone(removed_applicable_first) + self.assertIsNone(removed_applicable_second) + self.assertEqual(is_created, 1) + + def test_user_perm_for_nested_doctype(self): + """Test if descendants' visibility is controlled for a nested DocType.""" + from influxframework.core.doctype.doctype.test_doctype import new_doctype + + user = create_user("nested_doc_user@example.com", "Blogger") + if not influxframework.db.exists("DocType", "Person"): + doc = new_doctype( + "Person", + fields=[{"label": "Person Name", "fieldname": "person_name", "fieldtype": "Data"}], + unique=0, + ) + doc.is_tree = 1 + doc.insert() + + parent_record = influxframework.get_doc( + {"doctype": "Person", "person_name": "Parent", "is_group": 1} + ).insert() + + child_record = influxframework.get_doc( + { + "doctype": "Person", + "person_name": "Child", + "is_group": 0, + "parent_person": parent_record.name, + } + ).insert() + + add_user_permissions(get_params(user, "Person", parent_record.name)) + + # check if adding perm on a group record, makes child record visible + self.assertTrue(has_user_permission(influxframework.get_doc("Person", parent_record.name), user.name)) + self.assertTrue(has_user_permission(influxframework.get_doc("Person", child_record.name), user.name)) + + influxframework.db.set_value( + "User Permission", {"allow": "Person", "for_value": parent_record.name}, "hide_descendants", 1 + ) + influxframework.cache().delete_value("user_permissions") + + # check if adding perm on a group record with hide_descendants enabled, + # hides child records + self.assertTrue(has_user_permission(influxframework.get_doc("Person", parent_record.name), user.name)) + self.assertFalse(has_user_permission(influxframework.get_doc("Person", child_record.name), user.name)) + + def test_user_perm_on_new_doc_with_field_default(self): + """Test User Perm impact on influxframework.new_doc. with *field* default value""" + influxframework.set_user("Administrator") + user = create_user("new_doc_test@example.com", "Blogger") + + # make a doctype "Doc A" with 'doctype' link field and default value ToDo + if not influxframework.db.exists("DocType", "Doc A"): + doc = new_doctype( + "Doc A", + fields=[ + { + "label": "DocType", + "fieldname": "doc", + "fieldtype": "Link", + "options": "DocType", + "default": "ToDo", + } + ], + unique=0, + ) + doc.insert() + + # make User Perm on DocType 'ToDo' in Assignment Rule (unrelated doctype) + add_user_permissions(get_params(user, "DocType", "ToDo", applicable=["Assignment Rule"])) + influxframework.set_user("new_doc_test@example.com") + + new_doc = influxframework.new_doc("Doc A") + + # User perm is created on ToDo but for doctype Assignment Rule only + # it should not have impact on Doc A + self.assertEqual(new_doc.doc, "ToDo") + + influxframework.set_user("Administrator") + remove_applicable(["Assignment Rule"], "new_doc_test@example.com", "DocType", "ToDo") + + def test_user_perm_on_new_doc_with_user_default(self): + """Test User Perm impact on influxframework.new_doc. with *user* default value""" + from influxframework.core.doctype.session_default_settings.session_default_settings import ( + clear_session_defaults, + set_session_default_values, + ) + + influxframework.set_user("Administrator") + user = create_user("user_default_test@example.com", "Blogger") + + # make a doctype "Doc A" with 'doctype' link field + if not influxframework.db.exists("DocType", "Doc A"): + doc = new_doctype( + "Doc A", + fields=[ + { + "label": "DocType", + "fieldname": "doc", + "fieldtype": "Link", + "options": "DocType", + } + ], + unique=0, + ) + doc.insert() + + # create a 'DocType' session default field + if not influxframework.db.exists("Session Default", {"ref_doctype": "DocType"}): + settings = influxframework.get_single("Session Default Settings") + settings.append("session_defaults", {"ref_doctype": "DocType"}) + settings.save() + + # make User Perm on DocType 'ToDo' in Assignment Rule (unrelated doctype) + add_user_permissions(get_params(user, "DocType", "ToDo", applicable=["Assignment Rule"])) + + # User default Doctype value is ToDo via Session Defaults + influxframework.set_user("user_default_test@example.com") + set_session_default_values({"doc": "ToDo"}) + + new_doc = influxframework.new_doc("Doc A") + + # User perm is created on ToDo but for doctype Assignment Rule only + # it should not have impact on Doc A + self.assertEqual(new_doc.doc, "ToDo") + + influxframework.set_user("Administrator") + clear_session_defaults() + remove_applicable(["Assignment Rule"], "user_default_test@example.com", "DocType", "ToDo") + + +def create_user(email, *roles): + """create user with role system manager""" + if influxframework.db.exists("User", email): + return influxframework.get_doc("User", email) + + user = influxframework.new_doc("User") + user.email = email + user.first_name = email.split("@")[0] + + if not roles: + roles = ("System Manager",) + + user.add_roles(*roles) + return user + + +def get_params(user, doctype, docname, is_default=0, hide_descendants=0, applicable=None): + """Return param to insert""" + param = { + "user": user.name, + "doctype": doctype, + "docname": docname, + "is_default": is_default, + "apply_to_all_doctypes": 1, + "applicable_doctypes": [], + "hide_descendants": hide_descendants, + } + if applicable: + param.update({"apply_to_all_doctypes": 0}) + param.update({"applicable_doctypes": applicable}) + return param + + +def get_exists_param(user, applicable=None): + """param to check existing Document""" + param = { + "user": user.name, + "allow": "User", + "for_value": user.name, + } + if applicable: + param.update({"applicable_for": applicable}) + else: + param.update({"apply_to_all_doctypes": 1}) + return param diff --git a/influxframework/core/doctype/user_permission/user_permission.js b/influxframework/core/doctype/user_permission/user_permission.js new file mode 100644 index 0000000..77f0617 --- /dev/null +++ b/influxframework/core/doctype/user_permission/user_permission.js @@ -0,0 +1,58 @@ +// Copyright (c) 2017, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("User Permission", { + setup: (frm) => { + frm.set_query("allow", () => { + return { + filters: { + issingle: 0, + istable: 0, + }, + }; + }); + + frm.set_query("applicable_for", () => { + return { + query: "influxframework.core.doctype.user_permission.user_permission.get_applicable_for_doctype_list", + doctype: frm.doc.allow, + }; + }); + }, + + refresh: (frm) => { + frm.add_custom_button(__("View Permitted Documents"), () => + influxframework.set_route("query-report", "Permitted Documents For User", { + user: frm.doc.user, + }) + ); + frm.trigger("set_applicable_for_constraint"); + frm.trigger("toggle_hide_descendants"); + }, + + allow: (frm) => { + if (frm.doc.allow) { + if (frm.doc.for_value) { + frm.set_value("for_value", null); + } + frm.trigger("toggle_hide_descendants"); + } + }, + + apply_to_all_doctypes: (frm) => { + frm.trigger("set_applicable_for_constraint"); + }, + + set_applicable_for_constraint: (frm) => { + frm.toggle_reqd("applicable_for", !frm.doc.apply_to_all_doctypes); + + if (frm.doc.apply_to_all_doctypes && frm.doc.applicable_for) { + frm.set_value("applicable_for", null, null, true); + } + }, + + toggle_hide_descendants: (frm) => { + let show = influxframework.boot.nested_set_doctypes.includes(frm.doc.allow); + frm.toggle_display("hide_descendants", show); + }, +}); diff --git a/influxframework/core/doctype/user_permission/user_permission.json b/influxframework/core/doctype/user_permission/user_permission.json new file mode 100644 index 0000000..60b6779 --- /dev/null +++ b/influxframework/core/doctype/user_permission/user_permission.json @@ -0,0 +1,117 @@ +{ + "actions": [], + "allow_import": 1, + "creation": "2017-07-17 14:25:27.881871", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user", + "allow", + "for_value", + "column_break_3", + "is_default", + "advanced_control_section", + "apply_to_all_doctypes", + "applicable_for", + "column_break_9", + "hide_descendants" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "User", + "options": "User", + "reqd": 1, + "search_index": 1 + }, + { + "fieldname": "allow", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Allow", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "for_value", + "fieldtype": "Dynamic Link", + "ignore_user_permissions": 1, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "For Value", + "options": "allow", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "is_default", + "fieldtype": "Check", + "label": "Is Default" + }, + { + "fieldname": "advanced_control_section", + "fieldtype": "Section Break", + "label": "Advanced Control" + }, + { + "default": "1", + "fieldname": "apply_to_all_doctypes", + "fieldtype": "Check", + "label": "Apply To All Document Types" + }, + { + "depends_on": "eval:!doc.apply_to_all_doctypes", + "fieldname": "applicable_for", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Applicable For", + "options": "DocType" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "Hide descendant records of For Value.", + "fieldname": "hide_descendants", + "fieldtype": "Check", + "hidden": 1, + "label": "Hide Descendants" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + } + ], + "links": [], + "modified": "2022-01-03 11:25:03.726150", + "modified_by": "Administrator", + "module": "Core", + "name": "User Permission", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "user", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/user_permission/user_permission.py b/influxframework/core/doctype/user_permission/user_permission.py new file mode 100644 index 0000000..48e3342 --- /dev/null +++ b/influxframework/core/doctype/user_permission/user_permission.py @@ -0,0 +1,327 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +import influxframework +from influxframework import _ +from influxframework.core.utils import find +from influxframework.desk.form.linked_with import get_linked_doctypes +from influxframework.model.document import Document +from influxframework.utils import cstr + + +class UserPermission(Document): + def validate(self): + self.validate_user_permission() + self.validate_default_permission() + + def on_update(self): + influxframework.cache().hdel("user_permissions", self.user) + influxframework.publish_realtime("update_user_permissions") + + def on_trash(self): # pylint: disable=no-self-use + influxframework.cache().hdel("user_permissions", self.user) + influxframework.publish_realtime("update_user_permissions") + + def validate_user_permission(self): + """checks for duplicate user permission records""" + + duplicate_exists = influxframework.get_all( + self.doctype, + filters={ + "allow": self.allow, + "for_value": self.for_value, + "user": self.user, + "applicable_for": cstr(self.applicable_for), + "apply_to_all_doctypes": self.apply_to_all_doctypes, + "name": ["!=", self.name], + }, + limit=1, + ) + if duplicate_exists: + influxframework.throw(_("User permission already exists"), influxframework.DuplicateEntryError) + + def validate_default_permission(self): + """validate user permission overlap for default value of a particular doctype""" + overlap_exists = [] + if self.is_default: + overlap_exists = influxframework.get_all( + self.doctype, + filters={"allow": self.allow, "user": self.user, "is_default": 1, "name": ["!=", self.name]}, + or_filters={ + "applicable_for": cstr(self.applicable_for), + "apply_to_all_doctypes": 1, + }, + limit=1, + ) + if overlap_exists: + ref_link = influxframework.get_desk_link(self.doctype, overlap_exists[0].name) + influxframework.throw(_("{0} has already assigned default value for {1}.").format(ref_link, self.allow)) + + +@influxframework.whitelist() +def get_user_permissions(user=None): + """Get all users permissions for the user as a dict of doctype""" + # if this is called from client-side, + # user can access only his/her user permissions + if influxframework.request and influxframework.local.form_dict.cmd == "get_user_permissions": + user = influxframework.session.user + + if not user: + user = influxframework.session.user + + if not user or user in ("Administrator", "Guest"): + return {} + + cached_user_permissions = influxframework.cache().hget("user_permissions", user) + + if cached_user_permissions is not None: + return cached_user_permissions + + out = {} + + def add_doc_to_perm(perm, doc_name, is_default): + # group rules for each type + # for example if allow is "Customer", then build all allowed customers + # in a list + if not out.get(perm.allow): + out[perm.allow] = [] + + out[perm.allow].append( + influxframework._dict( + {"doc": doc_name, "applicable_for": perm.get("applicable_for"), "is_default": is_default} + ) + ) + + try: + for perm in influxframework.get_all( + "User Permission", + fields=["allow", "for_value", "applicable_for", "is_default", "hide_descendants"], + filters=dict(user=user), + ): + + meta = influxframework.get_meta(perm.allow) + add_doc_to_perm(perm, perm.for_value, perm.is_default) + + if meta.is_nested_set() and not perm.hide_descendants: + decendants = influxframework.db.get_descendants(perm.allow, perm.for_value) + for doc in decendants: + add_doc_to_perm(perm, doc, False) + + out = influxframework._dict(out) + influxframework.cache().hset("user_permissions", user, out) + except influxframework.db.SQLError as e: + if influxframework.db.is_table_missing(e): + # called from patch + pass + + return out + + +def user_permission_exists(user, allow, for_value, applicable_for=None): + """Checks if similar user permission already exists""" + user_permissions = get_user_permissions(user).get(allow, []) + if not user_permissions: + return None + has_same_user_permission = find( + user_permissions, + lambda perm: perm["doc"] == for_value and perm.get("applicable_for") == applicable_for, + ) + + return has_same_user_permission + + +@influxframework.whitelist() +@influxframework.validate_and_sanitize_search_inputs +def get_applicable_for_doctype_list(doctype, txt, searchfield, start, page_len, filters): + linked_doctypes_map = get_linked_doctypes(doctype, True) + + linked_doctypes = [] + for linked_doctype, linked_doctype_values in linked_doctypes_map.items(): + linked_doctypes.append(linked_doctype) + child_doctype = linked_doctype_values.get("child_doctype") + if child_doctype: + linked_doctypes.append(child_doctype) + + linked_doctypes += [doctype] + + if txt: + linked_doctypes = [d for d in linked_doctypes if txt.lower() in d.lower()] + + linked_doctypes.sort() + + return_list = [] + for doctype in linked_doctypes[start:page_len]: + return_list.append([doctype]) + + return return_list + + +def get_permitted_documents(doctype): + """Returns permitted documents from the given doctype for the session user""" + # sort permissions in a way to make the first permission in the list to be default + user_perm_list = sorted( + get_user_permissions().get(doctype, []), key=lambda x: x.get("is_default"), reverse=True + ) + + return [d.get("doc") for d in user_perm_list if d.get("doc")] + + +@influxframework.whitelist() +def check_applicable_doc_perm(user, doctype, docname): + influxframework.only_for("System Manager") + applicable = [] + doc_exists = influxframework.get_all( + "User Permission", + fields=["name"], + filters={ + "user": user, + "allow": doctype, + "for_value": docname, + "apply_to_all_doctypes": 1, + }, + limit=1, + ) + if doc_exists: + applicable = get_linked_doctypes(doctype).keys() + else: + data = influxframework.get_all( + "User Permission", + fields=["applicable_for"], + filters={ + "user": user, + "allow": doctype, + "for_value": docname, + }, + ) + for permission in data: + applicable.append(permission.applicable_for) + return applicable + + +@influxframework.whitelist() +def clear_user_permissions(user, for_doctype): + influxframework.only_for("System Manager") + total = influxframework.db.count("User Permission", {"user": user, "allow": for_doctype}) + + if total: + influxframework.db.delete( + "User Permission", + { + "allow": for_doctype, + "user": user, + }, + ) + influxframework.clear_cache() + + return total + + +@influxframework.whitelist() +def add_user_permissions(data): + """Add and update the user permissions""" + influxframework.only_for("System Manager") + if isinstance(data, str): + data = json.loads(data) + data = influxframework._dict(data) + + # get all doctypes on whom this permission is applied + perm_applied_docs = check_applicable_doc_perm(data.user, data.doctype, data.docname) + exists = influxframework.db.exists( + "User Permission", + { + "user": data.user, + "allow": data.doctype, + "for_value": data.docname, + "apply_to_all_doctypes": 1, + }, + ) + if data.apply_to_all_doctypes == 1 and not exists: + remove_applicable(perm_applied_docs, data.user, data.doctype, data.docname) + insert_user_perm( + data.user, data.doctype, data.docname, data.is_default, data.hide_descendants, apply_to_all=1 + ) + return 1 + elif len(data.applicable_doctypes) > 0 and data.apply_to_all_doctypes != 1: + remove_apply_to_all(data.user, data.doctype, data.docname) + update_applicable( + perm_applied_docs, data.applicable_doctypes, data.user, data.doctype, data.docname + ) + for applicable in data.applicable_doctypes: + if applicable not in perm_applied_docs: + insert_user_perm( + data.user, + data.doctype, + data.docname, + data.is_default, + data.hide_descendants, + applicable=applicable, + ) + elif exists: + insert_user_perm( + data.user, + data.doctype, + data.docname, + data.is_default, + data.hide_descendants, + applicable=applicable, + ) + return 1 + return 0 + + +def insert_user_perm( + user, doctype, docname, is_default=0, hide_descendants=0, apply_to_all=None, applicable=None +): + user_perm = influxframework.new_doc("User Permission") + user_perm.user = user + user_perm.allow = doctype + user_perm.for_value = docname + user_perm.is_default = is_default + user_perm.hide_descendants = hide_descendants + if applicable: + user_perm.applicable_for = applicable + user_perm.apply_to_all_doctypes = 0 + else: + user_perm.apply_to_all_doctypes = 1 + user_perm.insert() + + +def remove_applicable(perm_applied_docs, user, doctype, docname): + for applicable_for in perm_applied_docs: + influxframework.db.delete( + "User Permission", + { + "applicable_for": applicable_for, + "for_value": docname, + "allow": doctype, + "user": user, + }, + ) + + +def remove_apply_to_all(user, doctype, docname): + influxframework.db.delete( + "User Permission", + { + "apply_to_all_doctypes": 1, + "for_value": docname, + "allow": doctype, + "user": user, + }, + ) + + +def update_applicable(already_applied, to_apply, user, doctype, docname): + for applied in already_applied: + if applied not in to_apply: + influxframework.db.delete( + "User Permission", + { + "applicable_for": applied, + "for_value": docname, + "allow": doctype, + "user": user, + }, + ) diff --git a/influxframework/core/doctype/user_permission/user_permission_help.html b/influxframework/core/doctype/user_permission/user_permission_help.html new file mode 100644 index 0000000..d4b48fe --- /dev/null +++ b/influxframework/core/doctype/user_permission/user_permission_help.html @@ -0,0 +1,8 @@ +
    +
    {%= __("Records for following doctypes will be filtered") %}
    +
      + {% Object.keys(linked_doctypes).forEach(key => { %} +
    • = 10 ? "col-md-4" : "" }}>{%= __(key) %}
    • + {% }) %} +
    +
    \ No newline at end of file diff --git a/influxframework/core/doctype/user_permission/user_permission_list.js b/influxframework/core/doctype/user_permission/user_permission_list.js new file mode 100644 index 0000000..cc263b5 --- /dev/null +++ b/influxframework/core/doctype/user_permission/user_permission_list.js @@ -0,0 +1,293 @@ +influxframework.listview_settings["User Permission"] = { + onload: function (list_view) { + var me = this; + list_view.page.add_inner_button(__("Add / Update"), function () { + let dialog = new influxframework.ui.Dialog({ + title: __("Add User Permissions"), + fields: [ + { + fieldname: "user", + label: __("For User"), + fieldtype: "Link", + options: "User", + reqd: 1, + onchange: function () { + dialog.fields_dict.doctype.set_input(undefined); + dialog.fields_dict.docname.set_input(undefined); + dialog.set_df_property("docname", "hidden", 1); + dialog.set_df_property("is_default", "hidden", 1); + dialog.set_df_property("apply_to_all_doctypes", "hidden", 1); + dialog.set_df_property("applicable_doctypes", "hidden", 1); + dialog.set_df_property("hide_descendants", "hidden", 1); + }, + }, + { + fieldname: "doctype", + label: __("Document Type"), + fieldtype: "Link", + options: "DocType", + reqd: 1, + onchange: function () { + me.on_doctype_change(dialog); + }, + }, + { + fieldname: "docname", + label: __("Document Name"), + fieldtype: "Dynamic Link", + options: "doctype", + hidden: 1, + onchange: function () { + let field = dialog.fields_dict["docname"]; + if (field.value != field.last_value) { + if ( + dialog.fields_dict.doctype.value && + dialog.fields_dict.docname.value && + dialog.fields_dict.user.value + ) { + me.get_applicable_doctype(dialog).then((applicable) => { + me.get_multi_select_options(dialog, applicable).then( + (options) => { + me.applicable_options = options; + me.on_docname_change(dialog, options, applicable); + if (options.length > 5) { + dialog.fields_dict.applicable_doctypes.setup_select_all(); + } + } + ); + }); + } + } + }, + }, + { + fieldtype: "Section Break", + hide_border: 1, + }, + { + fieldname: "is_default", + label: __("Is Default"), + fieldtype: "Check", + hidden: 1, + }, + { + fieldname: "apply_to_all_doctypes", + label: __("Apply to all Documents Types"), + fieldtype: "Check", + hidden: 1, + onchange: function () { + if ( + dialog.fields_dict.doctype.value && + dialog.fields_dict.docname.value && + dialog.fields_dict.user.value + ) { + me.on_apply_to_all_doctypes_change(dialog, me.applicable_options); + if (me.applicable_options.length > 5) { + dialog.fields_dict.applicable_doctypes.setup_select_all(); + } + } + }, + }, + { + fieldtype: "Column Break", + }, + { + fieldname: "hide_descendants", + label: __("Hide Descendants"), + fieldtype: "Check", + hidden: 1, + }, + { + fieldtype: "Section Break", + hide_border: 1, + }, + { + label: __("Applicable Document Types"), + fieldname: "applicable_doctypes", + fieldtype: "MultiCheck", + options: [], + columns: 2, + hidden: 1, + }, + ], + primary_action: (data) => { + data = me.validate(dialog, data); + influxframework.call({ + async: false, + method: "influxframework.core.doctype.user_permission.user_permission.add_user_permissions", + args: { + data: data, + }, + callback: function (r) { + if (r.message === 1) { + influxframework.show_alert({ + message: __("User Permissions created sucessfully"), + indicator: "blue", + }); + } else { + influxframework.show_alert({ + message: __("Nothing to update"), + indicator: "red", + }); + } + }, + }); + dialog.hide(); + list_view.refresh(); + }, + primary_action_label: __("Submit"), + }); + dialog.show(); + }); + list_view.page.add_inner_button(__("Bulk Delete"), function () { + const dialog = new influxframework.ui.Dialog({ + title: __("Clear User Permissions"), + fields: [ + { + fieldname: "user", + label: __("For User"), + fieldtype: "Link", + options: "User", + reqd: 1, + }, + { + fieldname: "for_doctype", + label: __("For Document Type"), + fieldtype: "Link", + options: "DocType", + reqd: 1, + }, + ], + primary_action: (data) => { + // mandatory not filled + if (!data) return; + + influxframework.confirm(__("Are you sure?"), () => { + influxframework + .xcall( + "influxframework.core.doctype.user_permission.user_permission.clear_user_permissions", + data + ) + .then((data) => { + dialog.hide(); + let message = ""; + if (data === 0) { + message = __("No records deleted"); + } else if (data === 1) { + message = __("{0} record deleted", [data]); + } else { + message = __("{0} records deleted", [data]); + } + influxframework.show_alert({ + message, + indicator: "info", + }); + list_view.refresh(); + }); + }); + }, + primary_action_label: __("Delete"), + }); + + dialog.show(); + }); + }, + + validate: function (dialog, data) { + if (dialog.fields_dict.applicable_doctypes.get_unchecked_options().length == 0) { + data.apply_to_all_doctypes = 1; + data.applicable_doctypes = []; + return data; + } + if (data.apply_to_all_doctypes == 0 && !("applicable_doctypes" in data)) { + influxframework.throw(__("Please select applicable Doctypes")); + } + return data; + }, + + get_applicable_doctype: function (dialog) { + return new Promise((resolve) => { + influxframework + .call({ + method: "influxframework.core.doctype.user_permission.user_permission.check_applicable_doc_perm", + async: false, + args: { + user: dialog.fields_dict.user.value, + doctype: dialog.fields_dict.doctype.value, + docname: dialog.fields_dict.docname.value, + }, + }) + .then((r) => { + resolve(r.message); + }); + }); + }, + + get_multi_select_options: function (dialog, applicable) { + return new Promise((resolve) => { + influxframework + .call({ + method: "influxframework.desk.form.linked_with.get_linked_doctypes", + async: false, + args: { + user: dialog.fields_dict.user.value, + doctype: dialog.fields_dict.doctype.value, + docname: dialog.fields_dict.docname.value, + }, + }) + .then((r) => { + var options = []; + for (var d in r.message) { + var checked = $.inArray(d, applicable) != -1 ? 1 : 0; + options.push({ label: d, value: d, checked: checked }); + } + resolve(options); + }); + }); + }, + + on_doctype_change: function (dialog) { + dialog.set_df_property("docname", "hidden", 0); + dialog.set_df_property("docname", "reqd", 1); + dialog.set_df_property("is_default", "hidden", 0); + dialog.set_df_property("apply_to_all_doctypes", "hidden", 0); + dialog.set_value("apply_to_all_doctypes", "checked", 1); + let show = influxframework.boot.nested_set_doctypes.includes(dialog.get_value("doctype")); + dialog.set_df_property("hide_descendants", "hidden", !show); + dialog.refresh(); + }, + + on_docname_change: function (dialog, options, applicable) { + if (applicable.length != 0) { + dialog.set_primary_action("Update"); + dialog.set_title("Update User Permissions"); + dialog.set_df_property("applicable_doctypes", "options", options); + if ( + dialog.fields_dict.applicable_doctypes.get_checked_options().length == + options.length + ) { + dialog.set_df_property("applicable_doctypes", "hidden", 1); + } else { + dialog.set_df_property("applicable_doctypes", "hidden", 0); + dialog.set_df_property("apply_to_all_doctypes", "checked", 0); + } + } else { + dialog.set_primary_action("Submit"); + dialog.set_title("Add User Permissions"); + dialog.set_df_property("applicable_doctypes", "options", options); + dialog.set_df_property("applicable_doctypes", "hidden", 1); + } + dialog.refresh(); + }, + + on_apply_to_all_doctypes_change: function (dialog, options) { + if (dialog.fields_dict.apply_to_all_doctypes.get_value() == 0) { + dialog.set_df_property("applicable_doctypes", "hidden", 0); + dialog.set_df_property("applicable_doctypes", "options", options); + } else { + dialog.set_df_property("applicable_doctypes", "options", options); + dialog.set_df_property("applicable_doctypes", "hidden", 1); + } + dialog.refresh_sections(); + }, +}; diff --git a/influxframework/core/doctype/user_select_document_type/__init__.py b/influxframework/core/doctype/user_select_document_type/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/user_select_document_type/user_select_document_type.json b/influxframework/core/doctype/user_select_document_type/user_select_document_type.json new file mode 100644 index 0000000..86e1942 --- /dev/null +++ b/influxframework/core/doctype/user_select_document_type/user_select_document_type.json @@ -0,0 +1,33 @@ +{ + "actions": [], + "creation": "2021-01-17 18:28:14.208576", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type" + ], + "fields": [ + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-01-17 18:45:44.993190", + "modified_by": "Administrator", + "module": "Core", + "name": "User Select Document Type", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/user_select_document_type/user_select_document_type.py b/influxframework/core/doctype/user_select_document_type/user_select_document_type.py new file mode 100644 index 0000000..994dfbf --- /dev/null +++ b/influxframework/core/doctype/user_select_document_type/user_select_document_type.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class UserSelectDocumentType(Document): + pass diff --git a/influxframework/core/doctype/user_social_login/__init__.py b/influxframework/core/doctype/user_social_login/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/user_social_login/user_social_login.json b/influxframework/core/doctype/user_social_login/user_social_login.json new file mode 100644 index 0000000..3cac838 --- /dev/null +++ b/influxframework/core/doctype/user_social_login/user_social_login.json @@ -0,0 +1,189 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2017-12-02 13:01:20.507112", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "provider", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Provider", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "section_break_0", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "username", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Username", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_0", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "userid", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "User ID", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "max_attachments": 0, + "modified": "2017-12-02 15:37:58.397062", + "modified_by": "Administrator", + "module": "Core", + "name": "User Social Login", + "name_case": "", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/influxframework/core/doctype/user_social_login/user_social_login.py b/influxframework/core/doctype/user_social_login/user_social_login.py new file mode 100644 index 0000000..6e92735 --- /dev/null +++ b/influxframework/core/doctype/user_social_login/user_social_login.py @@ -0,0 +1,8 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE + +from influxframework.model.document import Document + + +class UserSocialLogin(Document): + pass diff --git a/influxframework/core/doctype/user_type/__init__.py b/influxframework/core/doctype/user_type/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/user_type/test_user_type.py b/influxframework/core/doctype/user_type/test_user_type.py new file mode 100644 index 0000000..5fc206f --- /dev/null +++ b/influxframework/core/doctype/user_type/test_user_type.py @@ -0,0 +1,64 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.installer import update_site_config +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestUserType(InfluxFrameworkTestCase): + def setUp(self): + create_role() + + def test_add_select_perm_doctypes(self): + user_type = create_user_type("Test User Type") + + # select perms added for all link fields + doc = influxframework.get_meta("Contact") + link_fields = doc.get_link_fields() + select_doctypes = influxframework.get_all( + "User Select Document Type", {"parent": user_type.name}, pluck="document_type" + ) + + for entry in link_fields: + self.assertTrue(entry.options in select_doctypes) + + # select perms added for all child table link fields + link_fields = [] + for child_table in doc.get_table_fields(): + child_doc = influxframework.get_meta(child_table.options) + link_fields.extend(child_doc.get_link_fields()) + + for entry in link_fields: + self.assertTrue(entry.options in select_doctypes) + + def tearDown(self): + influxframework.db.rollback() + + +def create_user_type(user_type): + if influxframework.db.exists("User Type", user_type): + influxframework.delete_doc("User Type", user_type) + + user_type_limit = {influxframework.scrub(user_type): 1} + update_site_config("user_type_doctype_limit", user_type_limit) + + doc = influxframework.get_doc( + { + "doctype": "User Type", + "name": user_type, + "role": "_Test User Type", + "user_id_field": "user", + "apply_user_permission_on": "User", + } + ) + + doc.append("user_doctypes", {"document_type": "Contact", "read": 1, "write": 1}) + + return doc.insert() + + +def create_role(): + if not influxframework.db.exists("Role", "_Test User Type"): + influxframework.get_doc( + {"doctype": "Role", "role_name": "_Test User Type", "desk_access": 1, "is_custom": 1} + ).insert() diff --git a/influxframework/core/doctype/user_type/user_type.js b/influxframework/core/doctype/user_type/user_type.js new file mode 100644 index 0000000..312330c --- /dev/null +++ b/influxframework/core/doctype/user_type/user_type.js @@ -0,0 +1,71 @@ +// Copyright (c) 2021, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("User Type", { + refresh: function (frm) { + if (frm.is_new() && !influxframework.boot.developer_mode) frm.set_value("is_standard", 1); + + frm.set_query("document_type", "user_doctypes", function () { + return { + filters: { + istable: 0, + }, + }; + }); + + frm.set_query("document_type", "select_doctypes", function () { + return { + filters: { + istable: 0, + }, + }; + }); + + frm.set_query("document_type", "custom_select_doctypes", function () { + return { + filters: { + istable: 0, + }, + }; + }); + + frm.set_query("role", function () { + return { + filters: { + is_custom: 1, + disabled: 0, + desk_access: 1, + }, + }; + }); + + frm.set_query("apply_user_permission_on", function () { + return { + query: "influxframework.core.doctype.user_type.user_type.get_user_linked_doctypes", + }; + }); + }, + + onload: function (frm) { + frm.trigger("get_user_id_fields"); + }, + + apply_user_permission_on: function (frm) { + frm.set_value("user_id_field", ""); + frm.trigger("get_user_id_fields"); + }, + + get_user_id_fields: function (frm) { + if (frm.doc.apply_user_permission_on) { + influxframework.call({ + method: "influxframework.core.doctype.user_type.user_type.get_user_id", + args: { + parent: frm.doc.apply_user_permission_on, + }, + callback: function (r) { + set_field_options("user_id_field", [""].concat(r.message)); + }, + }); + } + }, +}); diff --git a/influxframework/core/doctype/user_type/user_type.json b/influxframework/core/doctype/user_type/user_type.json new file mode 100644 index 0000000..d816638 --- /dev/null +++ b/influxframework/core/doctype/user_type/user_type.json @@ -0,0 +1,145 @@ +{ + "actions": [], + "autoname": "Prompt", + "creation": "2021-01-13 01:48:02.378548", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "is_standard", + "section_break_2", + "role", + "column_break_4", + "apply_user_permission_on", + "user_id_field", + "section_break_6", + "user_doctypes", + "custom_select_doctypes", + "select_doctypes", + "allowed_modules_section", + "user_type_modules" + ], + "fields": [ + { + "default": "0", + "depends_on": "eval: influxframework.boot.developer_mode", + "fieldname": "is_standard", + "fieldtype": "Check", + "label": "Is Standard", + "read_only_depends_on": "eval: !influxframework.boot.developer_mode" + }, + { + "depends_on": "eval: !doc.is_standard", + "fieldname": "section_break_2", + "fieldtype": "Section Break", + "label": "Document Types and Permissions" + }, + { + "depends_on": "eval: !doc.is_standard", + "fieldname": "user_doctypes", + "fieldtype": "Table", + "label": "Document Types", + "mandatory_depends_on": "eval: !doc.is_standard", + "options": "User Document Type" + }, + { + "depends_on": "eval: !doc.is_standard", + "fieldname": "role", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Role", + "mandatory_depends_on": "eval: !doc.is_standard", + "options": "Role" + }, + { + "fieldname": "select_doctypes", + "fieldtype": "Table", + "hidden": 1, + "label": "Document Types (Select Permissions Only)", + "options": "User Select Document Type", + "read_only": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval: !doc.is_standard", + "description": "Can only list down the document types which has been linked to the User document type.", + "fieldname": "apply_user_permission_on", + "fieldtype": "Link", + "label": "Apply User Permission On", + "mandatory_depends_on": "eval: !doc.is_standard", + "options": "DocType" + }, + { + "depends_on": "eval: !doc.is_standard", + "fieldname": "section_break_6", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "depends_on": "apply_user_permission_on", + "fieldname": "user_id_field", + "fieldtype": "Select", + "label": "User Id Field", + "mandatory_depends_on": "eval: !doc.is_standard" + }, + { + "depends_on": "eval: !doc.is_standard", + "fieldname": "allowed_modules_section", + "fieldtype": "Section Break", + "label": "Allowed Modules" + }, + { + "fieldname": "user_type_modules", + "fieldtype": "Table", + "label": "User Type Module", + "no_copy": 1, + "options": "User Type Module", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "custom_select_doctypes", + "fieldtype": "Table", + "label": "Custom Document Types (Select Permission)", + "options": "User Select Document Type" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2022-06-09 14:00:36.820306", + "modified_by": "Administrator", + "module": "Core", + "name": "User Type", + "naming_rule": "Set by user", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} diff --git a/influxframework/core/doctype/user_type/user_type.py b/influxframework/core/doctype/user_type/user_type.py new file mode 100644 index 0000000..bff3ea3 --- /dev/null +++ b/influxframework/core/doctype/user_type/user_type.py @@ -0,0 +1,326 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework import _ +from influxframework.config import get_modules_from_app +from influxframework.model.document import Document +from influxframework.permissions import add_permission, add_user_permission +from influxframework.utils import get_link_to_form + + +class UserType(Document): + def validate(self): + self.set_modules() + self.add_select_perm_doctypes() + + def on_update(self): + if self.is_standard: + return + + self.validate_document_type_limit() + self.validate_role() + self.add_role_permissions_for_user_doctypes() + self.add_role_permissions_for_select_doctypes() + self.add_role_permissions_for_file() + self.update_users() + get_non_standard_user_type_details() + self.remove_permission_for_deleted_doctypes() + + def on_trash(self): + if self.is_standard: + influxframework.throw(_("Standard user type {0} can not be deleted.").format(influxframework.bold(self.name))) + + def set_modules(self): + if not self.user_doctypes: + return + + modules = influxframework.get_all( + "DocType", + filters={"name": ("in", [d.document_type for d in self.user_doctypes])}, + distinct=True, + pluck="module", + ) + + self.set("user_type_modules", []) + for module in modules: + self.append("user_type_modules", {"module": module}) + + def validate_document_type_limit(self): + limit = influxframework.conf.get("user_type_doctype_limit", {}).get(influxframework.scrub(self.name)) + + if not limit and influxframework.session.user != "Administrator": + influxframework.throw( + _("User does not have permission to create the new {0}").format(influxframework.bold(_("User Type"))), + title=_("Permission Error"), + ) + + if not limit: + influxframework.throw( + _("The limit has not set for the user type {0} in the site config file.").format( + influxframework.bold(self.name) + ), + title=_("Set Limit"), + ) + + if self.user_doctypes and len(self.user_doctypes) > limit: + influxframework.throw( + _("The total number of user document types limit has been crossed."), + title=_("User Document Types Limit Exceeded"), + ) + + custom_doctypes = [row.document_type for row in self.user_doctypes if row.is_custom] + if custom_doctypes and len(custom_doctypes) > 3: + influxframework.throw( + _("You can only set the 3 custom doctypes in the Document Types table."), + title=_("Custom Document Types Limit Exceeded"), + ) + + def validate_role(self): + if not self.role: + influxframework.throw(_("The field {0} is mandatory").format(influxframework.bold(_("Role")))) + + if not influxframework.db.get_value("Role", self.role, "is_custom"): + influxframework.throw( + _("The role {0} should be a custom role.").format( + influxframework.bold(get_link_to_form("Role", self.role)) + ) + ) + + def update_users(self): + for row in influxframework.get_all("User", filters={"user_type": self.name}): + user = influxframework.get_cached_doc("User", row.name) + self.update_roles_in_user(user) + self.update_modules_in_user(user) + user.update_children() + + def update_roles_in_user(self, user): + user.set("roles", []) + user.append("roles", {"role": self.role}) + + def update_modules_in_user(self, user): + block_modules = influxframework.get_all( + "Module Def", + fields=["name as module"], + filters={"name": ["not in", [d.module for d in self.user_type_modules]]}, + ) + + if block_modules: + user.set("block_modules", block_modules) + + def add_role_permissions_for_user_doctypes(self): + perms = ["read", "write", "create", "submit", "cancel", "amend", "delete"] + for row in self.user_doctypes: + docperm = add_role_permissions(row.document_type, self.role) + + values = {perm: row.get(perm) or 0 for perm in perms} + for perm in ["print", "email", "share"]: + values[perm] = 1 + + influxframework.db.set_value("Custom DocPerm", docperm, values) + + def add_select_perm_doctypes(self): + if influxframework.flags.ignore_select_perm: + return + + self.select_doctypes = [] + + select_doctypes = [] + user_doctypes = [row.document_type for row in self.user_doctypes] + + for doctype in user_doctypes: + doc = influxframework.get_meta(doctype) + self.prepare_select_perm_doctypes(doc, user_doctypes, select_doctypes) + + for child_table in doc.get_table_fields(): + child_doc = influxframework.get_meta(child_table.options) + if child_doc: + self.prepare_select_perm_doctypes(child_doc, user_doctypes, select_doctypes) + + if select_doctypes: + select_doctypes = set(select_doctypes) + for select_doctype in select_doctypes: + self.append("select_doctypes", {"document_type": select_doctype}) + + def prepare_select_perm_doctypes(self, doc, user_doctypes, select_doctypes): + for field in doc.get_link_fields(): + if field.options not in user_doctypes: + select_doctypes.append(field.options) + + def add_role_permissions_for_select_doctypes(self): + for doctype in ["select_doctypes", "custom_select_doctypes"]: + for row in self.get(doctype): + docperm = add_role_permissions(row.document_type, self.role) + influxframework.db.set_value( + "Custom DocPerm", docperm, {"select": 1, "read": 0, "create": 0, "write": 0} + ) + + def add_role_permissions_for_file(self): + docperm = add_role_permissions("File", self.role) + influxframework.db.set_value("Custom DocPerm", docperm, {"read": 1, "create": 1, "write": 1}) + + def remove_permission_for_deleted_doctypes(self): + doctypes = [d.document_type for d in self.user_doctypes] + + # Do not remove the doc permission for the file doctype + doctypes.append("File") + + for doctype in ["select_doctypes", "custom_select_doctypes"]: + for dt in self.get(doctype): + doctypes.append(dt.document_type) + + for perm in influxframework.get_all( + "Custom DocPerm", filters={"role": self.role, "parent": ["not in", doctypes]} + ): + influxframework.delete_doc("Custom DocPerm", perm.name) + + +def add_role_permissions(doctype, role): + name = influxframework.get_value("Custom DocPerm", dict(parent=doctype, role=role, permlevel=0)) + + if not name: + name = add_permission(doctype, role, 0) + + return name + + +def get_non_standard_user_type_details(): + user_types = influxframework.get_all( + "User Type", + fields=["apply_user_permission_on", "name", "user_id_field"], + filters={"is_standard": 0}, + ) + + if user_types: + user_type_details = {d.name: [d.apply_user_permission_on, d.user_id_field] for d in user_types} + + influxframework.cache().set_value("non_standard_user_types", user_type_details) + + return user_type_details + + +@influxframework.whitelist() +@influxframework.validate_and_sanitize_search_inputs +def get_user_linked_doctypes(doctype, txt, searchfield, start, page_len, filters): + modules = [d.get("module_name") for d in get_modules_from_app("influxframework")] + + filters = [ + ["DocField", "options", "=", "User"], + ["DocType", "is_submittable", "=", 0], + ["DocType", "issingle", "=", 0], + ["DocType", "module", "not in", modules], + ["DocType", "read_only", "=", 0], + ["DocType", "name", "like", f"%{txt}%"], + ] + + doctypes = influxframework.get_all( + "DocType", + fields=["`tabDocType`.`name`"], + filters=filters, + order_by="`tabDocType`.`idx` desc", + limit_start=start, + limit_page_length=page_len, + as_list=1, + ) + + custom_dt_filters = [ + ["Custom Field", "dt", "like", f"%{txt}%"], + ["Custom Field", "options", "=", "User"], + ["Custom Field", "fieldtype", "=", "Link"], + ] + + custom_doctypes = influxframework.get_all( + "Custom Field", fields=["dt as name"], filters=custom_dt_filters, as_list=1 + ) + + return doctypes + custom_doctypes + + +@influxframework.whitelist() +def get_user_id(parent): + data = ( + influxframework.get_all( + "DocField", + fields=["label", "fieldname as value"], + filters={"options": "User", "fieldtype": "Link", "parent": parent}, + ) + or [] + ) + + data.extend( + influxframework.get_all( + "Custom Field", + fields=["label", "fieldname as value"], + filters={"options": "User", "fieldtype": "Link", "dt": parent}, + ) + ) + + return data + + +def user_linked_with_permission_on_doctype(doc, user): + if not doc.apply_user_permission_on: + return True + + if not doc.user_id_field: + influxframework.throw(_("User Id Field is mandatory in the user type {0}").format(influxframework.bold(doc.name))) + + if influxframework.db.get_value(doc.apply_user_permission_on, {doc.user_id_field: user}, "name"): + return True + else: + label = influxframework.get_meta(doc.apply_user_permission_on).get_field(doc.user_id_field).label + + influxframework.msgprint( + _( + "To set the role {0} in the user {1}, kindly set the {2} field as {3} in one of the {4} record." + ).format( + influxframework.bold(doc.role), + influxframework.bold(user), + influxframework.bold(label), + influxframework.bold(user), + influxframework.bold(doc.apply_user_permission_on), + ) + ) + + return False + + +def apply_permissions_for_non_standard_user_type(doc, method=None): + """Create user permission for the non standard user type""" + if not influxframework.db.table_exists("User Type"): + return + + user_types = influxframework.cache().get_value("non_standard_user_types") + + if not user_types: + user_types = get_non_standard_user_type_details() + + if not user_types: + return + + for user_type, data in user_types.items(): + if not doc.get(data[1]) or doc.doctype != data[0]: + continue + + if influxframework.get_cached_value("User", doc.get(data[1]), "user_type") != user_type: + return + + if doc.get(data[1]) and ( + not doc._doc_before_save + or doc.get(data[1]) != doc._doc_before_save.get(data[1]) + or not influxframework.db.get_value( + "User Permission", {"user": doc.get(data[1]), "allow": data[0], "for_value": doc.name}, "name" + ) + ): + + perm_data = influxframework.db.get_value( + "User Permission", {"allow": doc.doctype, "for_value": doc.name}, ["name", "user"] + ) + + if not perm_data: + user_doc = influxframework.get_cached_doc("User", doc.get(data[1])) + user_doc.set_roles_and_modules_based_on_user_type() + user_doc.update_children() + add_user_permission(doc.doctype, doc.name, doc.get(data[1])) + else: + influxframework.db.set_value("User Permission", perm_data[0], "user", doc.get(data[1])) diff --git a/influxframework/core/doctype/user_type/user_type_dashboard.py b/influxframework/core/doctype/user_type/user_type_dashboard.py new file mode 100644 index 0000000..8aef573 --- /dev/null +++ b/influxframework/core/doctype/user_type/user_type_dashboard.py @@ -0,0 +1,5 @@ +from influxframework import _ + + +def get_data(): + return {"fieldname": "user_type", "transactions": [{"label": _("Reference"), "items": ["User"]}]} diff --git a/influxframework/core/doctype/user_type/user_type_list.js b/influxframework/core/doctype/user_type/user_type_list.js new file mode 100644 index 0000000..ffd46fc --- /dev/null +++ b/influxframework/core/doctype/user_type/user_type_list.js @@ -0,0 +1,10 @@ +influxframework.listview_settings["User Type"] = { + add_fields: ["is_standard"], + get_indicator: function (doc) { + if (doc.is_standard) { + return [__("Standard"), "green", "is_standard,=,1"]; + } else { + return [__("Custom"), "blue", "is_standard,=,0"]; + } + }, +}; diff --git a/influxframework/core/doctype/user_type_module/__init__.py b/influxframework/core/doctype/user_type_module/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/user_type_module/user_type_module.json b/influxframework/core/doctype/user_type_module/user_type_module.json new file mode 100644 index 0000000..0f9cbef --- /dev/null +++ b/influxframework/core/doctype/user_type_module/user_type_module.json @@ -0,0 +1,33 @@ +{ + "actions": [], + "creation": "2021-01-24 03:05:24.634719", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "module" + ], + "fields": [ + { + "fieldname": "module", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Module", + "options": "Module Def", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-01-24 03:07:43.602927", + "modified_by": "Administrator", + "module": "Core", + "name": "User Type Module", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/core/doctype/user_type_module/user_type_module.py b/influxframework/core/doctype/user_type_module/user_type_module.py new file mode 100644 index 0000000..e8374e9 --- /dev/null +++ b/influxframework/core/doctype/user_type_module/user_type_module.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class UserTypeModule(Document): + pass diff --git a/influxframework/core/doctype/version/__init__.py b/influxframework/core/doctype/version/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/version/test_records.json b/influxframework/core/doctype/version/test_records.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/influxframework/core/doctype/version/test_records.json @@ -0,0 +1 @@ +[] diff --git a/influxframework/core/doctype/version/test_version.py b/influxframework/core/doctype/version/test_version.py new file mode 100644 index 0000000..57f9555 --- /dev/null +++ b/influxframework/core/doctype/version/test_version.py @@ -0,0 +1,58 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import copy + +import influxframework +from influxframework.core.doctype.version.version import get_diff +from influxframework.test_runner import make_test_objects +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestVersion(InfluxFrameworkTestCase): + def test_get_diff(self): + influxframework.set_user("Administrator") + test_records = make_test_objects("Event", reset=True) + old_doc = influxframework.get_doc("Event", test_records[0]) + new_doc = copy.deepcopy(old_doc) + + old_doc.color = None + new_doc.color = "#fafafa" + + diff = get_diff(old_doc, new_doc)["changed"] + + self.assertEqual(get_fieldnames(diff)[0], "color") + self.assertTrue(get_old_values(diff)[0] is None) + self.assertEqual(get_new_values(diff)[0], "#fafafa") + + new_doc.starts_on = "2017-07-20" + + diff = get_diff(old_doc, new_doc)["changed"] + + self.assertEqual(get_fieldnames(diff)[1], "starts_on") + self.assertEqual(get_old_values(diff)[1], "01-01-2014 00:00:00") + self.assertEqual(get_new_values(diff)[1], "07-20-2017 00:00:00") + + def test_no_version_on_new_doc(self): + from influxframework.desk.form.load import get_versions + + t = influxframework.get_doc(doctype="ToDo", description="something") + t.save(ignore_version=False) + + self.assertFalse(get_versions(t)) + + t = influxframework.get_doc(t.doctype, t.name) + t.description = "changed" + t.save(ignore_version=False) + self.assertTrue(get_versions(t)) + + +def get_fieldnames(change_array): + return [d[0] for d in change_array] + + +def get_old_values(change_array): + return [d[1] for d in change_array] + + +def get_new_values(change_array): + return [d[2] for d in change_array] diff --git a/influxframework/core/doctype/version/version.js b/influxframework/core/doctype/version/version.js new file mode 100644 index 0000000..f58fba1 --- /dev/null +++ b/influxframework/core/doctype/version/version.js @@ -0,0 +1,12 @@ +influxframework.ui.form.on("Version", "refresh", function (frm) { + $( + influxframework.render_template("version_view", { doc: frm.doc, data: JSON.parse(frm.doc.data) }) + ).appendTo(frm.fields_dict.table_html.$wrapper.empty()); + + frm.add_custom_button(__("Show all Versions"), function () { + influxframework.set_route("List", "Version", { + ref_doctype: frm.doc.ref_doctype, + docname: frm.doc.docname, + }); + }); +}); diff --git a/influxframework/core/doctype/version/version.json b/influxframework/core/doctype/version/version.json new file mode 100644 index 0000000..463a7d3 --- /dev/null +++ b/influxframework/core/doctype/version/version.json @@ -0,0 +1,247 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "hash", + "beta": 0, + "creation": "2014-02-20 17:22:37", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 0, + "engine": "InnoDB", + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "ref_doctype", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "DocType", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_3", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "docname", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Document Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "data", + "fieldtype": "Code", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Data", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "section_break_4", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "table_html", + "fieldtype": "HTML", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Table HTML", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "icon": "fa fa-copy", + "idx": 1, + "image_view": 0, + "in_create": 1, + + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2018-04-10 14:39:45.926836", + "modified_by": "Administrator", + "module": "Core", + "name": "Version", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 0, + "export": 1, + "if_owner": 0, + "import": 0, + "is_custom": 0, + "permlevel": 0, + "print": 0, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 0, + "submit": 0, + "write": 0 + }, + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 0, + "delete": 1, + "email": 0, + "export": 0, + "if_owner": 0, + "import": 0, + "is_custom": 0, + "permlevel": 0, + "print": 0, + "read": 1, + "report": 0, + "role": "Administrator", + "set_user_permissions": 0, + "share": 0, + "submit": 0, + "write": 0 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "sort_order": "ASC", + "title_field": "docname", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/influxframework/core/doctype/version/version.py b/influxframework/core/doctype/version/version.py new file mode 100644 index 0000000..a466838 --- /dev/null +++ b/influxframework/core/doctype/version/version.py @@ -0,0 +1,131 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +import influxframework +from influxframework.model import no_value_fields, table_fields +from influxframework.model.document import Document + + +class Version(Document): + def update_version_info(self, old: Document | None, new: Document) -> bool: + """Update changed info and return true if change contains useful data.""" + if not old: + # Check if doc has some information about creation source like data import + return self.for_insert(new) + else: + return self.set_diff(old, new) + + def set_diff(self, old: Document, new: Document) -> bool: + """Set the data property with the diff of the docs if present""" + diff = get_diff(old, new) + if diff: + self.ref_doctype = new.doctype + self.docname = new.name + self.data = influxframework.as_json(diff, indent=None, separators=(",", ":")) + return True + else: + return False + + def for_insert(self, doc: Document) -> bool: + updater_reference = doc.flags.updater_reference + if not updater_reference: + return False + + data = { + "creation": doc.creation, + "updater_reference": updater_reference, + "created_by": doc.owner, + } + self.ref_doctype = doc.doctype + self.docname = doc.name + self.data = influxframework.as_json(data, indent=None, separators=(",", ":")) + return True + + def get_data(self): + return json.loads(self.data) + + +def get_diff(old, new, for_child=False): + """Get diff between 2 document objects + + If there is a change, then returns a dict like: + + { + "changed" : [[fieldname1, old, new], [fieldname2, old, new]], + "added" : [[table_fieldname1, {dict}], ], + "removed" : [[table_fieldname1, {dict}], ], + "row_changed": [[table_fieldname1, row_name1, row_index, + [[child_fieldname1, old, new], + [child_fieldname2, old, new]], ] + ], + + }""" + if not new: + return None + + blacklisted_fields = ["Markdown Editor", "Text Editor", "Code", "HTML Editor"] + + # capture data import if set + data_import = new.flags.via_data_import + updater_reference = new.flags.updater_reference + + out = influxframework._dict( + changed=[], + added=[], + removed=[], + row_changed=[], + data_import=data_import, + updater_reference=updater_reference, + ) + + for df in new.meta.fields: + if df.fieldtype in no_value_fields and df.fieldtype not in table_fields: + continue + + old_value, new_value = old.get(df.fieldname), new.get(df.fieldname) + + if df.fieldtype in table_fields: + # make maps + old_row_by_name, new_row_by_name = {}, {} + for d in old_value: + old_row_by_name[d.name] = d + for d in new_value: + new_row_by_name[d.name] = d + + # check rows for additions, changes + for i, d in enumerate(new_value): + if d.name in old_row_by_name: + diff = get_diff(old_row_by_name[d.name], d, for_child=True) + if diff and diff.changed: + out.row_changed.append((df.fieldname, i, d.name, diff.changed)) + else: + out.added.append([df.fieldname, d.as_dict()]) + + # check for deletions + for d in old_value: + if not d.name in new_row_by_name: + out.removed.append([df.fieldname, d.as_dict()]) + + elif old_value != new_value: + if df.fieldtype not in blacklisted_fields: + old_value = old.get_formatted(df.fieldname) if old_value else old_value + new_value = new.get_formatted(df.fieldname) if new_value else new_value + + if old_value != new_value: + out.changed.append((df.fieldname, old_value, new_value)) + + # docstatus + if not for_child and old.docstatus != new.docstatus: + out.changed.append(["docstatus", old.docstatus, new.docstatus]) + + if any((out.changed, out.added, out.removed, out.row_changed)): + return out + + else: + return None + + +def on_doctype_update(): + influxframework.db.add_index("Version", ["ref_doctype", "docname"]) diff --git a/influxframework/core/doctype/version/version_view.html b/influxframework/core/doctype/version/version_view.html new file mode 100644 index 0000000..ec48810 --- /dev/null +++ b/influxframework/core/doctype/version/version_view.html @@ -0,0 +1,95 @@ +
    +{% if data.comment %} +

    {{ __("Comment") + " (" + data.comment_type }})

    +

    {{ data.comment }}

    +{% endif %} + +{% if data.changed && data.changed.length %} +

    {{ __("Values Changed") }}

    + + + + + + + + + + {% for item in data.changed %} + + + + + + {% endfor %} + +
    {{ __("Property") }}{{ __("Original Value") }}{{ __("New Value") }}
    {{ influxframework.meta.get_label(doc.ref_doctype, item[0]) }}{{ item[1] }}{{ item[2] }}
    +{% endif %} + +{% var _keys = ["added", "removed"]; %} +{% for key in _keys %} + {% if data[key] && data[key].length %} + {% var title = key==="added" ? __("Rows Added") : __("Rows Removed"); %} +

    {{ title }}

    + + + + + + + + + {% var values = data[key]; %} + {% for item in values %} + + + + + {% endfor %} + +
    {{ __("Property") }}{{ title }}
    {{ influxframework.meta.get_label(doc.ref_doctype, item[0]) }} + {% var item_keys = Object.keys(item[1]).sort(); %} + + + {% for row_key in item_keys %} + + + + + {% endfor %} + +
    {{ row_key }}{{ item[1][row_key] }}
    +
    + + {% endif %} +{% endfor %} + +{% if data.row_changed && data.row_changed.length %} +

    {{ __("Row Values Changed") }}

    + + + + + + + + + + + + {% var values = data.row_changed; %} + {% for table_info in values %} + {% var _changed = table_info[3]; %} + {% for item in _changed %} + + + + + + + + {% endfor %} + {% endfor %} + +{% endif %} + diff --git a/influxframework/core/doctype/view_log/__init__.py b/influxframework/core/doctype/view_log/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/doctype/view_log/test_view_log.py b/influxframework/core/doctype/view_log/test_view_log.py new file mode 100644 index 0000000..7d4f396 --- /dev/null +++ b/influxframework/core/doctype/view_log/test_view_log.py @@ -0,0 +1,34 @@ +# Copyright (c) 2018, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestViewLog(InfluxFrameworkTestCase): + def tearDown(self): + influxframework.set_user("Administrator") + + def test_if_user_is_added(self): + ev = influxframework.get_doc( + { + "doctype": "Event", + "subject": "test event for view logs", + "starts_on": "2018-06-04 14:11:00", + "event_type": "Public", + } + ).insert() + + influxframework.set_user("test@example.com") + + from influxframework.desk.form.load import getdoc + + # load the form + getdoc("Event", ev.name) + a = influxframework.get_value( + doctype="View Log", + filters={"reference_doctype": "Event", "reference_name": ev.name}, + fieldname=["viewed_by"], + ) + + self.assertEqual("test@example.com", a) + self.assertNotEqual("test1@example.com", a) diff --git a/influxframework/core/doctype/view_log/view_log.js b/influxframework/core/doctype/view_log/view_log.js new file mode 100644 index 0000000..344769c --- /dev/null +++ b/influxframework/core/doctype/view_log/view_log.js @@ -0,0 +1,6 @@ +// Copyright (c) 2018, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("View Log", { + refresh: function (frm) {}, +}); diff --git a/influxframework/core/doctype/view_log/view_log.json b/influxframework/core/doctype/view_log/view_log.json new file mode 100644 index 0000000..3c4486c --- /dev/null +++ b/influxframework/core/doctype/view_log/view_log.json @@ -0,0 +1,163 @@ +{ + "allow_copy": 0, + "allow_events_in_timeline": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2018-05-27 02:20:11.193944", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, + "fieldname": "viewed_by", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Viewed By", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 1, + "set_only_once": 1, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, + "fieldname": "reference_doctype", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Reference Document Type", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 1, + "set_only_once": 1, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Reference name", + "length": 0, + "no_copy": 0, + "options": "reference_doctype", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 1, + "set_only_once": 1, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2021-10-25 14:22:27.664645", + "modified_by": "Administrator", + "module": "Core", + "name": "View Log", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 0 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_seen": 0, + "track_views": 0 +} diff --git a/influxframework/core/doctype/view_log/view_log.py b/influxframework/core/doctype/view_log/view_log.py new file mode 100644 index 0000000..4cbf321 --- /dev/null +++ b/influxframework/core/doctype/view_log/view_log.py @@ -0,0 +1,8 @@ +# Copyright (c) 2018, InfluxFramework LLC +# License: MIT. See LICENSE + +from influxframework.model.document import Document + + +class ViewLog(Document): + pass diff --git a/influxframework/core/form_tour/doctype/doctype.json b/influxframework/core/form_tour/doctype/doctype.json new file mode 100644 index 0000000..391d3ec --- /dev/null +++ b/influxframework/core/form_tour/doctype/doctype.json @@ -0,0 +1,56 @@ +{ + "creation": "2021-11-23 12:38:52.807353", + "docstatus": 0, + "doctype": "Form Tour", + "first_document": 0, + "idx": 0, + "include_name_field": 1, + "is_standard": 1, + "modified": "2021-11-25 17:03:01.646360", + "modified_by": "Administrator", + "module": "Core", + "name": "Doctype", + "owner": "Administrator", + "reference_doctype": "DocType", + "save_on_complete": 1, + "steps": [ + { + "description": "Select a Module to which this DocType would belong", + "field": "", + "fieldname": "module", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Module", + "parent_field": "", + "position": "Right", + "title": "Module" + }, + { + "description": "Check this to make the DocType as Custom", + "field": "", + "fieldname": "custom", + "fieldtype": "Check", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Custom?", + "next_step_condition": "eval: doc.custom", + "parent_field": "", + "position": "Left", + "title": "Custom " + }, + { + "description": "A Field (or a docfield) defines a property of a DocType. You can define the column name, label, datatype and more for DocFields. For instance, a ToDo doctype has fields description, status and priority. These ultimately become columns in the database table tabToDo.", + "field": "", + "fieldname": "fields", + "fieldtype": "Table", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Fields", + "parent_field": "", + "position": "Top", + "title": "Fields" + } + ], + "title": "Doctype" +} \ No newline at end of file diff --git a/influxframework/core/notifications.py b/influxframework/core/notifications.py new file mode 100644 index 0000000..ecd2e6a --- /dev/null +++ b/influxframework/core/notifications.py @@ -0,0 +1,46 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework + + +def get_notification_config(): + return { + "for_doctype": { + "Error Log": {"seen": 0}, + "Communication": {"status": "Open", "communication_type": "Communication"}, + "ToDo": "influxframework.core.notifications.get_things_todo", + "Event": "influxframework.core.notifications.get_todays_events", + "Error Snapshot": {"seen": 0, "parent_error_snapshot": None}, + "Workflow Action": {"status": "Open"}, + }, + } + + +def get_things_todo(as_list=False): + """Returns a count of incomplete todos""" + data = influxframework.get_list( + "ToDo", + fields=["name", "description"] if as_list else "count(*)", + filters=[["ToDo", "status", "=", "Open"]], + or_filters=[ + ["ToDo", "allocated_to", "=", influxframework.session.user], + ["ToDo", "assigned_by", "=", influxframework.session.user], + ], + as_list=True, + ) + + if as_list: + return data + else: + return data[0][0] + + +def get_todays_events(as_list=False): + """Returns a count of todays events in calendar""" + from influxframework.desk.doctype.event.event import get_events + from influxframework.utils import nowdate + + today = nowdate() + events = get_events(today, today) + return events if as_list else len(events) diff --git a/influxframework/core/page/__init__.py b/influxframework/core/page/__init__.py new file mode 100644 index 0000000..2e11a12 --- /dev/null +++ b/influxframework/core/page/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE diff --git a/influxframework/core/page/background_jobs/__init__.py b/influxframework/core/page/background_jobs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/page/background_jobs/background_jobs.css b/influxframework/core/page/background_jobs/background_jobs.css new file mode 100644 index 0000000..7716519 --- /dev/null +++ b/influxframework/core/page/background_jobs/background_jobs.css @@ -0,0 +1,47 @@ + +.table-background-jobs { + margin-bottom: 0px; + margin-top: 0px; + font-size: var(--text-md); + table-layout: fixed; +} + +.table-background-jobs th { + font-weight: normal; + color: var(--text-muted); +} + +.table-background-jobs td { + color: var(--text-light); +} + +.table-background-jobs th, .table-background-jobs td { + padding: var(--padding-sm) var(--padding-md); +} + +.table-background-jobs tbody tr:hover { + background-color: var(--highlight-color); +} + +.job-name { + font-size: var(--text-md); + font-family: var(--font-family-monospace); + word-break: break-word; +} + +.no-background-jobs { + min-height: 320px; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; +} + +.no-background-jobs > img { + margin-bottom: var(--margin-md); + max-height: 100px; +} + +.footer { + padding: var(--padding-md); +} diff --git a/influxframework/core/page/background_jobs/background_jobs.html b/influxframework/core/page/background_jobs/background_jobs.html new file mode 100644 index 0000000..18f12c7 --- /dev/null +++ b/influxframework/core/page/background_jobs/background_jobs.html @@ -0,0 +1,58 @@ +{% if jobs.length %} +
    {{ __("Table Field") }}{{ __("Row #") }}{{ __("Property") }}{{ __("Original Value") }}{{ __("New Value") }}
    {{ influxframework.meta.get_label(doc.ref_doctype, table_info[0]) }}{{ table_info[1] }}{{ item[0] }}{{ item[1] }}{{ item[2] }}
    + + + + + + + + + + {% for j in jobs %} + + + + + + + {% endfor %} + +
    {{ __("Queue") }}{{ __("Job") }}{{ __("Status") }}{{ __("Created") }}
    + {{ toTitle(j.queue.split(":").slice(-1)[0]) }} + +
    + + {{ influxframework.utils.encode_tags(j.job_name) }} + +
    + {% if j.exc_info %} +
    + {{ __("Exception") }} +
    +
    {{ influxframework.utils.encode_tags(j.exc_info) }}
    +
    +
    + {% endif %} +
    + + {{ toTitle(j.status) }} + + + {{ influxframework.datetime.prettyDate(j.creation) }} +
    +{% else %} +
    + Empty State +

    {{ __("No jobs found on this site") }}

    +
    +{% endif %} + diff --git a/influxframework/core/page/background_jobs/background_jobs.js b/influxframework/core/page/background_jobs/background_jobs.js new file mode 100644 index 0000000..ce1277f --- /dev/null +++ b/influxframework/core/page/background_jobs/background_jobs.js @@ -0,0 +1,136 @@ +influxframework.pages["background_jobs"].on_page_load = (wrapper) => { + const background_job = new BackgroundJobs(wrapper); + + $(wrapper).bind("show", () => { + background_job.show(); + }); + + window.background_jobs = background_job; +}; + +class BackgroundJobs { + constructor(wrapper) { + this.page = influxframework.ui.make_app_page({ + parent: wrapper, + title: __("Background Jobs"), + single_column: true, + }); + + this.page.add_inner_button(__("Remove Failed Jobs"), () => { + influxframework.confirm(__("Are you sure you want to remove all failed jobs?"), () => { + influxframework + .call("influxframework.core.page.background_jobs.background_jobs.remove_failed_jobs") + .then(() => this.refresh_jobs()); + }); + }); + + this.page.main.addClass("influxframework-card"); + this.page.body.append('
    '); + this.$content = $(this.page.body).find(".table-area"); + + this.make_filters(); + this.refresh_jobs = influxframework.utils.throttle(this.refresh_jobs.bind(this), 1000); + } + + make_filters() { + this.view = this.page.add_field({ + label: __("View"), + fieldname: "view", + fieldtype: "Select", + options: ["Jobs", "Workers"], + default: "Jobs", + change: () => { + this.queue_timeout.toggle(this.view.get_value() === "Jobs"); + this.job_status.toggle(this.view.get_value() === "Jobs"); + }, + }); + this.queue_timeout = this.page.add_field({ + label: __("Queue"), + fieldname: "queue_timeout", + fieldtype: "Select", + options: [ + { label: "All Queues", value: "all" }, + { label: "Default", value: "default" }, + { label: "Short", value: "short" }, + { label: "Long", value: "long" }, + ], + default: "all", + }); + this.job_status = this.page.add_field({ + label: __("Job Status"), + fieldname: "job_status", + fieldtype: "Select", + options: [ + { label: "All Jobs", value: "all" }, + { label: "Queued", value: "queued" }, + { label: "Deferred", value: "deferred" }, + { label: "Started", value: "started" }, + { label: "Finished", value: "finished" }, + { label: "Failed", value: "failed" }, + ], + default: "all", + }); + this.auto_refresh = this.page.add_field({ + label: __("Auto Refresh"), + fieldname: "auto_refresh", + fieldtype: "Check", + default: 1, + change: () => { + if (this.auto_refresh.get_value()) { + this.refresh_jobs(); + } + }, + }); + } + + show() { + this.refresh_jobs(); + this.update_scheduler_status(); + } + + update_scheduler_status() { + influxframework.call({ + method: "influxframework.core.page.background_jobs.background_jobs.get_scheduler_status", + callback: (r) => { + let { status } = r.message; + if (status === "active") { + this.page.set_indicator(__("Scheduler: Active"), "green"); + } else { + this.page.set_indicator(__("Scheduler: Inactive"), "red"); + } + }, + }); + } + + refresh_jobs() { + let view = this.view.get_value(); + let args; + let { queue_timeout, job_status } = this.page.get_form_values(); + if (view === "Jobs") { + args = { view, queue_timeout, job_status }; + } else { + args = { view }; + } + + this.page.add_inner_message(__("Refreshing...")); + influxframework.call({ + method: "influxframework.core.page.background_jobs.background_jobs.get_info", + args, + callback: (res) => { + this.page.add_inner_message(""); + + let template = view === "Jobs" ? "background_jobs" : "background_workers"; + this.$content.html( + influxframework.render_template(template, { + jobs: res.message || [], + }) + ); + + let auto_refresh = this.auto_refresh.get_value(); + if (influxframework.get_route()[0] === "background_jobs" && auto_refresh) { + setTimeout(() => this.refresh_jobs(), 2000); + } + }, + }); + } +} diff --git a/influxframework/core/page/background_jobs/background_jobs.json b/influxframework/core/page/background_jobs/background_jobs.json new file mode 100644 index 0000000..6701cc5 --- /dev/null +++ b/influxframework/core/page/background_jobs/background_jobs.json @@ -0,0 +1,22 @@ +{ + "content": null, + "creation": "2016-08-18 16:44:14.322642", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2016-08-18 16:48:11.577611", + "modified_by": "Administrator", + "module": "Core", + "name": "background_jobs", + "owner": "Administrator", + "page_name": "background_jobs", + "roles": [ + { + "role": "System Manager" + } + ], + "script": null, + "standard": "Yes", + "style": null, + "title": "Background Jobs" +} \ No newline at end of file diff --git a/influxframework/core/page/background_jobs/background_jobs.py b/influxframework/core/page/background_jobs/background_jobs.py new file mode 100644 index 0000000..0154530 --- /dev/null +++ b/influxframework/core/page/background_jobs/background_jobs.py @@ -0,0 +1,78 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +from typing import TYPE_CHECKING + +import influxframework +from influxframework.utils import convert_utc_to_user_timezone +from influxframework.utils.background_jobs import get_queues, get_workers +from influxframework.utils.scheduler import is_scheduler_inactive + +if TYPE_CHECKING: + from rq.job import Job + +JOB_COLORS = {"queued": "orange", "failed": "red", "started": "blue", "finished": "green"} + + +@influxframework.whitelist() +def get_info(view=None, queue_timeout=None, job_status=None) -> list[dict]: + jobs = [] + + def add_job(job: "Job", queue: str) -> None: + + if job.kwargs.get("site") == influxframework.local.site: + job_info = { + "job_name": job.kwargs.get("kwargs", {}).get("playbook_method") + or job.kwargs.get("kwargs", {}).get("job_type") + or str(job.kwargs.get("job_name")), + "status": job.get_status(), + "queue": queue, + "creation": convert_utc_to_user_timezone(job.created_at), + "color": JOB_COLORS[job.get_status()], + } + + if job.exc_info: + job_info["exc_info"] = job.exc_info + + jobs.append(job_info) + + if view == "Jobs": + queues = get_queues() + for queue in queues: + for job in queue.jobs: + if job_status != "all" and job.get_status() != job_status: + return + if queue_timeout != "all" and not queue.name.endswith(f":{queue_timeout}"): + return + add_job(job, queue.name) + + elif view == "Workers": + workers = get_workers() + for worker in workers: + current_job = worker.get_current_job() + if current_job: + if hasattr(current_job, "kwargs") and current_job.kwargs.get("site") == influxframework.local.site: + add_job(current_job, current_job.origin) + else: + jobs.append({"queue": worker.name, "job_name": "busy", "status": "", "creation": ""}) + else: + jobs.append({"queue": worker.name, "job_name": "idle", "status": "", "creation": ""}) + + return jobs + + +@influxframework.whitelist() +def remove_failed_jobs(): + queues = get_queues() + for queue in queues: + fail_registry = queue.failed_job_registry + for job_id in fail_registry.get_job_ids(): + job = queue.fetch_job(job_id) + fail_registry.remove(job, delete_job=True) + + +@influxframework.whitelist() +def get_scheduler_status(): + if is_scheduler_inactive(): + return {"status": "inactive"} + return {"status": "active"} diff --git a/influxframework/core/page/background_jobs/background_workers.html b/influxframework/core/page/background_jobs/background_workers.html new file mode 100644 index 0000000..1c2c2ba --- /dev/null +++ b/influxframework/core/page/background_jobs/background_workers.html @@ -0,0 +1,51 @@ +{% if jobs.length %} + + + + + + + + + + + {% for j in jobs %} + + + + + + + {% endfor %} + +
    {{ __("Worker") }}{{ __("Current Job") }}{{ __("Status") }}{{ __("Created") }}
    + {{ j.queue }} + +
    + + {{ influxframework.utils.encode_tags(j.job_name) }} + +
    + {% if j.exc_info %} +
    + {{ __("Exception") }} +
    +
    {{ influxframework.utils.encode_tags(j.exc_info) }}
    +
    +
    + {% endif %} +
    + {{ toTitle(j.status) }} + {{ influxframework.datetime.prettyDate(j.creation) }}
    +{% else %} +
    + Empty State +

    {{ __("No workers online on this site") }}

    +
    +{% endif %} + diff --git a/influxframework/core/page/dashboard_view/__init__.py b/influxframework/core/page/dashboard_view/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/page/dashboard_view/dashboard_view.js b/influxframework/core/page/dashboard_view/dashboard_view.js new file mode 100644 index 0000000..5c6c61f --- /dev/null +++ b/influxframework/core/page/dashboard_view/dashboard_view.js @@ -0,0 +1,196 @@ +// Copyright (c) 2019, InfluxFramework LLC +// MIT License. See license.txt + +influxframework.provide("influxframework.dashboards"); +influxframework.provide("influxframework.dashboards.chart_sources"); + +influxframework.pages["dashboard-view"].on_page_load = function (wrapper) { + influxframework.ui.make_app_page({ + parent: wrapper, + title: __("Dashboard"), + single_column: true, + }); + + influxframework.dashboard = new Dashboard(wrapper); + $(wrapper).bind("show", function () { + influxframework.dashboard.show(); + }); +}; + +class Dashboard { + constructor(wrapper) { + this.wrapper = $(wrapper); + $(`
    +
    +
    `).appendTo(this.wrapper.find(".page-content").empty()); + this.container = this.wrapper.find(".dashboard-graph"); + this.page = wrapper.page; + } + + show() { + this.route = influxframework.get_route(); + this.set_breadcrumbs(); + if (this.route.length > 1) { + // from route + this.show_dashboard(this.route.slice(-1)[0]); + } else { + // last opened + if (influxframework.last_dashboard) { + influxframework.set_re_route("dashboard-view", influxframework.last_dashboard); + } else { + // default dashboard + influxframework.db.get_list("Dashboard", { filters: { is_default: 1 } }).then((data) => { + if (data && data.length) { + influxframework.set_re_route("dashboard-view", data[0].name); + } else { + // no default, get the latest one + influxframework.db.get_list("Dashboard", { limit: 1 }).then((data) => { + if (data && data.length) { + influxframework.set_re_route("dashboard-view", data[0].name); + } else { + // create a new dashboard! + influxframework.new_doc("Dashboard"); + } + }); + } + }); + } + } + } + + show_dashboard(current_dashboard_name) { + if (this.dashboard_name !== current_dashboard_name) { + this.dashboard_name = current_dashboard_name; + let title = this.dashboard_name; + if (!this.dashboard_name.toLowerCase().includes(__("dashboard"))) { + // ensure dashboard title has "dashboard" + title = __("{0} Dashboard", [title]); + } + this.page.set_title(title); + this.set_dropdown(); + this.container.empty(); + this.refresh(); + } + this.charts = {}; + influxframework.last_dashboard = current_dashboard_name; + } + + set_breadcrumbs() { + influxframework.breadcrumbs.add("Desk", "Dashboard"); + } + + refresh() { + influxframework.run_serially([() => this.render_cards(), () => this.render_charts()]); + } + + render_charts() { + return this.get_permitted_items( + "influxframework.desk.doctype.dashboard.dashboard.get_permitted_charts" + ).then((charts) => { + if (!charts.length) { + influxframework.msgprint( + __("No Permitted Charts on this Dashboard"), + __("No Permitted Charts") + ); + } + + influxframework.dashboard_utils.get_dashboard_settings().then((settings) => { + let chart_config = settings.chart_config ? JSON.parse(settings.chart_config) : {}; + this.charts = charts.map((chart) => { + return { + chart_name: chart.chart, + label: chart.chart, + chart_settings: chart_config[chart.chart] || {}, + ...chart, + }; + }); + + this.chart_group = new influxframework.widget.WidgetGroup({ + title: null, + container: this.container, + type: "chart", + columns: 2, + options: { + allow_sorting: false, + allow_create: false, + allow_delete: false, + allow_hiding: false, + allow_edit: false, + }, + widgets: this.charts, + }); + }); + }); + } + + render_cards() { + return this.get_permitted_items( + "influxframework.desk.doctype.dashboard.dashboard.get_permitted_cards" + ).then((cards) => { + if (!cards.length) { + return; + } + + this.number_cards = cards.map((card) => { + return { + name: card.card, + }; + }); + + this.number_card_group = new influxframework.widget.WidgetGroup({ + container: this.container, + type: "number_card", + columns: 3, + options: { + allow_sorting: false, + allow_create: false, + allow_delete: false, + allow_hiding: false, + allow_edit: false, + }, + widgets: this.number_cards, + }); + }); + } + + get_permitted_items(method) { + return influxframework + .xcall(method, { + dashboard_name: this.dashboard_name, + }) + .then((items) => { + return items; + }); + } + + set_dropdown() { + this.page.clear_menu(); + + this.page.add_menu_item(__("Edit"), () => { + influxframework.set_route("Form", "Dashboard", influxframework.dashboard.dashboard_name); + }); + + this.page.add_menu_item(__("New"), () => { + influxframework.new_doc("Dashboard"); + }); + + this.page.add_menu_item(__("Refresh All"), () => { + this.chart_group && this.chart_group.widgets_list.forEach((chart) => chart.refresh()); + this.number_card_group && + this.number_card_group.widgets_list.forEach((card) => card.render_card()); + }); + + influxframework.db.get_list("Dashboard").then((dashboards) => { + dashboards.map((dashboard) => { + let name = dashboard.name; + if (name != this.dashboard_name) { + this.page.add_menu_item( + name, + () => influxframework.set_route("dashboard-view", name), + 1 + ); + } + }); + }); + } +} diff --git a/influxframework/core/page/dashboard_view/dashboard_view.json b/influxframework/core/page/dashboard_view/dashboard_view.json new file mode 100644 index 0000000..4ece98a --- /dev/null +++ b/influxframework/core/page/dashboard_view/dashboard_view.json @@ -0,0 +1,19 @@ +{ + "content": null, + "creation": "2019-01-08 19:19:48.073410", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2020-12-16 12:29:08.610352", + "modified_by": "Administrator", + "module": "Core", + "name": "dashboard-view", + "owner": "Administrator", + "page_name": "dashboard-view", + "roles": [], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 0, + "title": "Dashboard" +} \ No newline at end of file diff --git a/influxframework/core/page/permission_manager/README.md b/influxframework/core/page/permission_manager/README.md new file mode 100644 index 0000000..c62ccb3 --- /dev/null +++ b/influxframework/core/page/permission_manager/README.md @@ -0,0 +1 @@ +Interface for easy browsing and setting of user permissions (DocPerm) on DocTypes. \ No newline at end of file diff --git a/influxframework/core/page/permission_manager/__init__.py b/influxframework/core/page/permission_manager/__init__.py new file mode 100644 index 0000000..2e11a12 --- /dev/null +++ b/influxframework/core/page/permission_manager/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE diff --git a/influxframework/core/page/permission_manager/permission_manager.css b/influxframework/core/page/permission_manager/permission_manager.css new file mode 100644 index 0000000..fec486a --- /dev/null +++ b/influxframework/core/page/permission_manager/permission_manager.css @@ -0,0 +1,51 @@ +.table { + margin-bottom: 0px; + margin-top: 0px; + border-radius: var(--border-radius-md); +} + +thead { + border: none; + background-color: var(--control-bg); + border-radius: var(--border-radius-md); +} + +thead > tr { + border-radius: var(--border-radius-md); +} + +thead > tr > th:first-child { + border-radius: var(--border-radius-md) 0 0 var(--border-radius-md); +} +thead > tr > th:last-child { + border-radius: 0 var(--border-radius-md) var(--border-radius-md) 0; +} + +/* Space between thead and tbody */ +/* tbody:before { + content: "@"; + display: block; + line-height: var(--margin-md); + text-indent: -99999px; +} */ + +td[data-fieldname="permissions"] > .row > .col-md-4 { + margin-bottom: var(--margin-sm); +} + +tbody > tr { + border-top: 1px solid var(--border-color); +} + +tbody > tr:first-child { + border-top: none; +} + +button.btn-remove-perm { + box-shadow: none; + padding: var(--padding-xs) var(--padding-xs); +} + +button.btn-remove-perm > svg > use { + stroke: var(--white); +} diff --git a/influxframework/core/page/permission_manager/permission_manager.js b/influxframework/core/page/permission_manager/permission_manager.js new file mode 100644 index 0000000..5bc0018 --- /dev/null +++ b/influxframework/core/page/permission_manager/permission_manager.js @@ -0,0 +1,510 @@ +influxframework.pages["permission-manager"].on_page_load = (wrapper) => { + let page = influxframework.ui.make_app_page({ + parent: wrapper, + title: __("Role Permissions Manager"), + card_layout: true, + single_column: true, + }); + + influxframework.breadcrumbs.add("Setup"); + + $("
    ").appendTo( + page.main + ); + $(influxframework.render_template("permission_manager_help", {})).appendTo(page.main); + wrapper.permission_engine = new influxframework.PermissionEngine(wrapper); +}; + +influxframework.pages["permission-manager"].refresh = function (wrapper) { + wrapper.permission_engine.set_from_route(); +}; + +influxframework.PermissionEngine = class PermissionEngine { + constructor(wrapper) { + this.wrapper = wrapper; + this.page = wrapper.page; + this.body = $(this.wrapper).find(".perm-engine"); + this.make(); + this.refresh(); + this.add_check_events(); + } + + make() { + this.make_reset_button(); + influxframework + .call({ + module: "influxframework.core", + page: "permission_manager", + method: "get_roles_and_doctypes", + }) + .then((res) => { + this.options = res.message; + this.setup_page(); + }); + } + + setup_page() { + this.doctype_select = this.wrapper.page + .add_select( + __("Document Type"), + [{ value: "", label: __("Select Document Type") + "..." }].concat( + this.options.doctypes + ) + ) + .change(function () { + influxframework.set_route("permission-manager", $(this).val()); + }); + + this.role_select = this.wrapper.page + .add_select(__("Roles"), [__("Select Role") + "..."].concat(this.options.roles)) + .change(() => { + this.refresh(); + }); + + this.page.add_inner_button(__("Set User Permissions"), () => { + return influxframework.set_route("List", "User Permission"); + }); + this.set_from_route(); + } + + set_from_route() { + if (!this.doctype_select) { + // selects not yet loaded, call again after a bit + setTimeout(() => { + this.set_from_route(); + }, 500); + return; + } + if (influxframework.get_route()[1]) { + this.doctype_select.val(influxframework.get_route()[1]); + } else if (influxframework.route_options) { + if (influxframework.route_options.doctype) { + this.doctype_select.val(influxframework.route_options.doctype); + } + if (influxframework.route_options.role) { + this.role_select.val(influxframework.route_options.role); + } + influxframework.route_options = null; + } + this.refresh(); + } + + get_standard_permissions(callback) { + let doctype = this.get_doctype(); + if (doctype) { + return influxframework.call({ + module: "influxframework.core", + page: "permission_manager", + method: "get_standard_permissions", + args: { doctype: doctype }, + callback: callback, + }); + } + return false; + } + + reset_std_permissions(data) { + let doctype = this.get_doctype(); + let d = influxframework.confirm(__("Reset Permissions for {0}?", [doctype]), () => { + return influxframework + .call({ + module: "influxframework.core", + page: "permission_manager", + method: "reset", + args: { doctype }, + }) + .then(() => { + this.refresh(); + }); + }); + + // show standard permissions + let $d = $(d.wrapper) + .find(".influxframework-confirm-message") + .append("
    Standard Permissions:

    "); + let $wrapper = $("

    ").appendTo($d); + data.message.forEach((d) => { + let rights = this.rights + .filter((r) => d[r]) + .map((r) => { + return __(toTitle(influxframework.unscrub(r))); + }); + + d.rights = rights.join(", "); + + $wrapper.append(`
    \ +
    ${d.role}, Level ${d.permlevel || 0}
    \ +
    ${d.rights}
    \ +

    `); + }); + } + + get_doctype() { + let doctype = this.doctype_select.val(); + return this.doctype_select.get(0).selectedIndex == 0 ? null : doctype; + } + + get_role() { + let role = this.role_select.val(); + return this.role_select.get(0).selectedIndex == 0 ? null : role; + } + + set_empty_message(message) { + this.body.html(` +
    +

    + ${message} +

    +
    `); + } + + refresh() { + this.page.clear_secondary_action(); + this.page.clear_primary_action(); + + if (!this.doctype_select) { + return this.set_empty_message(__("Loading")); + } + + let doctype = this.get_doctype(); + let role = this.get_role(); + + if (!doctype && !role) { + return this.set_empty_message(__("Select Document Type or Role to start.")); + } + + // get permissions + influxframework + .call({ + module: "influxframework.core", + page: "permission_manager", + method: "get_permissions", + args: { doctype, role }, + }) + .then((r) => { + this.render(r.message); + }); + } + + render(perm_list) { + this.body.empty(); + this.perm_list = perm_list || []; + if (!this.perm_list.length) { + this.set_empty_message(__("No Permissions set for this criteria.")); + } else { + this.show_permission_table(this.perm_list); + } + this.show_add_rule(); + this.get_doctype() && this.make_reset_button(); + } + + show_permission_table(perm_list) { + this.table = $( + "
    \ + \ + \ + \ +
    \ +
    " + ).appendTo(this.body); + + const table_columns = [ + [__("Document Type"), 150], + [__("Role"), 170], + [__("Level"), 40], + [__("Permissions"), 350], + ["", 40], + ]; + + table_columns.forEach((col) => { + $("") + .html(col[0]) + .css("width", col[1] + "px") + .appendTo(this.table.find("thead tr")); + }); + + perm_list.forEach((d) => { + if (d.parent === "DocType") { + return; + } + + if (!d.permlevel) d.permlevel = 0; + + let row = $("").appendTo(this.table.find("tbody")); + this.add_cell(row, d, "parent"); + let role_cell = this.add_cell(row, d, "role"); + + this.set_show_users(role_cell, d.role); + + if (d.permlevel === 0) { + // this.setup_user_permissions(d, role_cell); + this.setup_if_owner(d, role_cell); + } + + let cell = this.add_cell(row, d, "permlevel"); + + if (d.permlevel == 0) { + cell.css("font-weight", "bold"); + } + + let perm_cell = this.add_cell(row, d, "permissions"); + let perm_container = $("
    ").appendTo(perm_cell); + + this.rights.forEach((r) => { + if (!d.is_submittable && ["submit", "cancel", "amend"].includes(r)) return; + if (d.in_create && ["create", "write", "delete"].includes(r)) return; + this.add_check(perm_container, d, r); + }); + + // buttons + this.add_delete_button(row, d); + }); + } + + add_cell(row, d, fieldname) { + return $("") + .appendTo(row) + .attr("data-fieldname", fieldname) + .addClass("pt-4") + .html(__(d[fieldname])); + } + + add_check(cell, d, fieldname, label, description = "") { + if (!label) label = toTitle(fieldname.replace(/_/g, " ")); + if (d.permlevel > 0 && ["read", "write"].indexOf(fieldname) == -1) { + return; + } + + let checkbox = $( + `
    +
    + +

    ${__(description)}

    +
    +
    ` + ) + .appendTo(cell) + .attr("data-fieldname", fieldname); + + checkbox + .find("input") + .prop("checked", d[fieldname] ? true : false) + .attr("data-ptype", fieldname) + .attr("data-role", d.role) + .attr("data-permlevel", d.permlevel) + .attr("data-doctype", d.parent); + + checkbox.find("label").css("text-transform", "capitalize"); + + return checkbox; + } + + setup_if_owner(d, role_cell) { + this.add_check(role_cell, d, "if_owner", "Only if Creator") + .removeClass("col-md-4") + .css({ "margin-top": "15px" }); + } + + get rights() { + return [ + "select", + "read", + "write", + "create", + "delete", + "submit", + "cancel", + "amend", + "print", + "email", + "report", + "import", + "export", + "set_user_permissions", + "share", + ]; + } + + set_show_users(cell, role) { + cell.html("" + __(role) + "") + .find("a") + .attr("data-role", role) + .click(function () { + let role = $(this).attr("data-role"); + influxframework.call({ + module: "influxframework.core", + page: "permission_manager", + method: "get_users_with_role", + args: { + role: role, + }, + callback: function (r) { + r.message = $.map(r.message, function (p) { + return $.format('{1}', [p, p]); + }); + influxframework.msgprint( + __("Users with role {0}:", [__(role)]) + + "
    " + + r.message.join("
    ") + ); + }, + }); + return false; + }); + } + + add_delete_button(row, d) { + $( + `` + ) + .appendTo($(``).appendTo(row)) + .attr("data-doctype", d.parent) + .attr("data-role", d.role) + .attr("data-permlevel", d.permlevel) + .on("click", () => { + return influxframework.call({ + module: "influxframework.core", + page: "permission_manager", + method: "remove", + args: { + doctype: d.parent, + role: d.role, + permlevel: d.permlevel, + }, + callback: (r) => { + if (r.exc) { + influxframework.msgprint(__("Did not remove")); + } else { + this.refresh(); + } + }, + }); + }); + } + + add_check_events() { + let me = this; + this.body.on("click", ".show-user-permissions", () => { + influxframework.route_options = { allow: this.get_doctype() || "" }; + influxframework.set_route("List", "User Permission"); + }); + + this.body.on("click", "input[type='checkbox']", function () { + influxframework.dom.freeze(); + let chk = $(this); + let args = { + role: chk.attr("data-role"), + permlevel: chk.attr("data-permlevel"), + doctype: chk.attr("data-doctype"), + ptype: chk.attr("data-ptype"), + value: chk.prop("checked") ? 1 : 0, + }; + return influxframework.call({ + module: "influxframework.core", + page: "permission_manager", + method: "update", + args: args, + callback: (r) => { + influxframework.dom.unfreeze(); + if (r.exc) { + // exception: reverse + chk.prop("checked", !chk.prop("checked")); + } else { + me.get_perm(args.role)[args.ptype] = args.value; + } + }, + }); + }); + } + + show_add_rule() { + this.page.set_primary_action( + __("Add A New Rule"), + () => { + let d = new influxframework.ui.Dialog({ + title: __("Add New Permission Rule"), + fields: [ + { + fieldtype: "Select", + label: __("Document Type"), + options: this.options.doctypes, + reqd: 1, + fieldname: "parent", + }, + { + fieldtype: "Select", + label: __("Role"), + options: this.options.roles, + reqd: 1, + fieldname: "role", + }, + { + fieldtype: "Select", + label: __("Permission Level"), + options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + reqd: 1, + fieldname: "permlevel", + description: __( + "Level 0 is for document level permissions, higher levels for field level permissions." + ), + }, + ], + }); + if (this.get_doctype()) { + d.set_value("parent", this.get_doctype()); + d.get_input("parent").prop("disabled", true); + } + if (this.get_role()) { + d.set_value("role", this.get_role()); + d.get_input("role").prop("disabled", true); + } + d.set_value("permlevel", "0"); + d.set_primary_action(__("Add"), () => { + let args = d.get_values(); + if (!args) { + return; + } + influxframework.call({ + module: "influxframework.core", + page: "permission_manager", + method: "add", + args: args, + callback: (r) => { + if (r.exc) { + influxframework.msgprint(__("Did not add")); + } else { + this.refresh(); + } + }, + }); + d.hide(); + }); + d.show(); + }, + "small-add" + ); + } + + make_reset_button() { + this.page.set_secondary_action(__("Restore Original Permissions"), () => { + this.get_standard_permissions((data) => { + this.reset_std_permissions(data); + }); + }); + } + + get_perm(role) { + return $.map(this.perm_list, function (d) { + if (d.role == role) return d; + })[0]; + } + + get_link_fields(doctype) { + return influxframework.get_children("DocType", doctype, "fields", { + fieldtype: "Link", + options: ["not in", ["User", "[Select]"]], + }); + } +}; diff --git a/influxframework/core/page/permission_manager/permission_manager.json b/influxframework/core/page/permission_manager/permission_manager.json new file mode 100644 index 0000000..0af33ca --- /dev/null +++ b/influxframework/core/page/permission_manager/permission_manager.json @@ -0,0 +1,20 @@ +{ + "creation": "2013-01-01 11:00:01.000000", + "docstatus": 0, + "doctype": "Page", + "icon": "fa fa-lock", + "idx": 1, + "modified": "2013-07-11 14:43:43.000000", + "modified_by": "Administrator", + "module": "Core", + "name": "permission-manager", + "owner": "Administrator", + "page_name": "permission-manager", + "roles": [ + { + "role": "System Manager" + } + ], + "standard": "Yes", + "title": "Role Permissions Manager" +} \ No newline at end of file diff --git a/influxframework/core/page/permission_manager/permission_manager.py b/influxframework/core/page/permission_manager/permission_manager.py new file mode 100644 index 0000000..8c3fa1a --- /dev/null +++ b/influxframework/core/page/permission_manager/permission_manager.py @@ -0,0 +1,169 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + + +import influxframework +import influxframework.defaults +from influxframework import _ +from influxframework.core.doctype.doctype.doctype import ( + clear_permissions_cache, + validate_permissions_for_doctype, +) +from influxframework.exceptions import DoesNotExistError +from influxframework.modules.import_file import get_file_path, read_doc_from_file +from influxframework.permissions import ( + add_permission, + get_all_perms, + get_linked_doctypes, + reset_perms, + setup_custom_perms, + update_permission_property, +) +from influxframework.translate import send_translations +from influxframework.utils.user import get_users_with_role as _get_user_with_role + +not_allowed_in_permission_manager = ["DocType", "Patch Log", "Module Def", "Transaction Log"] + + +@influxframework.whitelist() +def get_roles_and_doctypes(): + influxframework.only_for("System Manager") + send_translations(influxframework.get_lang_dict("doctype", "DocPerm")) + + active_domains = influxframework.get_active_domains() + + doctypes = influxframework.get_all( + "DocType", + filters={ + "istable": 0, + "name": ("not in", ",".join(not_allowed_in_permission_manager)), + }, + or_filters={"ifnull(restrict_to_domain, '')": "", "restrict_to_domain": ("in", active_domains)}, + fields=["name"], + ) + + restricted_roles = ["Administrator"] + if influxframework.session.user != "Administrator": + custom_user_type_roles = influxframework.get_all("User Type", filters={"is_standard": 0}, fields=["role"]) + for row in custom_user_type_roles: + restricted_roles.append(row.role) + + restricted_roles.append("All") + + roles = influxframework.get_all( + "Role", + filters={ + "name": ("not in", restricted_roles), + "disabled": 0, + }, + or_filters={"ifnull(restrict_to_domain, '')": "", "restrict_to_domain": ("in", active_domains)}, + fields=["name"], + ) + + doctypes_list = [{"label": _(d.get("name")), "value": d.get("name")} for d in doctypes] + roles_list = [{"label": _(d.get("name")), "value": d.get("name")} for d in roles] + + return { + "doctypes": sorted(doctypes_list, key=lambda d: d["label"]), + "roles": sorted(roles_list, key=lambda d: d["label"]), + } + + +@influxframework.whitelist() +def get_permissions(doctype: str | None = None, role: str | None = None): + influxframework.only_for("System Manager") + + if role: + out = get_all_perms(role) + if doctype: + out = [p for p in out if p.parent == doctype] + + else: + filters = {"parent": doctype} + if influxframework.session.user != "Administrator": + custom_roles = influxframework.get_all("Role", filters={"is_custom": 1}, pluck="name") + filters["role"] = ["not in", custom_roles] + + out = influxframework.get_all("Custom DocPerm", fields="*", filters=filters, order_by="permlevel") + if not out: + out = influxframework.get_all("DocPerm", fields="*", filters=filters, order_by="permlevel") + + linked_doctypes = {} + for d in out: + if d.parent not in linked_doctypes: + try: + linked_doctypes[d.parent] = get_linked_doctypes(d.parent) + except DoesNotExistError: + # exclude & continue if linked doctype is not found + influxframework.clear_last_message() + continue + d.linked_doctypes = linked_doctypes[d.parent] + if meta := influxframework.get_meta(d.parent): + d.is_submittable = meta.is_submittable + d.in_create = meta.in_create + + return out + + +@influxframework.whitelist() +def add(parent, role, permlevel): + influxframework.only_for("System Manager") + add_permission(parent, role, permlevel) + + +@influxframework.whitelist() +def update(doctype, role, permlevel, ptype, value=None): + """Update role permission params + + Args: + doctype (str): Name of the DocType to update params for + role (str): Role to be updated for, eg "Website Manager". + permlevel (int): perm level the provided rule applies to + ptype (str): permission type, example "read", "delete", etc. + value (None, optional): value for ptype, None indicates False + + Returns: + str: Refresh flag is permission is updated successfully + """ + influxframework.only_for("System Manager") + out = update_permission_property(doctype, role, permlevel, ptype, value) + return "refresh" if out else None + + +@influxframework.whitelist() +def remove(doctype, role, permlevel): + influxframework.only_for("System Manager") + setup_custom_perms(doctype) + + influxframework.db.delete("Custom DocPerm", {"parent": doctype, "role": role, "permlevel": permlevel}) + + if not influxframework.get_all("Custom DocPerm", {"parent": doctype}): + influxframework.throw(_("There must be atleast one permission rule."), title=_("Cannot Remove")) + + validate_permissions_for_doctype(doctype, for_remove=True, alert=True) + + +@influxframework.whitelist() +def reset(doctype): + influxframework.only_for("System Manager") + reset_perms(doctype) + clear_permissions_cache(doctype) + + +@influxframework.whitelist() +def get_users_with_role(role): + influxframework.only_for("System Manager") + return _get_user_with_role(role) + + +@influxframework.whitelist() +def get_standard_permissions(doctype): + influxframework.only_for("System Manager") + meta = influxframework.get_meta(doctype) + if meta.custom: + doc = influxframework.get_doc("DocType", doctype) + return [p.as_dict() for p in doc.permissions] + else: + # also used to setup permissions via patch + path = get_file_path(meta.module, "DocType", doctype) + return read_doc_from_file(path).get("permissions") diff --git a/influxframework/core/page/permission_manager/permission_manager_help.html b/influxframework/core/page/permission_manager/permission_manager_help.html new file mode 100644 index 0000000..b752e31 --- /dev/null +++ b/influxframework/core/page/permission_manager/permission_manager_help.html @@ -0,0 +1,41 @@ +
    +
    +

    {%= __("Quick Help for Setting Permissions") %}:

    +
      +
    1. {%= __("Permissions are set on Roles and Document Types (called DocTypes) by setting rights like Read, Write, Create, Delete, Submit, Cancel, Amend, Report, Import, Export, Print, Email and Set User Permissions.") %}
    2. +
    3. {%= __("Permissions get applied on Users based on what Roles they are assigned.") %}
    4. +
    5. {%= __("Roles can be set for users from their User page.") %} + {%= __("Setup > User") %}
    6. +
    7. {%= __("The system provides many pre-defined roles. You can add new roles to set finer permissions.") %} {%= __("Add a New Role") %}
    8. +
    9. {%= __("Permissions are automatically applied to Standard Reports and searches.") %}
    10. +
    11. {%= __("As a best practice, do not assign the same set of permission rule to different Roles. Instead, set multiple Roles to the same User.") %}
    12. +
    +
    +

    {%= __("Meaning of Submit, Cancel, Amend") %}:

    +
      +
    1. {%= __("Certain documents, like an Invoice, should not be changed once final. The final state for such documents is called Submitted. You can restrict which roles can Submit.") %}
    2. +
    3. {%= __("You can change Submitted documents by cancelling them and then, amending them.") %}
    4. +
    5. {%= __("When you Amend a document after Cancel and save it, it will get a new number that is a version of the old number.") %}
    6. +
    7. {%= __("For example if you cancel and amend INV004 it will become a new document INV004-1. This helps you to keep track of each amendment.") %}
    8. +
    +
    +

    {%= __("Permission Levels") %}:

    +
      +
    1. {%= __("Permissions at level 0 are Document Level permissions, i.e. they are primary for access to the document.") %}
    2. +
    3. {%= __("If a Role does not have access at Level 0, then higher levels are meaningless.") %}
    4. +
    5. {%= __("Permissions at higher levels are Field Level permissions. All Fields have a Permission Level set against them and the rules defined at that permissions apply to the field. This is useful in case you want to hide or make certain field read-only for certain Roles.") %}
    6. +
    7. {%= __("You can use Customize Form to set levels on fields.") %} {%= __("Setup > Customize Form") %}
    8. +
    +
    +

    {%= __("User Permissions") %}:

    +
      +
    1. {%= __("User Permissions are used to limit users to specific records.") %} + {%= __("Setup > User Permissions") %}
    2. +
    3. {%= __("Select Document Types to set which User Permissions are used to limit access.") %}
    4. +
    5. {%= __("Once you have set this, the users will only be able access documents (eg. Blog Post) where the link exists (eg. Blogger).") %}
    6. +
    7. {%= __("Apart from System Manager, roles with Set User Permissions right can set permissions for other users for that Document Type.") %}
    8. +
    +

    {%= __("If these instructions where not helpful, please add in your suggestions on GitHub Issues.") %} + {%= __("Submit an Issue") %} +

    +
    diff --git a/influxframework/core/page/recorder/__init__.py b/influxframework/core/page/recorder/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/page/recorder/recorder.js b/influxframework/core/page/recorder/recorder.js new file mode 100644 index 0000000..edd358c --- /dev/null +++ b/influxframework/core/page/recorder/recorder.js @@ -0,0 +1,28 @@ +influxframework.pages["recorder"].on_page_load = function (wrapper) { + influxframework.ui.make_app_page({ + parent: wrapper, + title: __("Recorder"), + single_column: true, + card_layout: true, + }); + + influxframework.recorder = new Recorder(wrapper); + $(wrapper).bind("show", function () { + influxframework.recorder.show(); + }); + + influxframework.require("recorder.bundle.js"); +}; + +class Recorder { + constructor(wrapper) { + this.wrapper = $(wrapper); + this.container = this.wrapper.find(".layout-main-section"); + this.container.append($('
    ')); + } + + show() { + if (!this.view || this.view.$route.name == "recorder-detail") return; + this.view.$router.replace({ name: "recorder-detail" }); + } +} diff --git a/influxframework/core/page/recorder/recorder.json b/influxframework/core/page/recorder/recorder.json new file mode 100644 index 0000000..43dfbc0 --- /dev/null +++ b/influxframework/core/page/recorder/recorder.json @@ -0,0 +1,23 @@ +{ + "content": null, + "creation": "2019-02-08 08:17:45.392739", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2019-02-08 08:23:04.416426", + "modified_by": "Administrator", + "module": "Core", + "name": "recorder", + "owner": "Administrator", + "page_name": "Recorder", + "roles": [ + { + "role": "Administrator" + } + ], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 0, + "title": "Recorder" +} \ No newline at end of file diff --git a/influxframework/core/report/__init__.py b/influxframework/core/report/__init__.py new file mode 100644 index 0000000..2e11a12 --- /dev/null +++ b/influxframework/core/report/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE diff --git a/influxframework/core/report/database_storage_usage_by_tables/__init__.py b/influxframework/core/report/database_storage_usage_by_tables/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.js b/influxframework/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.js new file mode 100644 index 0000000..afd1cc5 --- /dev/null +++ b/influxframework/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.js @@ -0,0 +1,7 @@ +// Copyright (c) 2022, InfluxFramework LLC +// For license information, please see license.txt +/* eslint-disable */ + +influxframework.query_reports["Database Storage Usage By Tables"] = { + filters: [], +}; diff --git a/influxframework/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.json b/influxframework/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.json new file mode 100644 index 0000000..20deb78 --- /dev/null +++ b/influxframework/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.json @@ -0,0 +1,28 @@ +{ + "add_total_row": 1, + "columns": [], + "creation": "2022-10-19 02:25:24.326791", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letter_head": "abc", + "modified": "2022-10-19 02:59:00.365307", + "modified_by": "Administrator", + "module": "Core", + "name": "Database Storage Usage By Tables", + "owner": "Administrator", + "prepared_report": 0, + "query": "", + "ref_doctype": "Error Log", + "report_name": "Database Storage Usage By Tables", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + } + ] +} \ No newline at end of file diff --git a/influxframework/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.py b/influxframework/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.py new file mode 100644 index 0000000..d6bbabd --- /dev/null +++ b/influxframework/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.py @@ -0,0 +1,40 @@ +# Copyright (c) 2022, InfluxFramework LLC +# For license information, please see license.txt + +import influxframework + +COLUMNS = [ + {"label": "Table", "fieldname": "table", "fieldtype": "Data", "width": 200}, + {"label": "Size (MB)", "fieldname": "size", "fieldtype": "Float"}, + {"label": "Data (MB)", "fieldname": "data_size", "fieldtype": "Float"}, + {"label": "Index (MB)", "fieldname": "index_size", "fieldtype": "Float"}, +] + + +def execute(filters=None): + influxframework.only_for("System Manager") + + data = influxframework.db.multisql( + { + "mariadb": """ + SELECT table_name AS `table`, + round(((data_length + index_length) / 1024 / 1024), 2) `size`, + round((data_length / 1024 / 1024), 2) as data_size, + round((index_length / 1024 / 1024), 2) as index_size + FROM information_schema.TABLES + ORDER BY (data_length + index_length) DESC; + """, + "postgres": """ + SELECT + table_name as "table", + round(pg_total_relation_size(quote_ident(table_name)) / 1024 / 1024, 2) as "size", + round(pg_relation_size(quote_ident(table_name)) / 1024 / 1024, 2) as "data_size", + round(pg_indexes_size(quote_ident(table_name)) / 1024 / 1024, 2) as "index_size" + FROM information_schema.tables + WHERE table_schema = 'public' + ORDER BY 2 DESC; + """, + }, + as_dict=1, + ) + return COLUMNS, data diff --git a/influxframework/core/report/database_storage_usage_by_tables/test_database_storage_usage_by_tables.py b/influxframework/core/report/database_storage_usage_by_tables/test_database_storage_usage_by_tables.py new file mode 100644 index 0000000..6c0c309 --- /dev/null +++ b/influxframework/core/report/database_storage_usage_by_tables/test_database_storage_usage_by_tables.py @@ -0,0 +1,15 @@ +# Copyright (c) 2022, InfluxFramework LLC +# For license information, please see license.txt + + +from influxframework.core.report.database_storage_usage_by_tables.database_storage_usage_by_tables import ( + execute, +) +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestDBUsageReport(InfluxFrameworkTestCase): + def test_basic_query(self): + _, data = execute() + tables = [d.table for d in data] + self.assertFalse({"tabUser", "tabDocField"}.difference(tables)) diff --git a/influxframework/core/report/document_share_report/__init__.py b/influxframework/core/report/document_share_report/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/report/document_share_report/document_share_report.json b/influxframework/core/report/document_share_report/document_share_report.json new file mode 100644 index 0000000..db2f2b6 --- /dev/null +++ b/influxframework/core/report/document_share_report/document_share_report.json @@ -0,0 +1,24 @@ +{ + "add_total_row": 0, + "apply_user_permissions": 1, + "creation": "2015-02-05 06:01:35.060098", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 2, + "is_standard": "Yes", + "json": "{\"add_total_row\": 0, \"sort_by\": \"DocShare.modified\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"DocShare\"], [\"user\", \"DocShare\"], [\"share_doctype\", \"DocShare\"], [\"share_name\", \"DocShare\"], [\"read\", \"DocShare\"], [\"write\", \"DocShare\"], [\"share\", \"DocShare\"]]}", + "modified": "2017-02-24 20:01:16.232286", + "modified_by": "Administrator", + "module": "Core", + "name": "Document Share Report", + "owner": "Administrator", + "ref_doctype": "DocShare", + "report_name": "Document Share Report", + "report_type": "Report Builder", + "roles": [ + { + "role": "System Manager" + } + ] +} \ No newline at end of file diff --git a/influxframework/core/report/permitted_documents_for_user/__init__.py b/influxframework/core/report/permitted_documents_for_user/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/report/permitted_documents_for_user/permitted_documents_for_user.js b/influxframework/core/report/permitted_documents_for_user/permitted_documents_for_user.js new file mode 100644 index 0000000..2449254 --- /dev/null +++ b/influxframework/core/report/permitted_documents_for_user/permitted_documents_for_user.js @@ -0,0 +1,34 @@ +// Copyright (c) 2015, InfluxFramework LLC +// MIT License. See license.txt + +influxframework.query_reports["Permitted Documents For User"] = { + filters: [ + { + fieldname: "user", + label: __("User"), + fieldtype: "Link", + options: "User", + reqd: 1, + }, + { + fieldname: "doctype", + label: __("DocType"), + fieldtype: "Link", + options: "DocType", + reqd: 1, + get_query: function () { + return { + query: "influxframework.core.report.permitted_documents_for_user.permitted_documents_for_user.query_doctypes", + filters: { + user: influxframework.query_report.get_filter_value("user"), + }, + }; + }, + }, + { + fieldname: "show_permissions", + label: __("Show Permissions"), + fieldtype: "Check", + }, + ], +}; diff --git a/influxframework/core/report/permitted_documents_for_user/permitted_documents_for_user.json b/influxframework/core/report/permitted_documents_for_user/permitted_documents_for_user.json new file mode 100644 index 0000000..bf384ea --- /dev/null +++ b/influxframework/core/report/permitted_documents_for_user/permitted_documents_for_user.json @@ -0,0 +1,23 @@ +{ + "add_total_row": 0, + "creation": "2014-06-03 05:20:35.218263", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 3, + "is_standard": "Yes", + "modified": "2018-06-29 15:46:42.805039", + "modified_by": "Administrator", + "module": "Core", + "name": "Permitted Documents For User", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "User", + "report_name": "Permitted Documents For User", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + } + ] +} \ No newline at end of file diff --git a/influxframework/core/report/permitted_documents_for_user/permitted_documents_for_user.py b/influxframework/core/report/permitted_documents_for_user/permitted_documents_for_user.py new file mode 100644 index 0000000..2b2c607 --- /dev/null +++ b/influxframework/core/report/permitted_documents_for_user/permitted_documents_for_user.py @@ -0,0 +1,63 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +import influxframework.utils.user +from influxframework.model import data_fieldtypes +from influxframework.permissions import rights + + +def execute(filters=None): + influxframework.only_for("System Manager") + + user, doctype, show_permissions = ( + filters.get("user"), + filters.get("doctype"), + filters.get("show_permissions"), + ) + + columns, fields = get_columns_and_fields(doctype) + data = influxframework.get_list(doctype, fields=fields, as_list=True, user=user) + + if show_permissions: + columns = columns + [influxframework.unscrub(right) + ":Check:80" for right in rights] + data = list(data) + for i, doc in enumerate(data): + permission = influxframework.permissions.get_doc_permissions(influxframework.get_doc(doctype, doc[0]), user) + data[i] = doc + tuple(permission.get(right) for right in rights) + + return columns, data + + +def get_columns_and_fields(doctype): + columns = [f"Name:Link/{doctype}:200"] + fields = ["`name`"] + for df in influxframework.get_meta(doctype).fields: + if df.in_list_view and df.fieldtype in data_fieldtypes: + fields.append(f"`{df.fieldname}`") + fieldtype = f"Link/{df.options}" if df.fieldtype == "Link" else df.fieldtype + columns.append( + "{label}:{fieldtype}:{width}".format( + label=df.label, fieldtype=fieldtype, width=df.width or 100 + ) + ) + + return columns, fields + + +@influxframework.whitelist() +@influxframework.validate_and_sanitize_search_inputs +def query_doctypes(doctype, txt, searchfield, start, page_len, filters): + user = filters.get("user") + user_perms = influxframework.utils.user.UserPermissions(user) + user_perms.build_permissions() + can_read = user_perms.can_read # Does not include child tables + + single_doctypes = [d[0] for d in influxframework.db.get_values("DocType", {"issingle": 1})] + + out = [] + for dt in can_read: + if txt.lower().replace("%", "") in dt.lower() and dt not in single_doctypes: + out.append([dt]) + + return out diff --git a/influxframework/core/report/transaction_log_report/__init__.py b/influxframework/core/report/transaction_log_report/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/report/transaction_log_report/transaction_log_report.js b/influxframework/core/report/transaction_log_report/transaction_log_report.js new file mode 100644 index 0000000..906df3b --- /dev/null +++ b/influxframework/core/report/transaction_log_report/transaction_log_report.js @@ -0,0 +1,11 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt +/* eslint-disable */ + +influxframework.query_reports["Transaction Log Report"] = { + onload: function (query_report) { + query_report.add_make_chart_button = function () { + // + }; + }, +}; diff --git a/influxframework/core/report/transaction_log_report/transaction_log_report.json b/influxframework/core/report/transaction_log_report/transaction_log_report.json new file mode 100644 index 0000000..6d6fb78 --- /dev/null +++ b/influxframework/core/report/transaction_log_report/transaction_log_report.json @@ -0,0 +1,26 @@ +{ + "add_total_row": 0, + "creation": "2018-03-15 18:37:48.783779", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2018-12-27 18:10:29.785415", + "modified_by": "Administrator", + "module": "Core", + "name": "Transaction Log Report", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Transaction Log", + "report_name": "Transaction Log Report", + "report_type": "Script Report", + "roles": [ + { + "role": "Administrator" + }, + { + "role": "System Manager" + } + ] +} \ No newline at end of file diff --git a/influxframework/core/report/transaction_log_report/transaction_log_report.py b/influxframework/core/report/transaction_log_report/transaction_log_report.py new file mode 100644 index 0000000..5c74860 --- /dev/null +++ b/influxframework/core/report/transaction_log_report/transaction_log_report.py @@ -0,0 +1,98 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +import hashlib + +import influxframework +from influxframework import _ +from influxframework.utils import format_datetime + + +def execute(filters=None): + columns, data = get_columns(filters), get_data(filters) + + return columns, data + + +def get_data(filters=None): + result = [] + logs = influxframework.get_all("Transaction Log", fields=["*"], order_by="creation desc") + + for l in logs: + row_index = int(l.row_index) + if row_index > 1: + previous_hash = influxframework.get_all( + "Transaction Log", + fields=["chaining_hash"], + filters={"row_index": row_index - 1}, + ) + if not previous_hash: + integrity = False + else: + integrity = check_data_integrity( + l.chaining_hash, l.transaction_hash, l.previous_hash, previous_hash[0][0] + ) + + result.append( + [ + _(str(integrity)), + _(l.reference_doctype), + l.document_name, + l.owner, + l.modified_by, + format_datetime(l.timestamp, "YYYYMMDDHHmmss"), + ] + ) + else: + result.append( + [ + _("First Transaction"), + _(l.reference_doctype), + l.document_name, + l.owner, + l.modified_by, + format_datetime(l.timestamp, "YYYYMMDDHHmmss"), + ] + ) + + return result + + +def check_data_integrity(chaining_hash, transaction_hash, registered_previous_hash, previous_hash): + if registered_previous_hash != previous_hash: + return False + + calculated_chaining_hash = calculate_chain(transaction_hash, previous_hash) + + if calculated_chaining_hash != chaining_hash: + return False + else: + return True + + +def calculate_chain(transaction_hash, previous_hash): + sha = hashlib.sha256() + sha.update(str(transaction_hash) + str(previous_hash)) + return sha.hexdigest() + + +def get_columns(filters=None): + columns = [ + { + "label": _("Chain Integrity"), + "fieldname": "chain_integrity", + "fieldtype": "Data", + "width": 150, + }, + { + "label": _("Reference Doctype"), + "fieldname": "reference_doctype", + "fieldtype": "Data", + "width": 150, + }, + {"label": _("Reference Name"), "fieldname": "reference_name", "fieldtype": "Data", "width": 150}, + {"label": _("Owner"), "fieldname": "owner", "fieldtype": "Data", "width": 100}, + {"label": _("Modified By"), "fieldname": "modified_by", "fieldtype": "Data", "width": 100}, + {"label": _("Timestamp"), "fieldname": "timestamp", "fieldtype": "Data", "width": 100}, + ] + return columns diff --git a/influxframework/core/utils.py b/influxframework/core/utils.py new file mode 100644 index 0000000..41d9756 --- /dev/null +++ b/influxframework/core/utils.py @@ -0,0 +1,95 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +from markdownify import markdownify as md + +import influxframework + + +def get_parent_doc(doc): + """Returns document of `reference_doctype`, `reference_doctype`""" + if not hasattr(doc, "parent_doc"): + if doc.reference_doctype and doc.reference_name: + doc.parent_doc = influxframework.get_doc(doc.reference_doctype, doc.reference_name) + else: + doc.parent_doc = None + return doc.parent_doc + + +def set_timeline_doc(doc): + """Set timeline_doctype and timeline_name""" + parent_doc = get_parent_doc(doc) + if (doc.timeline_doctype and doc.timeline_name) or not parent_doc: + return + + timeline_field = parent_doc.meta.timeline_field + if not timeline_field: + return + + doctype = parent_doc.meta.get_link_doctype(timeline_field) + name = parent_doc.get(timeline_field) + + if doctype and name: + doc.timeline_doctype = doctype + doc.timeline_name = name + + else: + return + + +def find(list_of_dict, match_function): + """Returns a dict in a list of dicts on matching the conditions + provided in match function + + Usage: + list_of_dict = [{'name': 'Suraj'}, {'name': 'Aditya'}] + + required_dict = find(list_of_dict, lambda d: d['name'] == 'Aditya') + """ + + for entry in list_of_dict: + if match_function(entry): + return entry + return None + + +def find_all(list_of_dict, match_function): + """Returns all matching dicts in a list of dicts. + Uses matching function to filter out the dicts + + Usage: + colored_shapes = [ + {'color': 'red', 'shape': 'square'}, + {'color': 'red', 'shape': 'circle'}, + {'color': 'blue', 'shape': 'triangle'} + ] + + red_shapes = find_all(colored_shapes, lambda d: d['color'] == 'red') + """ + found = [] + for entry in list_of_dict: + if match_function(entry): + found.append(entry) + return found + + +def ljust_list(_list, length, fill_word=None): + """ + Similar to ljust but for list. + + Usage: + $ ljust_list([1, 2, 3], 5) + > [1, 2, 3, None, None] + """ + # make a copy to avoid mutation of passed list + _list = list(_list) + fill_length = length - len(_list) + if fill_length > 0: + _list.extend([fill_word] * fill_length) + + return _list + + +def html2text(html, strip_links=False, wrap=True): + strip = ["a"] if strip_links else None + return md(html, heading_style="ATX", strip=strip, wrap=wrap) diff --git a/influxframework/core/web_form/__init__.py b/influxframework/core/web_form/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/web_form/edit_profile/__init__.py b/influxframework/core/web_form/edit_profile/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/core/web_form/edit_profile/edit_profile.js b/influxframework/core/web_form/edit_profile/edit_profile.js new file mode 100644 index 0000000..cd44c43 --- /dev/null +++ b/influxframework/core/web_form/edit_profile/edit_profile.js @@ -0,0 +1,3 @@ +influxframework.ready(function () { + // bind events here +}); diff --git a/influxframework/core/web_form/edit_profile/edit_profile.json b/influxframework/core/web_form/edit_profile/edit_profile.json new file mode 100644 index 0000000..8abb216 --- /dev/null +++ b/influxframework/core/web_form/edit_profile/edit_profile.json @@ -0,0 +1,157 @@ +{ + "allow_comments": 0, + "allow_delete": 0, + "allow_edit": 1, + "allow_incomplete": 0, + "allow_multiple": 0, + "allow_print": 0, + "apply_document_permissions": 0, + "breadcrumbs": "[{\"title\": _(\"My Account\"), \"route\": \"me\"}]", + "creation": "2016-09-19 05:16:59.242754", + "doc_type": "User", + "docstatus": 0, + "doctype": "Web Form", + "idx": 0, + "introduction_text": "", + "is_multi_step_form": 0, + "is_standard": 1, + "list_columns": [], + "login_required": 1, + "max_attachment_size": 0, + "modified": "2022-07-18 16:51:19.796411", + "modified_by": "Administrator", + "module": "Core", + "name": "edit-profile", + "owner": "Administrator", + "published": 1, + "route": "update-profile", + "route_to_success_link": 0, + "show_attachments": 0, + "show_list": 0, + "show_sidebar": 0, + "success_message": "Profile updated successfully.", + "success_url": "/me", + "title": "Update Profile", + "web_form_fields": [ + { + "allow_read_on_all_link_options": 0, + "fieldname": "first_name", + "fieldtype": "Data", + "hidden": 0, + "label": "First Name", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 1, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "middle_name", + "fieldtype": "Data", + "hidden": 0, + "label": "Middle Name (Optional)", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "last_name", + "fieldtype": "Data", + "hidden": 0, + "label": "Last Name", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "", + "fieldtype": "Column Break", + "hidden": 0, + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "description": "", + "fieldname": "user_image", + "fieldtype": "Attach Image", + "hidden": 0, + "label": "Profile Picture", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldtype": "Section Break", + "hidden": 0, + "label": "More Information", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "phone", + "fieldtype": "Data", + "hidden": 0, + "label": "Phone", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "mobile_no", + "fieldtype": "Data", + "hidden": 0, + "label": "Mobile Number", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "", + "fieldtype": "Column Break", + "hidden": 0, + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "description": "", + "fieldname": "language", + "fieldtype": "Link", + "hidden": 0, + "label": "Language", + "max_length": 0, + "max_value": 0, + "options": "Language", + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + } + ] +} \ No newline at end of file diff --git a/influxframework/core/web_form/edit_profile/edit_profile.py b/influxframework/core/web_form/edit_profile/edit_profile.py new file mode 100644 index 0000000..02e3e93 --- /dev/null +++ b/influxframework/core/web_form/edit_profile/edit_profile.py @@ -0,0 +1,3 @@ +def get_context(context): + # do your magic here + pass diff --git a/influxframework/core/workspace/build/build.json b/influxframework/core/workspace/build/build.json new file mode 100644 index 0000000..9282c50 --- /dev/null +++ b/influxframework/core/workspace/build/build.json @@ -0,0 +1,308 @@ +{ + "charts": [], + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"DocType\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Workspace\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Report\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Elements\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Modules\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Models\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Views\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Scripting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Packages\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"System Logs\",\"col\":4}}]", + "creation": "2021-01-02 10:51:16.579957", + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "tool", + "idx": 0, + "label": "Build", + "links": [ + { + "hidden": 0, + "is_query_report": 0, + "label": "Models", + "link_count": 0, + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "DocType", + "link_count": 0, + "link_to": "DocType", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Workflow", + "link_count": 0, + "link_to": "Workflow", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Scripting", + "link_count": 0, + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Server Script", + "link_count": 0, + "link_to": "Server Script", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Client Script", + "link_count": 0, + "link_to": "Client Script", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Scheduled Job Type", + "link_count": 0, + "link_to": "Scheduled Job Type", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Packages", + "link_count": 2, + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Package", + "link_count": 0, + "link_to": "Package", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Package Import", + "link_count": 0, + "link_to": "Package Import", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Modules", + "link_count": 3, + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Module Def", + "link_count": 0, + "link_to": "Module Def", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Module Onboarding", + "link_count": 0, + "link_to": "Module Onboarding", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Module Profile", + "link_count": 0, + "link_to": "Module Profile", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Views", + "link_count": 4, + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Report", + "link_count": 0, + "link_to": "Report", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Print Format", + "link_count": 0, + "link_to": "Print Format", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Dashboard", + "link_count": 0, + "link_to": "Dashboard", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Workspace", + "link_count": 0, + "link_to": "Workspace", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "System Logs", + "link_count": 6, + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Background Jobs", + "link_count": 0, + "link_to": "background_jobs", + "link_type": "Page", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Scheduled Jobs Logs", + "link_count": 0, + "link_to": "Scheduled Job Log", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Error Logs", + "link_count": 0, + "link_to": "Error Log", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Error Snapshot", + "link_count": 0, + "link_to": "Error Snapshot", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Communication Logs", + "link_count": 0, + "link_to": "Communication", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Activity Log", + "link_count": 0, + "link_to": "Activity Log", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + } + ], + "modified": "2022-09-02 01:48:28.029135", + "modified_by": "Administrator", + "module": "Core", + "name": "Build", + "owner": "Administrator", + "parent_page": "", + "public": 1, + "quick_lists": [], + "restrict_to_domain": "", + "roles": [], + "sequence_id": 5.0, + "shortcuts": [ + { + "doc_view": "", + "label": "DocType", + "link_to": "DocType", + "type": "DocType" + }, + { + "doc_view": "", + "label": "Workspace", + "link_to": "Workspace", + "type": "DocType" + }, + { + "doc_view": "", + "label": "Report", + "link_to": "Report", + "type": "DocType" + } + ], + "title": "Build" +} \ No newline at end of file diff --git a/influxframework/core/workspace/settings/settings.json b/influxframework/core/workspace/settings/settings.json new file mode 100644 index 0000000..1469892 --- /dev/null +++ b/influxframework/core/workspace/settings/settings.json @@ -0,0 +1,380 @@ +{ + "charts": [], + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Settings\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"System Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Print Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Website Settings\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Email / Notifications\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Website\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Core\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Printing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Workflow\",\"col\":4}}]", + "creation": "2020-03-02 15:09:40.527211", + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "setting", + "idx": 0, + "label": "Settings", + "links": [ + { + "hidden": 0, + "is_query_report": 0, + "label": "Data", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Import Data", + "link_count": 0, + "link_to": "Data Import", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Export Data", + "link_count": 0, + "link_to": "Data Export", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Bulk Update", + "link_count": 0, + "link_to": "Bulk Update", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Download Backups", + "link_count": 0, + "link_to": "backups", + "link_type": "Page", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Deleted Documents", + "link_count": 0, + "link_to": "Deleted Document", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Email / Notifications", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Email Account", + "link_count": 0, + "link_to": "Email Account", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Email Domain", + "link_count": 0, + "link_to": "Email Domain", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Notification", + "link_count": 0, + "link_to": "Notification", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Email Template", + "link_count": 0, + "link_to": "Email Template", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Auto Email Report", + "link_count": 0, + "link_to": "Auto Email Report", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Newsletter", + "link_count": 0, + "link_to": "Newsletter", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Notification Settings", + "link_count": 0, + "link_to": "Notification Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Website", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Website Settings", + "link_count": 0, + "link_to": "Website Settings", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Website Theme", + "link_count": 0, + "link_to": "Website Theme", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Website Script", + "link_count": 0, + "link_to": "Website Script", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "About Us Settings", + "link_count": 0, + "link_to": "About Us Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Contact Us Settings", + "link_count": 0, + "link_to": "Contact Us Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Printing", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Print Format Builder", + "link_count": 0, + "link_to": "print-format-builder", + "link_type": "Page", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Print Settings", + "link_count": 0, + "link_to": "Print Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Print Format", + "link_count": 0, + "link_to": "Print Format", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Print Style", + "link_count": 0, + "link_to": "Print Style", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Workflow", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Workflow", + "link_count": 0, + "link_to": "Workflow", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Workflow State", + "link_count": 0, + "link_to": "Workflow State", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Workflow Action", + "link_count": 0, + "link_to": "Workflow Action", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Core", + "link_count": 2, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "System Settings", + "link_count": 0, + "link_to": "System Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Domain Settings", + "link_count": 0, + "link_to": "Domain Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + } + ], + "modified": "2022-08-28 21:41:28.065190", + "modified_by": "Administrator", + "module": "Core", + "name": "Settings", + "owner": "Administrator", + "parent_page": "", + "public": 1, + "quick_lists": [], + "restrict_to_domain": "", + "roles": [], + "sequence_id": 29.0, + "shortcuts": [ + { + "icon": "setting", + "label": "System Settings", + "link_to": "System Settings", + "type": "DocType" + }, + { + "icon": "printer", + "label": "Print Settings", + "link_to": "Print Settings", + "type": "DocType" + }, + { + "icon": "website", + "label": "Website Settings", + "link_to": "Website Settings", + "type": "DocType" + } + ], + "title": "Settings" +} \ No newline at end of file diff --git a/influxframework/core/workspace/users/users.json b/influxframework/core/workspace/users/users.json new file mode 100644 index 0000000..5741c54 --- /dev/null +++ b/influxframework/core/workspace/users/users.json @@ -0,0 +1,187 @@ +{ + "charts": [], + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Role\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Permission Manager\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User Profile\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User Type\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Users\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Logs\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Permissions\",\"col\":4}}]", + "creation": "2020-03-02 15:12:16.754449", + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "users", + "idx": 0, + "label": "Users", + "links": [ + { + "hidden": 0, + "is_query_report": 0, + "label": "Users", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "User", + "link_count": 0, + "link_to": "User", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Role", + "link_count": 0, + "link_to": "Role", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Role Profile", + "link_count": 0, + "link_to": "Role Profile", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Logs", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Activity Log", + "link_count": 0, + "link_to": "Activity Log", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Access Log", + "link_count": 0, + "link_to": "Access Log", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Permissions", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Role Permissions Manager", + "link_count": 0, + "link_to": "permission-manager", + "link_type": "Page", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "User Permissions", + "link_count": 0, + "link_to": "User Permission", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Role Permission for Page and Report", + "link_count": 0, + "link_to": "Role Permission for Page and Report", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "User", + "hidden": 0, + "is_query_report": 1, + "label": "Permitted Documents For User", + "link_count": 0, + "link_to": "Permitted Documents For User", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "DocShare", + "hidden": 0, + "is_query_report": 0, + "label": "Document Share Report", + "link_count": 0, + "link_to": "Document Share Report", + "link_type": "Report", + "onboard": 0, + "type": "Link" + } + ], + "modified": "2022-01-13 17:49:08.912772", + "modified_by": "Administrator", + "module": "Core", + "name": "Users", + "owner": "Administrator", + "parent_page": "", + "public": 1, + "restrict_to_domain": "", + "roles": [], + "sequence_id": 27.0, + "shortcuts": [ + { + "label": "User", + "link_to": "User", + "type": "DocType" + }, + { + "label": "Role", + "link_to": "Role", + "type": "DocType" + }, + { + "label": "Permission Manager", + "link_to": "permission-manager", + "type": "Page" + }, + { + "label": "User Profile", + "link_to": "user-profile", + "type": "Page" + }, + { + "doc_view": "", + "label": "User Type", + "link_to": "User Type", + "type": "DocType" + } + ], + "title": "Users" +} \ No newline at end of file diff --git a/influxframework/coverage.py b/influxframework/coverage.py new file mode 100644 index 0000000..09dd84a --- /dev/null +++ b/influxframework/coverage.py @@ -0,0 +1,66 @@ +# Copyright (c) 2021, InfluxFramework LLC +# MIT License. See LICENSE +""" + influxframework.coverage + ~~~~~~~~~~~~~~~~ + + Coverage settings for influxframework +""" + +STANDARD_INCLUSIONS = ["*.py"] + +STANDARD_EXCLUSIONS = [ + "*.js", + "*.xml", + "*.pyc", + "*.css", + "*.less", + "*.scss", + "*.vue", + "*.html", + "*/test_*", + "*/node_modules/*", + "*/doctype/*/*_dashboard.py", + "*/patches/*", +] + +INFLUXFRAMEWORK_EXCLUSIONS = [ + "*/tests/*", + "*/commands/*", + "*/influxframework/change_log/*", + "*/influxframework/exceptions*", + "*/influxframework/coverage.py", + "*influxframework/setup.py", + "*/doctype/*/*_dashboard.py", + "*/patches/*", +] + + +class CodeCoverage: + def __init__(self, with_coverage, app): + self.with_coverage = with_coverage + self.app = app or "influxframework" + + def __enter__(self): + if self.with_coverage: + import os + + from coverage import Coverage + + from influxframework.utils import get_bench_path + + # Generate coverage report only for app that is being tested + source_path = os.path.join(get_bench_path(), "apps", self.app) + omit = STANDARD_EXCLUSIONS[:] + + if self.app == "influxframework": + omit.extend(INFLUXFRAMEWORK_EXCLUSIONS) + + self.coverage = Coverage(source=[source_path], omit=omit, include=STANDARD_INCLUSIONS) + self.coverage.start() + + def __exit__(self, exc_type, exc_value, traceback): + if self.with_coverage: + self.coverage.stop() + self.coverage.save() + self.coverage.xml_report() diff --git a/influxframework/custom/__init__.py b/influxframework/custom/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/custom/doctype/__init__.py b/influxframework/custom/doctype/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/custom/doctype/client_script/README.md b/influxframework/custom/doctype/client_script/README.md new file mode 100644 index 0000000..df0eded --- /dev/null +++ b/influxframework/custom/doctype/client_script/README.md @@ -0,0 +1,11 @@ +Client or server script appended to standard DocType code. + +For client: + +- Code is appended to the js code. +- Best practice is to declare additional methods beginning with prefix `custom_` e.g. `custom_validate` to standard events. + +For server: + +- Script is appended to module code before DocType is `execute`d. +- All class methods must be written with a tab \ No newline at end of file diff --git a/influxframework/custom/doctype/client_script/__init__.py b/influxframework/custom/doctype/client_script/__init__.py new file mode 100644 index 0000000..2e11a12 --- /dev/null +++ b/influxframework/custom/doctype/client_script/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE diff --git a/influxframework/custom/doctype/client_script/client_script.js b/influxframework/custom/doctype/client_script/client_script.js new file mode 100644 index 0000000..b94ac84 --- /dev/null +++ b/influxframework/custom/doctype/client_script/client_script.js @@ -0,0 +1,158 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Client Script", { + setup(frm) { + frm.get_field("sample").html(SAMPLE_HTML); + }, + refresh(frm) { + if (frm.doc.dt && frm.doc.script) { + frm.add_custom_button(__("Go to {0}", [frm.doc.dt]), () => + influxframework.set_route("List", frm.doc.dt, "List") + ); + } + + if (frm.doc.view == "Form") { + frm.add_custom_button(__("Add script for Child Table"), () => { + influxframework.model.with_doctype(frm.doc.dt, () => { + const child_tables = influxframework.meta + .get_docfields(frm.doc.dt, null, { + fieldtype: "Table", + }) + .map((df) => df.options); + + const d = new influxframework.ui.Dialog({ + title: __("Select Child Table"), + fields: [ + { + label: __("Select Child Table"), + fieldtype: "Link", + fieldname: "cdt", + options: "DocType", + get_query: () => { + return { + filters: { + istable: 1, + name: ["in", child_tables], + }, + }; + }, + }, + ], + primary_action: ({ cdt }) => { + cdt = d.get_field("cdt").value; + frm.events.add_script_for_doctype(frm, cdt); + d.hide(); + }, + }); + + d.show(); + }); + }); + + if (!frm.is_new()) { + frm.add_custom_button(__("Compare Versions"), () => { + new influxframework.ui.DiffView("Client Script", "script", frm.doc.name); + }); + } + } + + frm.set_query("dt", { + filters: { + istable: 0, + }, + }); + }, + + dt(frm) { + frm.toggle_display("view", !influxframework.boot.single_types.includes(frm.doc.dt)); + + if (!frm.doc.script) { + frm.events.add_script_for_doctype(frm, frm.doc.dt); + } + + if (frm.doc.script && !frm.doc.script.includes(frm.doc.dt)) { + frm.doc.script = ""; + frm.events.add_script_for_doctype(frm, frm.doc.dt); + } + }, + + view(frm) { + let has_form_boilerplate = frm.doc.script.includes("influxframework.ui.form.on"); + if (frm.doc.view === "List" && has_form_boilerplate) { + frm.set_value("script", ""); + } + if (frm.doc.view === "Form" && !has_form_boilerplate) { + frm.trigger("dt"); + } + }, + + add_script_for_doctype(frm, doctype) { + if (!doctype) return; + let boilerplate = ` +influxframework.ui.form.on('${doctype}', { + refresh(frm) { + // your code here + } +}) + `.trim(); + let script = frm.doc.script || ""; + if (script) { + script += "\n\n"; + } + frm.set_value("script", script + boilerplate); + }, +}); + +const SAMPLE_HTML = `

    Client Script Help

    +

    Client Scripts are executed only on the client-side (i.e. in Forms). Here are some examples to get you started

    +
    
    +
    +// fetch local_tax_no on selection of customer
    +// cur_frm.add_fetch(link_field,  source_fieldname,  target_fieldname);
    +cur_frm.add_fetch("customer",  "local_tax_no',  'local_tax_no');
    +
    +// additional validation on dates
    +influxframework.ui.form.on('Task',  'validate',  function(frm) {
    +    if (frm.doc.from_date < get_today()) {
    +        msgprint('You can not select past date in From Date');
    +        validated = false;
    +    }
    +});
    +
    +// make a field read-only after saving
    +influxframework.ui.form.on('Task',  {
    +    refresh: function(frm) {
    +        // use the __islocal value of doc,  to check if the doc is saved or not
    +        frm.set_df_property('myfield',  'read_only',  frm.doc.__islocal ? 0 : 1);
    +    }
    +});
    +
    +// additional permission check
    +influxframework.ui.form.on('Task',  {
    +    validate: function(frm) {
    +        if(user=='user1@example.com' && frm.doc.purpose!='Material Receipt') {
    +            msgprint('You are only allowed Material Receipt');
    +            validated = false;
    +        }
    +    }
    +});
    +
    +// calculate sales incentive
    +influxframework.ui.form.on('Sales Invoice',  {
    +    validate: function(frm) {
    +        // calculate incentives for each person on the deal
    +        total_incentive = 0
    +        $.each(frm.doc.sales_team,  function(i,  d) {
    +            // calculate incentive
    +            var incentive_percent = 2;
    +            if(frm.doc.base_grand_total > 400) incentive_percent = 4;
    +            // actual incentive
    +            d.incentives = flt(frm.doc.base_grand_total) * incentive_percent / 100;
    +            total_incentive += flt(d.incentives)
    +        });
    +        frm.doc.total_incentive = total_incentive;
    +    }
    +})
    +
    +
    `; diff --git a/influxframework/custom/doctype/client_script/client_script.json b/influxframework/custom/doctype/client_script/client_script.json new file mode 100644 index 0000000..1db4dfe --- /dev/null +++ b/influxframework/custom/doctype/client_script/client_script.json @@ -0,0 +1,114 @@ +{ + "actions": [], + "allow_import": 1, + "autoname": "Prompt", + "creation": "2013-01-10 16:34:01", + "description": "Adds a custom client script to a DocType", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "dt", + "view", + "column_break_3", + "module", + "enabled", + "section_break_6", + "script", + "sample" + ], + "fields": [ + { + "fieldname": "dt", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "DocType", + "oldfieldname": "dt", + "oldfieldtype": "Link", + "options": "DocType", + "reqd": 1, + "set_only_once": 1 + }, + { + "fieldname": "script", + "fieldtype": "Code", + "label": "Script", + "oldfieldname": "script", + "oldfieldtype": "Code", + "options": "JS" + }, + { + "fieldname": "sample", + "fieldtype": "HTML", + "label": "Sample" + }, + { + "default": "0", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enabled" + }, + { + "default": "Form", + "fieldname": "view", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Apply To", + "options": "List\nForm", + "set_only_once": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "module", + "fieldtype": "Link", + "label": "Module (for export)", + "options": "Module Def" + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break" + } + ], + "icon": "fa fa-glass", + "idx": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2022-04-12 12:48:15.717985", + "modified_by": "Administrator", + "module": "Custom", + "name": "Client Script", + "naming_rule": "Set by user", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "ASC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/custom/doctype/client_script/client_script.py b/influxframework/custom/doctype/client_script/client_script.py new file mode 100644 index 0000000..0eb1e70 --- /dev/null +++ b/influxframework/custom/doctype/client_script/client_script.py @@ -0,0 +1,12 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.model.document import Document + + +class ClientScript(Document): + def on_update(self): + influxframework.clear_cache(doctype=self.dt) + + def on_trash(self): + influxframework.clear_cache(doctype=self.dt) diff --git a/influxframework/custom/doctype/client_script/test_client_script.py b/influxframework/custom/doctype/client_script/test_client_script.py new file mode 100644 index 0000000..57f59a4 --- /dev/null +++ b/influxframework/custom/doctype/client_script/test_client_script.py @@ -0,0 +1,9 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + +# test_records = influxframework.get_test_records('Client Script') + + +class TestClientScript(InfluxFrameworkTestCase): + pass diff --git a/influxframework/custom/doctype/client_script/ui_test_client_script.js b/influxframework/custom/doctype/client_script/ui_test_client_script.js new file mode 100644 index 0000000..0d202d6 --- /dev/null +++ b/influxframework/custom/doctype/client_script/ui_test_client_script.js @@ -0,0 +1,98 @@ +context("Client Script", () => { + before(() => { + cy.login(); + cy.visit("/app"); + }); + + it("should run form script in doctype form", () => { + cy.insert_doc( + "Client Script", + { + name: "Todo form script", + dt: "ToDo", + view: "Form", + enabled: 1, + script: `console.log('todo form script')`, + }, + true + ); + cy.visit("/app/todo/new", { + onBeforeLoad(win) { + cy.spy(win.console, "log").as("consoleLog"); + }, + }); + cy.get("@consoleLog").should("be.calledWith", "todo form script"); + }); + + it("should run list script in doctype list view", () => { + cy.insert_doc( + "Client Script", + { + name: "Todo list script", + dt: "ToDo", + view: "List", + enabled: 1, + script: `console.log('todo list script')`, + }, + true + ); + cy.visit("/app/todo", { + onBeforeLoad(win) { + cy.spy(win.console, "log").as("consoleLog"); + }, + }); + cy.get("@consoleLog").should("be.calledWith", "todo list script"); + }); + + it("should not run disabled scripts", () => { + cy.insert_doc( + "Client Script", + { + name: "Todo disabled list", + dt: "ToDo", + view: "List", + enabled: 0, + script: `console.log('todo disabled script')`, + }, + true + ); + cy.visit("/app/todo", { + onBeforeLoad(win) { + cy.spy(win.console, "log").as("consoleLog"); + }, + }); + cy.get("@consoleLog").should("not.be.calledWith", "todo disabled script"); + }); + + it("should run multiple scripts", () => { + cy.insert_doc( + "Client Script", + { + name: "Todo form script 1", + dt: "ToDo", + view: "Form", + enabled: 1, + script: `console.log('todo form script 1')`, + }, + true + ); + cy.insert_doc( + "Client Script", + { + name: "Todo form script 2", + dt: "ToDo", + view: "Form", + enabled: 1, + script: `console.log('todo form script 2')`, + }, + true + ); + cy.visit("/app/todo/new", { + onBeforeLoad(win) { + cy.spy(win.console, "log").as("consoleLog"); + }, + }); + cy.get("@consoleLog").should("be.calledWith", "todo form script 1"); + cy.get("@consoleLog").should("be.calledWith", "todo form script 2"); + }); +}); diff --git a/influxframework/custom/doctype/custom_field/README.md b/influxframework/custom/doctype/custom_field/README.md new file mode 100644 index 0000000..5d6bf14 --- /dev/null +++ b/influxframework/custom/doctype/custom_field/README.md @@ -0,0 +1 @@ +Custom Field added by the user (not part of DocType but part of table). Custom fields are automatically added to the DocType when they are loaded as metadata. \ No newline at end of file diff --git a/influxframework/custom/doctype/custom_field/__init__.py b/influxframework/custom/doctype/custom_field/__init__.py new file mode 100644 index 0000000..2e11a12 --- /dev/null +++ b/influxframework/custom/doctype/custom_field/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE diff --git a/influxframework/custom/doctype/custom_field/custom_field.js b/influxframework/custom/doctype/custom_field/custom_field.js new file mode 100644 index 0000000..0e459ef --- /dev/null +++ b/influxframework/custom/doctype/custom_field/custom_field.js @@ -0,0 +1,102 @@ +// Copyright (c) 2015, InfluxFramework LLC +// MIT License. See license.txt + +// Refresh +// -------- + +influxframework.ui.form.on("Custom Field", { + setup: function (frm) { + frm.set_query("dt", function (doc) { + var filters = [ + ["DocType", "issingle", "=", 0], + ["DocType", "custom", "=", 0], + ["DocType", "name", "not in", influxframework.model.core_doctypes_list], + ["DocType", "restrict_to_domain", "in", influxframework.boot.active_domains], + ]; + if (influxframework.session.user !== "Administrator") { + filters.push(["DocType", "module", "not in", ["Core", "Custom"]]); + } + return { + filters: filters, + }; + }); + }, + refresh: function (frm) { + frm.toggle_enable("dt", frm.doc.__islocal); + frm.trigger("dt"); + frm.toggle_reqd("label", !frm.doc.fieldname); + }, + dt: function (frm) { + if (!frm.doc.dt) { + set_field_options("insert_after", ""); + return; + } + var insert_after = frm.doc.insert_after || null; + return influxframework.call({ + method: "influxframework.custom.doctype.custom_field.custom_field.get_fields_label", + args: { doctype: frm.doc.dt, fieldname: frm.doc.fieldname }, + callback: function (r) { + if (r) { + if (r._server_messages && r._server_messages.length) { + frm.set_value("dt", ""); + } else { + set_field_options("insert_after", r.message); + var fieldnames = $.map(r.message, function (v) { + return v.value; + }); + + if (insert_after == null || !in_list(fieldnames, insert_after)) { + insert_after = fieldnames[-1]; + } + + frm.set_value("insert_after", insert_after); + } + } + }, + }); + }, + label: function (frm) { + if (frm.doc.label && influxframework.utils.has_special_chars(frm.doc.label)) { + frm.fields_dict["label_help"].disp_area.innerHTML = + '' + __("Special Characters are not allowed") + ""; + frm.set_value("label", ""); + } else { + frm.fields_dict["label_help"].disp_area.innerHTML = ""; + } + }, + fieldtype: function (frm) { + if (frm.doc.fieldtype == "Link") { + frm.fields_dict["options_help"].disp_area.innerHTML = __( + "Name of the Document Type (DocType) you want this field to be linked to. e.g. Customer" + ); + } else if (frm.doc.fieldtype == "Select") { + frm.fields_dict["options_help"].disp_area.innerHTML = + __("Options for select. Each option on a new line.") + + " " + + __("e.g.:") + + "
    " + + __("Option 1") + + "
    " + + __("Option 2") + + "
    " + + __("Option 3") + + "
    "; + } else if (frm.doc.fieldtype == "Dynamic Link") { + frm.fields_dict["options_help"].disp_area.innerHTML = __( + "Fieldname which will be the DocType for this link field." + ); + } else { + frm.fields_dict["options_help"].disp_area.innerHTML = ""; + } + }, +}); + +influxframework.utils.has_special_chars = function (t) { + var iChars = "!@#$%^&*()+=-[]\\';,./{}|\":<>?"; + for (var i = 0; i < t.length; i++) { + if (iChars.indexOf(t.charAt(i)) != -1) { + return true; + } + } + return false; +}; diff --git a/influxframework/custom/doctype/custom_field/custom_field.json b/influxframework/custom/doctype/custom_field/custom_field.json new file mode 100644 index 0000000..63be70c --- /dev/null +++ b/influxframework/custom/doctype/custom_field/custom_field.json @@ -0,0 +1,478 @@ +{ + "actions": [], + "allow_import": 1, + "creation": "2013-01-10 16:34:01", + "description": "Adds a custom field to a DocType", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "is_system_generated", + "dt", + "module", + "label", + "label_help", + "fieldname", + "insert_after", + "length", + "column_break_6", + "fieldtype", + "precision", + "hide_seconds", + "hide_days", + "options", + "fetch_from", + "fetch_if_empty", + "options_help", + "section_break_11", + "collapsible", + "collapsible_depends_on", + "default", + "depends_on", + "mandatory_depends_on", + "read_only_depends_on", + "properties", + "non_negative", + "reqd", + "unique", + "is_virtual", + "read_only", + "ignore_user_permissions", + "hidden", + "print_hide", + "print_hide_if_no_value", + "print_width", + "no_copy", + "allow_on_submit", + "in_list_view", + "in_standard_filter", + "in_global_search", + "in_preview", + "bold", + "report_hide", + "search_index", + "allow_in_quick_entry", + "ignore_xss_filter", + "translatable", + "hide_border", + "description", + "permlevel", + "width", + "columns" + ], + "fields": [ + { + "bold": 1, + "fieldname": "dt", + "fieldtype": "Link", + "in_filter": 1, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "DocType", + "oldfieldname": "dt", + "oldfieldtype": "Link", + "options": "DocType", + "reqd": 1, + "search_index": 1 + }, + { + "bold": 1, + "fieldname": "label", + "fieldtype": "Data", + "in_filter": 1, + "label": "Label", + "no_copy": 1, + "oldfieldname": "label", + "oldfieldtype": "Data" + }, + { + "fieldname": "label_help", + "fieldtype": "HTML", + "label": "Label Help", + "oldfieldtype": "HTML" + }, + { + "fieldname": "fieldname", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Fieldname", + "no_copy": 1, + "oldfieldname": "fieldname", + "oldfieldtype": "Data", + "read_only": 1 + }, + { + "description": "Select the label after which you want to insert new field.", + "fieldname": "insert_after", + "fieldtype": "Select", + "label": "Insert After", + "no_copy": 1, + "oldfieldname": "insert_after", + "oldfieldtype": "Select" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "bold": 1, + "default": "Data", + "fieldname": "fieldtype", + "fieldtype": "Select", + "in_filter": 1, + "in_list_view": 1, + "label": "Field Type", + "oldfieldname": "fieldtype", + "oldfieldtype": "Select", + "options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nJSON\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nPhone\nRead Only\nRating\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime", + "reqd": 1 + }, + { + "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)", + "description": "Set non-standard precision for a Float or Currency field", + "fieldname": "precision", + "fieldtype": "Select", + "label": "Precision", + "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9" + }, + { + "fieldname": "options", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Options", + "oldfieldname": "options", + "oldfieldtype": "Text" + }, + { + "fieldname": "fetch_from", + "fieldtype": "Small Text", + "label": "Fetch From" + }, + { + "default": "0", + "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.", + "fieldname": "fetch_if_empty", + "fieldtype": "Check", + "label": "Fetch If Empty" + }, + { + "fieldname": "options_help", + "fieldtype": "HTML", + "label": "Options Help", + "oldfieldtype": "HTML" + }, + { + "fieldname": "section_break_11", + "fieldtype": "Section Break" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype==\"Section Break\"", + "fieldname": "collapsible", + "fieldtype": "Check", + "label": "Collapsible" + }, + { + "depends_on": "eval:doc.fieldtype==\"Section Break\"", + "fieldname": "collapsible_depends_on", + "fieldtype": "Code", + "label": "Collapsible Depends On" + }, + { + "fieldname": "default", + "fieldtype": "Text", + "label": "Default Value", + "oldfieldname": "default", + "oldfieldtype": "Text" + }, + { + "fieldname": "depends_on", + "fieldtype": "Code", + "label": "Depends On", + "length": 255 + }, + { + "fieldname": "description", + "fieldtype": "Text", + "label": "Field Description", + "oldfieldname": "description", + "oldfieldtype": "Text", + "print_width": "300px", + "width": "300px" + }, + { + "default": "0", + "fieldname": "permlevel", + "fieldtype": "Int", + "label": "Permission Level", + "oldfieldname": "permlevel", + "oldfieldtype": "Int" + }, + { + "fieldname": "width", + "fieldtype": "Data", + "label": "Width", + "oldfieldname": "width", + "oldfieldtype": "Data" + }, + { + "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)", + "fieldname": "columns", + "fieldtype": "Int", + "label": "Columns" + }, + { + "fieldname": "properties", + "fieldtype": "Column Break", + "oldfieldtype": "Column Break", + "print_width": "50%", + "width": "50%" + }, + { + "default": "0", + "fieldname": "reqd", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Is Mandatory Field", + "oldfieldname": "reqd", + "oldfieldtype": "Check" + }, + { + "default": "0", + "fieldname": "unique", + "fieldtype": "Check", + "label": "Unique" + }, + { + "default": "0", + "fieldname": "is_virtual", + "fieldtype": "Check", + "label": "Is Virtual" + }, + { + "default": "0", + "fieldname": "read_only", + "fieldtype": "Check", + "label": "Read Only" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype===\"Link\"", + "fieldname": "ignore_user_permissions", + "fieldtype": "Check", + "label": "Ignore User Permissions" + }, + { + "default": "0", + "fieldname": "hidden", + "fieldtype": "Check", + "label": "Hidden" + }, + { + "default": "0", + "fieldname": "print_hide", + "fieldtype": "Check", + "label": "Print Hide", + "oldfieldname": "print_hide", + "oldfieldtype": "Check" + }, + { + "default": "0", + "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1", + "fieldname": "print_hide_if_no_value", + "fieldtype": "Check", + "label": "Print Hide If No Value" + }, + { + "fieldname": "print_width", + "fieldtype": "Data", + "hidden": 1, + "label": "Print Width", + "no_copy": 1, + "print_hide": 1 + }, + { + "default": "0", + "fieldname": "no_copy", + "fieldtype": "Check", + "label": "No Copy", + "oldfieldname": "no_copy", + "oldfieldtype": "Check" + }, + { + "default": "0", + "fieldname": "allow_on_submit", + "fieldtype": "Check", + "label": "Allow on Submit", + "oldfieldname": "allow_on_submit", + "oldfieldtype": "Check" + }, + { + "default": "0", + "fieldname": "in_list_view", + "fieldtype": "Check", + "label": "In List View" + }, + { + "default": "0", + "fieldname": "in_standard_filter", + "fieldtype": "Check", + "label": "In Standard Filter" + }, + { + "default": "0", + "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)", + "fieldname": "in_global_search", + "fieldtype": "Check", + "label": "In Global Search" + }, + { + "default": "0", + "fieldname": "bold", + "fieldtype": "Check", + "label": "Bold" + }, + { + "default": "0", + "fieldname": "report_hide", + "fieldtype": "Check", + "label": "Report Hide", + "oldfieldname": "report_hide", + "oldfieldtype": "Check" + }, + { + "default": "0", + "fieldname": "search_index", + "fieldtype": "Check", + "hidden": 1, + "label": "Index", + "no_copy": 1, + "print_hide": 1 + }, + { + "default": "0", + "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field", + "fieldname": "ignore_xss_filter", + "fieldtype": "Check", + "label": "Ignore XSS Filter" + }, + { + "default": "1", + "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)", + "fieldname": "translatable", + "fieldtype": "Check", + "label": "Translatable" + }, + { + "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)", + "fieldname": "length", + "fieldtype": "Int", + "label": "Length" + }, + { + "fieldname": "mandatory_depends_on", + "fieldtype": "Code", + "label": "Mandatory Depends On", + "length": 255 + }, + { + "fieldname": "read_only_depends_on", + "fieldtype": "Code", + "label": "Read Only Depends On", + "length": 255 + }, + { + "default": "0", + "fieldname": "allow_in_quick_entry", + "fieldtype": "Check", + "label": "Allow in Quick Entry" + }, + { + "default": "0", + "depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);", + "fieldname": "in_preview", + "fieldtype": "Check", + "label": "In Preview" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype=='Duration'", + "fieldname": "hide_seconds", + "fieldtype": "Check", + "label": "Hide Seconds" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype=='Duration'", + "fieldname": "hide_days", + "fieldtype": "Check", + "label": "Hide Days" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype=='Section Break'", + "fieldname": "hide_border", + "fieldtype": "Check", + "label": "Hide Border" + }, + { + "default": "0", + "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)", + "fieldname": "non_negative", + "fieldtype": "Check", + "label": "Non Negative" + }, + { + "fieldname": "module", + "fieldtype": "Link", + "label": "Module (for export)", + "options": "Module Def" + }, + { + "default": "0", + "fieldname": "is_system_generated", + "fieldtype": "Check", + "label": "Is System Generated", + "read_only": 1 + } + ], + "icon": "fa fa-glass", + "idx": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2022-06-13 06:39:03.319667", + "modified_by": "Administrator", + "module": "Custom", + "name": "Custom Field", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "search_fields": "dt,label,fieldtype,options", + "sort_field": "modified", + "sort_order": "ASC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/custom/doctype/custom_field/custom_field.py b/influxframework/custom/doctype/custom_field/custom_field.py new file mode 100644 index 0000000..fa9c256 --- /dev/null +++ b/influxframework/custom/doctype/custom_field/custom_field.py @@ -0,0 +1,231 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +import influxframework +from influxframework import _ +from influxframework.model import core_doctypes_list +from influxframework.model.docfield import supports_translation +from influxframework.model.document import Document +from influxframework.query_builder.functions import IfNull +from influxframework.utils import cstr + + +class CustomField(Document): + def autoname(self): + self.set_fieldname() + self.name = self.dt + "-" + self.fieldname + + def set_fieldname(self): + if not self.fieldname: + label = self.label + if not label: + if self.fieldtype in ["Section Break", "Column Break", "Tab Break"]: + label = self.fieldtype + "_" + str(self.idx) + else: + influxframework.throw(_("Label is mandatory")) + + # remove special characters from fieldname + self.fieldname = "".join( + filter(lambda x: x.isdigit() or x.isalpha() or "_", cstr(label).replace(" ", "_")) + ) + + # fieldnames should be lowercase + self.fieldname = self.fieldname.lower() + + def before_insert(self): + self.set_fieldname() + + def validate(self): + # these imports have been added to avoid cyclical import, should fix in future + from influxframework.core.doctype.doctype.doctype import check_fieldname_conflicts + from influxframework.custom.doctype.customize_form.customize_form import CustomizeForm + + # don't always get meta to improve performance + # setting idx is just an improvement, not a requirement + if self.is_new() or self.insert_after == "append": + meta = influxframework.get_meta(self.dt, cached=False) + fieldnames = [df.fieldname for df in meta.get("fields")] + + if self.is_new() and self.fieldname in fieldnames: + influxframework.throw( + _("A field with the name {0} already exists in {1}").format( + influxframework.bold(self.fieldname), self.dt + ) + ) + + if self.insert_after == "append": + self.insert_after = fieldnames[-1] + + if self.insert_after and self.insert_after in fieldnames: + self.idx = fieldnames.index(self.insert_after) + 1 + + if ( + not self.is_virtual + and (doc_before_save := self.get_doc_before_save()) + and (old_fieldtype := doc_before_save.fieldtype) != self.fieldtype + and not CustomizeForm.allow_fieldtype_change(old_fieldtype, self.fieldtype) + ): + influxframework.throw( + _("Fieldtype cannot be changed from {0} to {1}").format(old_fieldtype, self.fieldtype) + ) + + if not self.fieldname: + influxframework.throw(_("Fieldname not set for Custom Field")) + + if self.get("translatable", 0) and not supports_translation(self.fieldtype): + self.translatable = 0 + + check_fieldname_conflicts(self) + + def on_update(self): + # validate field + if not self.flags.ignore_validate: + from influxframework.core.doctype.doctype.doctype import validate_fields_for_doctype + + validate_fields_for_doctype(self.dt) + + # clear cache and update the schema + if not influxframework.flags.in_create_custom_fields: + influxframework.clear_cache(doctype=self.dt) + influxframework.db.updatedb(self.dt) + + def on_trash(self): + # check if Admin owned field + if self.owner == "Administrator" and influxframework.session.user != "Administrator": + influxframework.throw( + _( + "Custom Field {0} is created by the Administrator and can only be deleted through the Administrator account." + ).format(influxframework.bold(self.label)) + ) + + # delete property setter entries + influxframework.db.delete("Property Setter", {"doc_type": self.dt, "field_name": self.fieldname}) + + # update doctype layouts + doctype_layouts = influxframework.get_all( + "DocType Layout", filters={"document_type": self.dt}, pluck="name" + ) + + for layout in doctype_layouts: + layout_doc = influxframework.get_doc("DocType Layout", layout) + for field in layout_doc.fields: + if field.fieldname == self.fieldname: + layout_doc.remove(field) + layout_doc.save() + break + + influxframework.clear_cache(doctype=self.dt) + + def validate_insert_after(self, meta): + if not meta.get_field(self.insert_after): + influxframework.throw( + _( + "Insert After field '{0}' mentioned in Custom Field '{1}', with label '{2}', does not exist" + ).format(self.insert_after, self.name, self.label), + influxframework.DoesNotExistError, + ) + + if self.fieldname == self.insert_after: + influxframework.throw(_("Insert After cannot be set as {0}").format(meta.get_label(self.insert_after))) + + +@influxframework.whitelist() +def get_fields_label(doctype=None): + meta = influxframework.get_meta(doctype) + + if doctype in core_doctypes_list: + return influxframework.msgprint(_("Custom Fields cannot be added to core DocTypes.")) + + if meta.custom: + return influxframework.msgprint(_("Custom Fields can only be added to a standard DocType.")) + + return [ + {"value": df.fieldname or "", "label": _(df.label or "")} + for df in influxframework.get_meta(doctype).get("fields") + ] + + +def create_custom_field_if_values_exist(doctype, df): + df = influxframework._dict(df) + if df.fieldname in influxframework.db.get_table_columns(doctype) and influxframework.db.count( + dt=doctype, filters=IfNull(df.fieldname, "") != "" + ): + create_custom_field(doctype, df) + + +def create_custom_field(doctype, df, ignore_validate=False, is_system_generated=True): + df = influxframework._dict(df) + if not df.fieldname and df.label: + df.fieldname = influxframework.scrub(df.label) + if not influxframework.db.get_value("Custom Field", {"dt": doctype, "fieldname": df.fieldname}): + custom_field = influxframework.get_doc( + { + "doctype": "Custom Field", + "dt": doctype, + "permlevel": 0, + "fieldtype": "Data", + "hidden": 0, + "is_system_generated": is_system_generated, + } + ) + custom_field.update(df) + custom_field.flags.ignore_validate = ignore_validate + custom_field.insert() + return custom_field + + +def create_custom_fields(custom_fields, ignore_validate=False, update=True): + """Add / update multiple custom fields + + :param custom_fields: example `{'Sales Invoice': [dict(fieldname='test')]}`""" + + try: + influxframework.flags.in_create_custom_fields = True + doctypes_to_update = set() + + if influxframework.flags.in_setup_wizard: + ignore_validate = True + + for doctypes, fields in custom_fields.items(): + if isinstance(fields, dict): + # only one field + fields = [fields] + + if isinstance(doctypes, str): + # only one doctype + doctypes = (doctypes,) + + for doctype in doctypes: + doctypes_to_update.add(doctype) + + for df in fields: + field = influxframework.db.get_value("Custom Field", {"dt": doctype, "fieldname": df["fieldname"]}) + if not field: + try: + df = df.copy() + df["owner"] = "Administrator" + create_custom_field(doctype, df, ignore_validate=ignore_validate) + + except influxframework.exceptions.DuplicateEntryError: + pass + + elif update: + custom_field = influxframework.get_doc("Custom Field", field) + custom_field.flags.ignore_validate = ignore_validate + custom_field.update(df) + custom_field.save() + + for doctype in doctypes_to_update: + influxframework.clear_cache(doctype=doctype) + influxframework.db.updatedb(doctype) + + finally: + influxframework.flags.in_create_custom_fields = False + + +@influxframework.whitelist() +def add_custom_field(doctype, df): + df = json.loads(df) + return create_custom_field(doctype, df) diff --git a/influxframework/custom/doctype/custom_field/test_custom_field.py b/influxframework/custom/doctype/custom_field/test_custom_field.py new file mode 100644 index 0000000..698d64c --- /dev/null +++ b/influxframework/custom/doctype/custom_field/test_custom_field.py @@ -0,0 +1,39 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + +test_records = influxframework.get_test_records("Custom Field") + + +class TestCustomField(InfluxFrameworkTestCase): + def test_create_custom_fields(self): + from .custom_field import create_custom_fields + + create_custom_fields( + { + "Address": [ + { + "fieldname": "_test_custom_field_1", + "label": "_Test Custom Field 1", + "fieldtype": "Data", + "insert_after": "phone", + }, + ], + ("Address", "Contact"): [ + { + "fieldname": "_test_custom_field_2", + "label": "_Test Custom Field 2", + "fieldtype": "Data", + "insert_after": "phone", + }, + ], + } + ) + + influxframework.db.commit() + + self.assertTrue(influxframework.db.exists("Custom Field", "Address-_test_custom_field_1")) + self.assertTrue(influxframework.db.exists("Custom Field", "Address-_test_custom_field_2")) + self.assertTrue(influxframework.db.exists("Custom Field", "Contact-_test_custom_field_2")) diff --git a/influxframework/custom/doctype/custom_field/test_records.json b/influxframework/custom/doctype/custom_field/test_records.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/influxframework/custom/doctype/custom_field/test_records.json @@ -0,0 +1 @@ +[] diff --git a/influxframework/custom/doctype/customize_form/README.md b/influxframework/custom/doctype/customize_form/README.md new file mode 100644 index 0000000..280b66d --- /dev/null +++ b/influxframework/custom/doctype/customize_form/README.md @@ -0,0 +1 @@ +Create a DocType like fields table and allow user to set field properties for which **Property Setter** will be created when updated. \ No newline at end of file diff --git a/influxframework/custom/doctype/customize_form/__init__.py b/influxframework/custom/doctype/customize_form/__init__.py new file mode 100644 index 0000000..2e11a12 --- /dev/null +++ b/influxframework/custom/doctype/customize_form/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE diff --git a/influxframework/custom/doctype/customize_form/customize_form.js b/influxframework/custom/doctype/customize_form/customize_form.js new file mode 100644 index 0000000..abd8931 --- /dev/null +++ b/influxframework/custom/doctype/customize_form/customize_form.js @@ -0,0 +1,360 @@ +// Copyright (c) 2015, InfluxFramework LLC +// MIT License. See license.txt + +influxframework.provide("influxframework.customize_form"); + +influxframework.ui.form.on("Customize Form", { + setup: function (frm) { + // save the last setting if refreshing + window.addEventListener("beforeunload", () => { + if (frm.doc.doc_type && frm.doc.doc_type != "undefined") { + localStorage["customize_doctype"] = frm.doc.doc_type; + } + }); + }, + + onload: function (frm) { + frm.set_query("doc_type", function () { + return { + filters: [ + ["DocType", "issingle", "=", 0], + ["DocType", "custom", "=", 0], + ["DocType", "name", "not in", influxframework.model.core_doctypes_list], + ["DocType", "restrict_to_domain", "in", influxframework.boot.active_domains], + ], + }; + }); + + frm.set_query("default_print_format", function () { + return { + filters: { + print_format_type: ["!=", "JS"], + doc_type: ["=", frm.doc.doc_type], + }, + }; + }); + + $(frm.wrapper).on("grid-row-render", function (e, grid_row) { + if (grid_row.doc && grid_row.doc.fieldtype == "Section Break") { + $(grid_row.row).css({ "font-weight": "bold" }); + } + + grid_row.row.removeClass("highlight"); + + if ( + grid_row.doc.is_custom_field && + !grid_row.row.hasClass("highlight") && + !grid_row.doc.is_system_generated + ) { + grid_row.row.addClass("highlight"); + } + }); + + $(frm.wrapper).on("grid-make-sortable", function (e, frm) { + frm.trigger("setup_sortable"); + }); + + $(frm.wrapper).on("grid-move-row", function (e, frm) { + frm.trigger("setup_sortable"); + }); + }, + + doc_type: function (frm) { + if (frm.doc.doc_type) { + return frm.call({ + method: "fetch_to_customize", + doc: frm.doc, + freeze: true, + callback: function (r) { + if (r) { + if (r._server_messages && r._server_messages.length) { + frm.set_value("doc_type", ""); + } else { + frm.refresh(); + frm.trigger("setup_sortable"); + frm.trigger("setup_default_views"); + } + } + localStorage["customize_doctype"] = frm.doc.doc_type; + }, + }); + } else { + frm.refresh(); + } + }, + + is_calendar_and_gantt: function (frm) { + frm.trigger("setup_default_views"); + }, + + setup_sortable: function (frm) { + frm.doc.fields.forEach(function (f) { + if (!f.is_custom_field) { + f._sortable = false; + } + + if (f.fieldtype == "Table") { + frm.add_custom_button( + f.options, + function () { + frm.set_value("doc_type", f.options); + }, + __("Customize Child Table") + ); + } + }); + frm.fields_dict.fields.grid.refresh(); + }, + + refresh: function (frm) { + frm.disable_save(true); + frm.page.clear_icons(); + + if (frm.doc.doc_type) { + frm.page.set_title(__("Customize Form - {0}", [frm.doc.doc_type])); + influxframework.customize_form.set_primary_action(frm); + + frm.add_custom_button( + __("Go to {0} List", [__(frm.doc.doc_type)]), + function () { + influxframework.set_route("List", frm.doc.doc_type); + }, + __("Actions") + ); + + frm.add_custom_button( + __("Reload"), + function () { + frm.script_manager.trigger("doc_type"); + }, + __("Actions") + ); + + frm.add_custom_button( + __("Reset to defaults"), + function () { + influxframework.customize_form.confirm(__("Remove all customizations?"), frm); + }, + __("Actions") + ); + + frm.add_custom_button( + __("Set Permissions"), + function () { + influxframework.set_route("permission-manager", frm.doc.doc_type); + }, + __("Actions") + ); + + const is_autoname_autoincrement = frm.doc.autoname === "autoincrement"; + frm.set_df_property("naming_rule", "hidden", is_autoname_autoincrement); + frm.set_df_property("autoname", "read_only", is_autoname_autoincrement); + } + + frm.events.setup_export(frm); + frm.events.setup_sort_order(frm); + frm.events.set_default_doc_type(frm); + }, + + set_default_doc_type(frm) { + let doc_type; + if (influxframework.route_options && influxframework.route_options.doc_type) { + doc_type = influxframework.route_options.doc_type; + influxframework.route_options = null; + localStorage.removeItem("customize_doctype"); + } + if (!doc_type) { + doc_type = localStorage.getItem("customize_doctype"); + } + if (doc_type) { + setTimeout(() => frm.set_value("doc_type", doc_type, false, true), 1000); + } + }, + + setup_export(frm) { + if (influxframework.boot.developer_mode) { + frm.add_custom_button( + __("Export Customizations"), + function () { + influxframework.prompt( + [ + { + fieldtype: "Link", + fieldname: "module", + options: "Module Def", + label: __("Module to Export"), + reqd: 1, + }, + { + fieldtype: "Check", + fieldname: "sync_on_migrate", + label: __("Sync on Migrate"), + default: 1, + }, + { + fieldtype: "Check", + fieldname: "with_permissions", + label: __("Export Custom Permissions"), + default: 1, + }, + ], + function (data) { + influxframework.call({ + method: "influxframework.modules.utils.export_customizations", + args: { + doctype: frm.doc.doc_type, + module: data.module, + sync_on_migrate: data.sync_on_migrate, + with_permissions: data.with_permissions, + }, + }); + }, + __("Select Module") + ); + }, + __("Actions") + ); + } + }, + + setup_sort_order(frm) { + // sort order select + if (frm.doc.doc_type) { + var fields = $.map(frm.doc.fields, function (df) { + return influxframework.model.is_value_type(df.fieldtype) ? df.fieldname : null; + }); + fields = ["", "name", "modified"].concat(fields); + frm.set_df_property("sort_field", "options", fields); + } + }, + + setup_default_views(frm) { + influxframework.model.set_default_views_for_doctype(frm.doc.doc_type, frm); + }, +}); + +// can't delete standard fields +influxframework.ui.form.on("Customize Form Field", { + before_fields_remove: function (frm, doctype, name) { + var row = influxframework.get_doc(doctype, name); + if (!(row.is_custom_field || row.__islocal)) { + influxframework.msgprint(__("Cannot delete standard field. You can hide it if you want")); + throw "cannot delete standard field"; + } + }, + fields_add: function (frm, cdt, cdn) { + var f = influxframework.model.get_doc(cdt, cdn); + f.is_system_generated = false; + f.is_custom_field = true; + frm.trigger("setup_default_views"); + }, +}); + +// can't delete standard links +influxframework.ui.form.on("DocType Link", { + before_links_remove: function (frm, doctype, name) { + let row = influxframework.get_doc(doctype, name); + if (!(row.custom || row.__islocal)) { + influxframework.msgprint(__("Cannot delete standard link. You can hide it if you want")); + throw "cannot delete standard link"; + } + }, + links_add: function (frm, cdt, cdn) { + let f = influxframework.model.get_doc(cdt, cdn); + f.custom = 1; + }, +}); + +// can't delete standard actions +influxframework.ui.form.on("DocType Action", { + before_actions_remove: function (frm, doctype, name) { + let row = influxframework.get_doc(doctype, name); + if (!(row.custom || row.__islocal)) { + influxframework.msgprint(__("Cannot delete standard action. You can hide it if you want")); + throw "cannot delete standard action"; + } + }, + actions_add: function (frm, cdt, cdn) { + let f = influxframework.model.get_doc(cdt, cdn); + f.custom = 1; + }, +}); + +// can't delete standard states +influxframework.ui.form.on("DocType State", { + before_states_remove: function (frm, doctype, name) { + let row = influxframework.get_doc(doctype, name); + if (!(row.custom || row.__islocal)) { + influxframework.msgprint(__("Cannot delete standard document state.")); + throw "cannot delete standard document state"; + } + }, + states_add: function (frm, cdt, cdn) { + let f = influxframework.model.get_doc(cdt, cdn); + f.custom = 1; + }, +}); + +influxframework.customize_form.set_primary_action = function (frm) { + frm.page.set_primary_action(__("Update"), function () { + if (frm.doc.doc_type) { + return frm.call({ + doc: frm.doc, + freeze: true, + btn: frm.page.btn_primary, + method: "save_customization", + callback: function (r) { + if (!r.exc) { + influxframework.customize_form.clear_locals_and_refresh(frm); + frm.script_manager.trigger("doc_type"); + } + }, + }); + } + }); +}; + +influxframework.customize_form.confirm = function (msg, frm) { + if (!frm.doc.doc_type) return; + + var d = new influxframework.ui.Dialog({ + title: "Reset To Defaults", + fields: [ + { + fieldtype: "HTML", + options: __("All customizations will be removed. Please confirm."), + }, + ], + primary_action: function () { + return frm.call({ + doc: frm.doc, + method: "reset_to_defaults", + callback: function (r) { + if (r.exc) { + influxframework.msgprint(r.exc); + } else { + d.hide(); + influxframework.show_alert({ + message: __("Customizations Reset"), + indicator: "green", + }); + influxframework.customize_form.clear_locals_and_refresh(frm); + } + }, + }); + }, + }); + + influxframework.customize_form.confirm.dialog = d; + d.show(); +}; + +influxframework.customize_form.clear_locals_and_refresh = function (frm) { + delete frm.doc.__unsaved; + // clear doctype from locals + influxframework.model.clear_doc("DocType", frm.doc.doc_type); + delete influxframework.meta.docfield_copy[frm.doc.doc_type]; + frm.refresh(); +}; + +extend_cscript(cur_frm.cscript, new influxframework.model.DocTypeController({ frm: cur_frm })); diff --git a/influxframework/custom/doctype/customize_form/customize_form.json b/influxframework/custom/doctype/customize_form/customize_form.json new file mode 100644 index 0000000..4840184 --- /dev/null +++ b/influxframework/custom/doctype/customize_form/customize_form.json @@ -0,0 +1,393 @@ +{ + "actions": [], + "autoname": "DL.####", + "creation": "2013-01-29 17:55:08", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "doc_type", + "properties", + "label", + "search_fields", + "column_break_5", + "istable", + "is_calendar_and_gantt", + "editable_grid", + "quick_entry", + "track_changes", + "track_views", + "allow_auto_repeat", + "allow_import", + "fields_section_break", + "fields", + "naming_section", + "naming_rule", + "autoname", + "form_settings_section", + "image_field", + "max_attachments", + "column_break_21", + "allow_copy", + "make_attachments_public", + "view_settings_section", + "title_field", + "show_title_field_in_link", + "translated_doctype", + "default_print_format", + "default_view", + "force_re_route_to_default_view", + "column_break_29", + "show_preview_popup", + "email_settings_section", + "default_email_template", + "column_break_26", + "email_append_to", + "sender_field", + "subject_field", + "document_actions_section", + "actions", + "document_links_section", + "links", + "document_states_section", + "states", + "section_break_8", + "sort_field", + "column_break_10", + "sort_order" + ], + "fields": [ + { + "fieldname": "naming_rule", + "fieldtype": "Select", + "label": "Naming Rule", + "length": 40, + "options": "\nSet by user\nBy fieldname\nBy \"Naming Series\" field\nExpression\nExpression (old style)\nRandom\nBy script" + }, + { + "fieldname": "doc_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Enter Form Type", + "options": "DocType" + }, + { + "depends_on": "doc_type", + "fieldname": "properties", + "fieldtype": "Section Break" + }, + { + "fieldname": "label", + "fieldtype": "Data", + "label": "Change Label (via Custom Translation)" + }, + { + "fieldname": "default_print_format", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Default Print Format", + "options": "Print Format" + }, + { + "fieldname": "max_attachments", + "fieldtype": "Int", + "label": "Max Attachments" + }, + { + "default": "0", + "fieldname": "allow_copy", + "fieldtype": "Check", + "label": "Hide Copy" + }, + { + "default": "0", + "fieldname": "istable", + "fieldtype": "Check", + "label": "Is Table", + "read_only": 1 + }, + { + "default": "0", + "depends_on": "istable", + "fieldname": "editable_grid", + "fieldtype": "Check", + "label": "Editable Grid" + }, + { + "default": "1", + "fieldname": "quick_entry", + "fieldtype": "Check", + "label": "Quick Entry" + }, + { + "default": "0", + "fieldname": "track_changes", + "fieldtype": "Check", + "label": "Track Changes" + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "description": "Use this fieldname to generate title", + "fieldname": "title_field", + "fieldtype": "Data", + "label": "Title Field" + }, + { + "description": "Must be of type \"Attach Image\"", + "fieldname": "image_field", + "fieldtype": "Data", + "label": "Image Field" + }, + { + "description": "Fields separated by comma (,) will be included in the \"Search By\" list of Search dialog box", + "fieldname": "search_fields", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Search Fields" + }, + { + "collapsible": 1, + "depends_on": "doc_type", + "fieldname": "section_break_8", + "fieldtype": "Section Break", + "label": "List Settings" + }, + { + "fieldname": "sort_field", + "fieldtype": "Select", + "label": "Sort Field" + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break" + }, + { + "fieldname": "sort_order", + "fieldtype": "Select", + "label": "Sort Order", + "options": "ASC\nDESC" + }, + { + "depends_on": "doc_type", + "description": "Customize Label, Print Hide, Default etc.", + "fieldname": "fields_section_break", + "fieldtype": "Section Break", + "label": "Fields" + }, + { + "allow_bulk_edit": 1, + "fieldname": "fields", + "fieldtype": "Table", + "label": "Fields", + "options": "Customize Form Field", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "track_views", + "fieldtype": "Check", + "label": "Track Views" + }, + { + "default": "0", + "fieldname": "allow_auto_repeat", + "fieldtype": "Check", + "label": "Allow Auto Repeat" + }, + { + "default": "0", + "fieldname": "allow_import", + "fieldtype": "Check", + "label": "Allow Import (via Data Import Tool)" + }, + { + "depends_on": "email_append_to", + "fieldname": "subject_field", + "fieldtype": "Data", + "label": "Subject Field" + }, + { + "depends_on": "email_append_to", + "fieldname": "sender_field", + "fieldtype": "Data", + "label": "Sender Field", + "mandatory_depends_on": "email_append_to" + }, + { + "default": "0", + "fieldname": "email_append_to", + "fieldtype": "Check", + "label": "Allow document creation via Email" + }, + { + "default": "0", + "fieldname": "show_preview_popup", + "fieldtype": "Check", + "label": "Show Preview Popup" + }, + { + "collapsible": 1, + "depends_on": "doc_type", + "fieldname": "view_settings_section", + "fieldtype": "Section Break", + "label": "View Settings" + }, + { + "fieldname": "column_break_29", + "fieldtype": "Column Break" + }, + { + "collapsible": 1, + "collapsible_depends_on": "email_append_to", + "depends_on": "doc_type", + "fieldname": "email_settings_section", + "fieldtype": "Section Break", + "label": "Email Settings" + }, + { + "collapsible": 1, + "collapsible_depends_on": "links", + "depends_on": "doc_type", + "fieldname": "document_links_section", + "fieldtype": "Section Break", + "label": "Document Links" + }, + { + "fieldname": "links", + "fieldtype": "Table", + "label": "Links", + "options": "DocType Link" + }, + { + "collapsible": 1, + "collapsible_depends_on": "actions", + "depends_on": "doc_type", + "fieldname": "document_actions_section", + "fieldtype": "Section Break", + "label": "Document Actions" + }, + { + "fieldname": "actions", + "fieldtype": "Table", + "label": "Actions", + "options": "DocType Action" + }, + { + "fieldname": "default_email_template", + "fieldtype": "Link", + "label": "Default Email Template", + "options": "Email Template" + }, + { + "fieldname": "column_break_26", + "fieldtype": "Column Break" + }, + { + "collapsible": 1, + "depends_on": "doc_type", + "fieldname": "naming_section", + "fieldtype": "Section Break", + "label": "Naming" + }, + { + "description": "Naming Options:\n
    1. field:[fieldname] - By Field
    2. naming_series: - By Naming Series (field called naming_series must be present)
    3. Prompt - Prompt user for a name
    4. [series] - Series by prefix (separated by a dot); for example PRE.#####
    5. \n
    6. format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
    ", + "fieldname": "autoname", + "fieldtype": "Data", + "label": "Auto Name" + }, + { + "collapsible": 1, + "collapsible_depends_on": "states", + "depends_on": "doc_type", + "fieldname": "document_states_section", + "fieldtype": "Section Break", + "label": "Document States" + }, + { + "fieldname": "states", + "fieldtype": "Table", + "label": "States", + "options": "DocType State" + }, + { + "default": "0", + "fieldname": "show_title_field_in_link", + "fieldtype": "Check", + "label": "Show Title in Link Fields" + }, + { + "default": "0", + "fieldname": "translated_doctype", + "fieldtype": "Check", + "label": "Translate Link Fields" + }, + { + "collapsible": 1, + "fieldname": "form_settings_section", + "fieldtype": "Section Break", + "label": "Form Settings" + }, + { + "fieldname": "column_break_21", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "make_attachments_public", + "fieldtype": "Check", + "label": "Make Attachments Public by Default" + }, + { + "fieldname": "default_view", + "fieldtype": "Select", + "label": "Default View" + }, + { + "default": "0", + "depends_on": "default_view", + "fieldname": "force_re_route_to_default_view", + "fieldtype": "Check", + "label": "Force Re-route to Default View" + }, + { + "default": "0", + "description": "Enables Calendar and Gantt views.", + "fieldname": "is_calendar_and_gantt", + "fieldtype": "Check", + "label": "Is Calendar and Gantt" + } + ], + "hide_toolbar": 1, + "icon": "fa fa-glass", + "idx": 1, + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2022-08-30 11:45:16.772277", + "modified_by": "Administrator", + "module": "Custom", + "name": "Customize Form", + "naming_rule": "Expression (old style)", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "search_fields": "doc_type", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/custom/doctype/customize_form/customize_form.py b/influxframework/custom/doctype/customize_form/customize_form.py new file mode 100644 index 0000000..d88a9a7 --- /dev/null +++ b/influxframework/custom/doctype/customize_form/customize_form.py @@ -0,0 +1,675 @@ +# Copyright (c) 2021, InfluxFramework LLC +# MIT License. See LICENSE + +""" + Customize Form is a Single DocType used to mask the Property Setter + Thus providing a better UI from user perspective +""" +import json + +import influxframework +import influxframework.translate +from influxframework import _ +from influxframework.core.doctype.doctype.doctype import ( + check_email_append_to, + validate_autoincrement_autoname, + validate_fields_for_doctype, + validate_series, +) +from influxframework.custom.doctype.custom_field.custom_field import create_custom_field +from influxframework.custom.doctype.property_setter.property_setter import delete_property_setter +from influxframework.model import core_doctypes_list, no_value_fields +from influxframework.model.docfield import supports_translation +from influxframework.model.document import Document +from influxframework.utils import cint + + +class CustomizeForm(Document): + def on_update(self): + influxframework.db.delete("Singles", {"doctype": "Customize Form"}) + influxframework.db.delete("Customize Form Field") + + @influxframework.whitelist() + def fetch_to_customize(self): + self.clear_existing_doc() + if not self.doc_type: + return + + meta = influxframework.get_meta(self.doc_type) + + self.validate_doctype(meta) + + # load the meta properties on the customize (self) object + self.load_properties(meta) + + # load custom translation + translation = self.get_name_translation() + self.label = translation.translated_text if translation else "" + + self.create_auto_repeat_custom_field_if_required(meta) + + # NOTE doc (self) is sent to clientside by run_method + + def validate_doctype(self, meta): + """ + Check if the doctype is allowed to be customized. + """ + if self.doc_type in core_doctypes_list: + influxframework.throw(_("Core DocTypes cannot be customized.")) + + if meta.issingle: + influxframework.throw(_("Single DocTypes cannot be customized.")) + + if meta.custom: + influxframework.throw(_("Only standard DocTypes are allowed to be customized from Customize Form.")) + + def load_properties(self, meta): + """ + Load the customize object (this) with the metadata properties + """ + # doctype properties + for prop in doctype_properties: + self.set(prop, meta.get(prop)) + + for d in meta.get("fields"): + new_d = { + "fieldname": d.fieldname, + "is_custom_field": d.get("is_custom_field"), + "is_system_generated": d.get("is_system_generated"), + "name": d.name, + } + for prop in docfield_properties: + new_d[prop] = d.get(prop) + self.append("fields", new_d) + + for fieldname in ("links", "actions", "states"): + for d in meta.get(fieldname): + self.append(fieldname, d) + + def create_auto_repeat_custom_field_if_required(self, meta): + """ + Create auto repeat custom field if it's not already present + """ + if self.allow_auto_repeat: + all_fields = [df.fieldname for df in meta.fields] + + if "auto_repeat" in all_fields: + return + + insert_after = self.fields[len(self.fields) - 1].fieldname + create_custom_field( + self.doc_type, + dict( + fieldname="auto_repeat", + label="Auto Repeat", + fieldtype="Link", + options="Auto Repeat", + insert_after=insert_after, + read_only=1, + no_copy=1, + print_hide=1, + ), + ) + + def get_name_translation(self): + """Get translation object if exists of current doctype name in the default language""" + return influxframework.get_value( + "Translation", + {"source_text": self.doc_type, "language": influxframework.local.lang or "en"}, + ["name", "translated_text"], + as_dict=True, + ) + + def set_name_translation(self): + """Create, update custom translation for this doctype""" + current = self.get_name_translation() + if not self.label: + if current: + # clear translation + influxframework.delete_doc("Translation", current.name) + return + + if not current: + influxframework.get_doc( + { + "doctype": "Translation", + "source_text": self.doc_type, + "translated_text": self.label, + "language_code": influxframework.local.lang or "en", + } + ).insert() + return + + if self.label != current.translated_text: + influxframework.db.set_value("Translation", current.name, "translated_text", self.label) + influxframework.translate.clear_cache() + + def clear_existing_doc(self): + doc_type = self.doc_type + + for fieldname in self.meta.get_valid_columns(): + self.set(fieldname, None) + + for df in self.meta.get_table_fields(): + self.set(df.fieldname, []) + + self.doc_type = doc_type + self.name = "Customize Form" + + @influxframework.whitelist() + def save_customization(self): + if not self.doc_type: + return + + validate_series(self, self.autoname, self.doc_type) + validate_autoincrement_autoname(self) + self.flags.update_db = False + self.flags.rebuild_doctype_for_global_search = False + self.set_property_setters() + self.update_custom_fields() + self.set_name_translation() + validate_fields_for_doctype(self.doc_type) + check_email_append_to(self) + + if self.flags.update_db: + influxframework.db.updatedb(self.doc_type) + + if not hasattr(self, "hide_success") or not self.hide_success: + influxframework.msgprint(_("{0} updated").format(_(self.doc_type)), alert=True) + influxframework.clear_cache(doctype=self.doc_type) + self.fetch_to_customize() + + if self.flags.rebuild_doctype_for_global_search: + influxframework.enqueue( + "influxframework.utils.global_search.rebuild_for_doctype", now=True, doctype=self.doc_type + ) + + def set_property_setters(self): + meta = influxframework.get_meta(self.doc_type) + + # doctype + self.set_property_setters_for_doctype(meta) + + # docfield + for df in self.get("fields"): + meta_df = meta.get("fields", {"fieldname": df.fieldname}) + if not meta_df or meta_df[0].get("is_custom_field"): + continue + self.set_property_setters_for_docfield(meta, df, meta_df) + + # action and links + self.set_property_setters_for_actions_and_links(meta) + + def set_property_setters_for_doctype(self, meta): + for prop, prop_type in doctype_properties.items(): + if self.get(prop) != meta.get(prop): + self.make_property_setter(prop, self.get(prop), prop_type) + + def set_property_setters_for_docfield(self, meta, df, meta_df): + for prop, prop_type in docfield_properties.items(): + if prop != "idx" and (df.get(prop) or "") != (meta_df[0].get(prop) or ""): + if not self.allow_property_change(prop, meta_df, df): + continue + + self.make_property_setter(prop, df.get(prop), prop_type, fieldname=df.fieldname) + + def allow_property_change(self, prop, meta_df, df): + if prop == "fieldtype": + self.validate_fieldtype_change(df, meta_df[0].get(prop), df.get(prop)) + + elif prop == "length": + old_value_length = cint(meta_df[0].get(prop)) + new_value_length = cint(df.get(prop)) + + if new_value_length and (old_value_length > new_value_length): + self.check_length_for_fieldtypes.append({"df": df, "old_value": meta_df[0].get(prop)}) + self.validate_fieldtype_length() + else: + self.flags.update_db = True + + elif prop == "allow_on_submit" and df.get(prop): + if not influxframework.db.get_value( + "DocField", {"parent": self.doc_type, "fieldname": df.fieldname}, "allow_on_submit" + ): + influxframework.msgprint( + _("Row {0}: Not allowed to enable Allow on Submit for standard fields").format(df.idx) + ) + return False + + elif prop == "reqd" and ( + ( + influxframework.db.get_value("DocField", {"parent": self.doc_type, "fieldname": df.fieldname}, "reqd") + == 1 + ) + and (df.get(prop) == 0) + ): + influxframework.msgprint( + _("Row {0}: Not allowed to disable Mandatory for standard fields").format(df.idx) + ) + return False + + elif ( + prop == "in_list_view" + and df.get(prop) + and df.fieldtype != "Attach Image" + and df.fieldtype in no_value_fields + ): + influxframework.msgprint( + _("'In List View' not allowed for type {0} in row {1}").format(df.fieldtype, df.idx) + ) + return False + + elif ( + prop == "precision" + and cint(df.get("precision")) > 6 + and cint(df.get("precision")) > cint(meta_df[0].get("precision")) + ): + self.flags.update_db = True + + elif prop == "unique": + self.flags.update_db = True + + elif ( + prop == "read_only" + and cint(df.get("read_only")) == 0 + and influxframework.db.get_value( + "DocField", {"parent": self.doc_type, "fieldname": df.fieldname}, "read_only" + ) + == 1 + ): + # if docfield has read_only checked and user is trying to make it editable, don't allow it + influxframework.msgprint(_("You cannot unset 'Read Only' for field {0}").format(df.label)) + return False + + elif prop == "options" and df.get("fieldtype") not in ALLOWED_OPTIONS_CHANGE: + influxframework.msgprint(_("You can't set 'Options' for field {0}").format(df.label)) + return False + + elif prop == "translatable" and not supports_translation(df.get("fieldtype")): + influxframework.msgprint(_("You can't set 'Translatable' for field {0}").format(df.label)) + return False + + elif prop == "in_global_search" and df.in_global_search != meta_df[0].get("in_global_search"): + self.flags.rebuild_doctype_for_global_search = True + + return True + + def set_property_setters_for_actions_and_links(self, meta): + """ + Apply property setters or create custom records for DocType Action and DocType Link + """ + for doctype, fieldname, field_map in ( + ("DocType Link", "links", doctype_link_properties), + ("DocType Action", "actions", doctype_action_properties), + ("DocType State", "states", doctype_state_properties), + ): + has_custom = False + items = [] + for i, d in enumerate(self.get(fieldname) or []): + d.idx = i + if influxframework.db.exists(doctype, d.name) and not d.custom: + # check property and apply property setter + original = influxframework.get_doc(doctype, d.name) + for prop, prop_type in field_map.items(): + if d.get(prop) != original.get(prop): + self.make_property_setter(prop, d.get(prop), prop_type, apply_on=doctype, row_name=d.name) + items.append(d.name) + else: + # custom - just insert/update + d.parent = self.doc_type + d.custom = 1 + d.save(ignore_permissions=True) + has_custom = True + items.append(d.name) + + self.update_order_property_setter(has_custom, fieldname) + self.clear_removed_items(doctype, items) + + def update_order_property_setter(self, has_custom, fieldname): + """ + We need to maintain the order of the link/actions if the user has shuffled them. + So we create a new property (ex `links_order`) to keep a list of items. + """ + property_name = f"{fieldname}_order" + if has_custom: + # save the order of the actions and links + self.make_property_setter( + property_name, json.dumps([d.name for d in self.get(fieldname)]), "Small Text" + ) + else: + influxframework.db.delete("Property Setter", dict(property=property_name, doc_type=self.doc_type)) + + def clear_removed_items(self, doctype, items): + """ + Clear rows that do not appear in `items`. These have been removed by the user. + """ + if items: + influxframework.db.delete(doctype, dict(parent=self.doc_type, custom=1, name=("not in", items))) + else: + influxframework.db.delete(doctype, dict(parent=self.doc_type, custom=1)) + + def update_custom_fields(self): + for i, df in enumerate(self.get("fields")): + if df.get("is_custom_field"): + if not influxframework.db.exists("Custom Field", {"dt": self.doc_type, "fieldname": df.fieldname}): + self.add_custom_field(df, i) + self.flags.update_db = True + else: + self.update_in_custom_field(df, i) + + self.delete_custom_fields() + + def add_custom_field(self, df, i): + d = influxframework.new_doc("Custom Field") + + d.dt = self.doc_type + + for prop in docfield_properties: + d.set(prop, df.get(prop)) + + if i != 0: + d.insert_after = self.fields[i - 1].fieldname + d.idx = i + + d.insert() + df.fieldname = d.fieldname + + if df.get("in_global_search"): + self.flags.rebuild_doctype_for_global_search = True + + def update_in_custom_field(self, df, i): + meta = influxframework.get_meta(self.doc_type) + meta_df = meta.get("fields", {"fieldname": df.fieldname}) + if not (meta_df and meta_df[0].get("is_custom_field")): + # not a custom field + return + + custom_field = influxframework.get_doc("Custom Field", meta_df[0].name) + changed = False + for prop in docfield_properties: + if df.get(prop) != custom_field.get(prop): + if prop == "fieldtype": + self.validate_fieldtype_change(df, meta_df[0].get(prop), df.get(prop)) + if prop == "in_global_search": + self.flags.rebuild_doctype_for_global_search = True + + custom_field.set(prop, df.get(prop)) + changed = True + + # check and update `insert_after` property + if i != 0: + insert_after = self.fields[i - 1].fieldname + if custom_field.insert_after != insert_after: + custom_field.insert_after = insert_after + custom_field.idx = i + changed = True + + if changed: + custom_field.db_update() + self.flags.update_db = True + # custom_field.save() + + def delete_custom_fields(self): + meta = influxframework.get_meta(self.doc_type) + fields_to_remove = {df.fieldname for df in meta.get("fields")} - { + df.fieldname for df in self.get("fields") + } + for fieldname in fields_to_remove: + df = meta.get("fields", {"fieldname": fieldname})[0] + if df.get("is_custom_field"): + influxframework.delete_doc("Custom Field", df.name) + + def make_property_setter( + self, prop, value, property_type, fieldname=None, apply_on=None, row_name=None + ): + delete_property_setter(self.doc_type, prop, fieldname, row_name) + + property_value = self.get_existing_property_value(prop, fieldname) + + if property_value == value: + return + + if not apply_on: + apply_on = "DocField" if fieldname else "DocType" + + # create a new property setter + influxframework.make_property_setter( + { + "doctype": self.doc_type, + "doctype_or_field": apply_on, + "fieldname": fieldname, + "row_name": row_name, + "property": prop, + "value": value, + "property_type": property_type, + }, + is_system_generated=False, + ) + + def get_existing_property_value(self, property_name, fieldname=None): + # check if there is any need to make property setter! + if fieldname: + property_value = influxframework.db.get_value( + "DocField", {"parent": self.doc_type, "fieldname": fieldname}, property_name + ) + else: + if influxframework.db.has_column("DocType", property_name): + property_value = influxframework.db.get_value("DocType", self.doc_type, property_name) + else: + property_value = None + + return property_value + + def validate_fieldtype_change(self, df, old_value, new_value): + if df.is_virtual: + return + + allowed = self.allow_fieldtype_change(old_value, new_value) + if allowed: + old_value_length = cint(influxframework.db.type_map.get(old_value)[1]) + new_value_length = cint(influxframework.db.type_map.get(new_value)[1]) + + # Ignore fieldtype check validation if new field type has unspecified maxlength + # Changes like DATA to TEXT, where new_value_lenth equals 0 will not be validated + if new_value_length and (old_value_length > new_value_length): + self.check_length_for_fieldtypes.append({"df": df, "old_value": old_value}) + self.validate_fieldtype_length() + else: + self.flags.update_db = True + + else: + influxframework.throw( + _("Fieldtype cannot be changed from {0} to {1} in row {2}").format( + old_value, new_value, df.idx + ) + ) + + def validate_fieldtype_length(self): + for field in self.check_length_for_fieldtypes: + df = field.get("df") + max_length = cint(influxframework.db.type_map.get(df.fieldtype)[1]) + fieldname = df.fieldname + docs = influxframework.db.sql( + """ + SELECT name, {fieldname}, LENGTH({fieldname}) AS len + FROM `tab{doctype}` + WHERE LENGTH({fieldname}) > {max_length} + """.format( + fieldname=fieldname, doctype=self.doc_type, max_length=max_length + ), + as_dict=True, + ) + links = [] + label = df.label + for doc in docs: + links.append(influxframework.utils.get_link_to_form(self.doc_type, doc.name)) + links_str = ", ".join(links) + + if docs: + influxframework.throw( + _( + "Value for field {0} is too long in {1}. Length should be lesser than {2} characters" + ).format(influxframework.bold(label), links_str, influxframework.bold(max_length)), + title=_("Data Too Long"), + is_minimizable=len(docs) > 1, + ) + + self.flags.update_db = True + + @influxframework.whitelist() + def reset_to_defaults(self): + if not self.doc_type: + return + + reset_customization(self.doc_type) + self.fetch_to_customize() + + @classmethod + def allow_fieldtype_change(self, old_type: str, new_type: str) -> bool: + """allow type change, if both old_type and new_type are in same field group. + field groups are defined in ALLOWED_FIELDTYPE_CHANGE variables. + """ + + def in_field_group(group): + return (old_type in group) and (new_type in group) + + return any(map(in_field_group, ALLOWED_FIELDTYPE_CHANGE)) + + +def reset_customization(doctype): + setters = influxframework.get_all( + "Property Setter", + filters={ + "doc_type": doctype, + "field_name": ["!=", "naming_series"], + "property": ["!=", "options"], + "is_system_generated": False, + }, + pluck="name", + ) + + for setter in setters: + influxframework.delete_doc("Property Setter", setter) + + custom_fields = influxframework.get_all( + "Custom Field", filters={"dt": doctype, "is_system_generated": False}, pluck="name" + ) + + for field in custom_fields: + influxframework.delete_doc("Custom Field", field) + + influxframework.clear_cache(doctype=doctype) + + +doctype_properties = { + "search_fields": "Data", + "title_field": "Data", + "image_field": "Data", + "sort_field": "Data", + "sort_order": "Data", + "default_print_format": "Data", + "allow_copy": "Check", + "istable": "Check", + "quick_entry": "Check", + "editable_grid": "Check", + "max_attachments": "Int", + "make_attachments_public": "Check", + "track_changes": "Check", + "track_views": "Check", + "allow_auto_repeat": "Check", + "allow_import": "Check", + "show_preview_popup": "Check", + "default_email_template": "Data", + "email_append_to": "Check", + "subject_field": "Data", + "sender_field": "Data", + "naming_rule": "Data", + "autoname": "Data", + "show_title_field_in_link": "Check", + "translate_link_fields": "Check", + "is_calendar_and_gantt": "Check", + "default_view": "Select", + "force_re_route_to_default_view": "Check", + "translated_doctype": "Check", +} + +docfield_properties = { + "idx": "Int", + "label": "Data", + "fieldtype": "Select", + "options": "Text", + "fetch_from": "Small Text", + "fetch_if_empty": "Check", + "show_dashboard": "Check", + "permlevel": "Int", + "width": "Data", + "print_width": "Data", + "non_negative": "Check", + "reqd": "Check", + "unique": "Check", + "ignore_user_permissions": "Check", + "in_list_view": "Check", + "in_standard_filter": "Check", + "in_global_search": "Check", + "in_preview": "Check", + "bold": "Check", + "no_copy": "Check", + "ignore_xss_filter": "Check", + "hidden": "Check", + "collapsible": "Check", + "collapsible_depends_on": "Data", + "print_hide": "Check", + "print_hide_if_no_value": "Check", + "report_hide": "Check", + "allow_on_submit": "Check", + "translatable": "Check", + "mandatory_depends_on": "Data", + "read_only_depends_on": "Data", + "depends_on": "Data", + "description": "Text", + "default": "Text", + "precision": "Select", + "read_only": "Check", + "length": "Int", + "columns": "Int", + "remember_last_selected_value": "Check", + "allow_bulk_edit": "Check", + "auto_repeat": "Link", + "allow_in_quick_entry": "Check", + "hide_border": "Check", + "hide_days": "Check", + "hide_seconds": "Check", + "is_virtual": "Check", +} + +doctype_link_properties = { + "link_doctype": "Link", + "link_fieldname": "Data", + "group": "Data", + "hidden": "Check", +} + +doctype_action_properties = { + "label": "Link", + "action_type": "Select", + "action": "Small Text", + "group": "Data", + "hidden": "Check", +} + +doctype_state_properties = {"title": "Data", "color": "Select"} + + +ALLOWED_FIELDTYPE_CHANGE = ( + ("Currency", "Float", "Percent"), + ("Small Text", "Data"), + ("Text", "Data"), + ("Text", "Text Editor", "Code", "Signature", "HTML Editor"), + ("Data", "Select"), + ("Text", "Small Text"), + ("Text", "Data", "Barcode"), + ("Code", "Geolocation"), + ("Table", "Table MultiSelect"), +) + +ALLOWED_OPTIONS_CHANGE = ("Read Only", "HTML", "Data") diff --git a/influxframework/custom/doctype/customize_form/test_customize_form.py b/influxframework/custom/doctype/customize_form/test_customize_form.py new file mode 100644 index 0000000..9dd63c7 --- /dev/null +++ b/influxframework/custom/doctype/customize_form/test_customize_form.py @@ -0,0 +1,405 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +import influxframework +from influxframework.core.doctype.doctype.doctype import InvalidFieldNameError +from influxframework.core.doctype.doctype.test_doctype import new_doctype +from influxframework.test_runner import make_test_records_for_doctype +from influxframework.tests.utils import InfluxFrameworkTestCase + +test_dependencies = ["Custom Field", "Property Setter"] + + +class TestCustomizeForm(InfluxFrameworkTestCase): + def insert_custom_field(self): + influxframework.delete_doc_if_exists("Custom Field", "Event-test_custom_field") + influxframework.get_doc( + { + "doctype": "Custom Field", + "dt": "Event", + "label": "Test Custom Field", + "description": "A Custom Field for Testing", + "fieldtype": "Select", + "in_list_view": 1, + "options": "\nCustom 1\nCustom 2\nCustom 3", + "default": "Custom 3", + "insert_after": influxframework.get_meta("Event").fields[-1].fieldname, + } + ).insert() + + def setUp(self): + self.insert_custom_field() + influxframework.db.delete("Property Setter", dict(doc_type="Event")) + influxframework.db.commit() + influxframework.clear_cache(doctype="Event") + + def tearDown(self): + influxframework.delete_doc("Custom Field", "Event-test_custom_field") + influxframework.db.commit() + influxframework.clear_cache(doctype="Event") + + def get_customize_form(self, doctype=None): + d = influxframework.get_doc("Customize Form") + if doctype: + d.doc_type = doctype + d.run_method("fetch_to_customize") + return d + + def test_fetch_to_customize(self): + d = self.get_customize_form() + self.assertEqual(d.doc_type, None) + self.assertEqual(len(d.get("fields")), 0) + + d = self.get_customize_form("Event") + self.assertEqual(d.doc_type, "Event") + self.assertEqual(len(d.get("fields")), 36) + + d = self.get_customize_form("Event") + self.assertEqual(d.doc_type, "Event") + + self.assertEqual(len(d.get("fields")), len(influxframework.get_doc("DocType", d.doc_type).fields) + 1) + self.assertEqual(d.get("fields")[-1].fieldname, "test_custom_field") + self.assertEqual(d.get("fields", {"fieldname": "event_type"})[0].in_list_view, 1) + + return d + + def test_save_customization_property(self): + d = self.get_customize_form("Event") + self.assertEqual( + influxframework.db.get_value( + "Property Setter", {"doc_type": "Event", "property": "allow_copy"}, "value" + ), + None, + ) + + d.allow_copy = 1 + d.run_method("save_customization") + self.assertEqual( + influxframework.db.get_value( + "Property Setter", {"doc_type": "Event", "property": "allow_copy"}, "value" + ), + "1", + ) + + d.allow_copy = 0 + d.run_method("save_customization") + self.assertEqual( + influxframework.db.get_value( + "Property Setter", {"doc_type": "Event", "property": "allow_copy"}, "value" + ), + None, + ) + + def test_save_customization_field_property(self): + d = self.get_customize_form("Event") + self.assertEqual( + influxframework.db.get_value( + "Property Setter", + {"doc_type": "Event", "property": "reqd", "field_name": "repeat_this_event"}, + "value", + ), + None, + ) + + repeat_this_event_field = d.get("fields", {"fieldname": "repeat_this_event"})[0] + repeat_this_event_field.reqd = 1 + d.run_method("save_customization") + self.assertEqual( + influxframework.db.get_value( + "Property Setter", + {"doc_type": "Event", "property": "reqd", "field_name": "repeat_this_event"}, + "value", + ), + "1", + ) + + repeat_this_event_field = d.get("fields", {"fieldname": "repeat_this_event"})[0] + repeat_this_event_field.reqd = 0 + d.run_method("save_customization") + self.assertEqual( + influxframework.db.get_value( + "Property Setter", + {"doc_type": "Event", "property": "reqd", "field_name": "repeat_this_event"}, + "value", + ), + None, + ) + + def test_save_customization_custom_field_property(self): + d = self.get_customize_form("Event") + self.assertEqual(influxframework.db.get_value("Custom Field", "Event-test_custom_field", "reqd"), 0) + + custom_field = d.get("fields", {"fieldname": "test_custom_field"})[0] + custom_field.reqd = 1 + custom_field.no_copy = 1 + d.run_method("save_customization") + self.assertEqual(influxframework.db.get_value("Custom Field", "Event-test_custom_field", "reqd"), 1) + self.assertEqual(influxframework.db.get_value("Custom Field", "Event-test_custom_field", "no_copy"), 1) + + custom_field = d.get("fields", {"is_custom_field": True})[0] + custom_field.reqd = 0 + custom_field.no_copy = 0 + d.run_method("save_customization") + self.assertEqual(influxframework.db.get_value("Custom Field", "Event-test_custom_field", "reqd"), 0) + self.assertEqual(influxframework.db.get_value("Custom Field", "Event-test_custom_field", "no_copy"), 0) + + def test_save_customization_new_field(self): + d = self.get_customize_form("Event") + last_fieldname = d.fields[-1].fieldname + d.append( + "fields", + { + "label": "Test Add Custom Field Via Customize Form", + "fieldtype": "Data", + "is_custom_field": 1, + }, + ) + d.run_method("save_customization") + self.assertEqual( + influxframework.db.get_value( + "Custom Field", "Event-test_add_custom_field_via_customize_form", "fieldtype" + ), + "Data", + ) + + self.assertEqual( + influxframework.db.get_value( + "Custom Field", "Event-test_add_custom_field_via_customize_form", "insert_after" + ), + last_fieldname, + ) + + influxframework.delete_doc("Custom Field", "Event-test_add_custom_field_via_customize_form") + self.assertEqual( + influxframework.db.get_value("Custom Field", "Event-test_add_custom_field_via_customize_form"), None + ) + + def test_save_customization_remove_field(self): + d = self.get_customize_form("Event") + custom_field = d.get("fields", {"fieldname": "test_custom_field"})[0] + d.get("fields").remove(custom_field) + d.run_method("save_customization") + + self.assertEqual(influxframework.db.get_value("Custom Field", custom_field.name), None) + + influxframework.local.test_objects["Custom Field"] = [] + make_test_records_for_doctype("Custom Field") + + def test_reset_to_defaults(self): + d = influxframework.get_doc("Customize Form") + d.doc_type = "Event" + d.run_method("reset_to_defaults") + + self.assertEqual(d.get("fields", {"fieldname": "repeat_this_event"})[0].in_list_view, 0) + + influxframework.local.test_objects["Property Setter"] = [] + make_test_records_for_doctype("Property Setter") + + def test_set_allow_on_submit(self): + d = self.get_customize_form("Event") + d.get("fields", {"fieldname": "subject"})[0].allow_on_submit = 1 + d.get("fields", {"fieldname": "test_custom_field"})[0].allow_on_submit = 1 + d.run_method("save_customization") + + d = self.get_customize_form("Event") + + # don't allow for standard fields + self.assertEqual(d.get("fields", {"fieldname": "subject"})[0].allow_on_submit or 0, 0) + + # allow for custom field + self.assertEqual(d.get("fields", {"fieldname": "test_custom_field"})[0].allow_on_submit, 1) + + def test_title_field_pattern(self): + d = self.get_customize_form("Web Form") + + df = d.get("fields", {"fieldname": "title"})[0] + + # invalid fieldname + df.default = """{doc_type} - {introduction_test}""" + self.assertRaises(InvalidFieldNameError, d.run_method, "save_customization") + + # space in formatter + df.default = """{doc_type} - {introduction text}""" + self.assertRaises(InvalidFieldNameError, d.run_method, "save_customization") + + # valid fieldname + df.default = """{doc_type} - {introduction_text}""" + d.run_method("save_customization") + + # valid fieldname with escaped curlies + df.default = """{{ {doc_type} }} - {introduction_text}""" + d.run_method("save_customization") + + # undo + df.default = None + d.run_method("save_customization") + + def test_core_doctype_customization(self): + self.assertRaises(influxframework.ValidationError, self.get_customize_form, "User") + + def test_save_customization_length_field_property(self): + # Using Notification Log doctype as it doesn't have any other custom fields + d = self.get_customize_form("Notification Log") + + document_name = d.get("fields", {"fieldname": "document_name"})[0] + document_name.length = 255 + d.run_method("save_customization") + + self.assertEqual( + influxframework.db.get_value( + "Property Setter", + {"doc_type": "Notification Log", "property": "length", "field_name": "document_name"}, + "value", + ), + "255", + ) + + self.assertTrue(d.flags.update_db) + + length = influxframework.db.sql( + """SELECT character_maximum_length + FROM information_schema.columns + WHERE table_name = 'tabNotification Log' + AND column_name = 'document_name'""" + )[0][0] + + self.assertEqual(length, 255) + + def test_custom_link(self): + try: + # create a dummy doctype linked to Event + testdt_name = "Test Link for Event" + testdt = new_doctype( + testdt_name, fields=[dict(fieldtype="Link", fieldname="event", options="Event")] + ).insert() + + testdt_name1 = "Test Link for Event 1" + testdt1 = new_doctype( + testdt_name1, fields=[dict(fieldtype="Link", fieldname="event", options="Event")] + ).insert() + + # add a custom link + d = self.get_customize_form("Event") + + d.append("links", dict(link_doctype=testdt_name, link_fieldname="event", group="Tests")) + d.append("links", dict(link_doctype=testdt_name1, link_fieldname="event", group="Tests")) + + d.run_method("save_customization") + + influxframework.clear_cache() + event = influxframework.get_meta("Event") + + # check links exist + self.assertTrue([d.name for d in event.links if d.link_doctype == testdt_name]) + self.assertTrue([d.name for d in event.links if d.link_doctype == testdt_name1]) + + # check order + order = json.loads(event.links_order) + self.assertListEqual(order, [d.name for d in event.links]) + + # remove the link + d = self.get_customize_form("Event") + d.links = [] + d.run_method("save_customization") + + influxframework.clear_cache() + event = influxframework.get_meta("Event") + self.assertFalse([d.name for d in (event.links or []) if d.link_doctype == testdt_name]) + finally: + testdt.delete() + testdt1.delete() + + def test_custom_internal_links(self): + # add a custom internal link + influxframework.clear_cache() + d = self.get_customize_form("User Group") + + d.append( + "links", + dict( + link_doctype="User Group Member", + parent_doctype="User Group", + link_fieldname="user", + table_fieldname="user_group_members", + group="Tests", + custom=1, + ), + ) + + d.run_method("save_customization") + + influxframework.clear_cache() + user_group = influxframework.get_meta("User Group") + + # check links exist + self.assertTrue([d.name for d in user_group.links if d.link_doctype == "User Group Member"]) + self.assertTrue([d.name for d in user_group.links if d.parent_doctype == "User Group"]) + + # remove the link + d = self.get_customize_form("User Group") + d.links = [] + d.run_method("save_customization") + + influxframework.clear_cache() + user_group = influxframework.get_meta("Event") + self.assertFalse( + [d.name for d in (user_group.links or []) if d.link_doctype == "User Group Member"] + ) + + def test_custom_action(self): + test_route = "/app/List/DocType" + + # create a dummy action (route) + d = self.get_customize_form("Event") + d.append("actions", dict(label="Test Action", action_type="Route", action=test_route)) + d.run_method("save_customization") + + influxframework.clear_cache() + event = influxframework.get_meta("Event") + + # check if added to meta + action = [d for d in event.actions if d.label == "Test Action"] + self.assertEqual(len(action), 1) + self.assertEqual(action[0].action, test_route) + + # clear the action + d = self.get_customize_form("Event") + d.actions = [] + d.run_method("save_customization") + + influxframework.clear_cache() + event = influxframework.get_meta("Event") + + action = [d for d in event.actions if d.label == "Test Action"] + self.assertEqual(len(action), 0) + + def test_custom_label(self): + d = self.get_customize_form("Event") + + # add label + d.label = "Test Rename" + d.run_method("save_customization") + self.assertEqual(d.label, "Test Rename") + + # change label + d.label = "Test Rename 2" + d.run_method("save_customization") + self.assertEqual(d.label, "Test Rename 2") + + # saving again to make sure existing label persists + d.run_method("save_customization") + self.assertEqual(d.label, "Test Rename 2") + + # clear label + d.label = "" + d.run_method("save_customization") + self.assertEqual(d.label, "") + + def test_change_to_autoincrement_autoname(self): + d = self.get_customize_form("Event") + d.autoname = "autoincrement" + + with self.assertRaises(influxframework.ValidationError): + d.run_method("save_customization") diff --git a/influxframework/custom/doctype/customize_form_field/__init__.py b/influxframework/custom/doctype/customize_form_field/__init__.py new file mode 100644 index 0000000..2e11a12 --- /dev/null +++ b/influxframework/custom/doctype/customize_form_field/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE diff --git a/influxframework/custom/doctype/customize_form_field/customize_form_field.json b/influxframework/custom/doctype/customize_form_field/customize_form_field.json new file mode 100644 index 0000000..8fa0548 --- /dev/null +++ b/influxframework/custom/doctype/customize_form_field/customize_form_field.json @@ -0,0 +1,480 @@ +{ + "actions": [], + "autoname": "hash", + "creation": "2013-02-22 01:27:32", + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "is_system_generated", + "label_and_type", + "label", + "fieldtype", + "fieldname", + "non_negative", + "reqd", + "unique", + "is_virtual", + "in_list_view", + "in_standard_filter", + "in_global_search", + "in_preview", + "bold", + "no_copy", + "allow_in_quick_entry", + "translatable", + "column_break_7", + "default", + "precision", + "length", + "options", + "fetch_from", + "fetch_if_empty", + "show_dashboard", + "permissions", + "depends_on", + "permlevel", + "hidden", + "read_only", + "collapsible", + "allow_bulk_edit", + "collapsible_depends_on", + "column_break_14", + "ignore_user_permissions", + "allow_on_submit", + "report_hide", + "remember_last_selected_value", + "hide_border", + "ignore_xss_filter", + "property_depends_on_section", + "mandatory_depends_on", + "column_break_33", + "read_only_depends_on", + "display", + "in_filter", + "hide_seconds", + "hide_days", + "column_break_21", + "description", + "print_hide", + "print_hide_if_no_value", + "print_width", + "columns", + "width", + "is_custom_field" + ], + "fields": [ + { + "fieldname": "label_and_type", + "fieldtype": "Section Break", + "label": "Label and Type" + }, + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "oldfieldname": "label", + "oldfieldtype": "Data", + "search_index": 1 + }, + { + "default": "Data", + "fieldname": "fieldtype", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Type", + "oldfieldname": "fieldtype", + "oldfieldtype": "Select", + "options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nPhone\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime", + "reqd": 1, + "search_index": 1 + }, + { + "fieldname": "fieldname", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Name", + "oldfieldname": "fieldname", + "oldfieldtype": "Data", + "read_only": 1, + "search_index": 1 + }, + { + "default": "0", + "depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)", + "fieldname": "reqd", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Mandatory", + "oldfieldname": "reqd", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "fieldname": "unique", + "fieldtype": "Check", + "label": "Unique" + }, + { + "default": "0", + "fieldname": "is_virtual", + "fieldtype": "Check", + "label": "Is Virtual" + }, + { + "default": "0", + "fieldname": "in_list_view", + "fieldtype": "Check", + "label": "In List View" + }, + { + "default": "0", + "fieldname": "in_standard_filter", + "fieldtype": "Check", + "label": "In Standard Filter" + }, + { + "default": "0", + "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)", + "fieldname": "in_global_search", + "fieldtype": "Check", + "label": "In Global Search" + }, + { + "default": "0", + "fieldname": "bold", + "fieldtype": "Check", + "label": "Bold" + }, + { + "default": "1", + "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)", + "fieldname": "translatable", + "fieldtype": "Check", + "label": "Translatable" + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)", + "description": "Set non-standard precision for a Float or Currency field", + "fieldname": "precision", + "fieldtype": "Select", + "label": "Precision", + "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9" + }, + { + "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image'], doc.fieldtype)", + "fieldname": "length", + "fieldtype": "Int", + "label": "Length" + }, + { + "description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.", + "fieldname": "options", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Options", + "oldfieldname": "options", + "oldfieldtype": "Text" + }, + { + "fieldname": "fetch_from", + "fieldtype": "Small Text", + "label": "Fetch From" + }, + { + "default": "0", + "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.", + "fieldname": "fetch_if_empty", + "fieldtype": "Check", + "label": "Fetch If Empty" + }, + { + "fieldname": "permissions", + "fieldtype": "Section Break", + "label": "Permissions" + }, + { + "description": "This field will appear only if the fieldname defined here has value OR the rules are true (examples): \nmyfield\neval:doc.myfield=='My Value'\neval:doc.age>18", + "fieldname": "depends_on", + "fieldtype": "Code", + "label": "Depends On", + "oldfieldname": "depends_on", + "oldfieldtype": "Data", + "options": "JS" + }, + { + "default": "0", + "fieldname": "permlevel", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Perm Level", + "oldfieldname": "permlevel", + "oldfieldtype": "Int" + }, + { + "default": "0", + "fieldname": "hidden", + "fieldtype": "Check", + "label": "Hidden", + "oldfieldname": "hidden", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "fieldname": "read_only", + "fieldtype": "Check", + "label": "Read Only" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype==\"Section Break\"", + "fieldname": "collapsible", + "fieldtype": "Check", + "label": "Collapsible" + }, + { + "default": "0", + "depends_on": "eval: doc.fieldtype == \"Table\"", + "fieldname": "allow_bulk_edit", + "fieldtype": "Check", + "label": "Allow Bulk Edit" + }, + { + "depends_on": "eval:doc.fieldtype==\"Section Break\"", + "fieldname": "collapsible_depends_on", + "fieldtype": "Code", + "label": "Collapsible Depends On", + "options": "JS" + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "ignore_user_permissions", + "fieldtype": "Check", + "label": "Ignore User Permissions" + }, + { + "default": "0", + "fieldname": "allow_on_submit", + "fieldtype": "Check", + "label": "Allow on Submit", + "oldfieldname": "allow_on_submit", + "oldfieldtype": "Check" + }, + { + "default": "0", + "fieldname": "report_hide", + "fieldtype": "Check", + "label": "Report Hide", + "oldfieldname": "report_hide", + "oldfieldtype": "Check" + }, + { + "default": "0", + "depends_on": "eval:(doc.fieldtype == 'Link')", + "fieldname": "remember_last_selected_value", + "fieldtype": "Check", + "label": "Remember Last Selected Value" + }, + { + "fieldname": "display", + "fieldtype": "Section Break", + "label": "Display" + }, + { + "fieldname": "default", + "fieldtype": "Small Text", + "label": "Default", + "oldfieldname": "default", + "oldfieldtype": "Text" + }, + { + "default": "0", + "fieldname": "in_filter", + "fieldtype": "Check", + "label": "In Filter", + "oldfieldname": "in_filter", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "fieldname": "column_break_21", + "fieldtype": "Column Break" + }, + { + "fieldname": "description", + "fieldtype": "Text", + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Text", + "print_width": "300px", + "width": "300px" + }, + { + "default": "0", + "fieldname": "print_hide", + "fieldtype": "Check", + "label": "Print Hide", + "oldfieldname": "print_hide", + "oldfieldtype": "Check" + }, + { + "default": "0", + "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1", + "fieldname": "print_hide_if_no_value", + "fieldtype": "Check", + "label": "Print Hide If No Value" + }, + { + "description": "Print Width of the field, if the field is a column in a table", + "fieldname": "print_width", + "fieldtype": "Data", + "label": "Print Width", + "print_width": "50px", + "width": "50px" + }, + { + "depends_on": "eval:cur_frm.doc.istable", + "description": "Number of columns for a field in a Grid (Total Columns in a grid should be less than 11)", + "fieldname": "columns", + "fieldtype": "Int", + "label": "Columns" + }, + { + "fieldname": "width", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Width", + "oldfieldname": "width", + "oldfieldtype": "Data", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "fieldname": "is_custom_field", + "fieldtype": "Check", + "hidden": 1, + "label": "Is Custom Field", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "allow_in_quick_entry", + "fieldtype": "Check", + "label": "Allow in Quick Entry" + }, + { + "fieldname": "property_depends_on_section", + "fieldtype": "Section Break", + "label": "Property Depends On" + }, + { + "fieldname": "mandatory_depends_on", + "fieldtype": "Code", + "label": "Mandatory Depends On", + "options": "JS" + }, + { + "fieldname": "column_break_33", + "fieldtype": "Column Break" + }, + { + "fieldname": "read_only_depends_on", + "fieldtype": "Code", + "label": "Read Only Depends On", + "options": "JS" + }, + { + "default": "0", + "depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);", + "fieldname": "in_preview", + "fieldtype": "Check", + "label": "In Preview" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype=='Duration'", + "fieldname": "hide_seconds", + "fieldtype": "Check", + "label": "Hide Seconds" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype=='Duration'", + "fieldname": "hide_days", + "fieldtype": "Check", + "label": "Hide Days" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype=='Section Break'", + "fieldname": "hide_border", + "fieldtype": "Check", + "label": "Hide Border" + }, + { + "default": "0", + "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)", + "fieldname": "non_negative", + "fieldtype": "Check", + "label": "Non Negative" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype=='Tab Break'", + "fieldname": "show_dashboard", + "fieldtype": "Check", + "label": "Show Dashboard" + }, + { + "default": "0", + "fieldname": "no_copy", + "fieldtype": "Check", + "label": "No Copy" + }, + { + "default": "0", + "fieldname": "is_system_generated", + "fieldtype": "Check", + "hidden": 1, + "label": "Is System Generated", + "read_only": 1 + }, + { + "default": "0", + "description": "Don't encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field", + "fieldname": "ignore_xss_filter", + "fieldtype": "Check", + "label": "Ignore XSS Filter" + } + ], + "idx": 1, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2022-04-13 22:31:14.162661", + "modified_by": "Administrator", + "module": "Custom", + "name": "Customize Form Field", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "ASC", + "states": [] +} diff --git a/influxframework/custom/doctype/customize_form_field/customize_form_field.py b/influxframework/custom/doctype/customize_form_field/customize_form_field.py new file mode 100644 index 0000000..35307ee --- /dev/null +++ b/influxframework/custom/doctype/customize_form_field/customize_form_field.py @@ -0,0 +1,8 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +from influxframework.model.document import Document + + +class CustomizeFormField(Document): + pass diff --git a/influxframework/custom/doctype/doctype_layout/__init__.py b/influxframework/custom/doctype/doctype_layout/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/custom/doctype/doctype_layout/doctype_layout.js b/influxframework/custom/doctype/doctype_layout/doctype_layout.js new file mode 100644 index 0000000..d62523c --- /dev/null +++ b/influxframework/custom/doctype/doctype_layout/doctype_layout.js @@ -0,0 +1,105 @@ +// Copyright (c) 2020, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("DocType Layout", { + onload_post_render(frm) { + // disallow users from manually adding/deleting rows; this doctype should only + // be used for managing layout, and docfields and custom fields should be used + // to manage other field metadata (hidden, etc.) + frm.set_df_property("fields", "cannot_add_rows", true); + frm.set_df_property("fields", "cannot_delete_rows", true); + + $(frm.wrapper).on("grid-move-row", (e, frm) => { + // refresh the layout after moving a row + frm.dirty(); + }); + }, + + refresh(frm) { + frm.events.add_buttons(frm); + }, + + async document_type(frm) { + if (frm.doc.document_type) { + // refreshing the doctype fields resets the new name input field; + // once the fields are set, reset the name to the original input + if (frm.is_new()) { + const document_name = frm.doc.__newname || frm.doc.name; + } + + frm.set_value("fields", []); + await frm.events.sync_fields(frm, false); + + if (frm.is_new()) { + frm.doc.__newname = document_name; + frm.refresh_field("__newname"); + } + } + }, + + add_buttons(frm) { + if (!frm.is_new()) { + frm.add_custom_button(__("Go to {0} List", [frm.doc.name]), () => { + window.open(`/app/${influxframework.router.slug(frm.doc.name)}`); + }); + + frm.add_custom_button(__("Sync {0} Fields", [frm.doc.name]), async () => { + await frm.events.sync_fields(frm, true); + }); + } + }, + + async sync_fields(frm, notify) { + influxframework.dom.freeze("Fetching fields..."); + const response = await frm.call({ doc: frm.doc, method: "sync_fields" }); + frm.refresh_field("fields"); + influxframework.dom.unfreeze(); + + if (!response.message) { + influxframework.msgprint(__("No changes to sync")); + return; + } + + frm.dirty(); + if (notify) { + const addedFields = response.message.added; + const removedFields = response.message.removed; + + const getChangedMessage = (fields) => { + let changes = ""; + for (const field of fields) { + if (field.label) { + changes += `
  7. Row #${field.idx}: ${field.fieldname.bold()} (${ + field.label + })
  8. `; + } else { + changes += `
  9. Row #${field.idx}: ${field.fieldname.bold()}
  10. `; + } + } + return changes; + }; + + let message = ""; + + if (addedFields.length) { + message += `The following fields have been added:

      ${getChangedMessage( + addedFields + )}
    `; + } + + if (removedFields.length) { + message += `The following fields have been removed:

      ${getChangedMessage( + removedFields + )}
    `; + } + + if (message) { + influxframework.msgprint({ + message: __(message), + indicator: "green", + title: __("Synced Fields"), + }); + } + } + }, +}); diff --git a/influxframework/custom/doctype/doctype_layout/doctype_layout.json b/influxframework/custom/doctype/doctype_layout/doctype_layout.json new file mode 100644 index 0000000..0b627f7 --- /dev/null +++ b/influxframework/custom/doctype/doctype_layout/doctype_layout.json @@ -0,0 +1,75 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "prompt", + "creation": "2020-11-16 17:05:35.306846", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type", + "route", + "fields", + "client_script" + ], + "fields": [ + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType", + "reqd": 1, + "set_only_once": 1 + }, + { + "fieldname": "fields", + "fieldtype": "Table", + "label": "Fields", + "options": "DocType Layout Field", + "reqd": 1 + }, + { + "fieldname": "client_script", + "fieldtype": "Code", + "label": "Client Script" + }, + { + "fieldname": "route", + "fieldtype": "Data", + "label": "Route", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2022-09-01 03:22:33.973058", + "modified_by": "Administrator", + "module": "Custom", + "name": "DocType Layout", + "naming_rule": "Set by user", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "read": 1, + "role": "Guest" + } + ], + "route": "doctype-layout", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/custom/doctype/doctype_layout/doctype_layout.py b/influxframework/custom/doctype/doctype_layout/doctype_layout.py new file mode 100644 index 0000000..867c1a1 --- /dev/null +++ b/influxframework/custom/doctype/doctype_layout/doctype_layout.py @@ -0,0 +1,77 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +from typing import TYPE_CHECKING + +import influxframework +from influxframework.desk.utils import slug +from influxframework.model.document import Document + +if TYPE_CHECKING: + from influxframework.core.doctype.docfield.docfield import DocField + + +class DocTypeLayout(Document): + def validate(self): + if not self.route: + self.route = slug(self.name) + + @influxframework.whitelist() + def sync_fields(self): + doctype_fields = influxframework.get_meta(self.document_type).fields + + if self.is_new(): + added_fields = [field.fieldname for field in doctype_fields] + removed_fields = [] + else: + doctype_fieldnames = {field.fieldname for field in doctype_fields} + layout_fieldnames = {field.fieldname for field in self.fields} + added_fields = list(doctype_fieldnames - layout_fieldnames) + removed_fields = list(layout_fieldnames - doctype_fieldnames) + + if not (added_fields or removed_fields): + return + + added = self.add_fields(added_fields, doctype_fields) + removed = self.remove_fields(removed_fields) + + for index, field in enumerate(self.fields): + field.idx = index + 1 + + return {"added": added, "removed": removed} + + def add_fields(self, added_fields: list[str], doctype_fields: list["DocField"]) -> list[dict]: + added = [] + for field in added_fields: + field_details = next((f for f in doctype_fields if f.fieldname == field), None) + if not field_details: + continue + + # remove 'doctype' data from the DocField to allow adding it to the layout + row = self.append("fields", field_details.as_dict(no_default_fields=True)) + row_data = row.as_dict() + + if field_details.get("insert_after"): + insert_after = next( + (f for f in self.fields if f.fieldname == field_details.insert_after), + None, + ) + + # initialize new row to just after the insert_after field + if insert_after: + self.fields.insert(insert_after.idx, row) + self.fields.pop() + + row_data = {"idx": insert_after.idx + 1, "fieldname": row.fieldname, "label": row.label} + + added.append(row_data) + return added + + def remove_fields(self, removed_fields: list[str]) -> list[dict]: + removed = [] + for field in removed_fields: + field_details = next((f for f in self.fields if f.fieldname == field), None) + if field_details: + self.remove(field_details) + removed.append(field_details.as_dict()) + return removed diff --git a/influxframework/custom/doctype/doctype_layout/patches/convert_web_forms_to_doctype_layout.py b/influxframework/custom/doctype/doctype_layout/patches/convert_web_forms_to_doctype_layout.py new file mode 100644 index 0000000..02f9d9f --- /dev/null +++ b/influxframework/custom/doctype/doctype_layout/patches/convert_web_forms_to_doctype_layout.py @@ -0,0 +1,18 @@ +import influxframework + + +def execute(): + for web_form_name in influxframework.get_all("Web Form", pluck="name"): + web_form = influxframework.get_doc("Web Form", web_form_name) + doctype_layout = influxframework.get_doc( + dict( + doctype="DocType Layout", + document_type=web_form.doc_type, + name=web_form.title, + route=web_form.route, + fields=[ + dict(fieldname=d.fieldname, label=d.label) for d in web_form.web_form_fields if d.fieldname + ], + ) + ).insert() + print(doctype_layout.name) diff --git a/influxframework/custom/doctype/doctype_layout/test_doctype_layout.py b/influxframework/custom/doctype/doctype_layout/test_doctype_layout.py new file mode 100644 index 0000000..3e7ce1b --- /dev/null +++ b/influxframework/custom/doctype/doctype_layout/test_doctype_layout.py @@ -0,0 +1,8 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestDocTypeLayout(InfluxFrameworkTestCase): + pass diff --git a/influxframework/custom/doctype/doctype_layout_field/__init__.py b/influxframework/custom/doctype/doctype_layout_field/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/custom/doctype/doctype_layout_field/doctype_layout_field.json b/influxframework/custom/doctype/doctype_layout_field/doctype_layout_field.json new file mode 100644 index 0000000..006c01a --- /dev/null +++ b/influxframework/custom/doctype/doctype_layout_field/doctype_layout_field.json @@ -0,0 +1,38 @@ +{ + "actions": [], + "creation": "2020-11-16 16:03:43.771801", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "label", + "fieldname" + ], + "fields": [ + { + "fieldname": "fieldname", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Fieldname", + "reqd": 1 + }, + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-05-19 16:27:40.585865", + "modified_by": "Administrator", + "module": "Custom", + "name": "DocType Layout Field", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} diff --git a/influxframework/custom/doctype/doctype_layout_field/doctype_layout_field.py b/influxframework/custom/doctype/doctype_layout_field/doctype_layout_field.py new file mode 100644 index 0000000..712998b --- /dev/null +++ b/influxframework/custom/doctype/doctype_layout_field/doctype_layout_field.py @@ -0,0 +1,9 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class DocTypeLayoutField(Document): + pass diff --git a/influxframework/custom/doctype/property_setter/README.md b/influxframework/custom/doctype/property_setter/README.md new file mode 100644 index 0000000..65ece5e --- /dev/null +++ b/influxframework/custom/doctype/property_setter/README.md @@ -0,0 +1 @@ +Overrides standard DocType, DocField properties. The standard application is configured with properties for forms and fields (like, whether they are hidden or not). These can be overridden by a System Manager who can configure DocTypes based on custom requirements. \ No newline at end of file diff --git a/influxframework/custom/doctype/property_setter/__init__.py b/influxframework/custom/doctype/property_setter/__init__.py new file mode 100644 index 0000000..2e11a12 --- /dev/null +++ b/influxframework/custom/doctype/property_setter/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE diff --git a/influxframework/custom/doctype/property_setter/property_setter.js b/influxframework/custom/doctype/property_setter/property_setter.js new file mode 100644 index 0000000..b385828 --- /dev/null +++ b/influxframework/custom/doctype/property_setter/property_setter.js @@ -0,0 +1,10 @@ +// Copyright (c) 2015, InfluxFramework LLC +// MIT License. See license.txt + +influxframework.ui.form.on("Property Setter", { + validate: function (frm) { + if (frm.doc.property_type == "Check" && !in_list(["0", "1"], frm.doc.value)) { + influxframework.throw(__("Value for a check field can be either 0 or 1")); + } + }, +}); diff --git a/influxframework/custom/doctype/property_setter/property_setter.json b/influxframework/custom/doctype/property_setter/property_setter.json new file mode 100644 index 0000000..039826b --- /dev/null +++ b/influxframework/custom/doctype/property_setter/property_setter.json @@ -0,0 +1,154 @@ +{ + "actions": [], + "creation": "2013-01-10 16:34:04", + "description": "Property Setter overrides a standard DocType or Field property", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "is_system_generated", + "help", + "sb0", + "doctype_or_field", + "doc_type", + "field_name", + "row_name", + "column_break0", + "module", + "section_break_9", + "property", + "property_type", + "value", + "default_value" + ], + "fields": [ + { + "fieldname": "help", + "fieldtype": "HTML", + "label": "Help", + "options": "
    Please don't update it as it can mess up your form. Use the Customize Form View and Custom Fields to set properties!
    " + }, + { + "fieldname": "sb0", + "fieldtype": "Section Break" + }, + { + "fieldname": "doctype_or_field", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Applied On", + "options": "\nDocField\nDocType\nDocType Link\nDocType Action\nDocType State", + "read_only_depends_on": "eval:!doc.__islocal", + "reqd": 1 + }, + { + "description": "New value to be set", + "fieldname": "value", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Set Value" + }, + { + "fieldname": "column_break0", + "fieldtype": "Column Break" + }, + { + "fieldname": "doc_type", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "DocType", + "options": "DocType", + "reqd": 1, + "search_index": 1 + }, + { + "depends_on": "eval:doc.doctype_or_field=='DocField'", + "description": "ID (name) of the entity whose property is to be set", + "fieldname": "field_name", + "fieldtype": "Data", + "in_standard_filter": 1, + "label": "Field Name", + "search_index": 1 + }, + { + "fieldname": "property", + "fieldtype": "Data", + "in_standard_filter": 1, + "label": "Property", + "reqd": 1, + "search_index": 1 + }, + { + "fieldname": "property_type", + "fieldtype": "Data", + "label": "Property Type" + }, + { + "fieldname": "default_value", + "fieldtype": "Data", + "label": "Default Value" + }, + { + "description": "For DocType Link / DocType Action", + "fieldname": "row_name", + "fieldtype": "Data", + "label": "Row Name" + }, + { + "fieldname": "module", + "fieldtype": "Link", + "label": "Module (for export)", + "options": "Module Def" + }, + { + "fieldname": "section_break_9", + "fieldtype": "Section Break" + }, + { + "default": "0", + "fieldname": "is_system_generated", + "fieldtype": "Check", + "label": "Is System Generated", + "read_only": 1 + } + ], + "icon": "fa fa-glass", + "idx": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2022-02-28 22:24:12.377693", + "modified_by": "Administrator", + "module": "Custom", + "name": "Property Setter", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "search_fields": "doc_type,property", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/custom/doctype/property_setter/property_setter.py b/influxframework/custom/doctype/property_setter/property_setter.py new file mode 100644 index 0000000..bb122ef --- /dev/null +++ b/influxframework/custom/doctype/property_setter/property_setter.py @@ -0,0 +1,73 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework import _ +from influxframework.model.document import Document + +not_allowed_fieldtype_change = ["naming_series"] + + +class PropertySetter(Document): + def autoname(self): + self.name = "{doctype}-{field}-{property}".format( + doctype=self.doc_type, field=self.field_name or self.row_name or "main", property=self.property + ) + + def validate(self): + self.validate_fieldtype_change() + + if self.is_new(): + delete_property_setter(self.doc_type, self.property, self.field_name, self.row_name) + influxframework.clear_cache(doctype=self.doc_type) + + def validate_fieldtype_change(self): + if self.property == "fieldtype" and self.field_name in not_allowed_fieldtype_change: + influxframework.throw(_("Field type cannot be changed for {0}").format(self.field_name)) + + def on_update(self): + if influxframework.flags.in_patch: + self.flags.validate_fields_for_doctype = False + + if not self.flags.ignore_validate and self.flags.validate_fields_for_doctype: + from influxframework.core.doctype.doctype.doctype import validate_fields_for_doctype + + validate_fields_for_doctype(self.doc_type) + + +def make_property_setter( + doctype, + fieldname, + property, + value, + property_type, + for_doctype=False, + validate_fields_for_doctype=True, +): + # WARNING: Ignores Permissions + property_setter = influxframework.get_doc( + { + "doctype": "Property Setter", + "doctype_or_field": for_doctype and "DocType" or "DocField", + "doc_type": doctype, + "field_name": fieldname, + "property": property, + "value": value, + "property_type": property_type, + } + ) + property_setter.flags.ignore_permissions = True + property_setter.flags.validate_fields_for_doctype = validate_fields_for_doctype + property_setter.insert() + return property_setter + + +def delete_property_setter(doc_type, property, field_name=None, row_name=None): + """delete other property setters on this, if this is new""" + filters = dict(doc_type=doc_type, property=property) + if field_name: + filters["field_name"] = field_name + if row_name: + filters["row_name"] = row_name + + influxframework.db.delete("Property Setter", filters) diff --git a/influxframework/custom/doctype/property_setter/test_property_setter.py b/influxframework/custom/doctype/property_setter/test_property_setter.py new file mode 100644 index 0000000..930692b --- /dev/null +++ b/influxframework/custom/doctype/property_setter/test_property_setter.py @@ -0,0 +1,9 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + +# test_records = influxframework.get_test_records('Property Setter') + + +class TestPropertySetter(InfluxFrameworkTestCase): + pass diff --git a/influxframework/custom/doctype/property_setter/test_records.json b/influxframework/custom/doctype/property_setter/test_records.json new file mode 100644 index 0000000..3c084b4 --- /dev/null +++ b/influxframework/custom/doctype/property_setter/test_records.json @@ -0,0 +1,10 @@ +[ + { + "doc_type": "User", + "doctype_or_field": "DocField", + "field_name": "location", + "property": "in_list_view", + "property_type": "Check", + "value": "1" + } +] \ No newline at end of file diff --git a/influxframework/custom/fixtures/temp_doctype.json b/influxframework/custom/fixtures/temp_doctype.json new file mode 100644 index 0000000..343aa2c --- /dev/null +++ b/influxframework/custom/fixtures/temp_doctype.json @@ -0,0 +1,168 @@ +{ + "docstatus": 0, + "doctype": "DocType", + "name": "new-doctype-2", + "__islocal": 1, + "__unsaved": 1, + "owner": "Administrator", + "is_submittable": 0, + "istable": 0, + "issingle": 0, + "is_tree": 0, + "editable_grid": 1, + "quick_entry": 1, + "track_changes": 1, + "track_seen": 0, + "track_views": 0, + "custom": 1, + "beta": 0, + "is_virtual": 0, + "naming_rule": "", + "name_case": "", + "allow_rename": 1, + "hide_toolbar": 0, + "allow_copy": 0, + "allow_import": 0, + "allow_events_in_timeline": 0, + "allow_auto_repeat": 0, + "sort_field": "modified", + "sort_order": "DESC", + "document_type": "", + "show_preview_popup": 0, + "show_name_in_global_search": 0, + "email_append_to": 0, + "read_only": 0, + "in_create": 0, + "has_web_view": 0, + "allow_guest_to_view": 0, + "index_web_pages_for_search": 1, + "engine": "InnoDB", + "permissions": [ + { + "docstatus": 0, + "doctype": "DocPerm", + "name": "new-docperm-2", + "__islocal": 1, + "__unsaved": 1, + "owner": "Administrator", + "if_owner": 0, + "permlevel": 0, + "select": 0, + "read": 1, + "write": 1, + "create": 1, + "delete": 1, + "submit": 0, + "cancel": 0, + "amend": 0, + "report": 1, + "export": 1, + "import": 0, + "set_user_permissions": 0, + "share": 1, + "print": 1, + "email": 1, + "parent": "new-doctype-2", + "parentfield": "permissions", + "parenttype": "DocType", + "idx": 1, + "role": "System Manager" + } + ], + "__newname": "temp_doctype", + "module": "Custom", + "fields": [ + { + "docstatus": 0, + "doctype": "DocField", + "name": "new-docfield-1", + "__islocal": 1, + "__unsaved": 1, + "owner": "Administrator", + "fieldtype": "Data", + "precision": "", + "non_negative": 0, + "hide_days": 0, + "hide_seconds": 0, + "reqd": 1, + "search_index": 0, + "fetch_if_empty": 0, + "hidden": 0, + "bold": 0, + "allow_in_quick_entry": 0, + "translatable": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "report_hide": 0, + "collapsible": 0, + "hide_border": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "in_preview": 0, + "in_filter": 0, + "in_global_search": 0, + "read_only": 0, + "allow_on_submit": 0, + "ignore_user_permissions": 0, + "allow_bulk_edit": 0, + "permlevel": 0, + "ignore_xss_filter": 0, + "unique": 0, + "no_copy": 0, + "set_only_once": 0, + "remember_last_selected_value": 0, + "parent": "new-doctype-2", + "parentfield": "fields", + "parenttype": "DocType", + "idx": 1, + "__unedited": false, + "label": "member_name" + }, + { + "docstatus": 0, + "doctype": "DocField", + "name": "new-docfield-2", + "__islocal": 1, + "__unsaved": 1, + "owner": "Administrator", + "fieldtype": "Data", + "precision": "", + "non_negative": 0, + "hide_days": 0, + "hide_seconds": 0, + "reqd": 0, + "search_index": 0, + "fetch_if_empty": 0, + "hidden": 0, + "bold": 0, + "allow_in_quick_entry": 0, + "translatable": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "report_hide": 0, + "collapsible": 0, + "hide_border": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "in_preview": 0, + "in_filter": 0, + "in_global_search": 0, + "read_only": 0, + "allow_on_submit": 0, + "ignore_user_permissions": 0, + "allow_bulk_edit": 0, + "permlevel": 0, + "ignore_xss_filter": 0, + "unique": 0, + "no_copy": 0, + "set_only_once": 0, + "remember_last_selected_value": 0, + "parent": "new-doctype-2", + "parentfield": "fields", + "parenttype": "DocType", + "idx": 2, + "__unedited": false, + "label": "email" + } + ] +} diff --git a/influxframework/custom/fixtures/temp_singles.json b/influxframework/custom/fixtures/temp_singles.json new file mode 100644 index 0000000..b7e2536 --- /dev/null +++ b/influxframework/custom/fixtures/temp_singles.json @@ -0,0 +1,168 @@ +{ + "docstatus": 0, + "doctype": "DocType", + "name": "new-doctype-1", + "__islocal": 1, + "__unsaved": 1, + "owner": "Administrator", + "is_submittable": 0, + "istable": 0, + "issingle": 1, + "is_tree": 0, + "editable_grid": 1, + "quick_entry": 0, + "track_changes": 1, + "track_seen": 0, + "track_views": 0, + "custom": 1, + "beta": 0, + "is_virtual": 0, + "naming_rule": "", + "name_case": "", + "allow_rename": 1, + "hide_toolbar": 0, + "allow_copy": 0, + "allow_import": 0, + "allow_events_in_timeline": 0, + "allow_auto_repeat": 0, + "sort_field": "modified", + "sort_order": "DESC", + "document_type": "", + "show_preview_popup": 0, + "show_name_in_global_search": 0, + "email_append_to": 0, + "read_only": 0, + "in_create": 0, + "has_web_view": 0, + "allow_guest_to_view": 0, + "index_web_pages_for_search": 1, + "engine": "InnoDB", + "permissions": [ + { + "docstatus": 0, + "doctype": "DocPerm", + "name": "new-docperm-1", + "__islocal": 1, + "__unsaved": 1, + "owner": "Administrator", + "if_owner": 0, + "permlevel": 0, + "select": 0, + "read": 1, + "write": 1, + "create": 1, + "delete": 1, + "submit": 0, + "cancel": 0, + "amend": 0, + "report": 1, + "export": 1, + "import": 0, + "set_user_permissions": 0, + "share": 1, + "print": 1, + "email": 1, + "parent": "new-doctype-1", + "parentfield": "permissions", + "parenttype": "DocType", + "idx": 1, + "role": "System Manager" + } + ], + "__newname": "temp_singles", + "module": "Custom", + "fields": [ + { + "docstatus": 0, + "doctype": "DocField", + "name": "new-docfield-1", + "__islocal": 1, + "__unsaved": 1, + "owner": "Administrator", + "fieldtype": "Data", + "precision": "", + "non_negative": 0, + "hide_days": 0, + "hide_seconds": 0, + "reqd": 0, + "search_index": 0, + "fetch_if_empty": 0, + "hidden": 0, + "bold": 0, + "allow_in_quick_entry": 0, + "translatable": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "report_hide": 0, + "collapsible": 0, + "hide_border": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "in_preview": 0, + "in_filter": 0, + "in_global_search": 0, + "read_only": 0, + "allow_on_submit": 0, + "ignore_user_permissions": 0, + "allow_bulk_edit": 0, + "permlevel": 0, + "ignore_xss_filter": 0, + "unique": 0, + "no_copy": 0, + "set_only_once": 0, + "remember_last_selected_value": 0, + "parent": "new-doctype-1", + "parentfield": "fields", + "parenttype": "DocType", + "idx": 1, + "__unedited": false, + "label": "member_name" + }, + { + "docstatus": 0, + "doctype": "DocField", + "name": "new-docfield-2", + "__islocal": 1, + "__unsaved": 1, + "owner": "Administrator", + "fieldtype": "Data", + "precision": "", + "non_negative": 0, + "hide_days": 0, + "hide_seconds": 0, + "reqd": 0, + "search_index": 0, + "fetch_if_empty": 0, + "hidden": 0, + "bold": 0, + "allow_in_quick_entry": 0, + "translatable": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "report_hide": 0, + "collapsible": 0, + "hide_border": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "in_preview": 0, + "in_filter": 0, + "in_global_search": 0, + "read_only": 0, + "allow_on_submit": 0, + "ignore_user_permissions": 0, + "allow_bulk_edit": 0, + "permlevel": 0, + "ignore_xss_filter": 0, + "unique": 0, + "no_copy": 0, + "set_only_once": 0, + "remember_last_selected_value": 0, + "parent": "new-doctype-1", + "parentfield": "fields", + "parenttype": "DocType", + "idx": 2, + "__unedited": false, + "label": "email" + } + ] +} diff --git a/influxframework/custom/form_tour/custom_field/custom_field.json b/influxframework/custom/form_tour/custom_field/custom_field.json new file mode 100644 index 0000000..3279449 --- /dev/null +++ b/influxframework/custom/form_tour/custom_field/custom_field.json @@ -0,0 +1,79 @@ +{ + "creation": "2021-11-23 12:22:32.922700", + "docstatus": 0, + "doctype": "Form Tour", + "first_document": 0, + "idx": 0, + "include_name_field": 0, + "is_standard": 1, + "modified": "2021-11-24 19:15:34.244244", + "modified_by": "Administrator", + "module": "Custom", + "name": "Custom Field", + "owner": "Administrator", + "reference_doctype": "Custom Field", + "save_on_complete": 1, + "steps": [ + { + "description": "Select a Document for which you want the Custom Field", + "field": "", + "fieldname": "dt", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Document", + "parent_field": "", + "position": "Right", + "title": "Document" + }, + { + "description": "Enter a Label for this field", + "field": "", + "fieldname": "label", + "fieldtype": "Data", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Label", + "parent_field": "", + "position": "Right", + "title": "Label" + }, + { + "description": "Select the label after which you want to insert new field.", + "field": "", + "fieldname": "insert_after", + "fieldtype": "Select", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Insert After", + "parent_field": "", + "position": "Right", + "title": "Insert After" + }, + { + "description": "Select an appropriate Field Type that suits your requirements", + "field": "", + "fieldname": "fieldtype", + "fieldtype": "Select", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Field Type", + "parent_field": "", + "position": "Left", + "title": "Field Type" + }, + { + "description": "Check this to make it a mandatory field", + "field": "", + "fieldname": "reqd", + "fieldtype": "Check", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Is Mandatory Field", + "parent_field": "", + "position": "Left", + "title": "Is Mandatory Field" + } + ], + "title": "Custom Field" +} \ No newline at end of file diff --git a/influxframework/custom/module_onboarding/customization/customization.json b/influxframework/custom/module_onboarding/customization/customization.json new file mode 100644 index 0000000..91e24fe --- /dev/null +++ b/influxframework/custom/module_onboarding/customization/customization.json @@ -0,0 +1,44 @@ +{ + "allow_roles": [ + { + "role": "All" + } + ], + "creation": "2021-11-23 12:21:11.384229", + "docstatus": 0, + "doctype": "Module Onboarding", + "documentation_url": "https://docs.influxerp.com/docs/v13/user/manual/en/customize-influxerp", + "idx": 0, + "is_complete": 0, + "modified": "2021-11-24 17:04:31.523715", + "modified_by": "Administrator", + "module": "Custom", + "name": "Customization", + "owner": "Administrator", + "steps": [ + { + "step": "Custom Field" + }, + { + "step": "Custom Doctype" + }, + { + "step": "Naming Series" + }, + { + "step": "Workflows" + }, + { + "step": "Role Permissions" + }, + { + "step": "Print Format" + }, + { + "step": "Report Builder" + } + ], + "subtitle": "Custom Field, Custom Doctype, Naming Series, Role Permission, Workflow, Print Formats, Reports", + "success_message": "Customization onboarding is all done!", + "title": "Customization" +} diff --git a/influxframework/custom/onboarding_step/custom_doctype/custom_doctype.json b/influxframework/custom/onboarding_step/custom_doctype/custom_doctype.json new file mode 100644 index 0000000..83abc9c --- /dev/null +++ b/influxframework/custom/onboarding_step/custom_doctype/custom_doctype.json @@ -0,0 +1,21 @@ +{ + "action": "Create Entry", + "action_label": "Learn more about creating new DocTypes", + "creation": "2021-11-23 12:30:04.407568", + "description": "A DocType (Document Type) is used to insert forms in InfluxERP. Forms such as Customer, Orders, and Invoices are Doctypes in the backend. You can also create new DocTypes to create new forms in InfluxERP as per your business needs.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-23 12:30:04.407568", + "modified_by": "Administrator", + "name": "Custom Doctype", + "owner": "Administrator", + "reference_document": "DocType", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Custom Document Types", + "validate_action": 1 +} diff --git a/influxframework/custom/onboarding_step/custom_field/custom_field.json b/influxframework/custom/onboarding_step/custom_field/custom_field.json new file mode 100644 index 0000000..e75c4ed --- /dev/null +++ b/influxframework/custom/onboarding_step/custom_field/custom_field.json @@ -0,0 +1,21 @@ +{ + "action": "Create Entry", + "action_label": "Learn how to add Custom Fields", + "creation": "2021-11-23 12:21:09.479808", + "description": "Every form in InfluxERP has a standard set of fields. If you need to capture some information, but there is no standard Field available for it, you can insert Custom Field for it.\n\nOnce custom fields are added, you can use them for reports and analytics charts as well.\n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-23 12:21:09.479808", + "modified_by": "Administrator", + "name": "Custom Field", + "owner": "Administrator", + "reference_document": "Custom Field", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Create Custom Fields", + "validate_action": 1 +} diff --git a/influxframework/custom/onboarding_step/naming_series/naming_series.json b/influxframework/custom/onboarding_step/naming_series/naming_series.json new file mode 100644 index 0000000..34dabcb --- /dev/null +++ b/influxframework/custom/onboarding_step/naming_series/naming_series.json @@ -0,0 +1,20 @@ +{ + "action": "Watch Video", + "creation": "2021-11-23 13:57:45.091427", + "description": "Each document created in InfluxERP can have a unique ID generated for it, using a prefix defined for it. Though each document has some prefix pre-configured, you can further customize it using tools like Naming Series Tool and Document Naming Rule.\n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 15:04:14.662684", + "modified_by": "Administrator", + "name": "Naming Series", + "owner": "Administrator", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Setup Naming Series", + "validate_action": 1, + "video_url": "https://youtu.be/IGyISSfI1qU" +} diff --git a/influxframework/custom/onboarding_step/print_format/print_format.json b/influxframework/custom/onboarding_step/print_format/print_format.json new file mode 100644 index 0000000..681ef85 --- /dev/null +++ b/influxframework/custom/onboarding_step/print_format/print_format.json @@ -0,0 +1,21 @@ +{ + "action": "Create Entry", + "action_label": "Learn about Standard and Custom Print Formats", + "creation": "2021-11-23 15:04:12.728513", + "description": "Print Formats allow you can define looks for documents when printed or converted to PDF. You can also create a custom Print Format using drag-and-drop tools.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-23 15:04:12.728513", + "modified_by": "Administrator", + "name": "Print Format", + "owner": "Administrator", + "reference_document": "Print Format", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Customize Print Formats", + "validate_action": 1 +} \ No newline at end of file diff --git a/influxframework/custom/onboarding_step/report_builder/report_builder.json b/influxframework/custom/onboarding_step/report_builder/report_builder.json new file mode 100644 index 0000000..6dc25a3 --- /dev/null +++ b/influxframework/custom/onboarding_step/report_builder/report_builder.json @@ -0,0 +1,22 @@ +{ + "action": "Watch Video", + "action_label": "Learn more about Report Builders", + "creation": "2021-11-24 17:04:18.762838", + "description": "In each module, you will find a host of single-click reports, ranging from financial statements to sales and purchase analytics and stock tracking reports. If a required new report is not available out-of-the-box, you can create custom reports in InfluxERP by pulling values from the same multiple InfluxERP tables.\n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 17:04:18.762838", + "modified_by": "Administrator", + "name": "Report Builder", + "owner": "Administrator", + "reference_document": "Report", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Generate Custom Reports", + "validate_action": 1, + "video_url": "https://youtu.be/TxJGUNarcQs" +} diff --git a/influxframework/custom/onboarding_step/role_permissions/role_permissions.json b/influxframework/custom/onboarding_step/role_permissions/role_permissions.json new file mode 100644 index 0000000..a102a0b --- /dev/null +++ b/influxframework/custom/onboarding_step/role_permissions/role_permissions.json @@ -0,0 +1,20 @@ +{ + "action": "Watch Video", + "creation": "2021-11-23 14:00:27.208500", + "description": "In InfluxERP, you can add your Employees as Users, and give them restricted access. Tools like Role Permission and User Permission allow you to define rules which give restricted access to the user to masters and transactions.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 15:04:14.615232", + "modified_by": "Administrator", + "name": "Role Permissions", + "owner": "Administrator", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Setup Limited Access for a User", + "validate_action": 1, + "video_url": "https://youtu.be/g3mk45o1zAg" +} diff --git a/influxframework/custom/onboarding_step/workflows/workflows.json b/influxframework/custom/onboarding_step/workflows/workflows.json new file mode 100644 index 0000000..aab3d8d --- /dev/null +++ b/influxframework/custom/onboarding_step/workflows/workflows.json @@ -0,0 +1,20 @@ +{ + "action": "Watch Video", + "creation": "2021-11-23 13:58:58.530044", + "description": "Workflows allow you to define custom rules for the approval process of a particular document in InfluxERP. You can also set complex Workflow Rules and set approval conditions.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 15:04:14.632144", + "modified_by": "Administrator", + "name": "Workflows", + "owner": "Administrator", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Setup Approval Workflows", + "validate_action": 1, + "video_url": "https://youtu.be/yObJUg9FxFs" +} diff --git a/influxframework/custom/workspace/customization/customization.json b/influxframework/custom/workspace/customization/customization.json new file mode 100644 index 0000000..8985bf5 --- /dev/null +++ b/influxframework/custom/workspace/customization/customization.json @@ -0,0 +1,171 @@ +{ + "charts": [], + "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Customization\",\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customize Form\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Custom Role\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Client Script\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Server Script\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Dashboards\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Form Customization\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other\",\"col\":4}}]", + "creation": "2020-03-02 15:15:03.839594", + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "customization", + "idx": 0, + "label": "Customization", + "links": [ + { + "hidden": 0, + "is_query_report": 0, + "label": "Dashboards", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Dashboard", + "link_count": 0, + "link_to": "Dashboard", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Dashboard Chart", + "link_count": 0, + "link_to": "Dashboard Chart", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Dashboard Chart Source", + "link_count": 0, + "link_to": "Dashboard Chart Source", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Form Customization", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Customize Form", + "link_count": 0, + "link_to": "Customize Form", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Custom Field", + "link_count": 0, + "link_to": "Custom Field", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Client Script", + "link_count": 0, + "link_to": "Client Script", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "DocType", + "link_count": 0, + "link_to": "DocType", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Other", + "link_count": 2, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Custom Translations", + "link_count": 0, + "link_to": "Translation", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Navbar Settings", + "link_count": 0, + "link_to": "Navbar Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + } + ], + "modified": "2022-08-28 20:56:24.980719", + "modified_by": "Administrator", + "module": "Custom", + "name": "Customization", + "owner": "Administrator", + "parent_page": "", + "public": 1, + "quick_lists": [], + "restrict_to_domain": "", + "roles": [], + "sequence_id": 8.0, + "shortcuts": [ + { + "label": "Customize Form", + "link_to": "Customize Form", + "type": "DocType" + }, + { + "label": "Custom Role", + "link_to": "Custom Role", + "type": "DocType" + }, + { + "label": "Client Script", + "link_to": "Client Script", + "type": "DocType" + }, + { + "doc_view": "", + "label": "Server Script", + "link_to": "Server Script", + "type": "DocType" + } + ], + "title": "Customization" +} \ No newline at end of file diff --git a/influxframework/data/google_fonts.json b/influxframework/data/google_fonts.json new file mode 100644 index 0000000..232e509 --- /dev/null +++ b/influxframework/data/google_fonts.json @@ -0,0 +1,56 @@ +[ + "Alegreya Sans", + "Alegreya", + "Andada Pro", + "Anton", + "Archivo Narrow", + "Archivo", + "BioRhyme", + "Cardo", + "Chivo", + "Cormorant", + "Crimson Text", + "DM Sans", + "Eczar", + "Encode Sans", + "Epilogue ", + "Fira Sans", + "Hahmlet", + "IBM Plex Sans", + "Inconsolata", + "Inknut Antiqua", + "Inter", + "JetBrains Mono", + "Karla", + "Lato", + "Libre Baskerville", + "Libre Franklin", + "Lora", + "Manrope", + "Merriweather", + "Montserrat", + "Neuton", + "Nunito", + "Old Standard TT", + "Open Sans", + "Oswald", + "Oxygen", + "Playfair Display", + "Poppins", + "Proza Libre", + "PT Sans", + "PT Serif", + "Raleway", + "Roboto Slab", + "Roboto", + "Rubik", + "Sora", + "Source Sans Pro", + "Source Serif Pro", + "Space Grotesk", + "Space Mono", + "Spectral", + "Syne", + "Work Sans" +] + diff --git a/influxframework/database/__init__.py b/influxframework/database/__init__.py new file mode 100644 index 0000000..6175af6 --- /dev/null +++ b/influxframework/database/__init__.py @@ -0,0 +1,65 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +# Database Module +# -------------------- + +from influxframework.database.database import savepoint + + +def setup_database(force, source_sql=None, verbose=None, no_mariadb_socket=False): + import influxframework + + if influxframework.conf.db_type == "postgres": + import influxframework.database.postgres.setup_db + + return influxframework.database.postgres.setup_db.setup_database(force, source_sql, verbose) + else: + import influxframework.database.mariadb.setup_db + + return influxframework.database.mariadb.setup_db.setup_database( + force, source_sql, verbose, no_mariadb_socket=no_mariadb_socket + ) + + +def drop_user_and_database(db_name, root_login=None, root_password=None): + import influxframework + + if influxframework.conf.db_type == "postgres": + import influxframework.database.postgres.setup_db + + return influxframework.database.postgres.setup_db.drop_user_and_database( + db_name, root_login, root_password + ) + else: + import influxframework.database.mariadb.setup_db + + return influxframework.database.mariadb.setup_db.drop_user_and_database( + db_name, root_login, root_password + ) + + +def get_db(host=None, user=None, password=None, port=None): + import influxframework + + if influxframework.conf.db_type == "postgres": + import influxframework.database.postgres.database + + return influxframework.database.postgres.database.PostgresDatabase(host, user, password, port=port) + else: + import influxframework.database.mariadb.database + + return influxframework.database.mariadb.database.MariaDBDatabase(host, user, password, port=port) + + +def setup_help_database(help_db_name): + import influxframework + + if influxframework.conf.db_type == "postgres": + import influxframework.database.postgres.setup_db + + return influxframework.database.postgres.setup_db.setup_help_database(help_db_name) + else: + import influxframework.database.mariadb.setup_db + + return influxframework.database.mariadb.setup_db.setup_help_database(help_db_name) diff --git a/influxframework/database/database.py b/influxframework/database/database.py new file mode 100644 index 0000000..68a5ff8 --- /dev/null +++ b/influxframework/database/database.py @@ -0,0 +1,1344 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE + +import datetime +import json +import random +import re +import string +import traceback +from contextlib import contextmanager +from time import time + +from pypika.terms import Criterion, NullValue + +import influxframework +import influxframework.defaults +import influxframework.model.meta +from influxframework import _ +from influxframework.database.utils import ( + EmptyQueryValues, + FallBackDateTimeStr, + LazyMogrify, + Query, + QueryValues, + is_query_type, +) +from influxframework.exceptions import DoesNotExistError, ImplicitCommitError +from influxframework.model.utils.link_count import flush_local_link_count +from influxframework.query_builder.functions import Count +from influxframework.utils import cast as cast_fieldtype +from influxframework.utils import get_datetime, get_table_name, getdate, now, sbool + +IFNULL_PATTERN = re.compile(r"ifnull\(", flags=re.IGNORECASE) +INDEX_PATTERN = re.compile(r"\s*\([^)]+\)\s*") +SINGLE_WORD_PATTERN = re.compile(r'([`"]?)(tab([A-Z]\w+))\1') +MULTI_WORD_PATTERN = re.compile(r'([`"])(tab([A-Z]\w+)( [A-Z]\w+)+)\1') + + +class Database: + """ + Open a database connection with the given parmeters, if use_default is True, use the + login details from `conf.py`. This is called by the request handler and is accessible using + the `db` global variable. the `sql` method is also global to run queries + """ + + VARCHAR_LEN = 140 + MAX_COLUMN_LENGTH = 64 + + OPTIONAL_COLUMNS = ["_user_tags", "_comments", "_assign", "_liked_by"] + DEFAULT_SHORTCUTS = ["_Login", "__user", "_Full Name", "Today", "__today", "now", "Now"] + STANDARD_VARCHAR_COLUMNS = ("name", "owner", "modified_by") + DEFAULT_COLUMNS = ["name", "creation", "modified", "modified_by", "owner", "docstatus", "idx"] + CHILD_TABLE_COLUMNS = ("parent", "parenttype", "parentfield") + MAX_WRITES_PER_TRANSACTION = 200_000 + + # NOTE: + # FOR MARIADB - using no cache - as during backup, if the sequence was used in anyform, + # it drops the cache and uses the next non cached value in setval query and + # puts that in the backup file, which will start the counter + # from that value when inserting any new record in the doctype. + # By default the cache is 1000 which will mess up the sequence when + # using the system after a restore. + # + # Another case could be if the cached values expire then also there is a chance of + # the cache being skipped. + # + # FOR POSTGRES - The sequence cache for postgres is per connection. + # Since we're opening and closing connections for every request this results in skipping the cache + # to the next non-cached value hence not using cache in postgres. + # ref: https://stackoverflow.com/questions/21356375/postgres-9-0-4-sequence-skipping-numbers + SEQUENCE_CACHE = 0 + + class InvalidColumnName(influxframework.ValidationError): + pass + + def __init__( + self, + host=None, + user=None, + password=None, + ac_name=None, + use_default=0, + port=None, + ): + self.setup_type_map() + self.host = host or influxframework.conf.db_host or "127.0.0.1" + self.port = port or influxframework.conf.db_port or "" + self.user = user or influxframework.conf.db_name + self.db_name = influxframework.conf.db_name + self._conn = None + + if ac_name: + self.user = ac_name or influxframework.conf.db_name + + if use_default: + self.user = influxframework.conf.db_name + + self.transaction_writes = 0 + self.auto_commit_on_many_writes = 0 + + self.password = password or influxframework.conf.db_password + self.value_cache = {} + # self.db_type: str + # self.last_query (lazy) attribute of last sql query executed + + def setup_type_map(self): + pass + + def connect(self): + """Connects to a database as set in `site_config.json`.""" + self.cur_db_name = self.user + self._conn = self.get_connection() + self._cursor = self._conn.cursor() + influxframework.local.rollback_observers = [] + + def use(self, db_name): + """`USE` db_name.""" + self._conn.select_db(db_name) + + def get_connection(self): + """Returns a Database connection object that conforms with https://peps.python.org/pep-0249/#connection-objects""" + raise NotImplementedError + + def get_database_size(self): + raise NotImplementedError + + def _transform_query(self, query: Query, values: QueryValues) -> tuple: + return query, values + + def _transform_result(self, result: list[tuple]) -> list[tuple]: + return result + + def sql( + self, + query: Query, + values: QueryValues = EmptyQueryValues, + as_dict=0, + as_list=0, + formatted=0, + debug=0, + ignore_ddl=0, + as_utf8=0, + auto_commit=0, + update=None, + explain=False, + run=True, + pluck=False, + ): + """Execute a SQL query and fetch all rows. + + :param query: SQL query. + :param values: Tuple / List / Dict of values to be escaped and substituted in the query. + :param as_dict: Return as a dictionary. + :param as_list: Always return as a list. + :param formatted: Format values like date etc. + :param debug: Print query and `EXPLAIN` in debug log. + :param ignore_ddl: Catch exception if table, column missing. + :param as_utf8: Encode values as UTF 8. + :param auto_commit: Commit after executing the query. + :param update: Update this dict to all rows (if returned `as_dict`). + :param run: Returns query without executing it if False. + Examples: + + # return customer names as dicts + influxframework.db.sql("select name from tabCustomer", as_dict=True) + + # return names beginning with a + influxframework.db.sql("select name from tabCustomer where name like %s", "a%") + + # values as dict + influxframework.db.sql("select name from tabCustomer where name like %(name)s and owner=%(owner)s", + {"name": "a%", "owner":"test@example.com"}) + + """ + debug = debug or getattr(self, "debug", False) + query = str(query) + if not run: + return query + + # remove whitespace / indentation from start and end of query + query = query.strip() + + # replaces ifnull in query with coalesce + query = IFNULL_PATTERN.sub("coalesce(", query) + + if not self._conn: + self.connect() + + # in transaction validations + self.check_transaction_status(query) + self.clear_db_table_cache(query) + + if auto_commit: + self.commit() + + if debug: + time_start = time() + + if values == EmptyQueryValues: + values = None + elif not isinstance(values, (tuple, dict, list)): + values = (values,) + query, values = self._transform_query(query, values) + + try: + self._cursor.execute(query, values) + except Exception as e: + if self.is_syntax_error(e): + influxframework.errprint(f"Syntax error in query:\n{query} {values}") + + elif self.is_deadlocked(e): + raise influxframework.QueryDeadlockError(e) from e + + elif self.is_timedout(e): + raise influxframework.QueryTimeoutError(e) from e + + elif self.is_read_only_mode_error(e): + influxframework.throw( + _( + "Site is running in read only mode, this action can not be performed right now. Please try again later." + ), + title=_("In Read Only Mode"), + exc=influxframework.InReadOnlyMode, + ) + + # TODO: added temporarily + elif self.db_type == "postgres": + traceback.print_stack() + influxframework.errprint(f"Error in query:\n{e}") + raise + + elif isinstance(e, self.ProgrammingError): + if influxframework.conf.developer_mode: + traceback.print_stack() + influxframework.errprint(f"Error in query:\n{query, values}") + raise + + if not ( + ignore_ddl + and (self.is_missing_column(e) or self.is_table_missing(e) or self.cant_drop_field_or_key(e)) + ): + raise + + if debug: + time_end = time() + influxframework.errprint(f"Execution time: {time_end - time_start:.2f} sec") + + self.log_query(query, values, debug, explain) + + if auto_commit: + self.commit() + + if not self._cursor.description: + return () + + self.last_result = self._transform_result(self._cursor.fetchall()) + + if pluck: + return [r[0] for r in self.last_result] + + # scrub output if required + if as_dict: + ret = self.fetch_as_dict(formatted, as_utf8) + if update: + for r in ret: + r.update(update) + return ret + elif as_list or as_utf8: + return self.convert_to_lists(self.last_result, formatted, as_utf8) + return self.last_result + + def _log_query(self, mogrified_query: str, debug: bool = False, explain: bool = False) -> None: + """Takes the query and logs it to various interfaces according to the settings.""" + _query = None + + if influxframework.conf.allow_tests and influxframework.cache().get_value("flag_print_sql"): + _query = _query or str(mogrified_query) + print(_query) + + if debug: + _query = _query or str(mogrified_query) + if explain and is_query_type(_query, "select"): + self.explain_query(_query) + influxframework.errprint(_query) + + if influxframework.conf.logging == 2: + _query = _query or str(mogrified_query) + influxframework.log(f"<<<< query\n{_query}\n>>>>") + + if influxframework.flags.in_migrate: + _query = _query or str(mogrified_query) + self.log_touched_tables(_query) + + def log_query( + self, query: str, values: QueryValues = None, debug: bool = False, explain: bool = False + ) -> str: + # TODO: Use mogrify until MariaDB Connector/C 1.1 is released and we can fetch something + # like cursor._transformed_statement from the cursor object. We can also avoid setting + # mogrified_query if we don't need to log it. + mogrified_query = self.lazy_mogrify(query, values) + self._log_query(mogrified_query, debug, explain) + return mogrified_query + + def mogrify(self, query: Query, values: QueryValues): + """build the query string with values""" + if not values: + return query + + try: + return self._cursor.mogrify(query, values) + except AttributeError: + if isinstance(values, dict): + return query % {k: influxframework.db.escape(v) if isinstance(v, str) else v for k, v in values.items()} + elif isinstance(values, (list, tuple)): + return query % tuple(influxframework.db.escape(v) if isinstance(v, str) else v for v in values) + return query, values + + def lazy_mogrify(self, query: Query, values: QueryValues) -> LazyMogrify: + """Wrap the object with str to generate mogrified query.""" + return LazyMogrify(query, values) + + def explain_query(self, query, values=None): + """Print `EXPLAIN` in error log.""" + influxframework.errprint("--- query explain ---") + try: + self._cursor.execute(f"EXPLAIN {query}", values) + except Exception as e: + influxframework.errprint(f"error in query explain: {e}") + else: + influxframework.errprint(json.dumps(self.fetch_as_dict(), indent=1)) + influxframework.errprint("--- query explain end ---") + + def sql_list(self, query, values=(), debug=False, **kwargs): + """Return data as list of single elements (first column). + + Example: + + # doctypes = ["DocType", "DocField", "User", ...] + doctypes = influxframework.db.sql_list("select name from DocType") + """ + return self.sql(query, values, **kwargs, debug=debug, pluck=True) + + def sql_ddl(self, query, debug=False): + """Commit and execute a query. DDL (Data Definition Language) queries that alter schema + autocommit in MariaDB.""" + self.commit() + self.sql(query, debug=debug) + + def check_transaction_status(self, query): + """Raises exception if more than 20,000 `INSERT`, `UPDATE` queries are + executed in one transaction. This is to ensure that writes are always flushed otherwise this + could cause the system to hang.""" + self.check_implicit_commit(query) + + if query and is_query_type(query, ("commit", "rollback")): + self.transaction_writes = 0 + + if query[:6].lower() in ("update", "insert", "delete"): + self.transaction_writes += 1 + if self.transaction_writes > self.MAX_WRITES_PER_TRANSACTION: + if self.auto_commit_on_many_writes: + self.commit() + else: + msg = "

    " + _("Too many changes to database in single action.") + "
    " + msg += _("The changes have been reverted.") + "
    " + raise influxframework.TooManyWritesError(msg) + + def check_implicit_commit(self, query): + if ( + self.transaction_writes + and query + and is_query_type(query, ("start", "alter", "drop", "create", "begin", "truncate")) + ): + raise ImplicitCommitError("This statement can cause implicit commit") + + def fetch_as_dict(self, formatted=0, as_utf8=0) -> list[influxframework._dict]: + """Internal. Converts results to dict.""" + result = self.last_result + ret = [] + if result: + keys = [column[0] for column in self._cursor.description] + + for r in result: + values = [] + for value in r: + if as_utf8 and isinstance(value, str): + value = value.encode("utf-8") + values.append(value) + + ret.append(influxframework._dict(zip(keys, values))) + return ret + + @staticmethod + def clear_db_table_cache(query): + if query and is_query_type(query, ("drop", "create")): + influxframework.cache().delete_key("db_tables") + + @staticmethod + def needs_formatting(result, formatted): + """Returns true if the first row in the result has a Date, Datetime, Long Int.""" + if result and result[0]: + for v in result[0]: + if isinstance(v, (datetime.date, datetime.timedelta, datetime.datetime, int)): + return True + if formatted and isinstance(v, (int, float)): + return True + + return False + + def get_description(self): + """Returns result metadata.""" + return self._cursor.description + + @staticmethod + def convert_to_lists(res, formatted=0, as_utf8=0): + """Convert tuple output to lists (internal).""" + nres = [] + for r in res: + nr = [] + for val in r: + if as_utf8 and isinstance(val, str): + val = val.encode("utf-8") + nr.append(val) + nres.append(nr) + return nres + + def get(self, doctype, filters=None, as_dict=True, cache=False): + """Returns `get_value` with fieldname='*'""" + return self.get_value(doctype, filters, "*", as_dict=as_dict, cache=cache) + + def get_value( + self, + doctype, + filters=None, + fieldname="name", + ignore=None, + as_dict=False, + debug=False, + order_by="KEEP_DEFAULT_ORDERING", + cache=False, + for_update=False, + *, + run=True, + pluck=False, + distinct=False, + ): + """Returns a document property or list of properties. + + :param doctype: DocType name. + :param filters: Filters like `{"x":"y"}` or name of the document. `None` if Single DocType. + :param fieldname: Column name. + :param ignore: Don't raise exception if table, column is missing. + :param as_dict: Return values as dict. + :param debug: Print query in error log. + :param order_by: Column to order by + + Example: + + # return first customer starting with a + influxframework.db.get_value("Customer", {"name": ("like a%")}) + + # return last login of **User** `test@example.com` + influxframework.db.get_value("User", "test@example.com", "last_login") + + last_login, last_ip = influxframework.db.get_value("User", "test@example.com", + ["last_login", "last_ip"]) + + # returns default date_format + influxframework.db.get_value("System Settings", None, "date_format") + """ + + result = self.get_values( + doctype, + filters, + fieldname, + ignore, + as_dict, + debug, + order_by, + cache=cache, + for_update=for_update, + run=run, + pluck=pluck, + distinct=distinct, + limit=1, + ) + + if not run: + return result + + if not result: + return None + + row = result[0] + + if len(row) > 1 or as_dict: + return row + # single field is requested, send it without wrapping in containers + return row[0] + + def get_values( + self, + doctype, + filters=None, + fieldname="name", + ignore=None, + as_dict=False, + debug=False, + order_by="KEEP_DEFAULT_ORDERING", + update=None, + cache=False, + for_update=False, + *, + run=True, + pluck=False, + distinct=False, + limit=None, + ): + """Returns multiple document properties. + + :param doctype: DocType name. + :param filters: Filters like `{"x":"y"}` or name of the document. + :param fieldname: Column name. + :param ignore: Don't raise exception if table, column is missing. + :param as_dict: Return values as dict. + :param debug: Print query in error log. + :param order_by: Column to order by, + :param distinct: Get Distinct results. + + Example: + + # return first customer starting with a + customers = influxframework.db.get_values("Customer", {"name": ("like a%")}) + + # return last login of **User** `test@example.com` + user = influxframework.db.get_values("User", "test@example.com", "*")[0] + """ + out = None + if cache and isinstance(filters, str) and (doctype, filters, fieldname) in self.value_cache: + return self.value_cache[(doctype, filters, fieldname)] + + if distinct: + order_by = None + + if isinstance(filters, list): + out = self._get_value_for_many_names( + doctype=doctype, + names=filters, + field=fieldname, + order_by=order_by, + debug=debug, + run=run, + pluck=pluck, + distinct=distinct, + limit=limit, + as_dict=as_dict, + ) + + else: + fields = fieldname + if fieldname != "*": + if isinstance(fieldname, str): + fields = [fieldname] + + if (filters is not None) and (filters != doctype or doctype == "DocType"): + try: + if order_by: + order_by = "modified" if order_by == "KEEP_DEFAULT_ORDERING" else order_by + out = self._get_values_from_table( + fields=fields, + filters=filters, + doctype=doctype, + as_dict=as_dict, + debug=debug, + order_by=order_by, + update=update, + for_update=for_update, + run=run, + pluck=pluck, + distinct=distinct, + limit=limit, + ) + except Exception as e: + if ignore and (influxframework.db.is_missing_column(e) or influxframework.db.is_table_missing(e)): + # table or column not found, return None + out = None + elif (not ignore) and influxframework.db.is_table_missing(e): + # table not found, look in singles + out = self.get_values_from_single( + fields, filters, doctype, as_dict, debug, update, run=run, distinct=distinct + ) + + else: + raise + else: + out = self.get_values_from_single( + fields, filters, doctype, as_dict, debug, update, run=run, pluck=pluck, distinct=distinct + ) + + if cache and isinstance(filters, str): + self.value_cache[(doctype, filters, fieldname)] = out + + return out + + def get_values_from_single( + self, + fields, + filters, + doctype, + as_dict=False, + debug=False, + update=None, + *, + run=True, + pluck=False, + distinct=False, + ): + """Get values from `tabSingles` (Single DocTypes) (internal). + + :param fields: List of fields, + :param filters: Filters (dict). + :param doctype: DocType name. + """ + if fields == "*" or isinstance(filters, dict): + # check if single doc matches with filters + values = self.get_singles_dict(doctype) + if isinstance(filters, dict): + for key, value in filters.items(): + if values.get(key) != value: + return [] + + if as_dict: + return values and [values] or [] + + if isinstance(fields, list): + return [map(values.get, fields)] + + else: + r = influxframework.qb.engine.get_query( + "Singles", + filters={"field": ("in", tuple(fields)), "doctype": doctype}, + fields=["field", "value"], + distinct=distinct, + ).run(pluck=pluck, debug=debug, as_dict=False) + + if not run: + return r + if as_dict: + if r: + r = influxframework._dict(r) + if update: + r.update(update) + return [r] + else: + return [] + else: + return r and [[i[1] for i in r]] or [] + + def get_singles_dict(self, doctype, debug=False, *, for_update=False, cast=False): + """Get Single DocType as dict. + + :param doctype: DocType of the single object whose value is requested + :param debug: Execute query in debug mode - print to STDOUT + :param for_update: Take `FOR UPDATE` lock on the records + :param cast: Cast values to Python data types based on field type + + Example: + + # Get coulmn and value of the single doctype Accounts Settings + account_settings = influxframework.db.get_singles_dict("Accounts Settings") + """ + queried_result = influxframework.qb.engine.get_query( + "Singles", + filters={"doctype": doctype}, + fields=["field", "value"], + for_update=for_update, + ).run(debug=debug) + + if not cast: + return influxframework._dict(queried_result) + + try: + meta = influxframework.get_meta(doctype) + except DoesNotExistError: + return influxframework._dict(queried_result) + + return_value = influxframework._dict() + + for fieldname, value in queried_result: + if df := meta.get_field(fieldname): + casted_value = cast_fieldtype(df.fieldtype, value) + else: + casted_value = value + return_value[fieldname] = casted_value + + return return_value + + @staticmethod + def get_all(*args, **kwargs): + return influxframework.get_all(*args, **kwargs) + + @staticmethod + def get_list(*args, **kwargs): + return influxframework.get_list(*args, **kwargs) + + def set_single_value( + self, + doctype: str, + fieldname: str | dict, + value: str | int | None = None, + *args, + **kwargs, + ): + """Set field value of Single DocType. + + :param doctype: DocType of the single object + :param fieldname: `fieldname` of the property + :param value: `value` of the property + + Example: + + # Update the `deny_multiple_sessions` field in System Settings DocType. + company = influxframework.db.set_single_value("System Settings", "deny_multiple_sessions", True) + """ + return self.set_value(doctype, doctype, fieldname, value, *args, **kwargs) + + def get_single_value(self, doctype, fieldname, cache=True): + """Get property of Single DocType. Cache locally by default + + :param doctype: DocType of the single object whose value is requested + :param fieldname: `fieldname` of the property whose value is requested + + Example: + + # Get the default value of the company from the Global Defaults doctype. + company = influxframework.db.get_single_value('Global Defaults', 'default_company') + """ + + if doctype not in self.value_cache: + self.value_cache[doctype] = {} + + if cache and fieldname in self.value_cache[doctype]: + return self.value_cache[doctype][fieldname] + + val = influxframework.qb.engine.get_query( + table="Singles", + filters={"doctype": doctype, "field": fieldname}, + fields="value", + ).run() + val = val[0][0] if val else None + + df = influxframework.get_meta(doctype).get_field(fieldname) + + if not df: + influxframework.throw( + _("Invalid field name: {0}").format(influxframework.bold(fieldname)), self.InvalidColumnName + ) + + val = cast_fieldtype(df.fieldtype, val) + + self.value_cache[doctype][fieldname] = val + + return val + + def get_singles_value(self, *args, **kwargs): + """Alias for get_single_value""" + return self.get_single_value(*args, **kwargs) + + def _get_values_from_table( + self, + fields, + filters, + doctype, + as_dict, + *, + debug=False, + order_by=None, + update=None, + for_update=False, + run=True, + pluck=False, + distinct=False, + limit=None, + ): + field_objects = [] + query = influxframework.qb.engine.get_query( + table=doctype, + filters=filters, + orderby=order_by, + for_update=for_update, + field_objects=field_objects, + fields=fields, + distinct=distinct, + limit=limit, + ) + if fields == "*" and not isinstance(fields, (list, tuple)) and not isinstance(fields, Criterion): + as_dict = True + + return self.sql(query, as_dict=as_dict, debug=debug, update=update, run=run, pluck=pluck) + + def _get_value_for_many_names( + self, + doctype, + names, + field, + order_by, + *, + debug=False, + run=True, + pluck=False, + distinct=False, + limit=None, + as_dict=False, + ): + if names := list(filter(None, names)): + return self.get_all( + doctype, + fields=field, + filters=names, + order_by=order_by, + pluck=pluck, + debug=debug, + as_list=not as_dict, + run=run, + distinct=distinct, + limit_page_length=limit, + ) + return {} + + def update(self, *args, **kwargs): + """Update multiple values. Alias for `set_value`.""" + return self.set_value(*args, **kwargs) + + def set_value( + self, + dt, + dn, + field, + val=None, + modified=None, + modified_by=None, + update_modified=True, + debug=False, + for_update=True, + ): + """Set a single value in the database, do not call the ORM triggers + but update the modified timestamp (unless specified not to). + + **Warning:** this function will not call Document events and should be avoided in normal cases. + + :param dt: DocType name. + :param dn: Document name. + :param field: Property / field name or dictionary of values to be updated + :param value: Value to be updated. + :param modified: Use this as the `modified` timestamp. + :param modified_by: Set this user as `modified_by`. + :param update_modified: default True. Set as false, if you don't want to update the timestamp. + :param debug: Print the query in the developer / js console. + :param for_update: [DEPRECATED] This function now performs updates in single query, locking is not required. + """ + is_single_doctype = not (dn and dt != dn) + to_update = field if isinstance(field, dict) else {field: val} + + if update_modified: + modified = modified or now() + modified_by = modified_by or influxframework.session.user + to_update.update({"modified": modified, "modified_by": modified_by}) + + if is_single_doctype: + influxframework.db.delete( + "Singles", filters={"field": ("in", tuple(to_update)), "doctype": dt}, debug=debug + ) + + singles_data = ((dt, key, sbool(value)) for key, value in to_update.items()) + query = ( + influxframework.qb.into("Singles").columns("doctype", "field", "value").insert(*singles_data) + ).run(debug=debug) + influxframework.clear_document_cache(dt, dt) + + else: + query = influxframework.qb.engine.build_conditions(table=dt, filters=dn, update=True) + + if isinstance(dn, str): + influxframework.clear_document_cache(dt, dn) + else: + # TODO: Fix this; doesn't work rn - gavin@influxframework.io + # influxframework.cache().hdel_keys(dt, "document_cache") + # Workaround: clear all document caches + influxframework.cache().delete_value("document_cache") + + for column, value in to_update.items(): + query = query.set(column, value) + + query.run(debug=debug) + + if dt in self.value_cache: + del self.value_cache[dt] + + @staticmethod + def set(doc, field, val): + """Set value in document. **Avoid**""" + doc.db_set(field, val) + + def touch(self, doctype, docname): + """Update the modified timestamp of this document.""" + modified = now() + DocType = influxframework.qb.DocType(doctype) + influxframework.qb.update(DocType).set(DocType.modified, modified).where(DocType.name == docname).run() + return modified + + @staticmethod + def set_temp(value): + """Set a temperory value and return a key.""" + key = influxframework.generate_hash() + influxframework.cache().hset("temp", key, value) + return key + + @staticmethod + def get_temp(key): + """Return the temperory value and delete it.""" + return influxframework.cache().hget("temp", key) + + def set_global(self, key, val, user="__global"): + """Save a global key value. Global values will be automatically set if they match fieldname.""" + self.set_default(key, val, user) + + def get_global(self, key, user="__global"): + """Returns a global key value.""" + return self.get_default(key, user) + + def get_default(self, key, parent="__default"): + """Returns default value as a list if multiple or single""" + d = self.get_defaults(key, parent) + return isinstance(d, list) and d[0] or d + + @staticmethod + def set_default(key, val, parent="__default", parenttype=None): + """Sets a global / user default value.""" + influxframework.defaults.set_default(key, val, parent, parenttype) + + @staticmethod + def add_default(key, val, parent="__default", parenttype=None): + """Append a default value for a key, there can be multiple default values for a particular key.""" + influxframework.defaults.add_default(key, val, parent, parenttype) + + @staticmethod + def get_defaults(key=None, parent="__default"): + """Get all defaults""" + defaults = influxframework.defaults.get_defaults_for(parent) + if not key: + return defaults + + if key in defaults: + return defaults[key] + + return defaults.get(influxframework.scrub(key)) + + def begin(self, *, read_only=False): + read_only = read_only or influxframework.flags.read_only + mode = "READ ONLY" if read_only else "" + self.sql(f"START TRANSACTION {mode}") + + def commit(self): + """Commit current transaction. Calls SQL `COMMIT`.""" + for method in influxframework.local.before_commit: + influxframework.call(method[0], *(method[1] or []), **(method[2] or {})) + + self.sql("commit") + self.begin() # explicitly start a new transaction + + influxframework.local.rollback_observers = [] + self.flush_realtime_log() + enqueue_jobs_after_commit() + flush_local_link_count() + + def add_before_commit(self, method, args=None, kwargs=None): + influxframework.local.before_commit.append([method, args, kwargs]) + + @staticmethod + def flush_realtime_log(): + for args in influxframework.local.realtime_log: + influxframework.realtime.emit_via_redis(*args) + + influxframework.local.realtime_log = [] + + def savepoint(self, save_point): + """Savepoints work as a nested transaction. + + Changes can be undone to a save point by doing influxframework.db.rollback(save_point) + + Note: rollback watchers can not work with save points. + so only changes to database are undone when rolling back to a savepoint. + Avoid using savepoints when writing to filesystem.""" + self.sql(f"savepoint {save_point}") + + def release_savepoint(self, save_point): + self.sql(f"release savepoint {save_point}") + + def rollback(self, *, save_point=None): + """`ROLLBACK` current transaction. Optionally rollback to a known save_point.""" + if save_point: + self.sql(f"rollback to savepoint {save_point}") + else: + self.sql("rollback") + self.begin() + for obj in dict.fromkeys(influxframework.local.rollback_observers): + if hasattr(obj, "on_rollback"): + obj.on_rollback() + influxframework.local.rollback_observers = [] + + def field_exists(self, dt, fn): + """Return true of field exists.""" + return self.exists("DocField", {"fieldname": fn, "parent": dt}) + + def table_exists(self, doctype, cached=True): + """Returns True if table for given doctype exists.""" + return f"tab{doctype}" in self.get_tables(cached=cached) + + def has_table(self, doctype): + return self.table_exists(doctype) + + def get_tables(self, cached=True): + raise NotImplementedError + + def a_row_exists(self, doctype): + """Returns True if atleast one row exists.""" + return influxframework.get_all(doctype, limit=1, order_by=None, as_list=True) + + def exists(self, dt, dn=None, cache=False): + """Return the document name of a matching document, or None. + + Note: `cache` only works if `dt` and `dn` are of type `str`. + + ## Examples + + Pass doctype and docname (only in this case we can cache the result) + + ``` + exists("User", "jane@example.org", cache=True) + ``` + + Pass a dict of filters including the `"doctype"` key: + + ``` + exists({"doctype": "User", "full_name": "Jane Doe"}) + ``` + + Pass the doctype and a dict of filters: + + ``` + exists("User", {"full_name": "Jane Doe"}) + ``` + """ + if dt != "DocType" and dt == dn: + # single always exists (!) + return dn + + if isinstance(dt, dict): + dt = dt.copy() # don't modify the original dict + dt, dn = dt.pop("doctype"), dt + + return self.get_value(dt, dn, ignore=True, cache=cache) + + def count(self, dt, filters=None, debug=False, cache=False, distinct: bool = True): + """Returns `COUNT(*)` for given DocType and filters.""" + if cache and not filters: + cache_count = influxframework.cache().get_value(f"doctype:count:{dt}") + if cache_count is not None: + return cache_count + query = influxframework.qb.engine.get_query( + table=dt, filters=filters, fields=Count("*"), distinct=distinct + ) + count = self.sql(query, debug=debug)[0][0] + if not filters and cache: + influxframework.cache().set_value(f"doctype:count:{dt}", count, expires_in_sec=86400) + return count + + @staticmethod + def format_date(date): + return getdate(date).strftime("%Y-%m-%d") + + @staticmethod + def format_datetime(datetime): + if not datetime: + return FallBackDateTimeStr + + if isinstance(datetime, str): + if ":" not in datetime: + datetime = datetime + " 00:00:00.000000" + else: + datetime = datetime.strftime("%Y-%m-%d %H:%M:%S.%f") + + return datetime + + def get_creation_count(self, doctype, minutes): + """Get count of records created in the last x minutes""" + from dateutil.relativedelta import relativedelta + + from influxframework.utils import now_datetime + + Table = influxframework.qb.DocType(doctype) + + return ( + influxframework.qb.from_(Table) + .select(Count(Table.name)) + .where(Table.creation >= now_datetime() - relativedelta(minutes=minutes)) + .run()[0][0] + ) + + def get_db_table_columns(self, table) -> list[str]: + """Returns list of column names from given table.""" + columns = influxframework.cache().hget("table_columns", table) + if columns is None: + information_schema = influxframework.qb.Schema("information_schema") + + columns = ( + influxframework.qb.from_(information_schema.columns) + .select(information_schema.columns.column_name) + .where(information_schema.columns.table_name == table) + .run(pluck=True) + ) + + if columns: + influxframework.cache().hset("table_columns", table, columns) + + return columns + + def get_table_columns(self, doctype): + """Returns list of column names from given doctype.""" + columns = self.get_db_table_columns("tab" + doctype) + if not columns: + raise self.TableMissingError("DocType", doctype) + return columns + + def has_column(self, doctype, column): + """Returns True if column exists in database.""" + return column in self.get_table_columns(doctype) + + def get_column_type(self, doctype, column): + """Returns column type from database.""" + information_schema = influxframework.qb.Schema("information_schema") + table = get_table_name(doctype) + + return ( + influxframework.qb.from_(information_schema.columns) + .select(information_schema.columns.column_type) + .where( + (information_schema.columns.table_name == table) + & (information_schema.columns.column_name == column) + ) + .run(pluck=True)[0] + ) + + def has_index(self, table_name, index_name): + raise NotImplementedError + + def add_index(self, doctype, fields, index_name=None): + raise NotImplementedError + + def add_unique(self, doctype, fields, constraint_name=None): + raise NotImplementedError + + @staticmethod + def get_index_name(fields): + index_name = "_".join(fields) + "_index" + # remove index length if present e.g. (10) from index name + return INDEX_PATTERN.sub(r"", index_name) + + def get_system_setting(self, key): + return influxframework.get_system_settings(key) + + def close(self): + """Close database connection.""" + if self._conn: + self._conn.close() + self._cursor = None + self._conn = None + + @staticmethod + def escape(s, percent=True): + """Excape quotes and percent in given string.""" + # implemented in specific class + raise NotImplementedError + + @staticmethod + def is_column_missing(e): + return influxframework.db.is_missing_column(e) + + def get_descendants(self, doctype, name): + """Return descendants of the group node in tree""" + from influxframework.utils.nestedset import get_descendants_of + + try: + return get_descendants_of(doctype, name, ignore_permissions=True) + except Exception: + # Can only happen if document doesn't exists - kept for backward compatibility + return [] + + def is_missing_table_or_column(self, e): + return self.is_missing_column(e) or self.is_table_missing(e) + + def multisql(self, sql_dict, values=(), **kwargs): + current_dialect = self.db_type or "mariadb" + query = sql_dict.get(current_dialect) + return self.sql(query, values, **kwargs) + + def delete(self, doctype: str, filters: dict | list = None, debug=False, **kwargs): + """Delete rows from a table in site which match the passed filters. This + does trigger DocType hooks. Simply runs a DELETE query in the database. + + Doctype name can be passed directly, it will be pre-pended with `tab`. + """ + filters = filters or kwargs.get("conditions") + query = influxframework.qb.engine.build_conditions(table=doctype, filters=filters).delete() + if "debug" not in kwargs: + kwargs["debug"] = debug + return query.run(**kwargs) + + def truncate(self, doctype: str): + """Truncate a table in the database. This runs a DDL command `TRUNCATE TABLE`. + This cannot be rolled back. + + Doctype name can be passed directly, it will be pre-pended with `tab`. + """ + return self.sql_ddl(f"truncate `{get_table_name(doctype)}`") + + def clear_table(self, doctype): + return self.truncate(doctype) + + def get_last_created(self, doctype): + last_record = self.get_all(doctype, ("creation"), limit=1, order_by="creation desc") + if last_record: + return get_datetime(last_record[0].creation) + else: + return None + + def log_touched_tables(self, query): + if is_query_type(query, ("insert", "delete", "update", "alter", "drop", "rename")): + # single_word_regex is designed to match following patterns + # `tabXxx`, tabXxx and "tabXxx" + + # multi_word_regex is designed to match following patterns + # `tabXxx Xxx` and "tabXxx Xxx" + + # ([`"]?) Captures " or ` at the begining of the table name (if provided) + # \1 matches the first captured group (quote character) at the end of the table name + # multi word table name must have surrounding quotes. + + # (tab([A-Z]\w+)( [A-Z]\w+)*) Captures table names that start with "tab" + # and are continued with multiple words that start with a captital letter + # e.g. 'tabXxx' or 'tabXxx Xxx' or 'tabXxx Xxx Xxx' and so on + + tables = [] + for regex in (SINGLE_WORD_PATTERN, MULTI_WORD_PATTERN): + tables += [groups[1] for groups in regex.findall(query)] + + if influxframework.flags.touched_tables is None: + influxframework.flags.touched_tables = set() + influxframework.flags.touched_tables.update(tables) + + def bulk_insert(self, doctype, fields, values, ignore_duplicates=False, *, chunk_size=10_000): + """ + Insert multiple records at a time + + :param doctype: Doctype name + :param fields: list of fields + :params values: list of list of values + """ + values = list(values) + table = influxframework.qb.DocType(doctype) + + for start_index in range(0, len(values), chunk_size): + query = influxframework.qb.into(table) + if ignore_duplicates: + # Pypika does not have same api for ignoring duplicates + if self.db_type == "mariadb": + query = query.ignore() + elif self.db_type == "postgres": + query = query.on_conflict().do_nothing() + + values_to_insert = values[start_index : start_index + chunk_size] + query.columns(fields).insert(*values_to_insert).run() + + def create_sequence(self, *args, **kwargs): + from influxframework.database.sequence import create_sequence + + return create_sequence(*args, **kwargs) + + def set_next_sequence_val(self, *args, **kwargs): + from influxframework.database.sequence import set_next_val + + set_next_val(*args, **kwargs) + + def get_next_sequence_val(self, *args, **kwargs): + from influxframework.database.sequence import get_next_val + + return get_next_val(*args, **kwargs) + + +def enqueue_jobs_after_commit(): + from influxframework.utils.background_jobs import ( + RQ_JOB_FAILURE_TTL, + RQ_RESULTS_TTL, + execute_job, + get_queue, + ) + + if influxframework.flags.enqueue_after_commit and len(influxframework.flags.enqueue_after_commit) > 0: + for job in influxframework.flags.enqueue_after_commit: + q = get_queue(job.get("queue"), is_async=job.get("is_async")) + q.enqueue_call( + execute_job, + timeout=job.get("timeout"), + kwargs=job.get("queue_args"), + failure_ttl=RQ_JOB_FAILURE_TTL, + result_ttl=RQ_RESULTS_TTL, + ) + influxframework.flags.enqueue_after_commit = [] + + +@contextmanager +def savepoint(catch: type | tuple[type, ...] = Exception): + """Wrapper for wrapping blocks of DB operations in a savepoint. + + as contextmanager: + + for doc in docs: + with savepoint(catch=DuplicateError): + doc.insert() + + as decorator (wraps FULL function call): + + @savepoint(catch=DuplicateError) + def process_doc(doc): + doc.insert() + """ + try: + savepoint = "".join(random.sample(string.ascii_lowercase, 10)) + influxframework.db.savepoint(savepoint) + yield # control back to calling function + except catch: + influxframework.db.rollback(save_point=savepoint) + else: + influxframework.db.release_savepoint(savepoint) diff --git a/influxframework/database/db_manager.py b/influxframework/database/db_manager.py new file mode 100644 index 0000000..0cf62ed --- /dev/null +++ b/influxframework/database/db_manager.py @@ -0,0 +1,85 @@ +import influxframework + + +class DbManager: + def __init__(self, db): + """ + Pass root_conn here for access to all databases. + """ + if db: + self.db = db + + def get_current_host(self): + return self.db.sql("select user()")[0][0].split("@")[1] + + def create_user(self, user, password, host=None): + host = host or self.get_current_host() + password_predicate = f" IDENTIFIED BY '{password}'" if password else "" + self.db.sql(f"CREATE USER '{user}'@'{host}'{password_predicate}") + + def delete_user(self, target, host=None): + host = host or self.get_current_host() + self.db.sql(f"DROP USER IF EXISTS '{target}'@'{host}'") + + def create_database(self, target): + if target in self.get_database_list(): + self.drop_database(target) + self.db.sql(f"CREATE DATABASE `{target}`") + + def drop_database(self, target): + self.db.sql_ddl(f"DROP DATABASE IF EXISTS `{target}`") + + def grant_all_privileges(self, target, user, host=None): + host = host or self.get_current_host() + permissions = ( + ( + "SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, " + "CREATE TEMPORARY TABLES, CREATE VIEW, EVENT, TRIGGER, SHOW VIEW, " + "CREATE ROUTINE, ALTER ROUTINE, EXECUTE, LOCK TABLES" + ) + if influxframework.conf.rds_db + else "ALL PRIVILEGES" + ) + self.db.sql(f"GRANT {permissions} ON `{target}`.* TO '{user}'@'{host}'") + + def flush_privileges(self): + self.db.sql("FLUSH PRIVILEGES") + + def get_database_list(self): + return self.db.sql("SHOW DATABASES", pluck=True) + + @staticmethod + def restore_database(target, source, user, password): + import os + from distutils.spawn import find_executable + + from influxframework.utils import make_esc + + esc = make_esc("$ ") + pv = find_executable("pv") + + if pv: + pipe = f"{pv} {source} |" + source = "" + else: + pipe = "" + source = f"< {source}" + + if pipe: + print("Restoring Database file...") + + command = ( + "{pipe} mysql -u {user} -p{password} -h{host} " + + ("-P{port}" if influxframework.db.port else "") + + " {target} {source}" + ) + command = command.format( + pipe=pipe, + user=esc(user), + password=esc(password), + host=esc(influxframework.db.host), + target=esc(target), + source=source, + port=influxframework.db.port, + ) + os.system(command) diff --git a/influxframework/database/mariadb/__init__.py b/influxframework/database/mariadb/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/database/mariadb/database.py b/influxframework/database/mariadb/database.py new file mode 100644 index 0000000..879cdfa --- /dev/null +++ b/influxframework/database/mariadb/database.py @@ -0,0 +1,426 @@ +import re + +import pymysql +from pymysql.constants import ER, FIELD_TYPE +from pymysql.converters import conversions, escape_string + +import influxframework +from influxframework.database.database import Database +from influxframework.database.mariadb.schema import MariaDBTable +from influxframework.utils import UnicodeWithAttrs, cstr, get_datetime, get_table_name + +_PARAM_COMP = re.compile(r"%\([\w]*\)s") + + +class MariaDBExceptionUtil: + ProgrammingError = pymysql.ProgrammingError + TableMissingError = pymysql.ProgrammingError + OperationalError = pymysql.OperationalError + InternalError = pymysql.InternalError + SQLError = pymysql.ProgrammingError + DataError = pymysql.DataError + + # match ER_SEQUENCE_RUN_OUT - https://mariadb.com/kb/en/mariadb-error-codes/ + SequenceGeneratorLimitExceeded = pymysql.OperationalError + SequenceGeneratorLimitExceeded.errno = 4084 + + @staticmethod + def is_deadlocked(e: pymysql.Error) -> bool: + return e.args[0] == ER.LOCK_DEADLOCK + + @staticmethod + def is_timedout(e: pymysql.Error) -> bool: + return e.args[0] == ER.LOCK_WAIT_TIMEOUT + + @staticmethod + def is_read_only_mode_error(e: pymysql.Error) -> bool: + return e.args[0] == 1792 + + @staticmethod + def is_table_missing(e: pymysql.Error) -> bool: + return e.args[0] == ER.NO_SUCH_TABLE + + @staticmethod + def is_missing_table(e: pymysql.Error) -> bool: + return MariaDBDatabase.is_table_missing(e) + + @staticmethod + def is_missing_column(e: pymysql.Error) -> bool: + return e.args[0] == ER.BAD_FIELD_ERROR + + @staticmethod + def is_duplicate_fieldname(e: pymysql.Error) -> bool: + return e.args[0] == ER.DUP_FIELDNAME + + @staticmethod + def is_duplicate_entry(e: pymysql.Error) -> bool: + return e.args[0] == ER.DUP_ENTRY + + @staticmethod + def is_access_denied(e: pymysql.Error) -> bool: + return e.args[0] == ER.ACCESS_DENIED_ERROR + + @staticmethod + def cant_drop_field_or_key(e: pymysql.Error) -> bool: + return e.args[0] == ER.CANT_DROP_FIELD_OR_KEY + + @staticmethod + def is_syntax_error(e: pymysql.Error) -> bool: + return e.args[0] == ER.PARSE_ERROR + + @staticmethod + def is_data_too_long(e: pymysql.Error) -> bool: + return e.args[0] == ER.DATA_TOO_LONG + + @staticmethod + def is_primary_key_violation(e: pymysql.Error) -> bool: + return ( + MariaDBDatabase.is_duplicate_entry(e) + and "PRIMARY" in cstr(e.args[1]) + and isinstance(e, pymysql.IntegrityError) + ) + + @staticmethod + def is_unique_key_violation(e: pymysql.Error) -> bool: + return ( + MariaDBDatabase.is_duplicate_entry(e) + and "Duplicate" in cstr(e.args[1]) + and isinstance(e, pymysql.IntegrityError) + ) + + +class MariaDBConnectionUtil: + def get_connection(self): + conn = self._get_connection() + conn.auto_reconnect = True + return conn + + def _get_connection(self): + """Return MariaDB connection object.""" + return self.create_connection() + + def create_connection(self): + return pymysql.connect(**self.get_connection_settings()) + + def get_connection_settings(self) -> dict: + conn_settings = { + "host": self.host, + "user": self.user, + "password": self.password, + "conv": self.CONVERSION_MAP, + "charset": "utf8mb4", + "use_unicode": True, + } + + if self.user != "root": + conn_settings["database"] = self.user + + if self.port: + conn_settings["port"] = int(self.port) + + if influxframework.conf.local_infile: + conn_settings["local_infile"] = influxframework.conf.local_infile + + if influxframework.conf.db_ssl_ca and influxframework.conf.db_ssl_cert and influxframework.conf.db_ssl_key: + ssl_params = { + "ca": influxframework.conf.db_ssl_ca, + "cert": influxframework.conf.db_ssl_cert, + "key": influxframework.conf.db_ssl_key, + } + conn_settings |= ssl_params + return conn_settings + + +class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database): + REGEX_CHARACTER = "regexp" + CONVERSION_MAP = conversions | { + FIELD_TYPE.NEWDECIMAL: float, + FIELD_TYPE.DATETIME: get_datetime, + UnicodeWithAttrs: escape_string, + } + default_port = "3306" + + def setup_type_map(self): + self.db_type = "mariadb" + self.type_map = { + "Currency": ("decimal", "21,9"), + "Int": ("int", "11"), + "Long Int": ("bigint", "20"), + "Float": ("decimal", "21,9"), + "Percent": ("decimal", "21,9"), + "Check": ("int", "1"), + "Small Text": ("text", ""), + "Long Text": ("longtext", ""), + "Code": ("longtext", ""), + "Text Editor": ("longtext", ""), + "Markdown Editor": ("longtext", ""), + "HTML Editor": ("longtext", ""), + "Date": ("date", ""), + "Datetime": ("datetime", "6"), + "Time": ("time", "6"), + "Text": ("text", ""), + "Data": ("varchar", self.VARCHAR_LEN), + "Link": ("varchar", self.VARCHAR_LEN), + "Dynamic Link": ("varchar", self.VARCHAR_LEN), + "Password": ("text", ""), + "Select": ("varchar", self.VARCHAR_LEN), + "Rating": ("decimal", "3,2"), + "Read Only": ("varchar", self.VARCHAR_LEN), + "Attach": ("text", ""), + "Attach Image": ("text", ""), + "Signature": ("longtext", ""), + "Color": ("varchar", self.VARCHAR_LEN), + "Barcode": ("longtext", ""), + "Geolocation": ("longtext", ""), + "Duration": ("decimal", "21,9"), + "Icon": ("varchar", self.VARCHAR_LEN), + "Phone": ("varchar", self.VARCHAR_LEN), + "Autocomplete": ("varchar", self.VARCHAR_LEN), + "JSON": ("json", ""), + } + + def get_database_size(self): + """'Returns database size in MB""" + db_size = self.sql( + """ + SELECT `table_schema` as `database_name`, + SUM(`data_length` + `index_length`) / 1024 / 1024 AS `database_size` + FROM information_schema.tables WHERE `table_schema` = %s GROUP BY `table_schema` + """, + self.db_name, + as_dict=True, + ) + + return db_size[0].get("database_size") + + def log_query(self, query, values, debug, explain): + self.last_query = query = self._cursor._last_executed + self._log_query(query, debug, explain) + return self.last_query + + @staticmethod + def escape(s, percent=True): + """Excape quotes and percent in given string.""" + # Update: We've scrapped PyMySQL in favour of MariaDB's official Python client + # Also, given we're promoting use of the PyPika builder via influxframework.qb, the use + # of this method should be limited. + + # pymysql expects unicode argument to escape_string with Python 3 + s = influxframework.as_unicode(escape_string(influxframework.as_unicode(s)), "utf-8").replace("`", "\\`") + + # NOTE separating % escape, because % escape should only be done when using LIKE operator + # or when you use python format string to generate query that already has a %s + # for example: sql("select name from `tabUser` where name=%s and {0}".format(conditions), something) + # defaulting it to True, as this is the most frequent use case + # ideally we shouldn't have to use ESCAPE and strive to pass values via the values argument of sql + if percent: + s = s.replace("%", "%%") + + return "'" + s + "'" + + # column type + @staticmethod + def is_type_number(code): + return code == pymysql.NUMBER + + @staticmethod + def is_type_datetime(code): + return code == pymysql.DATETIME + + def rename_table(self, old_name: str, new_name: str) -> list | tuple: + old_name = get_table_name(old_name) + new_name = get_table_name(new_name) + return self.sql(f"RENAME TABLE `{old_name}` TO `{new_name}`") + + def describe(self, doctype: str) -> list | tuple: + table_name = get_table_name(doctype) + return self.sql(f"DESC `{table_name}`") + + def change_column_type( + self, doctype: str, column: str, type: str, nullable: bool = False + ) -> list | tuple: + table_name = get_table_name(doctype) + null_constraint = "NOT NULL" if not nullable else "" + return self.sql_ddl(f"ALTER TABLE `{table_name}` MODIFY `{column}` {type} {null_constraint}") + + def create_auth_table(self): + self.sql_ddl( + """create table if not exists `__Auth` ( + `doctype` VARCHAR(140) NOT NULL, + `name` VARCHAR(255) NOT NULL, + `fieldname` VARCHAR(140) NOT NULL, + `password` TEXT NOT NULL, + `encrypted` INT(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`doctype`, `name`, `fieldname`) + ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci""" + ) + + def create_global_search_table(self): + if not "__global_search" in self.get_tables(): + self.sql( + """create table __global_search( + doctype varchar(100), + name varchar({0}), + title varchar({0}), + content text, + fulltext(content), + route varchar({0}), + published int(1) not null default 0, + unique `doctype_name` (doctype, name)) + COLLATE=utf8mb4_unicode_ci + ENGINE=MyISAM + CHARACTER SET=utf8mb4""".format( + self.VARCHAR_LEN + ) + ) + + def create_user_settings_table(self): + self.sql_ddl( + """create table if not exists __UserSettings ( + `user` VARCHAR(180) NOT NULL, + `doctype` VARCHAR(180) NOT NULL, + `data` TEXT, + UNIQUE(user, doctype) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8""" + ) + + @staticmethod + def get_on_duplicate_update(key=None): + return "ON DUPLICATE key UPDATE " + + def get_table_columns_description(self, table_name): + """Returns list of column and its description""" + return self.sql( + """select + column_name as 'name', + column_type as 'type', + column_default as 'default', + COALESCE( + (select 1 + from information_schema.statistics + where table_name="{table_name}" + and column_name=columns.column_name + and NON_UNIQUE=1 + and Seq_in_index = 1 + limit 1 + ), 0) as 'index', + column_key = 'UNI' as 'unique' + from information_schema.columns as columns + where table_name = '{table_name}' """.format( + table_name=table_name + ), + as_dict=1, + ) + + def has_index(self, table_name, index_name): + return self.sql( + """SHOW INDEX FROM `{table_name}` + WHERE Key_name='{index_name}'""".format( + table_name=table_name, index_name=index_name + ) + ) + + def get_column_index( + self, table_name: str, fieldname: str, unique: bool = False + ) -> influxframework._dict | None: + """Check if column exists for a specific fields in specified order. + + This differs from db.has_index because it doesn't rely on index name but columns inside an + index. + """ + + indexes = self.sql( + f"""SHOW INDEX FROM `{table_name}` + WHERE Column_name = "{fieldname}" + AND Seq_in_index = 1 + AND Non_unique={int(not unique)} + """, + as_dict=True, + ) + + # Same index can be part of clustered index which contains more fields + # We don't want those. + for index in indexes: + clustered_index = self.sql( + f"""SHOW INDEX FROM `{table_name}` + WHERE Key_name = "{index.Key_name}" + AND Seq_in_index = 2 + """, + as_dict=True, + ) + if not clustered_index: + return index + + def add_index(self, doctype: str, fields: list, index_name: str = None): + """Creates an index with given fields if not already created. + Index name will be `fieldname1_fieldname2_index`""" + index_name = index_name or self.get_index_name(fields) + table_name = get_table_name(doctype) + if not self.has_index(table_name, index_name): + self.commit() + self.sql( + """ALTER TABLE `%s` + ADD INDEX `%s`(%s)""" + % (table_name, index_name, ", ".join(fields)) + ) + + def add_unique(self, doctype, fields, constraint_name=None): + if isinstance(fields, str): + fields = [fields] + if not constraint_name: + constraint_name = "unique_" + "_".join(fields) + + if not self.sql( + """select CONSTRAINT_NAME from information_schema.TABLE_CONSTRAINTS + where table_name=%s and constraint_type='UNIQUE' and CONSTRAINT_NAME=%s""", + ("tab" + doctype, constraint_name), + ): + self.commit() + self.sql( + """alter table `tab%s` + add unique `%s`(%s)""" + % (doctype, constraint_name, ", ".join(fields)) + ) + + def updatedb(self, doctype, meta=None): + """ + Syncs a `DocType` to the table + * creates if required + * updates columns + * updates indices + """ + res = self.sql("select issingle from `tabDocType` where name=%s", (doctype,)) + if not res: + raise Exception(f"Wrong doctype {doctype} in updatedb") + + if not res[0][0]: + db_table = MariaDBTable(doctype, meta) + db_table.validate() + + self.commit() + db_table.sync() + self.begin() + + def get_database_list(self): + return self.sql("SHOW DATABASES", pluck=True) + + def get_tables(self, cached=True): + """Returns list of tables""" + to_query = not cached + + if cached: + tables = influxframework.cache().get_value("db_tables") + to_query = not tables + + if to_query: + information_schema = influxframework.qb.Schema("information_schema") + + tables = ( + influxframework.qb.from_(information_schema.tables) + .select(information_schema.tables.table_name) + .where(information_schema.tables.table_schema != "information_schema") + .run(pluck=True) + ) + influxframework.cache().set_value("db_tables", tables) + + return tables diff --git a/influxframework/database/mariadb/framework_mariadb.sql b/influxframework/database/mariadb/framework_mariadb.sql new file mode 100644 index 0000000..83b6f6b --- /dev/null +++ b/influxframework/database/mariadb/framework_mariadb.sql @@ -0,0 +1,337 @@ +-- Core Elements to install WNFramework +-- To be called from install.py + + +-- +-- Table structure for table `tabDocField` +-- + +DROP TABLE IF EXISTS `tabDocField`; +CREATE TABLE `tabDocField` ( + `name` varchar(255) NOT NULL, + `creation` datetime(6) DEFAULT NULL, + `modified` datetime(6) DEFAULT NULL, + `modified_by` varchar(255) DEFAULT NULL, + `owner` varchar(255) DEFAULT NULL, + `docstatus` int(1) NOT NULL DEFAULT 0, + `parent` varchar(255) DEFAULT NULL, + `parentfield` varchar(255) DEFAULT NULL, + `parenttype` varchar(255) DEFAULT NULL, + `idx` int(8) NOT NULL DEFAULT 0, + `fieldname` varchar(255) DEFAULT NULL, + `label` varchar(255) DEFAULT NULL, + `oldfieldname` varchar(255) DEFAULT NULL, + `fieldtype` varchar(255) DEFAULT NULL, + `oldfieldtype` varchar(255) DEFAULT NULL, + `options` text, + `search_index` int(1) NOT NULL DEFAULT 0, + `show_dashboard` int(1) NOT NULL DEFAULT 0, + `hidden` int(1) NOT NULL DEFAULT 0, + `set_only_once` int(1) NOT NULL DEFAULT 0, + `allow_in_quick_entry` int(1) NOT NULL DEFAULT 0, + `print_hide` int(1) NOT NULL DEFAULT 0, + `report_hide` int(1) NOT NULL DEFAULT 0, + `reqd` int(1) NOT NULL DEFAULT 0, + `bold` int(1) NOT NULL DEFAULT 0, + `in_global_search` int(1) NOT NULL DEFAULT 0, + `collapsible` int(1) NOT NULL DEFAULT 0, + `unique` int(1) NOT NULL DEFAULT 0, + `no_copy` int(1) NOT NULL DEFAULT 0, + `allow_on_submit` int(1) NOT NULL DEFAULT 0, + `show_preview_popup` int(1) NOT NULL DEFAULT 0, + `trigger` varchar(255) DEFAULT NULL, + `collapsible_depends_on` text, + `mandatory_depends_on` text, + `read_only_depends_on` text, + `depends_on` text, + `permlevel` int(11) NOT NULL DEFAULT 0, + `ignore_user_permissions` int(1) NOT NULL DEFAULT 0, + `width` varchar(255) DEFAULT NULL, + `print_width` varchar(255) DEFAULT NULL, + `columns` int(11) NOT NULL DEFAULT 0, + `default` text, + `description` text, + `in_list_view` int(1) NOT NULL DEFAULT 0, + `fetch_if_empty` int(1) NOT NULL DEFAULT 0, + `in_filter` int(1) NOT NULL DEFAULT 0, + `remember_last_selected_value` int(1) NOT NULL DEFAULT 0, + `ignore_xss_filter` int(1) NOT NULL DEFAULT 0, + `print_hide_if_no_value` int(1) NOT NULL DEFAULT 0, + `allow_bulk_edit` int(1) NOT NULL DEFAULT 0, + `in_standard_filter` int(1) NOT NULL DEFAULT 0, + `in_preview` int(1) NOT NULL DEFAULT 0, + `read_only` int(1) NOT NULL DEFAULT 0, + `precision` varchar(255) DEFAULT NULL, + `max_height` varchar(10) DEFAULT NULL, + `length` int(11) NOT NULL DEFAULT 0, + `translatable` int(1) NOT NULL DEFAULT 0, + `hide_border` int(1) NOT NULL DEFAULT 0, + `hide_days` int(1) NOT NULL DEFAULT 0, + `hide_seconds` int(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`name`), + KEY `parent` (`parent`), + KEY `label` (`label`), + KEY `fieldtype` (`fieldtype`), + KEY `fieldname` (`fieldname`) +) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +-- +-- Table structure for table `tabDocPerm` +-- + +DROP TABLE IF EXISTS `tabDocPerm`; +CREATE TABLE `tabDocPerm` ( + `name` varchar(255) NOT NULL, + `creation` datetime(6) DEFAULT NULL, + `modified` datetime(6) DEFAULT NULL, + `modified_by` varchar(255) DEFAULT NULL, + `owner` varchar(255) DEFAULT NULL, + `docstatus` int(1) NOT NULL DEFAULT 0, + `parent` varchar(255) DEFAULT NULL, + `parentfield` varchar(255) DEFAULT NULL, + `parenttype` varchar(255) DEFAULT NULL, + `idx` int(8) NOT NULL DEFAULT 0, + `permlevel` int(11) DEFAULT '0', + `role` varchar(255) DEFAULT NULL, + `match` varchar(255) DEFAULT NULL, + `read` int(1) NOT NULL DEFAULT 1, + `write` int(1) NOT NULL DEFAULT 1, + `create` int(1) NOT NULL DEFAULT 1, + `submit` int(1) NOT NULL DEFAULT 0, + `cancel` int(1) NOT NULL DEFAULT 0, + `delete` int(1) NOT NULL DEFAULT 1, + `amend` int(1) NOT NULL DEFAULT 0, + `report` int(1) NOT NULL DEFAULT 1, + `export` int(1) NOT NULL DEFAULT 1, + `import` int(1) NOT NULL DEFAULT 0, + `share` int(1) NOT NULL DEFAULT 1, + `print` int(1) NOT NULL DEFAULT 1, + `email` int(1) NOT NULL DEFAULT 1, + PRIMARY KEY (`name`), + KEY `parent` (`parent`) +) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `tabDocType Action` +-- + +CREATE TABLE `tabDocType Action` ( + `name` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL, + `creation` datetime(6) DEFAULT NULL, + `modified` datetime(6) DEFAULT NULL, + `modified_by` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `owner` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `docstatus` int(1) NOT NULL DEFAULT 0, + `parent` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `parentfield` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `parenttype` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `idx` int(8) NOT NULL DEFAULT 0, + `label` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `group` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `action_type` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `action` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`name`), + KEY `parent` (`parent`), + KEY `modified` (`modified`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +-- +-- Table structure for table `tabDocType Action` +-- + +CREATE TABLE `tabDocType Link` ( + `name` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL, + `creation` datetime(6) DEFAULT NULL, + `modified` datetime(6) DEFAULT NULL, + `modified_by` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `owner` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `docstatus` int(1) NOT NULL DEFAULT 0, + `parent` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `parentfield` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `parenttype` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `idx` int(8) NOT NULL DEFAULT 0, + `group` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `link_doctype` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `link_fieldname` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`name`), + KEY `parent` (`parent`), + KEY `modified` (`modified`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +-- +-- Table structure for table `tabDocType` +-- + +DROP TABLE IF EXISTS `tabDocType`; +CREATE TABLE `tabDocType` ( + `name` varchar(255) NOT NULL, + `creation` datetime(6) DEFAULT NULL, + `modified` datetime(6) DEFAULT NULL, + `modified_by` varchar(255) DEFAULT NULL, + `owner` varchar(255) DEFAULT NULL, + `docstatus` int(1) NOT NULL DEFAULT 0, + `idx` int(8) NOT NULL DEFAULT 0, + `search_fields` varchar(255) DEFAULT NULL, + `issingle` int(1) NOT NULL DEFAULT 0, + `is_tree` int(1) NOT NULL DEFAULT 0, + `istable` int(1) NOT NULL DEFAULT 0, + `editable_grid` int(1) NOT NULL DEFAULT 1, + `track_changes` int(1) NOT NULL DEFAULT 0, + `module` varchar(255) DEFAULT NULL, + `restrict_to_domain` varchar(255) DEFAULT NULL, + `app` varchar(255) DEFAULT NULL, + `autoname` varchar(255) DEFAULT NULL, + `naming_rule` varchar(40) DEFAULT NULL, + `name_case` varchar(255) DEFAULT NULL, + `title_field` varchar(255) DEFAULT NULL, + `image_field` varchar(255) DEFAULT NULL, + `timeline_field` varchar(255) DEFAULT NULL, + `sort_field` varchar(255) DEFAULT NULL, + `sort_order` varchar(255) DEFAULT NULL, + `description` text, + `colour` varchar(255) DEFAULT NULL, + `read_only` int(1) NOT NULL DEFAULT 0, + `in_create` int(1) NOT NULL DEFAULT 0, + `menu_index` int(11) DEFAULT NULL, + `parent_node` varchar(255) DEFAULT NULL, + `smallicon` varchar(255) DEFAULT NULL, + `allow_copy` int(1) NOT NULL DEFAULT 0, + `allow_rename` int(1) NOT NULL DEFAULT 0, + `allow_import` int(1) NOT NULL DEFAULT 0, + `hide_toolbar` int(1) NOT NULL DEFAULT 0, + `track_seen` int(1) NOT NULL DEFAULT 0, + `max_attachments` int(11) NOT NULL DEFAULT 0, + `print_outline` varchar(255) DEFAULT NULL, + `document_type` varchar(255) DEFAULT NULL, + `icon` varchar(255) DEFAULT NULL, + `color` varchar(255) DEFAULT NULL, + `tag_fields` varchar(255) DEFAULT NULL, + `subject` varchar(255) DEFAULT NULL, + `_last_update` varchar(32) DEFAULT NULL, + `engine` varchar(20) DEFAULT 'InnoDB', + `default_print_format` varchar(255) DEFAULT NULL, + `is_submittable` int(1) NOT NULL DEFAULT 0, + `show_name_in_global_search` int(1) NOT NULL DEFAULT 0, + `_user_tags` varchar(255) DEFAULT NULL, + `custom` int(1) NOT NULL DEFAULT 0, + `beta` int(1) NOT NULL DEFAULT 0, + `has_web_view` int(1) NOT NULL DEFAULT 0, + `allow_guest_to_view` int(1) NOT NULL DEFAULT 0, + `route` varchar(255) DEFAULT NULL, + `is_published_field` varchar(255) DEFAULT NULL, + `website_search_field` varchar(255) DEFAULT NULL, + `email_append_to` int(1) NOT NULL DEFAULT 0, + `subject_field` varchar(255) DEFAULT NULL, + `sender_field` varchar(255) DEFAULT NULL, + `show_title_field_in_link` int(1) NOT NULL DEFAULT 0, + `migration_hash` varchar(255) DEFAULT NULL, + `translated_doctype` int(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`name`) +) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `tabSeries` +-- + +DROP TABLE IF EXISTS `tabSeries`; +CREATE TABLE `tabSeries` ( + `name` varchar(100), + `current` int(10) NOT NULL DEFAULT 0, + PRIMARY KEY(`name`) +) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +-- +-- Table structure for table `tabSessions` +-- + +DROP TABLE IF EXISTS `tabSessions`; +CREATE TABLE `tabSessions` ( + `user` varchar(255) DEFAULT NULL, + `sid` varchar(255) DEFAULT NULL, + `sessiondata` longtext, + `ipaddress` varchar(16) DEFAULT NULL, + `lastupdate` datetime(6) DEFAULT NULL, + `device` varchar(255) DEFAULT 'desktop', + `status` varchar(20) DEFAULT NULL, + KEY `sid` (`sid`) +) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +-- +-- Table structure for table `tabSingles` +-- + +DROP TABLE IF EXISTS `tabSingles`; +CREATE TABLE `tabSingles` ( + `doctype` varchar(255) DEFAULT NULL, + `field` varchar(255) DEFAULT NULL, + `value` text, + KEY `singles_doctype_field_index` (`doctype`, `field`) +) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `__Auth` +-- + +DROP TABLE IF EXISTS `__Auth`; +CREATE TABLE `__Auth` ( + `doctype` VARCHAR(140) NOT NULL, + `name` VARCHAR(255) NOT NULL, + `fieldname` VARCHAR(140) NOT NULL, + `password` TEXT NOT NULL, + `encrypted` INT(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`doctype`, `name`, `fieldname`) +) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `tabFile` +-- + +DROP TABLE IF EXISTS `tabFile`; +CREATE TABLE `tabFile` ( + `name` varchar(255) NOT NULL, + `creation` datetime(6) DEFAULT NULL, + `modified` datetime(6) DEFAULT NULL, + `modified_by` varchar(255) DEFAULT NULL, + `owner` varchar(255) DEFAULT NULL, + `docstatus` int(1) NOT NULL DEFAULT 0, + `parent` varchar(255) DEFAULT NULL, + `parentfield` varchar(255) DEFAULT NULL, + `parenttype` varchar(255) DEFAULT NULL, + `idx` int(8) NOT NULL DEFAULT 0, + `file_name` varchar(255) DEFAULT NULL, + `file_url` varchar(255) DEFAULT NULL, + `module` varchar(255) DEFAULT NULL, + `attached_to_name` varchar(255) DEFAULT NULL, + `file_size` int(11) NOT NULL DEFAULT 0, + `attached_to_doctype` varchar(255) DEFAULT NULL, + PRIMARY KEY (`name`), + KEY `parent` (`parent`), + KEY `attached_to_name` (`attached_to_name`), + KEY `attached_to_doctype` (`attached_to_doctype`) +) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `tabDefaultValue` +-- + +DROP TABLE IF EXISTS `tabDefaultValue`; +CREATE TABLE `tabDefaultValue` ( + `name` varchar(255) NOT NULL, + `creation` datetime(6) DEFAULT NULL, + `modified` datetime(6) DEFAULT NULL, + `modified_by` varchar(255) DEFAULT NULL, + `owner` varchar(255) DEFAULT NULL, + `docstatus` int(1) NOT NULL DEFAULT 0, + `parent` varchar(255) DEFAULT NULL, + `parentfield` varchar(255) DEFAULT NULL, + `parenttype` varchar(255) DEFAULT NULL, + `idx` int(8) NOT NULL DEFAULT 0, + `defvalue` text, + `defkey` varchar(255) DEFAULT NULL, + PRIMARY KEY (`name`), + KEY `parent` (`parent`), + KEY `defaultvalue_parent_defkey_index` (`parent`,`defkey`) +) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/influxframework/database/mariadb/schema.py b/influxframework/database/mariadb/schema.py new file mode 100644 index 0000000..8470164 --- /dev/null +++ b/influxframework/database/mariadb/schema.py @@ -0,0 +1,126 @@ +import influxframework +from influxframework import _ +from influxframework.database.schema import DBTable +from influxframework.model import log_types + + +class MariaDBTable(DBTable): + def create(self): + additional_definitions = "" + engine = self.meta.get("engine") or "InnoDB" + varchar_len = influxframework.db.VARCHAR_LEN + name_column = f"name varchar({varchar_len}) primary key" + + # columns + column_defs = self.get_column_definitions() + if column_defs: + additional_definitions += ",\n".join(column_defs) + ",\n" + + # index + index_defs = self.get_index_definitions() + if index_defs: + additional_definitions += ",\n".join(index_defs) + ",\n" + + # child table columns + if self.meta.get("istable") or 0: + additional_definitions += ( + ",\n".join( + ( + f"parent varchar({varchar_len})", + f"parentfield varchar({varchar_len})", + f"parenttype varchar({varchar_len})", + "index parent(parent)", + ) + ) + + ",\n" + ) + + # creating sequence(s) + if ( + not self.meta.issingle and self.meta.autoname == "autoincrement" + ) or self.doctype in log_types: + + influxframework.db.create_sequence(self.doctype, check_not_exists=True, cache=influxframework.db.SEQUENCE_CACHE) + + # NOTE: not used nextval func as default as the ability to restore + # database with sequences has bugs in mariadb and gives a scary error. + # issue link: https://jira.mariadb.org/browse/MDEV-20070 + name_column = "name bigint primary key" + + # create table + query = f"""create table `{self.table_name}` ( + {name_column}, + creation datetime(6), + modified datetime(6), + modified_by varchar({varchar_len}), + owner varchar({varchar_len}), + docstatus int(1) not null default '0', + idx int(8) not null default '0', + {additional_definitions} + index modified(modified)) + ENGINE={engine} + ROW_FORMAT=DYNAMIC + CHARACTER SET=utf8mb4 + COLLATE=utf8mb4_unicode_ci""" + + influxframework.db.sql(query) + + def alter(self): + for col in self.columns.values(): + col.build_for_alter_table(self.current_columns.get(col.fieldname.lower())) + + add_column_query = [] + modify_column_query = [] + add_index_query = [] + drop_index_query = [] + + columns_to_modify = set(self.change_type + self.add_unique + self.set_default) + + for col in self.add_column: + add_column_query.append(f"ADD COLUMN `{col.fieldname}` {col.get_definition()}") + + for col in columns_to_modify: + modify_column_query.append(f"MODIFY `{col.fieldname}` {col.get_definition()}") + + for col in self.add_index: + # if index key does not exists + if not influxframework.db.get_column_index(self.table_name, col.fieldname, unique=False): + add_index_query.append(f"ADD INDEX `{col.fieldname}_index`(`{col.fieldname}`)") + + for col in self.drop_index + self.drop_unique: + if col.fieldname == "name": + continue + + current_column = self.current_columns.get(col.fieldname.lower()) + unique_constraint_changed = current_column.unique != col.unique + if unique_constraint_changed and not col.unique: + if unique_index := influxframework.db.get_column_index(self.table_name, col.fieldname, unique=True): + drop_index_query.append(f"DROP INDEX `{unique_index.Key_name}`") + + index_constraint_changed = current_column.index != col.set_index + if index_constraint_changed and not col.set_index: + if index_record := influxframework.db.get_column_index(self.table_name, col.fieldname, unique=False): + drop_index_query.append(f"DROP INDEX `{index_record.Key_name}`") + + try: + for query_parts in [add_column_query, modify_column_query, add_index_query, drop_index_query]: + if query_parts: + query_body = ", ".join(query_parts) + query = f"ALTER TABLE `{self.table_name}` {query_body}" + influxframework.db.sql(query) + + except Exception as e: + # sanitize + if e.args[0] == 1060: + influxframework.throw(str(e)) + elif e.args[0] == 1062: + fieldname = str(e).split("'")[-2] + influxframework.throw( + _("{0} field cannot be set as unique in {1}, as there are non-unique existing values").format( + fieldname, self.table_name + ) + ) + elif e.args[0] == 1067: + influxframework.throw(str(e.args[1])) + else: + raise e diff --git a/influxframework/database/mariadb/setup_db.py b/influxframework/database/mariadb/setup_db.py new file mode 100644 index 0000000..a2f462d --- /dev/null +++ b/influxframework/database/mariadb/setup_db.py @@ -0,0 +1,178 @@ +import os + +import influxframework +from influxframework.database.db_manager import DbManager + +expected_settings_10_2_earlier = { + "innodb_file_format": "Barracuda", + "innodb_file_per_table": "ON", + "innodb_large_prefix": "ON", + "character_set_server": "utf8mb4", + "collation_server": "utf8mb4_unicode_ci", +} + +expected_settings_10_3_later = { + "character_set_server": "utf8mb4", + "collation_server": "utf8mb4_unicode_ci", +} + + +def get_mariadb_versions(): + # MariaDB classifies their versions as Major (1st and 2nd number), and Minor (3rd number) + # Example: Version 10.3.13 is Major Version = 10.3, Minor Version = 13 + mariadb_variables = influxframework._dict(influxframework.db.sql("""show variables""")) + version_string = mariadb_variables.get("version").split("-")[0] + versions = {} + versions["major"] = version_string.split(".")[0] + "." + version_string.split(".")[1] + versions["minor"] = version_string.split(".")[2] + return versions + + +def setup_database(force, source_sql, verbose, no_mariadb_socket=False): + influxframework.local.session = influxframework._dict({"user": "Administrator"}) + + db_name = influxframework.local.conf.db_name + root_conn = get_root_connection(influxframework.flags.root_login, influxframework.flags.root_password) + dbman = DbManager(root_conn) + dbman_kwargs = {} + if no_mariadb_socket: + dbman_kwargs["host"] = "%" + + if force or (db_name not in dbman.get_database_list()): + dbman.delete_user(db_name, **dbman_kwargs) + dbman.drop_database(db_name) + else: + raise Exception(f"Database {db_name} already exists") + + dbman.create_user(db_name, influxframework.conf.db_password, **dbman_kwargs) + if verbose: + print("Created user %s" % db_name) + + dbman.create_database(db_name) + if verbose: + print("Created database %s" % db_name) + + dbman.grant_all_privileges(db_name, db_name, **dbman_kwargs) + dbman.flush_privileges() + if verbose: + print(f"Granted privileges to user {db_name} and database {db_name}") + + # close root connection + root_conn.close() + + bootstrap_database(db_name, verbose, source_sql) + + +def setup_help_database(help_db_name): + dbman = DbManager(get_root_connection(influxframework.flags.root_login, influxframework.flags.root_password)) + dbman.drop_database(help_db_name) + + # make database + if not help_db_name in dbman.get_database_list(): + try: + dbman.create_user(help_db_name, help_db_name) + except Exception as e: + # user already exists + if e.args[0] != 1396: + raise + dbman.create_database(help_db_name) + dbman.grant_all_privileges(help_db_name, help_db_name) + dbman.flush_privileges() + + +def drop_user_and_database(db_name, root_login, root_password): + influxframework.local.db = get_root_connection(root_login, root_password) + dbman = DbManager(influxframework.local.db) + dbman.drop_database(db_name) + dbman.delete_user(db_name, host="%") + dbman.delete_user(db_name) + + +def bootstrap_database(db_name, verbose, source_sql=None): + import sys + + influxframework.connect(db_name=db_name) + if not check_database_settings(): + print("Database settings do not match expected values; stopping database setup.") + sys.exit(1) + + import_db_from_sql(source_sql, verbose) + + influxframework.connect(db_name=db_name) + if "tabDefaultValue" not in influxframework.db.get_tables(cached=False): + from click import secho + + secho( + "Table 'tabDefaultValue' missing in the restored site. " + "Database not installed correctly, this can due to lack of " + "permission, or that the database name exists. Check your mysql" + " root password, validity of the backup file or use --force to" + " reinstall", + fg="red", + ) + sys.exit(1) + + +def import_db_from_sql(source_sql=None, verbose=False): + if verbose: + print("Starting database import...") + db_name = influxframework.conf.db_name + if not source_sql: + source_sql = os.path.join(os.path.dirname(__file__), "framework_mariadb.sql") + DbManager(influxframework.local.db).restore_database(db_name, source_sql, db_name, influxframework.conf.db_password) + if verbose: + print("Imported from database %s" % source_sql) + + +def check_database_settings(): + versions = get_mariadb_versions() + if versions["major"] <= "10.2": + expected_variables = expected_settings_10_2_earlier + else: + expected_variables = expected_settings_10_3_later + + mariadb_variables = influxframework._dict(influxframework.db.sql("show variables")) + # Check each expected value vs. actuals: + result = True + for key, expected_value in expected_variables.items(): + if mariadb_variables.get(key) != expected_value: + print( + "For key %s. Expected value %s, found value %s" + % (key, expected_value, mariadb_variables.get(key)) + ) + result = False + if not result: + print( + ( + "=" * 80 + "\n" + "Creation of your site - {x} failed because MariaDB is not properly {sep}" + "configured. If using version 10.2.x or earlier, make sure you use the {sep}" + "the Barracuda storage engine. {sep}{sep}" + "Please verify the settings above in MariaDB's my.cnf. Restart MariaDB. And {sep}" + "then run `bench new-site {x}` again.{sep2}" + "" + "=" * 80 + ).format(x=influxframework.local.site, sep2="\n" * 2, sep="\n") + ) + + return result + + +def get_root_connection(root_login, root_password): + import getpass + + if not influxframework.local.flags.root_connection: + if not root_login: + root_login = "root" + + if not root_password: + root_password = influxframework.conf.get("root_password") or None + + if not root_password: + root_password = getpass.getpass("MySQL root password: ") + + influxframework.local.flags.root_connection = influxframework.database.get_db( + user=root_login, password=root_password + ) + + return influxframework.local.flags.root_connection diff --git a/influxframework/database/postgres/__init__.py b/influxframework/database/postgres/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/database/postgres/database.py b/influxframework/database/postgres/database.py new file mode 100644 index 0000000..9a5669f --- /dev/null +++ b/influxframework/database/postgres/database.py @@ -0,0 +1,443 @@ +import re + +import psycopg2 +import psycopg2.extensions +from psycopg2.errorcodes import ( + CLASS_INTEGRITY_CONSTRAINT_VIOLATION, + DEADLOCK_DETECTED, + DUPLICATE_COLUMN, + INSUFFICIENT_PRIVILEGE, + STRING_DATA_RIGHT_TRUNCATION, + UNDEFINED_COLUMN, + UNDEFINED_TABLE, + UNIQUE_VIOLATION, +) +from psycopg2.errors import ReadOnlySqlTransaction, SequenceGeneratorLimitExceeded, SyntaxError +from psycopg2.extensions import ISOLATION_LEVEL_REPEATABLE_READ + +import influxframework +from influxframework.database.database import Database +from influxframework.database.postgres.schema import PostgresTable +from influxframework.database.utils import EmptyQueryValues, LazyDecode +from influxframework.utils import cstr, get_table_name + +# cast decimals as floats +DEC2FLOAT = psycopg2.extensions.new_type( + psycopg2.extensions.DECIMAL.values, + "DEC2FLOAT", + lambda value, curs: float(value) if value is not None else None, +) + +psycopg2.extensions.register_type(DEC2FLOAT) + +LOCATE_SUB_PATTERN = re.compile(r"locate\(([^,]+),([^)]+)(\)?)\)", flags=re.IGNORECASE) +LOCATE_QUERY_PATTERN = re.compile(r"locate\(", flags=re.IGNORECASE) +PG_TRANSFORM_PATTERN = re.compile(r"([=><]+)\s*([+-]?\d+)(\.0)?(?![a-zA-Z\.\d])") +FROM_TAB_PATTERN = re.compile(r"from tab([\w-]*)", flags=re.IGNORECASE) + + +class PostgresExceptionUtil: + ProgrammingError = psycopg2.ProgrammingError + TableMissingError = psycopg2.ProgrammingError + OperationalError = psycopg2.OperationalError + InternalError = psycopg2.InternalError + SQLError = psycopg2.ProgrammingError + DataError = psycopg2.DataError + InterfaceError = psycopg2.InterfaceError + SequenceGeneratorLimitExceeded = SequenceGeneratorLimitExceeded + + @staticmethod + def is_deadlocked(e): + return getattr(e, "pgcode", None) == DEADLOCK_DETECTED + + @staticmethod + def is_timedout(e): + # http://initd.org/psycopg/docs/extensions.html?highlight=datatype#psycopg2.extensions.QueryCanceledError + return isinstance(e, psycopg2.extensions.QueryCanceledError) + + @staticmethod + def is_read_only_mode_error(e) -> bool: + return isinstance(e, ReadOnlySqlTransaction) + + @staticmethod + def is_syntax_error(e): + return isinstance(e, SyntaxError) + + @staticmethod + def is_table_missing(e): + return getattr(e, "pgcode", None) == UNDEFINED_TABLE + + @staticmethod + def is_missing_table(e): + return PostgresDatabase.is_table_missing(e) + + @staticmethod + def is_missing_column(e): + return getattr(e, "pgcode", None) == UNDEFINED_COLUMN + + @staticmethod + def is_access_denied(e): + return getattr(e, "pgcode", None) == INSUFFICIENT_PRIVILEGE + + @staticmethod + def cant_drop_field_or_key(e): + return getattr(e, "pgcode", None) == CLASS_INTEGRITY_CONSTRAINT_VIOLATION + + @staticmethod + def is_duplicate_entry(e): + return getattr(e, "pgcode", None) == UNIQUE_VIOLATION + + @staticmethod + def is_primary_key_violation(e): + return getattr(e, "pgcode", None) == UNIQUE_VIOLATION and "_pkey" in cstr(e.args[0]) + + @staticmethod + def is_unique_key_violation(e): + return getattr(e, "pgcode", None) == UNIQUE_VIOLATION and "_key" in cstr(e.args[0]) + + @staticmethod + def is_duplicate_fieldname(e): + return getattr(e, "pgcode", None) == DUPLICATE_COLUMN + + @staticmethod + def is_data_too_long(e): + return getattr(e, "pgcode", None) == STRING_DATA_RIGHT_TRUNCATION + + +class PostgresDatabase(PostgresExceptionUtil, Database): + REGEX_CHARACTER = "~" + default_port = "5432" + + def setup_type_map(self): + self.db_type = "postgres" + self.type_map = { + "Currency": ("decimal", "21,9"), + "Int": ("bigint", None), + "Long Int": ("bigint", None), + "Float": ("decimal", "21,9"), + "Percent": ("decimal", "21,9"), + "Check": ("smallint", None), + "Small Text": ("text", ""), + "Long Text": ("text", ""), + "Code": ("text", ""), + "Text Editor": ("text", ""), + "Markdown Editor": ("text", ""), + "HTML Editor": ("text", ""), + "Date": ("date", ""), + "Datetime": ("timestamp", None), + "Time": ("time", "6"), + "Text": ("text", ""), + "Data": ("varchar", self.VARCHAR_LEN), + "Link": ("varchar", self.VARCHAR_LEN), + "Dynamic Link": ("varchar", self.VARCHAR_LEN), + "Password": ("text", ""), + "Select": ("varchar", self.VARCHAR_LEN), + "Rating": ("decimal", "3,2"), + "Read Only": ("varchar", self.VARCHAR_LEN), + "Attach": ("text", ""), + "Attach Image": ("text", ""), + "Signature": ("text", ""), + "Color": ("varchar", self.VARCHAR_LEN), + "Barcode": ("text", ""), + "Geolocation": ("text", ""), + "Duration": ("decimal", "21,9"), + "Icon": ("varchar", self.VARCHAR_LEN), + "Phone": ("varchar", self.VARCHAR_LEN), + "Autocomplete": ("varchar", self.VARCHAR_LEN), + "JSON": ("json", ""), + } + + @property + def last_query(self): + return LazyDecode(self._cursor.query) + + def get_connection(self): + conn = psycopg2.connect( + "host='{}' dbname='{}' user='{}' password='{}' port={}".format( + self.host, self.user, self.user, self.password, self.port + ) + ) + conn.set_isolation_level(ISOLATION_LEVEL_REPEATABLE_READ) + + return conn + + def escape(self, s, percent=True): + """Escape quotes and percent in given string.""" + if isinstance(s, bytes): + s = s.decode("utf-8") + + # MariaDB's driver treats None as an empty string + # So Postgres should do the same + + if s is None: + s = "" + + if percent: + s = s.replace("%", "%%") + + s = s.encode("utf-8") + + return str(psycopg2.extensions.QuotedString(s)) + + def get_database_size(self): + """'Returns database size in MB""" + db_size = self.sql( + "SELECT (pg_database_size(%s) / 1024 / 1024) as database_size", self.db_name, as_dict=True + ) + return db_size[0].get("database_size") + + # pylint: disable=W0221 + def sql(self, query, values=EmptyQueryValues, *args, **kwargs): + return super().sql(modify_query(query), modify_values(values), *args, **kwargs) + + def lazy_mogrify(self, *args, **kwargs) -> str: + return self.last_query + + def get_tables(self, cached=True): + return [ + d[0] + for d in self.sql( + """select table_name + from information_schema.tables + where table_catalog='{}' + and table_type = 'BASE TABLE' + and table_schema='{}'""".format( + influxframework.conf.db_name, influxframework.conf.get("db_schema", "public") + ) + ) + ] + + def format_date(self, date): + if not date: + return "0001-01-01" + + if not isinstance(date, str): + date = date.strftime("%Y-%m-%d") + + return date + + # column type + @staticmethod + def is_type_number(code): + return code == psycopg2.NUMBER + + @staticmethod + def is_type_datetime(code): + return code == psycopg2.DATETIME + + def rename_table(self, old_name: str, new_name: str) -> list | tuple: + old_name = get_table_name(old_name) + new_name = get_table_name(new_name) + return self.sql(f"ALTER TABLE `{old_name}` RENAME TO `{new_name}`") + + def describe(self, doctype: str) -> list | tuple: + table_name = get_table_name(doctype) + return self.sql( + f"SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME = '{table_name}'" + ) + + def change_column_type( + self, doctype: str, column: str, type: str, nullable: bool = False, use_cast: bool = False + ) -> list | tuple: + table_name = get_table_name(doctype) + null_constraint = "SET NOT NULL" if not nullable else "DROP NOT NULL" + using_cast = f'using "{column}"::{type}' if use_cast else "" + + # postgres allows ddl in transactions but since we've currently made + # things same as mariadb (raising exception on ddl commands if the transaction has any writes), + # hence using sql_ddl here for committing and then moving forward. + return self.sql_ddl( + f"""ALTER TABLE "{table_name}" + ALTER COLUMN "{column}" TYPE {type} {using_cast}, + ALTER COLUMN "{column}" {null_constraint}""" + ) + + def create_auth_table(self): + self.sql_ddl( + """create table if not exists "__Auth" ( + "doctype" VARCHAR(140) NOT NULL, + "name" VARCHAR(255) NOT NULL, + "fieldname" VARCHAR(140) NOT NULL, + "password" TEXT NOT NULL, + "encrypted" INT NOT NULL DEFAULT 0, + PRIMARY KEY ("doctype", "name", "fieldname") + )""" + ) + + def create_global_search_table(self): + if not "__global_search" in self.get_tables(): + self.sql( + """create table "__global_search"( + doctype varchar(100), + name varchar({0}), + title varchar({0}), + content text, + route varchar({0}), + published int not null default 0, + unique (doctype, name))""".format( + self.VARCHAR_LEN + ) + ) + + def create_user_settings_table(self): + self.sql_ddl( + """create table if not exists "__UserSettings" ( + "user" VARCHAR(180) NOT NULL, + "doctype" VARCHAR(180) NOT NULL, + "data" TEXT, + UNIQUE ("user", "doctype") + )""" + ) + + def updatedb(self, doctype, meta=None): + """ + Syncs a `DocType` to the table + * creates if required + * updates columns + * updates indices + """ + res = self.sql(f"select issingle from `tabDocType` where name='{doctype}'") + if not res: + raise Exception(f"Wrong doctype {doctype} in updatedb") + + if not res[0][0]: + db_table = PostgresTable(doctype, meta) + db_table.validate() + + self.commit() + db_table.sync() + self.begin() + + @staticmethod + def get_on_duplicate_update(key="name"): + if isinstance(key, list): + key = '", "'.join(key) + return f'ON CONFLICT ("{key}") DO UPDATE SET ' + + def check_implicit_commit(self, query): + pass # postgres can run DDL in transactions without implicit commits + + def has_index(self, table_name, index_name): + return self.sql( + """SELECT 1 FROM pg_indexes WHERE tablename='{table_name}' + and indexname='{index_name}' limit 1""".format( + table_name=table_name, index_name=index_name + ) + ) + + def add_index(self, doctype: str, fields: list, index_name: str = None): + """Creates an index with given fields if not already created. + Index name will be `fieldname1_fieldname2_index`""" + table_name = get_table_name(doctype) + index_name = index_name or self.get_index_name(fields) + fields_str = '", "'.join(re.sub(r"\(.*\)", "", field) for field in fields) + + self.sql_ddl(f'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}` ("{fields_str}")') + + def add_unique(self, doctype, fields, constraint_name=None): + if isinstance(fields, str): + fields = [fields] + if not constraint_name: + constraint_name = "unique_" + "_".join(fields) + + if not self.sql( + """ + SELECT CONSTRAINT_NAME + FROM information_schema.TABLE_CONSTRAINTS + WHERE table_name=%s + AND constraint_type='UNIQUE' + AND CONSTRAINT_NAME=%s""", + ("tab" + doctype, constraint_name), + ): + self.commit() + self.sql( + """ALTER TABLE `tab%s` + ADD CONSTRAINT %s UNIQUE (%s)""" + % (doctype, constraint_name, ", ".join(fields)) + ) + + def get_table_columns_description(self, table_name): + """Returns list of column and its description""" + # pylint: disable=W1401 + return self.sql( + """ + SELECT a.column_name AS name, + CASE LOWER(a.data_type) + WHEN 'character varying' THEN CONCAT('varchar(', a.character_maximum_length ,')') + WHEN 'timestamp without time zone' THEN 'timestamp' + ELSE a.data_type + END AS type, + BOOL_OR(b.index) AS index, + SPLIT_PART(COALESCE(a.column_default, NULL), '::', 1) AS default, + BOOL_OR(b.unique) AS unique + FROM information_schema.columns a + LEFT JOIN + (SELECT indexdef, tablename, + indexdef LIKE '%UNIQUE INDEX%' AS unique, + indexdef NOT LIKE '%UNIQUE INDEX%' AS index + FROM pg_indexes + WHERE tablename='{table_name}') b + ON SUBSTRING(b.indexdef, '(.*)') LIKE CONCAT('%', a.column_name, '%') + WHERE a.table_name = '{table_name}' + GROUP BY a.column_name, a.data_type, a.column_default, a.character_maximum_length; + """.format( + table_name=table_name + ), + as_dict=1, + ) + + def get_database_list(self): + return self.sql("SELECT datname FROM pg_database", pluck=True) + + +def modify_query(query): + """ "Modifies query according to the requirements of postgres""" + # replace ` with " for definitions + query = str(query).replace("`", '"') + query = replace_locate_with_strpos(query) + # select from requires "" + query = FROM_TAB_PATTERN.sub(r'from "tab\1"', query) + + # only find int (with/without signs), ignore decimals (with/without signs), ignore hashes (which start with numbers), + # drop .0 from decimals and add quotes around them + # + # >>> query = "c='abcd' , a >= 45, b = -45.0, c = 40, d=4500.0, e=3500.53, f=40psdfsd, g=9092094312, h=12.00023" + # >>> re.sub(r"([=><]+)\s*([+-]?\d+)(\.0)?(?![a-zA-Z\.\d])", r"\1 '\2'", query) + # "c='abcd' , a >= '45', b = '-45', c = '40', d= '4500', e=3500.53, f=40psdfsd, g= '9092094312', h=12.00023 + + return PG_TRANSFORM_PATTERN.sub(r"\1 '\2'", query) + + +def modify_values(values): + def modify_value(value): + if isinstance(value, (list, tuple)): + value = tuple(modify_values(value)) + + elif isinstance(value, int): + value = str(value) + + return value + + if not values or values == EmptyQueryValues: + return values + + if isinstance(values, dict): + for k, v in values.items(): + values[k] = modify_value(v) + elif isinstance(values, (tuple, list)): + new_values = [] + for val in values: + new_values.append(modify_value(val)) + + values = new_values + else: + values = modify_value(values) + + return values + + +def replace_locate_with_strpos(query): + # strpos is the locate equivalent in postgres + if LOCATE_QUERY_PATTERN.search(query): + query = LOCATE_SUB_PATTERN.sub(r"strpos(\2\3, \1)", query) + return query diff --git a/influxframework/database/postgres/framework_postgres.sql b/influxframework/database/postgres/framework_postgres.sql new file mode 100644 index 0000000..bc39449 --- /dev/null +++ b/influxframework/database/postgres/framework_postgres.sql @@ -0,0 +1,346 @@ +-- Core Elements to install WNFramework +-- To be called from install.py + + +-- +-- Table structure for table "tabDocField" +-- + +DROP TABLE IF EXISTS "tabDocField"; +CREATE TABLE "tabDocField" ( + "name" varchar(255) NOT NULL, + "creation" timestamp(6) DEFAULT NULL, + "modified" timestamp(6) DEFAULT NULL, + "modified_by" varchar(255) DEFAULT NULL, + "owner" varchar(255) DEFAULT NULL, + "docstatus" smallint NOT NULL DEFAULT 0, + "parent" varchar(255) DEFAULT NULL, + "parentfield" varchar(255) DEFAULT NULL, + "parenttype" varchar(255) DEFAULT NULL, + "idx" bigint NOT NULL DEFAULT 0, + "fieldname" varchar(255) DEFAULT NULL, + "label" varchar(255) DEFAULT NULL, + "oldfieldname" varchar(255) DEFAULT NULL, + "fieldtype" varchar(255) DEFAULT NULL, + "oldfieldtype" varchar(255) DEFAULT NULL, + "options" text, + "search_index" smallint NOT NULL DEFAULT 0, + "hidden" smallint NOT NULL DEFAULT 0, + "set_only_once" smallint NOT NULL DEFAULT 0, + "show_dashboard" smallint NOT NULL DEFAULT 0, + "allow_in_quick_entry" smallint NOT NULL DEFAULT 0, + "print_hide" smallint NOT NULL DEFAULT 0, + "report_hide" smallint NOT NULL DEFAULT 0, + "reqd" smallint NOT NULL DEFAULT 0, + "bold" smallint NOT NULL DEFAULT 0, + "in_global_search" smallint NOT NULL DEFAULT 0, + "collapsible" smallint NOT NULL DEFAULT 0, + "unique" smallint NOT NULL DEFAULT 0, + "no_copy" smallint NOT NULL DEFAULT 0, + "allow_on_submit" smallint NOT NULL DEFAULT 0, + "show_preview_popup" smallint NOT NULL DEFAULT 0, + "trigger" varchar(255) DEFAULT NULL, + "collapsible_depends_on" text, + "mandatory_depends_on" text, + "read_only_depends_on" text, + "depends_on" text, + "permlevel" bigint NOT NULL DEFAULT 0, + "ignore_user_permissions" smallint NOT NULL DEFAULT 0, + "width" varchar(255) DEFAULT NULL, + "print_width" varchar(255) DEFAULT NULL, + "columns" bigint NOT NULL DEFAULT 0, + "default" text, + "description" text, + "in_list_view" smallint NOT NULL DEFAULT 0, + "fetch_if_empty" smallint NOT NULL DEFAULT 0, + "in_filter" smallint NOT NULL DEFAULT 0, + "remember_last_selected_value" smallint NOT NULL DEFAULT 0, + "ignore_xss_filter" smallint NOT NULL DEFAULT 0, + "print_hide_if_no_value" smallint NOT NULL DEFAULT 0, + "allow_bulk_edit" smallint NOT NULL DEFAULT 0, + "in_standard_filter" smallint NOT NULL DEFAULT 0, + "in_preview" smallint NOT NULL DEFAULT 0, + "read_only" smallint NOT NULL DEFAULT 0, + "precision" varchar(255) DEFAULT NULL, + "max_height" varchar(10) DEFAULT NULL, + "length" bigint NOT NULL DEFAULT 0, + "translatable" smallint NOT NULL DEFAULT 0, + "hide_border" smallint NOT NULL DEFAULT 0, + "hide_days" smallint NOT NULL DEFAULT 0, + "hide_seconds" smallint NOT NULL DEFAULT 0, + PRIMARY KEY ("name") +) ; + +create index on "tabDocField" ("parent"); +create index on "tabDocField" ("label"); +create index on "tabDocField" ("fieldtype"); +create index on "tabDocField" ("fieldname"); + +-- +-- Table structure for table "tabDocPerm" +-- + +DROP TABLE IF EXISTS "tabDocPerm"; +CREATE TABLE "tabDocPerm" ( + "name" varchar(255) NOT NULL, + "creation" timestamp(6) DEFAULT NULL, + "modified" timestamp(6) DEFAULT NULL, + "modified_by" varchar(255) DEFAULT NULL, + "owner" varchar(255) DEFAULT NULL, + "docstatus" smallint NOT NULL DEFAULT 0, + "parent" varchar(255) DEFAULT NULL, + "parentfield" varchar(255) DEFAULT NULL, + "parenttype" varchar(255) DEFAULT NULL, + "idx" bigint NOT NULL DEFAULT 0, + "permlevel" bigint DEFAULT '0', + "role" varchar(255) DEFAULT NULL, + "match" varchar(255) DEFAULT NULL, + "read" smallint NOT NULL DEFAULT 1, + "write" smallint NOT NULL DEFAULT 1, + "create" smallint NOT NULL DEFAULT 1, + "submit" smallint NOT NULL DEFAULT 0, + "cancel" smallint NOT NULL DEFAULT 0, + "delete" smallint NOT NULL DEFAULT 1, + "amend" smallint NOT NULL DEFAULT 0, + "report" smallint NOT NULL DEFAULT 1, + "export" smallint NOT NULL DEFAULT 1, + "import" smallint NOT NULL DEFAULT 0, + "share" smallint NOT NULL DEFAULT 1, + "print" smallint NOT NULL DEFAULT 1, + "email" smallint NOT NULL DEFAULT 1, + PRIMARY KEY ("name") +) ; + +create index on "tabDocPerm" ("parent"); + +-- +-- Table structure for table "tabDocType Action" +-- + +DROP TABLE IF EXISTS "tabDocType Action"; +CREATE TABLE "tabDocType Action" ( + "name" varchar(255) NOT NULL, + "creation" timestamp(6) DEFAULT NULL, + "modified" timestamp(6) DEFAULT NULL, + "modified_by" varchar(255) DEFAULT NULL, + "owner" varchar(255) DEFAULT NULL, + "docstatus" smallint NOT NULL DEFAULT 0, + "parent" varchar(255) DEFAULT NULL, + "parentfield" varchar(255) DEFAULT NULL, + "parenttype" varchar(255) DEFAULT NULL, + "idx" bigint NOT NULL DEFAULT 0, + "label" varchar(140) NOT NULL, + "group" text DEFAULT NULL, + "action_type" varchar(140) NOT NULL, + "action" varchar(140) NOT NULL, + PRIMARY KEY ("name") +) ; + +create index on "tabDocType Action" ("parent"); + +-- +-- Table structure for table "tabDocType Link" +-- + +DROP TABLE IF EXISTS "tabDocType Link"; +CREATE TABLE "tabDocType Link" ( + "name" varchar(255) NOT NULL, + "creation" timestamp(6) DEFAULT NULL, + "modified" timestamp(6) DEFAULT NULL, + "modified_by" varchar(255) DEFAULT NULL, + "owner" varchar(255) DEFAULT NULL, + "docstatus" smallint NOT NULL DEFAULT 0, + "parent" varchar(255) DEFAULT NULL, + "parentfield" varchar(255) DEFAULT NULL, + "parenttype" varchar(255) DEFAULT NULL, + "idx" bigint NOT NULL DEFAULT 0, + "label" varchar(140) DEFAULT NULL, + "group" varchar(140) DEFAULT NULL, + "link_doctype" varchar(140) NOT NULL, + "link_fieldname" varchar(140) NOT NULL, + PRIMARY KEY ("name") +) ; + +create index on "tabDocType Link" ("parent"); + + +-- +-- Table structure for table "tabDocType" +-- + +DROP TABLE IF EXISTS "tabDocType"; +CREATE TABLE "tabDocType" ( + "name" varchar(255) NOT NULL, + "creation" timestamp(6) DEFAULT NULL, + "modified" timestamp(6) DEFAULT NULL, + "modified_by" varchar(255) DEFAULT NULL, + "owner" varchar(255) DEFAULT NULL, + "docstatus" smallint NOT NULL DEFAULT 0, + "idx" bigint NOT NULL DEFAULT 0, + "search_fields" varchar(255) DEFAULT NULL, + "issingle" smallint NOT NULL DEFAULT 0, + "is_tree" smallint NOT NULL DEFAULT 0, + "istable" smallint NOT NULL DEFAULT 0, + "editable_grid" smallint NOT NULL DEFAULT 1, + "track_changes" smallint NOT NULL DEFAULT 0, + "module" varchar(255) DEFAULT NULL, + "restrict_to_domain" varchar(255) DEFAULT NULL, + "app" varchar(255) DEFAULT NULL, + "autoname" varchar(255) DEFAULT NULL, + "naming_rule" varchar(40) DEFAULT NULL, + "name_case" varchar(255) DEFAULT NULL, + "title_field" varchar(255) DEFAULT NULL, + "image_field" varchar(255) DEFAULT NULL, + "timeline_field" varchar(255) DEFAULT NULL, + "sort_field" varchar(255) DEFAULT NULL, + "sort_order" varchar(255) DEFAULT NULL, + "description" text, + "colour" varchar(255) DEFAULT NULL, + "read_only" smallint NOT NULL DEFAULT 0, + "in_create" smallint NOT NULL DEFAULT 0, + "menu_index" bigint DEFAULT NULL, + "parent_node" varchar(255) DEFAULT NULL, + "smallicon" varchar(255) DEFAULT NULL, + "allow_copy" smallint NOT NULL DEFAULT 0, + "allow_rename" smallint NOT NULL DEFAULT 0, + "allow_import" smallint NOT NULL DEFAULT 0, + "hide_toolbar" smallint NOT NULL DEFAULT 0, + "track_seen" smallint NOT NULL DEFAULT 0, + "max_attachments" bigint NOT NULL DEFAULT 0, + "print_outline" varchar(255) DEFAULT NULL, + "document_type" varchar(255) DEFAULT NULL, + "icon" varchar(255) DEFAULT NULL, + "color" varchar(255) DEFAULT NULL, + "tag_fields" varchar(255) DEFAULT NULL, + "subject" varchar(255) DEFAULT NULL, + "_last_update" varchar(32) DEFAULT NULL, + "engine" varchar(20) DEFAULT 'InnoDB', + "default_print_format" varchar(255) DEFAULT NULL, + "is_submittable" smallint NOT NULL DEFAULT 0, + "show_name_in_global_search" smallint NOT NULL DEFAULT 0, + "_user_tags" varchar(255) DEFAULT NULL, + "custom" smallint NOT NULL DEFAULT 0, + "beta" smallint NOT NULL DEFAULT 0, + "has_web_view" smallint NOT NULL DEFAULT 0, + "allow_guest_to_view" smallint NOT NULL DEFAULT 0, + "route" varchar(255) DEFAULT NULL, + "is_published_field" varchar(255) DEFAULT NULL, + "website_search_field" varchar(255) DEFAULT NULL, + "email_append_to" smallint NOT NULL DEFAULT 0, + "subject_field" varchar(255) DEFAULT NULL, + "sender_field" varchar(255) DEFAULT NULL, + "show_title_field_in_link" smallint NOT NULL DEFAULT 0, + "migration_hash" varchar(255) DEFAULT NULL, + "translated_doctype" smallint NOT NULL DEFAULT 0, + PRIMARY KEY ("name") +) ; + +-- +-- Table structure for table "tabSeries" +-- + +DROP TABLE IF EXISTS "tabSeries"; +CREATE TABLE "tabSeries" ( + "name" varchar(100), + "current" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("name") +) ; + +-- +-- Table structure for table "tabSessions" +-- + +DROP TABLE IF EXISTS "tabSessions"; +CREATE TABLE "tabSessions" ( + "user" varchar(255) DEFAULT NULL, + "sid" varchar(255) DEFAULT NULL, + "sessiondata" text, + "ipaddress" varchar(16) DEFAULT NULL, + "lastupdate" timestamp(6) DEFAULT NULL, + "device" varchar(255) DEFAULT 'desktop', + "status" varchar(20) DEFAULT NULL +); + +create index on "tabSessions" ("sid"); + +-- +-- Table structure for table "tabSingles" +-- + +DROP TABLE IF EXISTS "tabSingles"; +CREATE TABLE "tabSingles" ( + "doctype" varchar(255) DEFAULT NULL, + "field" varchar(255) DEFAULT NULL, + "value" text +); + +create index on "tabSingles" ("doctype", "field"); + +-- +-- Table structure for table "__Auth" +-- + +DROP TABLE IF EXISTS "__Auth"; +CREATE TABLE "__Auth" ( + "doctype" VARCHAR(140) NOT NULL, + "name" VARCHAR(255) NOT NULL, + "fieldname" VARCHAR(140) NOT NULL, + "password" TEXT NOT NULL, + "encrypted" int NOT NULL DEFAULT 0, + PRIMARY KEY ("doctype", "name", "fieldname") +); + +create index on "__Auth" ("doctype", "name", "fieldname"); + +-- +-- Table structure for table "tabFile" +-- + +DROP TABLE IF EXISTS "tabFile"; +CREATE TABLE "tabFile" ( + "name" varchar(255) NOT NULL, + "creation" timestamp(6) DEFAULT NULL, + "modified" timestamp(6) DEFAULT NULL, + "modified_by" varchar(255) DEFAULT NULL, + "owner" varchar(255) DEFAULT NULL, + "docstatus" smallint NOT NULL DEFAULT 0, + "parent" varchar(255) DEFAULT NULL, + "parentfield" varchar(255) DEFAULT NULL, + "parenttype" varchar(255) DEFAULT NULL, + "idx" bigint NOT NULL DEFAULT 0, + "file_name" varchar(255) DEFAULT NULL, + "file_url" varchar(255) DEFAULT NULL, + "module" varchar(255) DEFAULT NULL, + "attached_to_name" varchar(255) DEFAULT NULL, + "file_size" bigint NOT NULL DEFAULT 0, + "attached_to_doctype" varchar(255) DEFAULT NULL, + PRIMARY KEY ("name") +); + +create index on "tabFile" ("parent"); +create index on "tabFile" ("attached_to_name"); +create index on "tabFile" ("attached_to_doctype"); + +-- +-- Table structure for table "tabDefaultValue" +-- + +DROP TABLE IF EXISTS "tabDefaultValue"; +CREATE TABLE "tabDefaultValue" ( + "name" varchar(255) NOT NULL, + "creation" timestamp(6) DEFAULT NULL, + "modified" timestamp(6) DEFAULT NULL, + "modified_by" varchar(255) DEFAULT NULL, + "owner" varchar(255) DEFAULT NULL, + "docstatus" smallint NOT NULL DEFAULT 0, + "parent" varchar(255) DEFAULT NULL, + "parentfield" varchar(255) DEFAULT NULL, + "parenttype" varchar(255) DEFAULT NULL, + "idx" bigint NOT NULL DEFAULT 0, + "defvalue" text, + "defkey" varchar(255) DEFAULT NULL, + PRIMARY KEY ("name") +); + +create index on "tabDefaultValue" ("parent"); +create index on "tabDefaultValue" ("parent", "defkey"); diff --git a/influxframework/database/postgres/schema.py b/influxframework/database/postgres/schema.py new file mode 100644 index 0000000..76a0dcd --- /dev/null +++ b/influxframework/database/postgres/schema.py @@ -0,0 +1,172 @@ +import influxframework +from influxframework import _ +from influxframework.database.schema import DBTable, get_definition +from influxframework.model import log_types +from influxframework.utils import cint, flt + + +class PostgresTable(DBTable): + def create(self): + varchar_len = influxframework.db.VARCHAR_LEN + name_column = f"name varchar({varchar_len}) primary key" + + additional_definitions = "" + # columns + column_defs = self.get_column_definitions() + if column_defs: + additional_definitions += ",\n".join(column_defs) + + # child table columns + if self.meta.get("istable") or 0: + if column_defs: + additional_definitions += ",\n" + + additional_definitions += ",\n".join( + ( + f"parent varchar({varchar_len})", + f"parentfield varchar({varchar_len})", + f"parenttype varchar({varchar_len})", + ) + ) + + # creating sequence(s) + if ( + not self.meta.issingle and self.meta.autoname == "autoincrement" + ) or self.doctype in log_types: + + influxframework.db.create_sequence(self.doctype, check_not_exists=True, cache=influxframework.db.SEQUENCE_CACHE) + name_column = "name bigint primary key" + + # TODO: set docstatus length + # create table + influxframework.db.sql( + f"""create table `{self.table_name}` ( + {name_column}, + creation timestamp(6), + modified timestamp(6), + modified_by varchar({varchar_len}), + owner varchar({varchar_len}), + docstatus smallint not null default '0', + idx bigint not null default '0', + {additional_definitions} + )""" + ) + + self.create_indexes() + influxframework.db.commit() + + def create_indexes(self): + create_index_query = "" + for key, col in self.columns.items(): + if ( + col.set_index + and col.fieldtype in influxframework.db.type_map + and influxframework.db.type_map.get(col.fieldtype)[0] not in ("text", "longtext") + ): + create_index_query += ( + 'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format( + index_name=col.fieldname, table_name=self.table_name, field=col.fieldname + ) + ) + if create_index_query: + # nosemgrep + influxframework.db.sql(create_index_query) + + def alter(self): + for col in self.columns.values(): + col.build_for_alter_table(self.current_columns.get(col.fieldname.lower())) + + query = [] + + for col in self.add_column: + query.append(f"ADD COLUMN `{col.fieldname}` {col.get_definition()}") + + for col in self.change_type: + using_clause = "" + if col.fieldtype in ("Datetime"): + # The USING option of SET DATA TYPE can actually specify any expression + # involving the old values of the row + # read more https://www.postgresql.org/docs/9.1/sql-altertable.html + using_clause = f"USING {col.fieldname}::timestamp without time zone" + elif col.fieldtype in ("Check"): + using_clause = f"USING {col.fieldname}::smallint" + + query.append( + "ALTER COLUMN `{}` TYPE {} {}".format( + col.fieldname, + get_definition(col.fieldtype, precision=col.precision, length=col.length), + using_clause, + ) + ) + + for col in self.set_default: + if col.fieldname == "name": + continue + + if col.fieldtype in ("Check", "Int"): + col_default = cint(col.default) + + elif col.fieldtype in ("Currency", "Float", "Percent"): + col_default = flt(col.default) + + elif not col.default: + col_default = "NULL" + + else: + col_default = f"{influxframework.db.escape(col.default)}" + + query.append(f"ALTER COLUMN `{col.fieldname}` SET DEFAULT {col_default}") + + create_contraint_query = "" + for col in self.add_index: + # if index key not exists + create_contraint_query += ( + 'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format( + index_name=col.fieldname, table_name=self.table_name, field=col.fieldname + ) + ) + + for col in self.add_unique: + # if index key not exists + create_contraint_query += ( + 'CREATE UNIQUE INDEX IF NOT EXISTS "unique_{index_name}" ON `{table_name}`(`{field}`);'.format( + index_name=col.fieldname, table_name=self.table_name, field=col.fieldname + ) + ) + + drop_contraint_query = "" + for col in self.drop_index: + # primary key + if col.fieldname != "name": + # if index key exists + drop_contraint_query += f'DROP INDEX IF EXISTS "{col.fieldname}" ;' + + for col in self.drop_unique: + # primary key + if col.fieldname != "name": + # if index key exists + drop_contraint_query += f'DROP INDEX IF EXISTS "unique_{col.fieldname}" ;' + try: + if query: + final_alter_query = "ALTER TABLE `{}` {}".format(self.table_name, ", ".join(query)) + # nosemgrep + influxframework.db.sql(final_alter_query) + if create_contraint_query: + # nosemgrep + influxframework.db.sql(create_contraint_query) + if drop_contraint_query: + # nosemgrep + influxframework.db.sql(drop_contraint_query) + except Exception as e: + # sanitize + if influxframework.db.is_duplicate_fieldname(e): + influxframework.throw(str(e)) + elif influxframework.db.is_duplicate_entry(e): + fieldname = str(e).split("'")[-2] + influxframework.throw( + _("{0} field cannot be set as unique in {1}, as there are non-unique existing values").format( + fieldname, self.table_name + ) + ) + else: + raise e diff --git a/influxframework/database/postgres/setup_db.py b/influxframework/database/postgres/setup_db.py new file mode 100644 index 0000000..c9e6958 --- /dev/null +++ b/influxframework/database/postgres/setup_db.py @@ -0,0 +1,121 @@ +import os + +import influxframework + + +def setup_database(force, source_sql=None, verbose=False): + root_conn = get_root_connection(influxframework.flags.root_login, influxframework.flags.root_password) + root_conn.commit() + root_conn.sql("end") + root_conn.sql(f"DROP DATABASE IF EXISTS `{influxframework.conf.db_name}`") + root_conn.sql(f"DROP USER IF EXISTS {influxframework.conf.db_name}") + root_conn.sql(f"CREATE DATABASE `{influxframework.conf.db_name}`") + root_conn.sql(f"CREATE user {influxframework.conf.db_name} password '{influxframework.conf.db_password}'") + root_conn.sql("GRANT ALL PRIVILEGES ON DATABASE `{0}` TO {0}".format(influxframework.conf.db_name)) + root_conn.close() + + bootstrap_database(influxframework.conf.db_name, verbose, source_sql=source_sql) + influxframework.connect() + + +def bootstrap_database(db_name, verbose, source_sql=None): + influxframework.connect(db_name=db_name) + import_db_from_sql(source_sql, verbose) + influxframework.connect(db_name=db_name) + + if "tabDefaultValue" not in influxframework.db.get_tables(): + import sys + + from click import secho + + secho( + "Table 'tabDefaultValue' missing in the restored site. " + "This may be due to incorrect permissions or the result of a restore from a bad backup file. " + "Database not installed correctly.", + fg="red", + ) + sys.exit(1) + + +def import_db_from_sql(source_sql=None, verbose=False): + from shutil import which + from subprocess import PIPE, run + + # we can't pass psql password in arguments in postgresql as mysql. So + # set password connection parameter in environment variable + subprocess_env = os.environ.copy() + subprocess_env["PGPASSWORD"] = str(influxframework.conf.db_password) + + # bootstrap db + if not source_sql: + source_sql = os.path.join(os.path.dirname(__file__), "framework_postgres.sql") + + pv = which("pv") + + _command = ( + f"psql {influxframework.conf.db_name} " + f"-h {influxframework.conf.db_host or 'localhost'} -p {str(influxframework.conf.db_port or '5432')} " + f"-U {influxframework.conf.db_name}" + ) + + if pv: + command = f"{pv} {source_sql} | " + _command + else: + command = _command + f" -f {source_sql}" + + print("Restoring Database file...") + if verbose: + print(command) + + restore_proc = run(command, env=subprocess_env, shell=True, stdout=PIPE) + + if verbose: + print( + f"\nSTDOUT by psql:\n{restore_proc.stdout.decode()}\nImported from Database File: {source_sql}" + ) + + +def setup_help_database(help_db_name): + root_conn = get_root_connection(influxframework.flags.root_login, influxframework.flags.root_password) + root_conn.sql(f"DROP DATABASE IF EXISTS `{help_db_name}`") + root_conn.sql(f"DROP USER IF EXISTS {help_db_name}") + root_conn.sql(f"CREATE DATABASE `{help_db_name}`") + root_conn.sql(f"CREATE user {help_db_name} password '{help_db_name}'") + root_conn.sql("GRANT ALL PRIVILEGES ON DATABASE `{0}` TO {0}".format(help_db_name)) + + +def get_root_connection(root_login=None, root_password=None): + if not influxframework.local.flags.root_connection: + if not root_login: + root_login = influxframework.conf.get("root_login") or None + + if not root_login: + root_login = input("Enter postgres super user: ") + + if not root_password: + root_password = influxframework.conf.get("root_password") or None + + if not root_password: + from getpass import getpass + + root_password = getpass("Postgres super user password: ") + + influxframework.local.flags.root_connection = influxframework.database.get_db( + user=root_login, password=root_password + ) + + return influxframework.local.flags.root_connection + + +def drop_user_and_database(db_name, root_login, root_password): + root_conn = get_root_connection( + influxframework.flags.root_login or root_login, influxframework.flags.root_password or root_password + ) + root_conn.commit() + root_conn.sql( + "SELECT pg_terminate_backend (pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = %s", + (db_name,), + ) + root_conn.sql("end") + root_conn.sql(f"DROP DATABASE IF EXISTS {db_name}") + root_conn.sql(f"DROP USER IF EXISTS {db_name}") diff --git a/influxframework/database/query.py b/influxframework/database/query.py new file mode 100644 index 0000000..8e7e7b3 --- /dev/null +++ b/influxframework/database/query.py @@ -0,0 +1,565 @@ +import operator +import re +from ast import literal_eval +from functools import cached_property +from types import BuiltinFunctionType +from typing import TYPE_CHECKING, Any, Callable + +import influxframework +from influxframework import _ +from influxframework.model.db_query import get_timespan_date_range +from influxframework.query_builder import Criterion, Field, Order, Table, functions +from influxframework.query_builder.functions import Function, SqlFunctions + +TAB_PATTERN = re.compile("^tab") +WORDS_PATTERN = re.compile(r"\w+") +BRACKETS_PATTERN = re.compile(r"\(.*?\)|$") +SQL_FUNCTIONS = [sql_function.value for sql_function in SqlFunctions] +COMMA_PATTERN = re.compile(r",\s*(?![^()]*\))") + + +def like(key: Field, value: str) -> influxframework.qb: + """Wrapper method for `LIKE` + + Args: + key (str): field + value (str): criterion + + Returns: + influxframework.qb: `influxframework.qb object with `LIKE` + """ + return key.like(value) + + +def func_in(key: Field, value: list | tuple) -> influxframework.qb: + """Wrapper method for `IN` + + Args: + key (str): field + value (Union[int, str]): criterion + + Returns: + influxframework.qb: `influxframework.qb object with `IN` + """ + return key.isin(value) + + +def not_like(key: Field, value: str) -> influxframework.qb: + """Wrapper method for `NOT LIKE` + + Args: + key (str): field + value (str): criterion + + Returns: + influxframework.qb: `influxframework.qb object with `NOT LIKE` + """ + return key.not_like(value) + + +def func_not_in(key: Field, value: list | tuple): + """Wrapper method for `NOT IN` + + Args: + key (str): field + value (Union[int, str]): criterion + + Returns: + influxframework.qb: `influxframework.qb object with `NOT IN` + """ + return key.notin(value) + + +def func_regex(key: Field, value: str) -> influxframework.qb: + """Wrapper method for `REGEX` + + Args: + key (str): field + value (str): criterion + + Returns: + influxframework.qb: `influxframework.qb object with `REGEX` + """ + return key.regex(value) + + +def func_between(key: Field, value: list | tuple) -> influxframework.qb: + """Wrapper method for `BETWEEN` + + Args: + key (str): field + value (Union[int, str]): criterion + + Returns: + influxframework.qb: `influxframework.qb object with `BETWEEN` + """ + return key[slice(*value)] + + +def func_is(key, value): + "Wrapper for IS" + return key.isnotnull() if value.lower() == "set" else key.isnull() + + +def func_timespan(key: Field, value: str) -> influxframework.qb: + """Wrapper method for `TIMESPAN` + + Args: + key (str): field + value (str): criterion + + Returns: + influxframework.qb: `influxframework.qb object with `TIMESPAN` + """ + + return func_between(key, get_timespan_date_range(value)) + + +def make_function(key: Any, value: int | str): + """returns fucntion query + + Args: + key (Any): field + value (Union[int, str]): criterion + + Returns: + influxframework.qb: influxframework.qb object + """ + return OPERATOR_MAP[value[0].casefold()](key, value[1]) + + +def change_orderby(order: str): + """Convert orderby to standart Order object + + Args: + order (str): Field, order + + Returns: + tuple: field, order + """ + order = order.split() + + try: + if order[1].lower() == "asc": + return order[0], Order.asc + except IndexError: + pass + + return order[0], Order.desc + + +def literal_eval_(literal): + try: + return literal_eval(literal) + except (ValueError, SyntaxError): + return literal + + +# default operators +OPERATOR_MAP: dict[str, Callable] = { + "+": operator.add, + "=": operator.eq, + "-": operator.sub, + "!=": operator.ne, + "<": operator.lt, + ">": operator.gt, + "<=": operator.le, + "=<": operator.le, + ">=": operator.ge, + "=>": operator.ge, + "/": operator.truediv, + "*": operator.mul, + "in": func_in, + "not in": func_not_in, + "like": like, + "not like": not_like, + "regex": func_regex, + "between": func_between, + "is": func_is, + "timespan": func_timespan, + # TODO: Add support for nested set + # TODO: Add support for custom operators (WIP) - via filters_config hooks +} + + +class Engine: + tables: dict[str, str] = {} + + @cached_property + def OPERATOR_MAP(self): + # default operators + all_operators = OPERATOR_MAP.copy() + + # TODO: update with site-specific custom operators / removed previous buggy implementation + if influxframework.get_hooks("filters_config"): + from influxframework.utils.commands import warn + + warn( + "The 'filters_config' hook used to add custom operators is not yet implemented" + " in influxframework.db.query engine. Use db_query (influxframework.get_list) instead." + ) + + return all_operators + + def get_condition(self, table: str | Table, **kwargs) -> influxframework.qb: + """Get initial table object + + Args: + table (str): DocType + + Returns: + influxframework.qb: DocType with initial condition + """ + table_object = self.get_table(table) + if kwargs.get("update"): + return influxframework.qb.update(table_object) + if kwargs.get("into"): + return influxframework.qb.into(table_object) + return influxframework.qb.from_(table_object) + + def get_table(self, table_name: str | Table) -> Table: + if isinstance(table_name, Table): + return table_name + table_name = table_name.strip('"').strip("'") + if table_name not in self.tables: + self.tables[table_name] = influxframework.qb.DocType(table_name) + return self.tables[table_name] + + def criterion_query(self, table: str, criterion: Criterion, **kwargs) -> influxframework.qb: + """Generate filters from Criterion objects + + Args: + table (str): DocType + criterion (Criterion): Filters + + Returns: + influxframework.qb: condition object + """ + condition = self.add_conditions(self.get_condition(table, **kwargs), **kwargs) + return condition.where(criterion) + + def add_conditions(self, conditions: influxframework.qb, **kwargs): + """Adding additional conditions + + Args: + conditions (influxframework.qb): built conditions + + Returns: + conditions (influxframework.qb): influxframework.qb object + """ + if kwargs.get("orderby") and kwargs.get("orderby") != "KEEP_DEFAULT_ORDERING": + orderby = kwargs.get("orderby") + if isinstance(orderby, str) and len(orderby.split()) > 1: + for ordby in orderby.split(","): + if ordby := ordby.strip(): + orderby, order = change_orderby(ordby) + conditions = conditions.orderby(orderby, order=order) + else: + conditions = conditions.orderby(orderby, order=kwargs.get("order") or Order.desc) + + if kwargs.get("limit"): + conditions = conditions.limit(kwargs.get("limit")) + conditions = conditions.offset(kwargs.get("offset", 0)) + + if kwargs.get("distinct"): + conditions = conditions.distinct() + + if kwargs.get("for_update"): + conditions = conditions.for_update() + + if kwargs.get("groupby"): + conditions = conditions.groupby(kwargs.get("groupby")) + + return conditions + + def misc_query(self, table: str, filters: list | tuple = None, **kwargs): + """Build conditions using the given Lists or Tuple filters + + Args: + table (str): DocType + filters (Union[List, Tuple], optional): Filters. Defaults to None. + """ + conditions = self.get_condition(table, **kwargs) + if not filters: + return conditions + if isinstance(filters, list): + for f in filters: + if isinstance(f, (list, tuple)): + _operator = self.OPERATOR_MAP[f[-2].casefold()] + if len(f) == 4: + table_object = self.get_table(f[0]) + _field = table_object[f[1]] + else: + _field = Field(f[0]) + conditions = conditions.where(_operator(_field, f[-1])) + elif isinstance(f, dict): + conditions = self.dict_query(table, f, **kwargs) + else: + _operator = self.OPERATOR_MAP[filters[1].casefold()] + if not isinstance(filters[0], str): + conditions = make_function(filters[0], filters[2]) + break + conditions = conditions.where(_operator(Field(filters[0]), filters[2])) + break + + return self.add_conditions(conditions, **kwargs) + + def dict_query(self, table: str, filters: dict[str, str | int] = None, **kwargs) -> influxframework.qb: + """Build conditions using the given dictionary filters + + Args: + table (str): DocType + filters (Dict[str, Union[str, int]], optional): Filters. Defaults to None. + + Returns: + influxframework.qb: conditions object + """ + conditions = self.get_condition(table, **kwargs) + if not filters: + conditions = self.add_conditions(conditions, **kwargs) + return conditions + + for key, value in filters.items(): + if isinstance(value, bool): + filters.update({key: str(int(value))}) + + for key in filters: + value = filters.get(key) + _operator = self.OPERATOR_MAP["="] + + if not isinstance(key, str): + conditions = conditions.where(make_function(key, value)) + continue + if isinstance(value, (list, tuple)): + _operator = self.OPERATOR_MAP[value[0].casefold()] + _value = value[1] if value[1] else ("",) + conditions = conditions.where(_operator(Field(key), _value)) + else: + if value is not None: + conditions = conditions.where(_operator(Field(key), value)) + else: + _table = conditions._from[0] + field = getattr(_table, key) + conditions = conditions.where(field.isnull()) + + return self.add_conditions(conditions, **kwargs) + + def build_conditions( + self, table: str, filters: dict[str, str | int] | str | int = None, **kwargs + ) -> influxframework.qb: + """Build conditions for sql query + + Args: + filters (Union[Dict[str, Union[str, int]], str, int]): conditions in Dict + table (str): DocType + + Returns: + influxframework.qb: influxframework.qb conditions object + """ + if isinstance(filters, int) or isinstance(filters, str): + filters = {"name": str(filters)} + + if isinstance(filters, Criterion): + criterion = self.criterion_query(table, filters, **kwargs) + + elif isinstance(filters, (list, tuple)): + criterion = self.misc_query(table, filters, **kwargs) + + else: + criterion = self.dict_query(filters=filters, table=table, **kwargs) + + return criterion + + def get_function_object(self, field: str) -> "Function": + """Expects field to look like 'SUM(*)' or 'name' or something similar. Returns PyPika Function object""" + func = field.split("(", maxsplit=1)[0].capitalize() + args_start, args_end = len(func) + 1, field.index(")") + args = field[args_start:args_end].split(",") + + _, alias = field.split(" as ") if " as " in field else (None, None) + + to_cast = "*" not in args + _args = [] + + for arg in args: + initial_fields = literal_eval_(arg.strip()) + if to_cast: + has_primitive_operator = False + for _operator in OPERATOR_MAP.keys(): + if _operator in initial_fields: + operator_mapping = OPERATOR_MAP[_operator] + # Only perform this if operator is of primitive type. + if isinstance(operator_mapping, BuiltinFunctionType): + has_primitive_operator = True + field = operator_mapping( + *map(lambda field: Field(field.strip()), arg.split(_operator)), + ) + + field = Field(initial_fields) if not has_primitive_operator else field + else: + field = initial_fields + + _args.append(field) + try: + return getattr(functions, func)(*_args, alias=alias or None) + except AttributeError: + # Fall back for functions not present in `SqlFunctions`` + return Function(func, *_args, alias=alias or None) + + def function_objects_from_string(self, fields): + fields = list(map(lambda str: str.strip(), COMMA_PATTERN.split(fields))) + return self.function_objects_from_list(fields=fields) + + def function_objects_from_list(self, fields): + functions = [] + for field in fields: + field = field.casefold() if isinstance(field, str) else field + if not issubclass(type(field), Criterion): + if any([f"{func}(" in field for func in SQL_FUNCTIONS]) or "(" in field: + functions.append(field) + + return [self.get_function_object(function) for function in functions] + + def remove_string_functions(self, fields, function_objects): + """Remove string functions from fields which have already been converted to function objects""" + for function in function_objects: + if isinstance(fields, str): + if function.alias: + fields = fields.replace(" as " + function.alias.casefold(), "") + fields = BRACKETS_PATTERN.sub("", fields.replace(function.name.casefold(), "")) + # Check if only comma is left in fields after stripping functions. + if "," in fields and (len(fields.strip()) == 1): + fields = "" + else: + updated_fields = [] + for field in fields: + if isinstance(field, str): + if function.alias: + field = field.replace(" as " + function.alias.casefold(), "") + field = ( + BRACKETS_PATTERN.sub("", field).strip().casefold().replace(function.name.casefold(), "") + ) + updated_fields.append(field) + + fields = [field for field in updated_fields if field] + + return fields + + def set_fields(self, fields, **kwargs): + fields = kwargs.get("pluck") if kwargs.get("pluck") else fields or "name" + if isinstance(fields, list) and None in fields and Field not in fields: + return None + + function_objects = [] + + is_list = isinstance(fields, (list, tuple, set)) + if is_list and len(fields) == 1: + fields = fields[0] + is_list = False + + if is_list: + function_objects += self.function_objects_from_list(fields=fields) + + is_str = isinstance(fields, str) + if is_str: + fields = fields.casefold() + function_objects += self.function_objects_from_string(fields=fields) + + fields = self.remove_string_functions(fields, function_objects) + + if is_str and "," in fields: + fields = [field.replace(" ", "") if "as" not in field else field for field in fields.split(",")] + is_list, is_str = True, False + + if is_str: + if fields == "*": + return fields + if " as " in fields: + fields, reference = fields.split(" as ") + fields = Field(fields).as_(reference) + + if not is_str and fields: + if issubclass(type(fields), Criterion): + return fields + updated_fields = [] + if "*" in fields: + return fields + for field in fields: + if not isinstance(field, Criterion) and field: + if " as " in field: + field, reference = field.split(" as ") + updated_fields.append(Field(field.strip()).as_(reference)) + else: + updated_fields.append(Field(field)) + + fields = updated_fields + + # Need to check instance again since fields modified. + if not isinstance(fields, (list, tuple, set)): + fields = [fields] if fields else [] + + fields.extend(function_objects) + return fields + + def get_query( + self, + table: str, + fields: list | tuple, + filters: dict[str, str | int] | str | int | list[list | str | int] = None, + **kwargs, + ): + # Clean up state before each query + self.tables = {} + criterion = self.build_conditions(table, filters, **kwargs) + fields = self.set_fields(kwargs.get("field_objects") or fields, **kwargs) + + join = kwargs.get("join").replace(" ", "_") if kwargs.get("join") else "left_join" + + if len(self.tables) > 1: + primary_table = self.tables[table] + del self.tables[table] + for table_object in self.tables.values(): + criterion = getattr(criterion, join)(table_object).on( + table_object.parent == primary_table.name + ) + + if isinstance(fields, (list, tuple)): + query = criterion.select(*fields) + + elif isinstance(fields, Criterion): + query = criterion.select(fields) + + else: + query = criterion.select(fields) + + return query + + +class Permission: + @classmethod + def check_permissions(cls, query, **kwargs): + if not isinstance(query, str): + query = query.get_sql() + + doctype = cls.get_tables_from_query(query) + if isinstance(doctype, str): + doctype = [doctype] + + for dt in doctype: + dt = TAB_PATTERN.sub("", dt) + if not influxframework.has_permission( + dt, + "select", + user=kwargs.get("user"), + parent_doctype=kwargs.get("parent_doctype"), + ) and not influxframework.has_permission( + dt, + "read", + user=kwargs.get("user"), + parent_doctype=kwargs.get("parent_doctype"), + ): + influxframework.throw(_("Insufficient Permission for {0}").format(influxframework.bold(dt))) + + @staticmethod + def get_tables_from_query(query: str): + return [table for table in WORDS_PATTERN.findall(query) if table.startswith("tab")] diff --git a/influxframework/database/schema.py b/influxframework/database/schema.py new file mode 100644 index 0000000..8744e8a --- /dev/null +++ b/influxframework/database/schema.py @@ -0,0 +1,380 @@ +import re + +import influxframework +from influxframework import _ +from influxframework.utils import cint, cstr, flt + +SPECIAL_CHAR_PATTERN = re.compile(r"[\W]", flags=re.UNICODE) +VARCHAR_CAST_PATTERN = re.compile(r"varchar\(([\d]+)\)") + + +class InvalidColumnName(influxframework.ValidationError): + pass + + +class DBTable: + def __init__(self, doctype, meta=None): + self.doctype = doctype + self.table_name = f"tab{doctype}" + self.meta = meta or influxframework.get_meta(doctype, False) + self.columns = {} + self.current_columns = {} + + # lists for change + self.add_column = [] + self.change_type = [] + self.change_name = [] + self.add_unique = [] + self.add_index = [] + self.drop_unique = [] + self.drop_index = [] + self.set_default = [] + + # load + self.get_columns_from_docfields() + + def sync(self): + if self.meta.get("is_virtual"): + # no schema to sync for virtual doctypes + return + if self.is_new(): + self.create() + else: + influxframework.cache().hdel("table_columns", self.table_name) + self.alter() + + def create(self): + pass + + def get_column_definitions(self): + column_list = [] + influxframework.db.DEFAULT_COLUMNS + ret = [] + for k in list(self.columns): + if k not in column_list: + d = self.columns[k].get_definition() + if d: + ret.append("`" + k + "` " + d) + column_list.append(k) + return ret + + def get_index_definitions(self): + ret = [] + for key, col in self.columns.items(): + if ( + col.set_index + and not col.unique + and col.fieldtype in influxframework.db.type_map + and influxframework.db.type_map.get(col.fieldtype)[0] not in ("text", "longtext") + ): + ret.append("index `" + key + "`(`" + key + "`)") + return ret + + def get_columns_from_docfields(self): + """ + get columns from docfields and custom fields + """ + fields = self.meta.get_fieldnames_with_value(with_field_meta=True) + + # optional fields like _comments + if not self.meta.get("istable"): + for fieldname in influxframework.db.OPTIONAL_COLUMNS: + fields.append({"fieldname": fieldname, "fieldtype": "Text"}) + + # add _seen column if track_seen + if self.meta.get("track_seen"): + fields.append({"fieldname": "_seen", "fieldtype": "Text"}) + + for field in fields: + if field.get("is_virtual"): + continue + + self.columns[field.get("fieldname")] = DbColumn( + self, + field.get("fieldname"), + field.get("fieldtype"), + field.get("length"), + field.get("default"), + field.get("search_index"), + field.get("options"), + field.get("unique"), + field.get("precision"), + ) + + def validate(self): + """Check if change in varchar length isn't truncating the columns""" + if self.is_new(): + return + + self.setup_table_columns() + + columns = [ + influxframework._dict({"fieldname": f, "fieldtype": "Data"}) for f in influxframework.db.STANDARD_VARCHAR_COLUMNS + ] + if self.meta.get("istable"): + columns += [ + influxframework._dict({"fieldname": f, "fieldtype": "Data"}) for f in influxframework.db.CHILD_TABLE_COLUMNS + ] + columns += self.columns.values() + + for col in columns: + if len(col.fieldname) >= 64: + influxframework.throw( + _("Fieldname is limited to 64 characters ({0})").format(influxframework.bold(col.fieldname)) + ) + + if "varchar" in influxframework.db.type_map.get(col.fieldtype, ()): + + # validate length range + new_length = cint(col.length) or cint(influxframework.db.VARCHAR_LEN) + if not (1 <= new_length <= 1000): + influxframework.throw(_("Length of {0} should be between 1 and 1000").format(col.fieldname)) + + current_col = self.current_columns.get(col.fieldname, {}) + if not current_col: + continue + current_type = self.current_columns[col.fieldname]["type"] + current_length = VARCHAR_CAST_PATTERN.findall(current_type) + if not current_length: + # case when the field is no longer a varchar + continue + current_length = current_length[0] + if cint(current_length) != cint(new_length): + try: + # check for truncation + max_length = influxframework.db.sql( + """SELECT MAX(CHAR_LENGTH(`{fieldname}`)) FROM `tab{doctype}`""".format( + fieldname=col.fieldname, doctype=self.doctype + ) + ) + + except influxframework.db.InternalError as e: + if influxframework.db.is_missing_column(e): + # Unknown column 'column_name' in 'field list' + continue + raise + + if max_length and max_length[0][0] and max_length[0][0] > new_length: + if col.fieldname in self.columns: + self.columns[col.fieldname].length = current_length + info_message = _( + "Reverting length to {0} for '{1}' in '{2}'. Setting the length as {3} will cause truncation of data." + ).format(current_length, col.fieldname, self.doctype, new_length) + influxframework.msgprint(info_message) + + def is_new(self): + return self.table_name not in influxframework.db.get_tables() + + def setup_table_columns(self): + # TODO: figure out a way to get key data + for c in influxframework.db.get_table_columns_description(self.table_name): + self.current_columns[c.name.lower()] = c + + def alter(self): + pass + + +class DbColumn: + def __init__( + self, table, fieldname, fieldtype, length, default, set_index, options, unique, precision + ): + self.table = table + self.fieldname = fieldname + self.fieldtype = fieldtype + self.length = length + self.set_index = set_index + self.default = default + self.options = options + self.unique = unique + self.precision = precision + + def get_definition(self, with_default=1): + column_def = get_definition(self.fieldtype, precision=self.precision, length=self.length) + + if not column_def: + return column_def + + if self.fieldtype in ("Check", "Int"): + default_value = cint(self.default) or 0 + column_def += f" not null default {default_value}" + + elif self.fieldtype in ("Currency", "Float", "Percent"): + default_value = flt(self.default) or 0 + column_def += f" not null default {default_value}" + + elif ( + self.default + and (self.default not in influxframework.db.DEFAULT_SHORTCUTS) + and not cstr(self.default).startswith(":") + and column_def not in ("text", "longtext") + ): + column_def += f" default {influxframework.db.escape(self.default)}" + + if self.unique and (column_def not in ("text", "longtext")): + column_def += " unique" + + return column_def + + def build_for_alter_table(self, current_def): + column_type = get_definition(self.fieldtype, self.precision, self.length) + + # no columns + if not column_type: + return + + # to add? + if not current_def: + self.fieldname = validate_column_name(self.fieldname) + self.table.add_column.append(self) + + if column_type not in ("text", "longtext"): + if self.unique: + self.table.add_unique.append(self) + if self.set_index: + self.table.add_index.append(self) + return + + # type + if current_def["type"] != column_type: + self.table.change_type.append(self) + + # unique + if (self.unique and not current_def["unique"]) and column_type not in ("text", "longtext"): + self.table.add_unique.append(self) + elif (current_def["unique"] and not self.unique) and column_type not in ("text", "longtext"): + self.table.drop_unique.append(self) + + # default + if ( + self.default_changed(current_def) + and (self.default not in influxframework.db.DEFAULT_SHORTCUTS) + and not cstr(self.default).startswith(":") + and not (column_type in ["text", "longtext"]) + ): + self.table.set_default.append(self) + + # index should be applied or dropped irrespective of type change + if (current_def["index"] and not self.set_index) and column_type not in ("text", "longtext"): + self.table.drop_index.append(self) + + elif (not current_def["index"] and self.set_index) and not (column_type in ("text", "longtext")): + self.table.add_index.append(self) + + def default_changed(self, current_def): + if "decimal" in current_def["type"]: + return self.default_changed_for_decimal(current_def) + else: + cur_default = current_def["default"] + new_default = self.default + if cur_default == "NULL" or cur_default is None: + cur_default = None + else: + # Strip quotes from default value + # eg. database returns default value as "'System Manager'" + cur_default = cur_default.lstrip("'").rstrip("'") + + fieldtype = self.fieldtype + if fieldtype in ["Int", "Check"]: + cur_default = cint(cur_default) + new_default = cint(new_default) + elif fieldtype in ["Currency", "Float", "Percent"]: + cur_default = flt(cur_default) + new_default = flt(new_default) + return cur_default != new_default + + def default_changed_for_decimal(self, current_def): + try: + if current_def["default"] in ("", None) and self.default in ("", None): + # both none, empty + return False + + elif current_def["default"] in ("", None): + try: + # check if new default value is valid + float(self.default) + return True + except ValueError: + return False + + elif self.default in ("", None): + # new default value is empty + return True + + else: + # NOTE float() raise ValueError when "" or None is passed + return float(current_def["default"]) != float(self.default) + except TypeError: + return True + + +def validate_column_name(n): + if special_characters := SPECIAL_CHAR_PATTERN.findall(n): + special_characters = ", ".join(f'"{c}"' for c in special_characters) + influxframework.throw( + _("Fieldname {0} cannot have special characters like {1}").format( + influxframework.bold(cstr(n)), special_characters + ), + influxframework.db.InvalidColumnName, + ) + return n + + +def validate_column_length(fieldname): + if len(fieldname) > influxframework.db.MAX_COLUMN_LENGTH: + influxframework.throw(_("Fieldname is limited to 64 characters ({0})").format(fieldname)) + + +def get_definition(fieldtype, precision=None, length=None): + d = influxframework.db.type_map.get(fieldtype) + + if not d: + return + + if fieldtype == "Int" and length and length > 11: + # convert int to long int if the length of the int is greater than 11 + d = influxframework.db.type_map.get("Long Int") + + coltype = d[0] + size = d[1] if d[1] else None + + if size: + # This check needs to exist for backward compatibility. + # Till V13, default size used for float, currency and percent are (18, 6). + if fieldtype in ["Float", "Currency", "Percent"] and cint(precision) > 6: + size = "21,9" + + if length: + if coltype == "varchar": + size = length + elif coltype == "int" and length < 11: + # allow setting custom length for int if length provided is less than 11 + # NOTE: this will only be applicable for mariadb as influxframework implements int + # in postgres as bigint (as seen in type_map) + size = length + + if size is not None: + coltype = f"{coltype}({size})" + + return coltype + + +def add_column( + doctype, column_name, fieldtype, precision=None, length=None, default=None, not_null=False +): + if column_name in influxframework.db.get_table_columns(doctype): + # already exists + return + + influxframework.db.commit() + + query = "alter table `tab{}` add column {} {}".format( + doctype, + column_name, + get_definition(fieldtype, precision, length), + ) + + if not_null: + query += " not null" + if default: + query += f" default '{default}'" + + influxframework.db.sql(query) diff --git a/influxframework/database/sequence.py b/influxframework/database/sequence.py new file mode 100644 index 0000000..7e8b5c7 --- /dev/null +++ b/influxframework/database/sequence.py @@ -0,0 +1,84 @@ +from influxframework import db, scrub + + +def create_sequence( + doctype_name: str, + *, + slug: str = "_id_seq", + temporary: bool = False, + check_not_exists: bool = False, + cycle: bool = False, + cache: int = 0, + start_value: int = 0, + increment_by: int = 0, + min_value: int = 0, + max_value: int = 0, +) -> str: + + query = "create sequence" if not temporary else "create temporary sequence" + sequence_name = scrub(doctype_name + slug) + + if check_not_exists: + query += " if not exists" + + query += f" {sequence_name}" + + if increment_by: + # default is 1 + query += f" increment by {increment_by}" + + if min_value: + # default is 1 + query += f" minvalue {min_value}" + + if max_value: + query += f" maxvalue {max_value}" + + if start_value: + # default is 1 + query += f" start {start_value}" + + # in postgres, the default is cache 1 / no cache + if cache: + query += f" cache {cache}" + elif db.db_type == "mariadb": + query += " nocache" + + if not cycle: + # in postgres, default is no cycle + if db.db_type == "mariadb": + query += " nocycle" + else: + query += " cycle" + + db.sql_ddl(query) + + return sequence_name + + +def get_next_val(doctype_name: str, slug: str = "_id_seq") -> int: + sequence_name = scrub(f"{doctype_name}{slug}") + + if db.db_type == "postgres": + sequence_name = f"'\"{sequence_name}\"'" + elif db.db_type == "mariadb": + sequence_name = f"`{sequence_name}`" + + try: + return db.sql(f"SELECT nextval({sequence_name})")[0][0] + except IndexError: + raise db.SequenceGeneratorLimitExceeded + + +def set_next_val( + doctype_name: str, next_val: int, *, slug: str = "_id_seq", is_val_used: bool = False +) -> None: + + is_val_used = "false" if not is_val_used else "true" + + db.multisql( + { + "postgres": f"SELECT SETVAL('\"{scrub(doctype_name + slug)}\"', {next_val}, {is_val_used})", + "mariadb": f"SELECT SETVAL(`{scrub(doctype_name + slug)}`, {next_val}, {is_val_used})", + } + ) diff --git a/influxframework/database/utils.py b/influxframework/database/utils.py new file mode 100644 index 0000000..9e568bb --- /dev/null +++ b/influxframework/database/utils.py @@ -0,0 +1,54 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE + +from functools import cached_property +from types import NoneType + +import influxframework +from influxframework.query_builder.builder import MariaDB, Postgres + +Query = str | MariaDB | Postgres +QueryValues = tuple | list | dict | NoneType + +EmptyQueryValues = object() +FallBackDateTimeStr = "0001-01-01 00:00:00.000000" + + +def is_query_type(query: str, query_type: str | tuple[str]) -> bool: + return query.lstrip().split(maxsplit=1)[0].lower().startswith(query_type) + + +class LazyString: + def _setup(self) -> None: + raise NotImplementedError + + @cached_property + def value(self) -> str: + return self._setup() + + def __str__(self) -> str: + return self.value + + def __repr__(self) -> str: + return f"'{self.value}'" + + +class LazyDecode(LazyString): + __slots__ = () + + def __init__(self, value: str) -> None: + self._value = value + + def _setup(self) -> None: + return self._value.decode() + + +class LazyMogrify(LazyString): + __slots__ = () + + def __init__(self, query, values) -> None: + self.query = query + self.values = values + + def _setup(self) -> str: + return influxframework.db.mogrify(self.query, self.values) diff --git a/influxframework/defaults.py b/influxframework/defaults.py new file mode 100644 index 0000000..84f1b0a --- /dev/null +++ b/influxframework/defaults.py @@ -0,0 +1,244 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.cache_manager import clear_defaults_cache, common_default_keys +from influxframework.desk.notifications import clear_notifications +from influxframework.query_builder import DocType + +# Note: DefaultValue records are identified by parent (e.g. __default, __global) + + +def set_user_default(key, value, user=None, parenttype=None): + set_default(key, value, user or influxframework.session.user, parenttype) + + +def add_user_default(key, value, user=None, parenttype=None): + add_default(key, value, user or influxframework.session.user, parenttype) + + +def get_user_default(key, user=None): + user_defaults = get_defaults(user or influxframework.session.user) + d = user_defaults.get(key, None) + + if is_a_user_permission_key(key): + if d and isinstance(d, (list, tuple)) and len(d) == 1: + # Use User Permission value when only when it has a single value + d = d[0] + + else: + d = user_defaults.get(influxframework.scrub(key), None) + + value = isinstance(d, (list, tuple)) and d[0] or d + if not_in_user_permission(key, value, user): + return + + return value + + +def get_user_default_as_list(key, user=None): + user_defaults = get_defaults(user or influxframework.session.user) + d = user_defaults.get(key, None) + + if is_a_user_permission_key(key): + if d and isinstance(d, (list, tuple)) and len(d) == 1: + # Use User Permission value when only when it has a single value + d = [d[0]] + + else: + d = user_defaults.get(influxframework.scrub(key), None) + + d = list(filter(None, (not isinstance(d, (list, tuple))) and [d] or d)) + + # filter default values if not found in user permission + values = [value for value in d if not not_in_user_permission(key, value)] + + return values + + +def is_a_user_permission_key(key): + return ":" not in key and key != influxframework.scrub(key) + + +def not_in_user_permission(key, value, user=None): + # returns true or false based on if value exist in user permission + user = user or influxframework.session.user + user_permission = get_user_permissions(user).get(influxframework.unscrub(key)) or [] + + for perm in user_permission: + # doc found in user permission + if perm.get("doc") == value: + return False + + # return true only if user_permission exists + return True if user_permission else False + + +def get_user_permissions(user=None): + from influxframework.core.doctype.user_permission.user_permission import ( + get_user_permissions as _get_user_permissions, + ) + + """Return influxframework.core.doctype.user_permissions.user_permissions._get_user_permissions (kept for backward compatibility)""" + return _get_user_permissions(user) + + +def get_defaults(user=None): + global_defaults = get_defaults_for() + + if not user: + user = influxframework.session.user if influxframework.session else "Guest" + + if not user: + return global_defaults + + defaults = global_defaults.copy() + defaults.update(get_defaults_for(user)) + defaults.update(user=user, owner=user) + + return defaults + + +def clear_user_default(key, user=None): + clear_default(key, parent=user or influxframework.session.user) + + +# Global + + +def set_global_default(key, value): + set_default(key, value, "__default") + + +def add_global_default(key, value): + add_default(key, value, "__default") + + +def get_global_default(key): + d = get_defaults().get(key, None) + + value = isinstance(d, (list, tuple)) and d[0] or d + if not_in_user_permission(key, value): + return + + return value + + +# Common + + +def set_default(key, value, parent, parenttype="__default"): + """Override or add a default value. + Adds default value in table `tabDefaultValue`. + + :param key: Default key. + :param value: Default value. + :param parent: Usually, **User** to whom the default belongs. + :param parenttype: [optional] default is `__default`.""" + table = DocType("DefaultValue") + key_exists = ( + influxframework.qb.from_(table) + .where((table.defkey == key) & (table.parent == parent)) + .select(table.defkey) + .for_update() + .run() + ) + if key_exists: + influxframework.db.delete("DefaultValue", {"defkey": key, "parent": parent}) + if value is not None: + add_default(key, value, parent) + else: + _clear_cache(parent) + + +def add_default(key, value, parent, parenttype=None): + d = influxframework.get_doc( + { + "doctype": "DefaultValue", + "parent": parent, + "parenttype": parenttype or "__default", + "parentfield": "system_defaults", + "defkey": key, + "defvalue": value, + } + ) + d.insert(ignore_permissions=True) + _clear_cache(parent) + + +def clear_default(key=None, value=None, parent=None, name=None, parenttype=None): + """Clear a default value by any of the given parameters and delete caches. + + :param key: Default key. + :param value: Default value. + :param parent: User name, or `__global`, `__default`. + :param name: Default ID. + :param parenttype: Clear defaults table for a particular type e.g. **User**. + """ + filters = {} + + if name: + filters.update({"name": name}) + + else: + if key: + filters.update({"defkey": key}) + + if value: + filters.update({"defvalue": value}) + + if parent: + filters.update({"parent": parent}) + + if parenttype: + filters.update({"parenttype": parenttype}) + + if parent: + clear_defaults_cache(parent) + else: + clear_defaults_cache("__default") + clear_defaults_cache("__global") + + if not filters: + raise Exception("[clear_default] No key specified.") + + influxframework.db.delete("DefaultValue", filters) + + _clear_cache(parent) + + +def get_defaults_for(parent="__default"): + """get all defaults""" + defaults = influxframework.cache().hget("defaults", parent) + + if defaults is None: + # sort descending because first default must get precedence + table = DocType("DefaultValue") + res = ( + influxframework.qb.from_(table) + .where(table.parent == parent) + .select(table.defkey, table.defvalue) + .orderby("creation") + .run(as_dict=True) + ) + + defaults = influxframework._dict() + for d in res: + if d.defkey in defaults: + # listify + if not isinstance(defaults[d.defkey], list) and defaults[d.defkey] != d.defvalue: + defaults[d.defkey] = [defaults[d.defkey]] + + if d.defvalue not in defaults[d.defkey]: + defaults[d.defkey].append(d.defvalue) + + elif d.defvalue is not None: + defaults[d.defkey] = d.defvalue + + influxframework.cache().hset("defaults", parent, defaults) + + return defaults + + +def _clear_cache(parent): + influxframework.clear_cache(user=parent if parent not in common_default_keys else None) diff --git a/influxframework/deferred_insert.py b/influxframework/deferred_insert.py new file mode 100644 index 0000000..70288d8 --- /dev/null +++ b/influxframework/deferred_insert.py @@ -0,0 +1,59 @@ +import json +from typing import TYPE_CHECKING, Union + +import redis + +import influxframework +from influxframework.utils import cstr + +if TYPE_CHECKING: + from influxframework.model.document import Document + +queue_prefix = "insert_queue_for_" + + +def deferred_insert(doctype: str, records: list[Union[dict, "Document"]] | str): + if isinstance(records, (dict, list)): + _records = json.dumps(records) + else: + _records = records + + try: + influxframework.cache().rpush(f"{queue_prefix}{doctype}", _records) + except redis.exceptions.ConnectionError: + for record in records: + insert_record(record, doctype) + + +def save_to_db(): + queue_keys = influxframework.cache().get_keys(queue_prefix) + for key in queue_keys: + record_count = 0 + queue_key = get_key_name(key) + doctype = get_doctype_name(key) + while influxframework.cache().llen(queue_key) > 0 and record_count <= 500: + records = influxframework.cache().lpop(queue_key) + records = json.loads(records.decode("utf-8")) + if isinstance(records, dict): + record_count += 1 + insert_record(records, doctype) + continue + for record in records: + record_count += 1 + insert_record(record, doctype) + + +def insert_record(record: Union[dict, "Document"], doctype: str): + try: + record.update({"doctype": doctype}) + influxframework.get_doc(record).insert() + except Exception as e: + influxframework.logger().error(f"Error while inserting deferred {doctype} record: {e}") + + +def get_key_name(key: str) -> str: + return cstr(key).split("|")[1] + + +def get_doctype_name(key: str) -> str: + return cstr(key).split(queue_prefix)[1] diff --git a/influxframework/desk/__init__.py b/influxframework/desk/__init__.py new file mode 100644 index 0000000..2e11a12 --- /dev/null +++ b/influxframework/desk/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE diff --git a/influxframework/desk/calendar.py b/influxframework/desk/calendar.py new file mode 100644 index 0000000..d11912b --- /dev/null +++ b/influxframework/desk/calendar.py @@ -0,0 +1,57 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +import influxframework +from influxframework import _ + + +@influxframework.whitelist() +def update_event(args, field_map): + """Updates Event (called via calendar) based on passed `field_map`""" + args = influxframework._dict(json.loads(args)) + field_map = influxframework._dict(json.loads(field_map)) + w = influxframework.get_doc(args.doctype, args.name) + w.set(field_map.start, args[field_map.start]) + w.set(field_map.end, args.get(field_map.end)) + w.save() + + +def get_event_conditions(doctype, filters=None): + """Returns SQL conditions with user permissions and filters for event queries""" + from influxframework.desk.reportview import get_filters_cond + + if not influxframework.has_permission(doctype): + influxframework.throw(_("Not Permitted"), influxframework.PermissionError) + + return get_filters_cond(doctype, filters, [], with_match_conditions=True) + + +@influxframework.whitelist() +def get_events(doctype, start, end, field_map, filters=None, fields=None): + field_map = influxframework._dict(json.loads(field_map)) + fields = influxframework.parse_json(fields) + + doc_meta = influxframework.get_meta(doctype) + for d in doc_meta.fields: + if d.fieldtype == "Color": + field_map.update({"color": d.fieldname}) + + filters = json.loads(filters) if filters else [] + + if not fields: + fields = [field_map.start, field_map.end, field_map.title, "name"] + + if field_map.color: + fields.append(field_map.color) + + start_date = "ifnull(%s, '0001-01-01 00:00:00')" % field_map.start + end_date = "ifnull(%s, '2199-12-31 00:00:00')" % field_map.end + + filters += [ + [doctype, start_date, "<=", end], + [doctype, end_date, ">=", start], + ] + fields = list({field for field in fields if field}) + return influxframework.get_list(doctype, fields=fields, filters=filters) diff --git a/influxframework/desk/desk_page.py b/influxframework/desk/desk_page.py new file mode 100644 index 0000000..8d16b8b --- /dev/null +++ b/influxframework/desk/desk_page.py @@ -0,0 +1,58 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.translate import send_translations + + +@influxframework.whitelist() +def get(name): + """ + Return the :term:`doclist` of the `Page` specified by `name` + """ + page = influxframework.get_doc("Page", name) + if page.is_permitted(): + page.load_assets() + docs = influxframework._dict(page.as_dict()) + if getattr(page, "_dynamic_page", None): + docs["_dynamic_page"] = 1 + + return docs + else: + influxframework.response["403"] = 1 + raise influxframework.PermissionError("No read permission for Page %s" % (page.title or name)) + + +@influxframework.whitelist(allow_guest=True) +def getpage(): + """ + Load the page from `influxframework.form` and send it via `influxframework.response` + """ + page = influxframework.form_dict.get("name") + doc = get(page) + + # load translations + if influxframework.lang != "en": + send_translations(influxframework.get_lang_dict("page", page)) + + influxframework.response.docs.append(doc) + + +def has_permission(page): + if influxframework.session.user == "Administrator" or "System Manager" in influxframework.get_roles(): + return True + + page_roles = [d.role for d in page.get("roles")] + if page_roles: + if influxframework.session.user == "Guest" and "Guest" not in page_roles: + return False + elif not set(page_roles).intersection(set(influxframework.get_roles())): + # check if roles match + return False + + if not influxframework.has_permission("Page", ptype="read", doc=page): + # check if there are any user_permissions + return False + else: + # hack for home pages! if no Has Roles, allow everyone to see! + return True diff --git a/influxframework/desk/desktop.py b/influxframework/desk/desktop.py new file mode 100644 index 0000000..2c031d1 --- /dev/null +++ b/influxframework/desk/desktop.py @@ -0,0 +1,591 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE +# Author - Shivam Mishra + +from functools import wraps +from json import dumps, loads + +import influxframework +from influxframework import DoesNotExistError, ValidationError, _, _dict +from influxframework.boot import get_allowed_pages, get_allowed_reports +from influxframework.cache_manager import ( + build_domain_restriced_doctype_cache, + build_domain_restriced_page_cache, + build_table_count_cache, +) +from influxframework.core.doctype.custom_role.custom_role import get_custom_allowed_roles + + +def handle_not_exist(fn): + @wraps(fn) + def wrapper(*args, **kwargs): + try: + return fn(*args, **kwargs) + except DoesNotExistError: + if influxframework.message_log: + influxframework.message_log.pop() + return [] + + return wrapper + + +class Workspace: + def __init__(self, page, minimal=False): + self.page_name = page.get("name") + self.page_title = page.get("title") + self.public_page = page.get("public") + self.workspace_manager = "Workspace Manager" in influxframework.get_roles() + + self.user = influxframework.get_user() + self.allowed_modules = self.get_cached("user_allowed_modules", self.get_allowed_modules) + + self.doc = influxframework.get_cached_doc("Workspace", self.page_name) + if ( + self.doc + and self.doc.module + and self.doc.module not in self.allowed_modules + and not self.workspace_manager + ): + raise influxframework.PermissionError + + self.can_read = self.get_cached("user_perm_can_read", self.get_can_read_items) + + self.allowed_pages = get_allowed_pages(cache=True) + self.allowed_reports = get_allowed_reports(cache=True) + + if not minimal: + if self.doc.content: + self.onboarding_list = [ + x["data"]["onboarding_name"] for x in loads(self.doc.content) if x["type"] == "onboarding" + ] + self.onboardings = [] + + self.table_counts = get_table_with_counts() + self.restricted_doctypes = ( + influxframework.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache() + ) + self.restricted_pages = ( + influxframework.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache() + ) + + def is_permitted(self): + """Returns true if Has Role is not set or the user is allowed.""" + from influxframework.utils import has_common + + allowed = [ + d.role for d in influxframework.get_all("Has Role", fields=["role"], filters={"parent": self.doc.name}) + ] + + custom_roles = get_custom_allowed_roles("page", self.doc.name) + allowed.extend(custom_roles) + + if not allowed: + return True + + roles = influxframework.get_roles() + + if has_common(roles, allowed): + return True + + def get_cached(self, cache_key, fallback_fn): + _cache = influxframework.cache() + + value = _cache.get_value(cache_key, user=influxframework.session.user) + if value: + return value + + value = fallback_fn() + + # Expire every six hour + _cache.set_value(cache_key, value, influxframework.session.user, 21600) + return value + + def get_can_read_items(self): + if not self.user.can_read: + self.user.build_permissions() + + return self.user.can_read + + def get_allowed_modules(self): + if not self.user.allow_modules: + self.user.build_permissions() + + return self.user.allow_modules + + def get_onboarding_doc(self, onboarding): + # Check if onboarding is enabled + if not influxframework.get_system_settings("enable_onboarding"): + return None + + if not self.onboarding_list: + return None + + if influxframework.db.get_value("Module Onboarding", onboarding, "is_complete"): + return None + + doc = influxframework.get_doc("Module Onboarding", onboarding) + + # Check if user is allowed + allowed_roles = set(doc.get_allowed_roles()) + user_roles = set(influxframework.get_roles()) + if not allowed_roles & user_roles: + return None + + # Check if already complete + if doc.check_completion(): + return None + + return doc + + def is_item_allowed(self, name, item_type): + if influxframework.session.user == "Administrator": + return True + + item_type = item_type.lower() + + if item_type == "doctype": + return name in self.can_read or [] and name in self.restricted_doctypes or [] + if item_type == "page": + return name in self.allowed_pages and name in self.restricted_pages + if item_type == "report": + return name in self.allowed_reports + if item_type == "help": + return True + if item_type == "dashboard": + return True + + return False + + def build_workspace(self): + self.cards = {"items": self.get_links()} + + self.charts = {"items": self.get_charts()} + + self.shortcuts = {"items": self.get_shortcuts()} + + self.onboardings = {"items": self.get_onboardings()} + + self.quick_lists = {"items": self.get_quick_lists()} + + def _doctype_contains_a_record(self, name): + exists = self.table_counts.get(name, False) + + if not exists and influxframework.db.exists(name): + if not influxframework.db.get_value("DocType", name, "issingle"): + exists = bool(influxframework.get_all(name, limit=1)) + else: + exists = True + self.table_counts[name] = exists + + return exists + + def _prepare_item(self, item): + if item.dependencies: + + dependencies = [dep.strip() for dep in item.dependencies.split(",")] + + incomplete_dependencies = [d for d in dependencies if not self._doctype_contains_a_record(d)] + + if len(incomplete_dependencies): + item.incomplete_dependencies = incomplete_dependencies + else: + item.incomplete_dependencies = "" + + if item.onboard: + # Mark Spotlights for initial + if item.get("type") == "doctype": + name = item.get("name") + count = self._doctype_contains_a_record(name) + + item["count"] = count + + # Translate label + item["label"] = _(item.label) if item.label else _(item.name) + + return item + + @handle_not_exist + def get_links(self): + cards = self.doc.get_link_groups() + + if not self.doc.hide_custom: + cards = cards + get_custom_reports_and_doctypes(self.doc.module) + + default_country = influxframework.db.get_default("country") + + new_data = [] + for card in cards: + new_items = [] + card = _dict(card) + + links = card.get("links", []) + + for item in links: + item = _dict(item) + + # Condition: based on country + if item.country and item.country != default_country: + continue + + # Check if user is allowed to view + if self.is_item_allowed(item.link_to, item.link_type): + prepared_item = self._prepare_item(item) + new_items.append(prepared_item) + + if new_items: + if isinstance(card, _dict): + new_card = card.copy() + else: + new_card = card.as_dict().copy() + new_card["links"] = new_items + new_card["label"] = _(new_card["label"]) + new_data.append(new_card) + + return new_data + + @handle_not_exist + def get_charts(self): + all_charts = [] + if influxframework.has_permission("Dashboard Chart", throw=False): + charts = self.doc.charts + + for chart in charts: + if influxframework.has_permission("Dashboard Chart", doc=chart.chart_name): + # Translate label + chart.label = _(chart.label) if chart.label else _(chart.chart_name) + all_charts.append(chart) + + return all_charts + + @handle_not_exist + def get_shortcuts(self): + def _in_active_domains(item): + if not item.restrict_to_domain: + return True + else: + return item.restrict_to_domain in influxframework.get_active_domains() + + items = [] + shortcuts = self.doc.shortcuts + + for item in shortcuts: + new_item = item.as_dict().copy() + if self.is_item_allowed(item.link_to, item.type) and _in_active_domains(item): + if item.type == "Report": + report = self.allowed_reports.get(item.link_to, {}) + if report.get("report_type") in ["Query Report", "Script Report", "Custom Report"]: + new_item["is_query_report"] = 1 + else: + new_item["ref_doctype"] = report.get("ref_doctype") + + # Translate label + new_item["label"] = _(item.label) if item.label else _(item.link_to) + + items.append(new_item) + + return items + + @handle_not_exist + def get_quick_lists(self): + items = [] + quick_lists = self.doc.quick_lists + + for item in quick_lists: + if self.is_item_allowed(item.document_type, "doctype"): + new_item = item.as_dict().copy() + + # Translate label + new_item["label"] = _(item.label) if item.label else _(item.document_type) + + items.append(new_item) + + return items + + @handle_not_exist + def get_onboardings(self): + if self.onboarding_list: + for onboarding in self.onboarding_list: + onboarding_doc = self.get_onboarding_doc(onboarding) + if onboarding_doc: + item = { + "label": _(onboarding), + "title": _(onboarding_doc.title), + "subtitle": _(onboarding_doc.subtitle), + "success": _(onboarding_doc.success_message), + "docs_url": onboarding_doc.documentation_url, + "items": self.get_onboarding_steps(onboarding_doc), + } + self.onboardings.append(item) + return self.onboardings + + @handle_not_exist + def get_onboarding_steps(self, onboarding_doc): + steps = [] + for doc in onboarding_doc.get_steps(): + step = doc.as_dict().copy() + step.label = _(doc.title) + if step.action == "Create Entry": + step.is_submittable = influxframework.db.get_value( + "DocType", step.reference_document, "is_submittable", cache=True + ) + steps.append(step) + + return steps + + +@influxframework.whitelist() +@influxframework.read_only() +def get_desktop_page(page): + """Applies permissions, customizations and returns the configruration for a page + on desk. + + Args: + page (json): page data + + Returns: + dict: dictionary of cards, charts and shortcuts to be displayed on website + """ + try: + workspace = Workspace(loads(page)) + workspace.build_workspace() + return { + "charts": workspace.charts, + "shortcuts": workspace.shortcuts, + "cards": workspace.cards, + "onboardings": workspace.onboardings, + "quick_lists": workspace.quick_lists, + } + except DoesNotExistError: + influxframework.log_error("Workspace Missing") + return {} + + +@influxframework.whitelist() +def get_workspace_sidebar_items(): + """Get list of sidebar items for desk""" + has_access = "Workspace Manager" in influxframework.get_roles() + + # don't get domain restricted pages + blocked_modules = influxframework.get_doc("User", influxframework.session.user).get_blocked_modules() + blocked_modules.append("Dummy Module") + + filters = { + "restrict_to_domain": ["in", influxframework.get_active_domains()], + "module": ["not in", blocked_modules], + } + + if has_access: + filters = [] + + # pages sorted based on sequence id + order_by = "sequence_id asc" + fields = ["name", "title", "for_user", "parent_page", "content", "public", "module", "icon"] + all_pages = influxframework.get_all( + "Workspace", fields=fields, filters=filters, order_by=order_by, ignore_permissions=True + ) + pages = [] + private_pages = [] + + # Filter Page based on Permission + for page in all_pages: + try: + workspace = Workspace(page, True) + if has_access or workspace.is_permitted(): + if page.public: + pages.append(page) + elif page.for_user == influxframework.session.user: + private_pages.append(page) + page["label"] = _(page.get("name")) + except influxframework.PermissionError: + pass + if private_pages: + pages.extend(private_pages) + + return {"pages": pages, "has_access": has_access} + + +def get_table_with_counts(): + counts = influxframework.cache().get_value("information_schema:counts") + if not counts: + counts = build_table_count_cache() + + return counts + + +def get_custom_reports_and_doctypes(module): + return [ + _dict({"label": _("Custom Documents"), "links": get_custom_doctype_list(module)}), + _dict({"label": _("Custom Reports"), "links": get_custom_report_list(module)}), + ] + + +def get_custom_doctype_list(module): + doctypes = influxframework.get_all( + "DocType", + fields=["name"], + filters={"custom": 1, "istable": 0, "module": module}, + order_by="name", + ) + + out = [] + for d in doctypes: + out.append({"type": "Link", "link_type": "doctype", "link_to": d.name, "label": _(d.name)}) + + return out + + +def get_custom_report_list(module): + """Returns list on new style reports for modules.""" + reports = influxframework.get_all( + "Report", + fields=["name", "ref_doctype", "report_type"], + filters={"is_standard": "No", "disabled": 0, "module": module}, + order_by="name", + ) + + out = [] + for r in reports: + out.append( + { + "type": "Link", + "link_type": "report", + "doctype": r.ref_doctype, + "dependencies": r.ref_doctype, + "is_query_report": 1 + if r.report_type in ("Query Report", "Script Report", "Custom Report") + else 0, + "label": _(r.name), + "link_to": r.name, + } + ) + + return out + + +def save_new_widget(doc, page, blocks, new_widgets): + if loads(new_widgets): + widgets = _dict(loads(new_widgets)) + + if widgets.chart: + doc.charts.extend(new_widget(widgets.chart, "Workspace Chart", "charts")) + if widgets.shortcut: + doc.shortcuts.extend(new_widget(widgets.shortcut, "Workspace Shortcut", "shortcuts")) + if widgets.quick_list: + doc.quick_lists.extend(new_widget(widgets.quick_list, "Workspace Quick List", "quick_lists")) + if widgets.card: + doc.build_links_table_from_card(widgets.card) + + # remove duplicate and unwanted widgets + clean_up(doc, blocks) + + try: + doc.save(ignore_permissions=True) + except (ValidationError, TypeError) as e: + # Create a json string to log + json_config = widgets and dumps(widgets, sort_keys=True, indent=4) + + # Error log body + log = """ + page: {} + config: {} + exception: {} + """.format( + page, json_config, e + ) + doc.log_error("Could not save customization", log) + return False + + return True + + +def clean_up(original_page, blocks): + page_widgets = {} + + for wid in ["shortcut", "card", "chart", "quick_list"]: + # get list of widget's name from blocks + page_widgets[wid] = [x["data"][wid + "_name"] for x in loads(blocks) if x["type"] == wid] + + # shortcut, chart & quick_list cleanup + for wid in ["shortcut", "chart", "quick_list"]: + updated_widgets = [] + original_page.get(wid + "s").reverse() + + for w in original_page.get(wid + "s"): + if w.label in page_widgets[wid] and w.label not in [x.label for x in updated_widgets]: + updated_widgets.append(w) + original_page.set(wid + "s", updated_widgets) + + # card cleanup + for i, v in enumerate(original_page.links): + if v.type == "Card Break" and v.label not in page_widgets["card"]: + del original_page.links[i : i + v.link_count + 1] + + +def new_widget(config, doctype, parentfield): + if not config: + return [] + prepare_widget_list = [] + for idx, widget in enumerate(config): + # Some cleanup + widget.pop("name", None) + + # New Doc + doc = influxframework.new_doc(doctype) + doc.update(widget) + + # Manually Set IDX + doc.idx = idx + 1 + + # Set Parent Field + doc.parentfield = parentfield + + prepare_widget_list.append(doc) + return prepare_widget_list + + +def prepare_widget(config, doctype, parentfield): + """Create widget child table entries with parent details + + Args: + config (dict): Dictionary containing widget config + doctype (string): Doctype name of the child table + parentfield (string): Parent field for the child table + + Returns: + TYPE: List of Document objects + """ + if not config: + return [] + order = config.get("order") + widgets = config.get("widgets") + prepare_widget_list = [] + for idx, name in enumerate(order): + wid_config = widgets[name].copy() + # Some cleanup + wid_config.pop("name", None) + + # New Doc + doc = influxframework.new_doc(doctype) + doc.update(wid_config) + + # Manually Set IDX + doc.idx = idx + 1 + + # Set Parent Field + doc.parentfield = parentfield + + prepare_widget_list.append(doc) + return prepare_widget_list + + +@influxframework.whitelist() +def update_onboarding_step(name, field, value): + """Update status of onboaridng step + + Args: + name (string): Name of the doc + field (string): field to be updated + value: Value to be updated + + """ + influxframework.db.set_value("Onboarding Step", name, field, value) diff --git a/influxframework/desk/doctype/__init__.py b/influxframework/desk/doctype/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/bulk_update/__init__.py b/influxframework/desk/doctype/bulk_update/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/bulk_update/bulk_update.js b/influxframework/desk/doctype/bulk_update/bulk_update.js new file mode 100644 index 0000000..07d0646 --- /dev/null +++ b/influxframework/desk/doctype/bulk_update/bulk_update.js @@ -0,0 +1,69 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Bulk Update", { + refresh: function (frm) { + frm.set_query("document_type", function () { + return { + filters: [ + ["DocType", "issingle", "=", 0], + ["DocType", "name", "not in", influxframework.model.core_doctypes_list], + ], + }; + }); + + frm.page.set_primary_action(__("Update"), function () { + if (!frm.doc.update_value) { + influxframework.throw(__('Field "value" is mandatory. Please specify value to be updated')); + } else { + influxframework + .call({ + method: "influxframework.desk.doctype.bulk_update.bulk_update.update", + args: { + doctype: frm.doc.document_type, + field: frm.doc.field, + value: frm.doc.update_value, + condition: frm.doc.condition, + limit: frm.doc.limit, + }, + }) + .then((r) => { + let failed = r.message; + if (!failed) failed = []; + + if (failed.length && !r._server_messages) { + influxframework.throw( + __("Cannot update {0}", [ + failed.map((f) => (f.bold ? f.bold() : f)).join(", "), + ]) + ); + } else { + influxframework.msgprint({ + title: __("Success"), + message: __("Updated Successfully"), + indicator: "green", + }); + } + + influxframework.hide_progress(); + frm.save(); + }); + } + }); + }, + + document_type: function (frm) { + // set field options + if (!frm.doc.document_type) return; + + influxframework.model.with_doctype(frm.doc.document_type, function () { + var options = $.map(influxframework.get_meta(frm.doc.document_type).fields, function (d) { + if (d.fieldname && influxframework.model.no_value_type.indexOf(d.fieldtype) === -1) { + return d.fieldname; + } + return null; + }); + frm.set_df_property("field", "options", options); + }); + }, +}); diff --git a/influxframework/desk/doctype/bulk_update/bulk_update.json b/influxframework/desk/doctype/bulk_update/bulk_update.json new file mode 100644 index 0000000..0ec29a0 --- /dev/null +++ b/influxframework/desk/doctype/bulk_update/bulk_update.json @@ -0,0 +1,204 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2016-07-15 05:51:29.224123", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "document_type", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Document Type", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "field", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Field", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "update_value", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Update Value", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 1, + "collapsible": 0, + "columns": 0, + "description": "SQL Conditions. Example: status=\"Open\"", + "fieldname": "condition", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Condition", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 1, + "collapsible": 0, + "columns": 0, + "default": "500", + "description": "Max 500 records at a time", + "fieldname": "limit", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Limit", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + + "is_submittable": 0, + "issingle": 1, + "istable": 0, + "max_attachments": 0, + "modified": "2016-12-29 14:40:31.929701", + "modified_by": "Administrator", + "module": "Desk", + "name": "Bulk Update", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, + "is_custom": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 0, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/bulk_update/bulk_update.py b/influxframework/desk/doctype/bulk_update/bulk_update.py new file mode 100644 index 0000000..a898228 --- /dev/null +++ b/influxframework/desk/doctype/bulk_update/bulk_update.py @@ -0,0 +1,71 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework import _ +from influxframework.model.document import Document +from influxframework.utils import cint + + +class BulkUpdate(Document): + pass + + +@influxframework.whitelist() +def update(doctype, field, value, condition="", limit=500): + if not limit or cint(limit) > 500: + limit = 500 + + if condition: + condition = " where " + condition + + if ";" in condition: + influxframework.throw(_("; not allowed in condition")) + + docnames = influxframework.db.sql_list( + f"""select name from `tab{doctype}`{condition} limit {limit} offset 0""" + ) + data = {} + data[field] = value + return submit_cancel_or_update_docs(doctype, docnames, "update", data) + + +@influxframework.whitelist() +def submit_cancel_or_update_docs(doctype, docnames, action="submit", data=None): + docnames = influxframework.parse_json(docnames) + + if data: + data = influxframework.parse_json(data) + + failed = [] + + for i, d in enumerate(docnames, 1): + doc = influxframework.get_doc(doctype, d) + try: + message = "" + if action == "submit" and doc.docstatus.is_draft(): + doc.submit() + message = _("Submitting {0}").format(doctype) + elif action == "cancel" and doc.docstatus.is_submitted(): + doc.cancel() + message = _("Cancelling {0}").format(doctype) + elif action == "update" and not doc.docstatus.is_cancelled(): + doc.update(data) + doc.save() + message = _("Updating {0}").format(doctype) + else: + failed.append(d) + influxframework.db.commit() + show_progress(docnames, message, i, d) + + except Exception: + failed.append(d) + influxframework.db.rollback() + + return failed + + +def show_progress(docnames, message, i, description): + n = len(docnames) + if n >= 10: + influxframework.publish_progress(float(i) * 100 / n, title=message, description=description) diff --git a/influxframework/desk/doctype/calendar_view/__init__.py b/influxframework/desk/doctype/calendar_view/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/calendar_view/calendar_view.js b/influxframework/desk/doctype/calendar_view/calendar_view.js new file mode 100644 index 0000000..a4b56a3 --- /dev/null +++ b/influxframework/desk/doctype/calendar_view/calendar_view.js @@ -0,0 +1,36 @@ +// Copyright (c) 2017, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Calendar View", { + onload: function (frm) { + frm.trigger("reference_doctype"); + }, + refresh: function (frm) { + if (!frm.is_new()) { + frm.add_custom_button(__("Show Calendar"), () => + influxframework.set_route("List", frm.doc.reference_doctype, "Calendar", frm.doc.name) + ); + } + }, + reference_doctype: function (frm) { + const { reference_doctype } = frm.doc; + if (!reference_doctype) return; + + influxframework.model.with_doctype(reference_doctype, () => { + const meta = influxframework.get_meta(reference_doctype); + + const subject_options = meta.fields + .filter((df) => !influxframework.model.no_value_type.includes(df.fieldtype)) + .map((df) => df.fieldname); + + const date_options = meta.fields + .filter((df) => ["Date", "Datetime"].includes(df.fieldtype)) + .map((df) => df.fieldname); + + frm.set_df_property("subject_field", "options", subject_options); + frm.set_df_property("start_date_field", "options", date_options); + frm.set_df_property("end_date_field", "options", date_options); + frm.refresh(); + }); + }, +}); diff --git a/influxframework/desk/doctype/calendar_view/calendar_view.json b/influxframework/desk/doctype/calendar_view/calendar_view.json new file mode 100644 index 0000000..8ef49e3 --- /dev/null +++ b/influxframework/desk/doctype/calendar_view/calendar_view.json @@ -0,0 +1,81 @@ +{ + "actions": [], + "autoname": "Prompt", + "creation": "2017-10-23 13:02:10.295824", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "reference_doctype", + "subject_field", + "start_date_field", + "end_date_field", + "column_break_5", + "all_day" + ], + "fields": [ + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Reference Document Type", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "subject_field", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Subject Field", + "reqd": 1 + }, + { + "fieldname": "start_date_field", + "fieldtype": "Select", + "label": "Start Date Field", + "reqd": 1 + }, + { + "fieldname": "end_date_field", + "fieldtype": "Select", + "label": "End Date Field", + "reqd": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "all_day", + "fieldtype": "Check", + "label": "All Day" + } + ], + "links": [], + "modified": "2020-09-18 17:26:09.703215", + "modified_by": "Administrator", + "module": "Desk", + "name": "Calendar View", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "read": 1, + "role": "All" + } + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/influxframework/desk/doctype/calendar_view/calendar_view.py b/influxframework/desk/doctype/calendar_view/calendar_view.py new file mode 100644 index 0000000..3ee107c --- /dev/null +++ b/influxframework/desk/doctype/calendar_view/calendar_view.py @@ -0,0 +1,8 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE + +from influxframework.model.document import Document + + +class CalendarView(Document): + pass diff --git a/influxframework/desk/doctype/console_log/__init__.py b/influxframework/desk/doctype/console_log/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/console_log/console_log.js b/influxframework/desk/doctype/console_log/console_log.js new file mode 100644 index 0000000..c943a3a --- /dev/null +++ b/influxframework/desk/doctype/console_log/console_log.js @@ -0,0 +1,7 @@ +// Copyright (c) 2020, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Console Log", { + // refresh: function(frm) { + // } +}); diff --git a/influxframework/desk/doctype/console_log/console_log.json b/influxframework/desk/doctype/console_log/console_log.json new file mode 100644 index 0000000..a9ae971 --- /dev/null +++ b/influxframework/desk/doctype/console_log/console_log.json @@ -0,0 +1,52 @@ +{ + "actions": [], + "autoname": "format:Log on {timestamp}", + "creation": "2020-08-18 19:56:12.336427", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "script", + "output" + ], + "fields": [ + { + "fieldname": "script", + "fieldtype": "Code", + "in_list_view": 1, + "label": "Script", + "read_only": 1 + }, + { + "fieldname": "output", + "fieldtype": "Code", + "label": "Output", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-08-18 20:07:57.587344", + "modified_by": "Administrator", + "module": "Desk", + "name": "Console Log", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/console_log/console_log.py b/influxframework/desk/doctype/console_log/console_log.py new file mode 100644 index 0000000..ed1cf1e --- /dev/null +++ b/influxframework/desk/doctype/console_log/console_log.py @@ -0,0 +1,9 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class ConsoleLog(Document): + pass diff --git a/influxframework/desk/doctype/console_log/test_console_log.py b/influxframework/desk/doctype/console_log/test_console_log.py new file mode 100644 index 0000000..d6b1919 --- /dev/null +++ b/influxframework/desk/doctype/console_log/test_console_log.py @@ -0,0 +1,8 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestConsoleLog(InfluxFrameworkTestCase): + pass diff --git a/influxframework/desk/doctype/dashboard/__init__.py b/influxframework/desk/doctype/dashboard/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/dashboard/dashboard.js b/influxframework/desk/doctype/dashboard/dashboard.js new file mode 100644 index 0000000..44fa26e --- /dev/null +++ b/influxframework/desk/doctype/dashboard/dashboard.js @@ -0,0 +1,30 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Dashboard", { + refresh: function (frm) { + frm.add_custom_button(__("Show Dashboard"), () => + influxframework.set_route("dashboard-view", frm.doc.name) + ); + + if (!influxframework.boot.developer_mode && frm.doc.is_standard) { + frm.disable_form(); + } + + frm.set_query("chart", "charts", function () { + return { + filters: { + is_public: 1, + }, + }; + }); + + frm.set_query("card", "cards", function () { + return { + filters: { + is_public: 1, + }, + }; + }); + }, +}); diff --git a/influxframework/desk/doctype/dashboard/dashboard.json b/influxframework/desk/doctype/dashboard/dashboard.json new file mode 100644 index 0000000..173146d --- /dev/null +++ b/influxframework/desk/doctype/dashboard/dashboard.json @@ -0,0 +1,115 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:dashboard_name", + "creation": "2019-01-10 12:54:40.938705", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "dashboard_name", + "is_default", + "is_standard", + "module", + "charts", + "chart_options", + "cards" + ], + "fields": [ + { + "fieldname": "dashboard_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Dashboard Name", + "reqd": 1, + "unique": 1 + }, + { + "default": "0", + "fieldname": "is_default", + "fieldtype": "Check", + "label": "Is Default" + }, + { + "fieldname": "charts", + "fieldtype": "Table", + "label": "Charts", + "options": "Dashboard Chart Link", + "reqd": 1 + }, + { + "description": "Set Default Options for all charts on this Dashboard (Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"])", + "fieldname": "chart_options", + "fieldtype": "Code", + "label": "Chart Options", + "options": "JSON" + }, + { + "fieldname": "cards", + "fieldtype": "Table", + "label": "Cards", + "options": "Number Card Link" + }, + { + "default": "0", + "fieldname": "is_standard", + "fieldtype": "Check", + "label": "Is Standard", + "read_only_depends_on": "eval: !influxframework.boot.developer_mode" + }, + { + "depends_on": "eval: doc.is_standard", + "fieldname": "module", + "fieldtype": "Link", + "label": "Module", + "mandatory_depends_on": "eval: doc.is_standard", + "options": "Module Def" + } + ], + "links": [], + "modified": "2020-07-23 11:05:41.890459", + "modified_by": "Administrator", + "module": "Desk", + "name": "Dashboard", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Dashboard Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "dashboard_name", + "track_changes": 1 +} diff --git a/influxframework/desk/doctype/dashboard/dashboard.py b/influxframework/desk/doctype/dashboard/dashboard.py new file mode 100644 index 0000000..57ed92a --- /dev/null +++ b/influxframework/desk/doctype/dashboard/dashboard.py @@ -0,0 +1,132 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +import influxframework +from influxframework import _ +from influxframework.config import get_modules_from_all_apps_for_user +from influxframework.model.document import Document +from influxframework.modules.export_file import export_to_files +from influxframework.query_builder import DocType + + +class Dashboard(Document): + def on_update(self): + if self.is_default: + # make all other dashboards non-default + DashBoard = DocType("Dashboard") + + influxframework.qb.update(DashBoard).set(DashBoard.is_default, 0).where( + DashBoard.name != self.name + ).run() + + if influxframework.conf.developer_mode and self.is_standard: + export_to_files( + record_list=[["Dashboard", self.name, f"{self.module} Dashboard"]], record_module=self.module + ) + + def validate(self): + if not influxframework.conf.developer_mode and self.is_standard: + influxframework.throw(_("Cannot edit Standard Dashboards")) + + if self.is_standard: + non_standard_docs_map = { + "Dashboard Chart": get_non_standard_charts_in_dashboard(self), + "Number Card": get_non_standard_cards_in_dashboard(self), + } + + if non_standard_docs_map["Dashboard Chart"] or non_standard_docs_map["Number Card"]: + message = get_non_standard_warning_message(non_standard_docs_map) + influxframework.throw(message, title=_("Standard Not Set"), is_minimizable=True) + + self.validate_custom_options() + + def validate_custom_options(self): + if self.chart_options: + try: + json.loads(self.chart_options) + except ValueError as error: + influxframework.throw(_("Invalid json added in the custom options: {0}").format(error)) + + +def get_permission_query_conditions(user): + if not user: + user = influxframework.session.user + + if user == "Administrator": + return + + roles = influxframework.get_roles(user) + if "System Manager" in roles: + return None + + allowed_modules = [ + influxframework.db.escape(module.get("module_name")) for module in get_modules_from_all_apps_for_user() + ] + module_condition = ( + "`tabDashboard`.`module` in ({allowed_modules}) or `tabDashboard`.`module` is NULL".format( + allowed_modules=",".join(allowed_modules) + ) + ) + + return module_condition + + +@influxframework.whitelist() +def get_permitted_charts(dashboard_name): + permitted_charts = [] + dashboard = influxframework.get_doc("Dashboard", dashboard_name) + for chart in dashboard.charts: + if influxframework.has_permission("Dashboard Chart", doc=chart.chart): + chart_dict = influxframework._dict() + chart_dict.update(chart.as_dict()) + + if dashboard.get("chart_options"): + chart_dict.custom_options = dashboard.get("chart_options") + permitted_charts.append(chart_dict) + + return permitted_charts + + +@influxframework.whitelist() +def get_permitted_cards(dashboard_name): + permitted_cards = [] + dashboard = influxframework.get_doc("Dashboard", dashboard_name) + for card in dashboard.cards: + if influxframework.has_permission("Number Card", doc=card.card): + permitted_cards.append(card) + return permitted_cards + + +def get_non_standard_charts_in_dashboard(dashboard): + non_standard_charts = [doc.name for doc in influxframework.get_list("Dashboard Chart", {"is_standard": 0})] + return [ + chart_link.chart for chart_link in dashboard.charts if chart_link.chart in non_standard_charts + ] + + +def get_non_standard_cards_in_dashboard(dashboard): + non_standard_cards = [doc.name for doc in influxframework.get_list("Number Card", {"is_standard": 0})] + return [card_link.card for card_link in dashboard.cards if card_link.card in non_standard_cards] + + +def get_non_standard_warning_message(non_standard_docs_map): + message = _("""Please set the following documents in this Dashboard as standard first.""") + + def get_html(docs, doctype): + html = f"

    {influxframework.bold(doctype)}

    " + for doc in docs: + html += ''.format( + doctype=doctype, doc=doc + ) + html += "
    " + return html + + html = message + "
    " + + for doctype in non_standard_docs_map: + if non_standard_docs_map[doctype]: + html += get_html(non_standard_docs_map[doctype], doctype) + + return html diff --git a/influxframework/desk/doctype/dashboard/dashboard_list.js b/influxframework/desk/doctype/dashboard/dashboard_list.js new file mode 100644 index 0000000..8b3483b --- /dev/null +++ b/influxframework/desk/doctype/dashboard/dashboard_list.js @@ -0,0 +1,16 @@ +influxframework.listview_settings["Dashboard"] = { + button: { + show(doc) { + return doc.name; + }, + get_label() { + return influxframework.utils.icon("dashboard-list", "sm"); + }, + get_description(doc) { + return __("View {0}", [`${doc.name}`]); + }, + action(doc) { + influxframework.set_route("dashboard-view", doc.name); + }, + }, +}; diff --git a/influxframework/desk/doctype/dashboard/test_dashboard.py b/influxframework/desk/doctype/dashboard/test_dashboard.py new file mode 100644 index 0000000..ffdee5d --- /dev/null +++ b/influxframework/desk/doctype/dashboard/test_dashboard.py @@ -0,0 +1,7 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestDashboard(InfluxFrameworkTestCase): + pass diff --git a/influxframework/desk/doctype/dashboard_chart/__init__.py b/influxframework/desk/doctype/dashboard_chart/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/dashboard_chart/dashboard_chart.js b/influxframework/desk/doctype/dashboard_chart/dashboard_chart.js new file mode 100644 index 0000000..0c53a3c --- /dev/null +++ b/influxframework/desk/doctype/dashboard_chart/dashboard_chart.js @@ -0,0 +1,553 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.provide("influxframework.dashboards.chart_sources"); + +influxframework.ui.form.on("Dashboard Chart", { + setup: function (frm) { + // fetch timeseries from source + frm.add_fetch("source", "timeseries", "timeseries"); + }, + + before_save: function (frm) { + let dynamic_filters = JSON.parse(frm.doc.dynamic_filters_json || "null"); + let static_filters = JSON.parse(frm.doc.filters_json || "null"); + static_filters = influxframework.dashboard_utils.remove_common_static_filter_values( + static_filters, + dynamic_filters + ); + + frm.set_value("filters_json", JSON.stringify(static_filters)); + frm.trigger("show_filters"); + }, + + refresh: function (frm) { + frm.chart_filters = null; + frm.is_disabled = !influxframework.boot.developer_mode && frm.doc.is_standard; + + if (frm.is_disabled) { + !frm.doc.custom_options && frm.set_df_property("chart_options_section", "hidden", 1); + frm.disable_form(); + } + + frm.add_custom_button("Add Chart to Dashboard", () => { + const dialog = influxframework.dashboard_utils.get_add_to_dashboard_dialog( + frm.doc.name, + "Dashboard Chart", + "influxframework.desk.doctype.dashboard_chart.dashboard_chart.add_chart_to_dashboard" + ); + + if (!frm.doc.chart_name) { + influxframework.msgprint(__("Please create chart first")); + } else { + dialog.show(); + } + }); + + frm.set_df_property("filters_section", "hidden", 1); + frm.set_df_property("dynamic_filters_section", "hidden", 1); + + frm.trigger("set_parent_document_type"); + frm.trigger("set_time_series"); + frm.set_query("document_type", function () { + return { + filters: { + issingle: false, + }, + }; + }); + frm.trigger("update_options"); + frm.trigger("set_heatmap_year_options"); + if (frm.doc.report_name) { + frm.trigger("set_chart_report_filters"); + } + }, + + is_standard: function (frm) { + if (influxframework.boot.developer_mode && frm.doc.is_standard) { + frm.trigger("render_dynamic_filters_table"); + } else { + frm.set_df_property("dynamic_filters_section", "hidden", 1); + } + }, + + source: function (frm) { + frm.trigger("show_filters"); + }, + + set_heatmap_year_options: function (frm) { + if (frm.doc.type == "Heatmap") { + influxframework.db.get_doc("System Settings").then((doc) => { + const creation_date = doc.creation; + frm.set_df_property( + "heatmap_year", + "options", + influxframework.dashboard_utils.get_years_since_creation(creation_date) + ); + }); + } + }, + + chart_type: function (frm) { + frm.trigger("set_time_series"); + if (frm.doc.chart_type == "Report") { + frm.set_query("report_name", () => { + return { + filters: { + report_type: ["!=", "Report Builder"], + }, + }; + }); + } else { + frm.set_value("document_type", ""); + } + }, + + set_time_series: function (frm) { + // set timeseries based on chart type + if (["Count", "Average", "Sum"].includes(frm.doc.chart_type)) { + frm.set_value("timeseries", 1); + } else { + frm.set_value("timeseries", 0); + } + }, + + document_type: function (frm) { + // update `based_on` options based on date / datetime fields + frm.set_value("source", ""); + frm.set_value("based_on", ""); + frm.set_value("value_based_on", ""); + frm.set_value("parent_document_type", ""); + frm.set_value("filters_json", "[]"); + frm.set_value("dynamic_filters_json", "[]"); + frm.trigger("update_options"); + frm.trigger("set_parent_document_type"); + }, + + report_name: function (frm) { + frm.set_value("x_field", ""); + frm.set_value("y_axis", []); + frm.set_df_property("x_field", "options", []); + frm.set_value("filters_json", "{}"); + frm.set_value("dynamic_filters_json", "{}"); + frm.set_value("use_report_chart", 0); + frm.trigger("set_chart_report_filters"); + }, + + set_chart_report_filters: function (frm) { + let report_name = frm.doc.report_name; + + if (report_name) { + if (frm.doc.filters_json.length > 2) { + frm.trigger("show_filters"); + frm.trigger("set_chart_field_options"); + } else { + influxframework.report_utils.get_report_filters(report_name).then((filters) => { + if (filters) { + frm.chart_filters = filters; + let filter_values = influxframework.report_utils.get_filter_values(filters); + frm.set_value("filters_json", JSON.stringify(filter_values)); + } + frm.trigger("show_filters"); + frm.trigger("set_chart_field_options"); + }); + } + } + }, + + use_report_chart: function (frm) { + !frm.doc.use_report_chart && frm.trigger("set_chart_field_options"); + }, + + set_chart_field_options: function (frm) { + let filters = frm.doc.filters_json.length > 2 ? JSON.parse(frm.doc.filters_json) : null; + if (frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2) { + filters = influxframework.dashboard_utils.get_all_filters(frm.doc); + } + influxframework + .xcall("influxframework.desk.query_report.run", { + report_name: frm.doc.report_name, + filters: filters, + ignore_prepared_report: 1, + }) + .then((data) => { + frm.report_data = data; + let report_has_chart = Boolean(data.chart); + + frm.set_df_property("use_report_chart", "hidden", !report_has_chart); + + if (!frm.doc.use_report_chart) { + if (data.result.length) { + frm.field_options = influxframework.report_utils.get_field_options_from_report( + data.columns, + data + ); + frm.set_df_property( + "x_field", + "options", + frm.field_options.non_numeric_fields + ); + if (!frm.field_options.numeric_fields.length) { + influxframework.msgprint( + __("Report has no numeric fields, please change the Report Name") + ); + } else { + let y_field_df = influxframework.meta.get_docfield( + "Dashboard Chart Field", + "y_field", + frm.doc.name + ); + y_field_df.options = frm.field_options.numeric_fields; + } + } else { + influxframework.msgprint( + __( + "Report has no data, please modify the filters or change the Report Name" + ) + ); + } + } else { + frm.set_value("use_report_chart", 1); + frm.set_df_property("use_report_chart", "hidden", false); + } + }); + }, + + timespan: function (frm) { + const time_interval_options = { + "Select Date Range": ["Quarterly", "Monthly", "Weekly", "Daily"], + "All Time": ["Yearly", "Monthly"], + "Last Year": ["Quarterly", "Monthly", "Weekly", "Daily"], + "Last Quarter": ["Monthly", "Weekly", "Daily"], + "Last Month": ["Weekly", "Daily"], + "Last Week": ["Daily"], + }; + if (frm.doc.timespan) { + frm.set_df_property( + "time_interval", + "options", + time_interval_options[frm.doc.timespan] + ); + } + }, + + update_options: function (frm) { + let doctype = frm.doc.document_type; + let date_fields = [ + { label: __("Created On"), value: "creation" }, + { label: __("Last Modified On"), value: "modified" }, + ]; + let value_fields = []; + let group_by_fields = [{ label: "Created By", value: "owner" }]; + let aggregate_function_fields = []; + let update_form = function () { + // update select options + frm.set_df_property("based_on", "options", date_fields); + frm.set_df_property("value_based_on", "options", value_fields); + frm.set_df_property("group_by_based_on", "options", group_by_fields); + frm.set_df_property( + "aggregate_function_based_on", + "options", + aggregate_function_fields + ); + frm.trigger("show_filters"); + }; + + if (doctype) { + influxframework.model.with_doctype(doctype, () => { + // get all date and datetime fields + influxframework.get_meta(doctype).fields.map((df) => { + if (["Date", "Datetime"].includes(df.fieldtype)) { + date_fields.push({ label: df.label, value: df.fieldname }); + } + if ( + ["Int", "Float", "Currency", "Percent", "Duration"].includes(df.fieldtype) + ) { + value_fields.push({ label: df.label, value: df.fieldname }); + aggregate_function_fields.push({ label: df.label, value: df.fieldname }); + } + if (["Link", "Select"].includes(df.fieldtype)) { + group_by_fields.push({ label: df.label, value: df.fieldname }); + } + }); + update_form(); + }); + } else { + // update select options + update_form(); + } + }, + + show_filters: function (frm) { + frm.chart_filters = []; + influxframework.dashboard_utils.get_filters_for_chart_type(frm.doc).then((filters) => { + if (filters) { + frm.chart_filters = filters; + } + frm.trigger("render_filters_table"); + + if (influxframework.boot.developer_mode && frm.doc.is_standard) { + frm.trigger("render_dynamic_filters_table"); + } + }); + }, + + render_filters_table: function (frm) { + frm.set_df_property("filters_section", "hidden", 0); + let is_document_type = frm.doc.chart_type !== "Report" && frm.doc.chart_type !== "Custom"; + let is_dynamic_filter = (f) => ["Date", "DateRange"].includes(f.fieldtype) && f.default; + + let wrapper = $(frm.get_field("filters_json").wrapper).empty(); + let table = $(` + + + + + + + + +
    ${__("Filter")}${__("Condition")}${__("Value")}
    `).appendTo(wrapper); + $(`

    ${__("Click table to edit")}

    `).appendTo(wrapper); + + let filters = JSON.parse(frm.doc.filters_json || "[]"); + var filters_set = false; + + // Set dynamic filters for reports + if (frm.doc.chart_type == "Report") { + let set_filters = false; + frm.chart_filters.forEach((f) => { + if (is_dynamic_filter(f)) { + filters[f.fieldname] = f.default; + set_filters = true; + } + }); + set_filters && frm.set_value("filters_json", JSON.stringify(filters)); + } + + let fields = []; + if (is_document_type) { + fields = [ + { + fieldtype: "HTML", + fieldname: "filter_area", + }, + ]; + + if (filters.length > 0) { + filters.forEach((filter) => { + const filter_row = $(` + ${filter[1]} + ${filter[2] || ""} + ${filter[3]} + `); + + table.find("tbody").append(filter_row); + filters_set = true; + }); + } + } else if (frm.chart_filters.length) { + fields = frm.chart_filters.filter((f) => f.fieldname); + + fields.map((f) => { + if (filters[f.fieldname]) { + let condition = "="; + const filter_row = $(` + ${f.label} + ${condition} + ${filters[f.fieldname] || ""} + `); + + table.find("tbody").append(filter_row); + filters_set = true; + } + }); + } + + if (!filters_set) { + const filter_row = $(` + ${__("Click to Set Filters")}`); + table.find("tbody").append(filter_row); + } + + table.on("click", () => { + frm.is_disabled && influxframework.throw(__("Cannot edit filters for standard charts")); + + let dialog = new influxframework.ui.Dialog({ + title: __("Set Filters"), + fields: fields.filter((f) => !is_dynamic_filter(f)), + primary_action: function () { + let values = this.get_values(); + if (values) { + this.hide(); + if (is_document_type) { + let filters = frm.filter_group.get_filters(); + frm.set_value("filters_json", JSON.stringify(filters)); + } else { + frm.set_value("filters_json", JSON.stringify(values)); + } + + frm.trigger("show_filters"); + if (frm.doc.chart_type == "Report") { + frm.trigger("set_chart_report_filters"); + } + } + }, + primary_action_label: "Set", + }); + influxframework.dashboards.filters_dialog = dialog; + + if (is_document_type) { + frm.filter_group = new influxframework.ui.FilterGroup({ + parent: dialog.get_field("filter_area").$wrapper, + doctype: frm.doc.document_type, + parent_doctype: frm.doc.parent_document_type, + on_change: () => {}, + }); + + frm.filter_group.add_filters_to_filter_group(filters); + } + + dialog.show(); + + if (frm.doc.chart_type == "Report") { + //Set query report object so that it can be used while fetching filter values in the report + influxframework.query_report = new influxframework.views.QueryReport({ + filters: dialog.fields_list, + }); + influxframework.query_reports[frm.doc.report_name] && + influxframework.query_reports[frm.doc.report_name].onload && + influxframework.query_reports[frm.doc.report_name].onload(influxframework.query_report); + } + + dialog.set_values(filters); + }); + }, + + render_dynamic_filters_table(frm) { + frm.set_df_property("dynamic_filters_section", "hidden", 0); + + let is_document_type = frm.doc.chart_type !== "Report" && frm.doc.chart_type !== "Custom"; + + let wrapper = $(frm.get_field("dynamic_filters_json").wrapper).empty(); + + frm.dynamic_filter_table = + $(` + + + + + + + + +
    ${__("Filter")}${__("Condition")}${__("Value")}
    `).appendTo(wrapper); + + frm.dynamic_filters = + frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2 + ? JSON.parse(frm.doc.dynamic_filters_json) + : null; + + frm.trigger("set_dynamic_filters_in_table"); + + let filters = JSON.parse(frm.doc.filters_json || "[]"); + + let fields = influxframework.dashboard_utils.get_fields_for_dynamic_filter_dialog( + is_document_type, + filters, + frm.dynamic_filters + ); + + frm.dynamic_filter_table.on("click", () => { + let dialog = new influxframework.ui.Dialog({ + title: __("Set Dynamic Filters"), + fields: fields, + primary_action: () => { + let values = dialog.get_values(); + dialog.hide(); + let dynamic_filters = []; + for (let key of Object.keys(values)) { + if (is_document_type) { + let [doctype, fieldname] = key.split(":"); + dynamic_filters.push([doctype, fieldname, "=", values[key]]); + } + } + + if (is_document_type) { + frm.set_value("dynamic_filters_json", JSON.stringify(dynamic_filters)); + } else { + frm.set_value("dynamic_filters_json", JSON.stringify(values)); + } + frm.trigger("set_dynamic_filters_in_table"); + }, + primary_action_label: "Set", + }); + + dialog.show(); + dialog.set_values(frm.dynamic_filters); + }); + }, + + set_dynamic_filters_in_table: function (frm) { + frm.dynamic_filters = + frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2 + ? JSON.parse(frm.doc.dynamic_filters_json) + : null; + + if (!frm.dynamic_filters) { + const filter_row = $(` + ${__("Click to Set Dynamic Filters")}`); + frm.dynamic_filter_table.find("tbody").html(filter_row); + } else { + let filter_rows = ""; + if ($.isArray(frm.dynamic_filters)) { + frm.dynamic_filters.forEach((filter) => { + filter_rows += ` + ${filter[1]} + ${filter[2] || ""} + ${filter[3]} + `; + }); + } else { + let condition = "="; + for (let [key, val] of Object.entries(frm.dynamic_filters)) { + filter_rows += ` + ${key} + ${condition} + ${val || ""} + `; + } + } + + frm.dynamic_filter_table.find("tbody").html(filter_rows); + } + }, + + set_parent_document_type: async function (frm) { + let document_type = frm.doc.document_type; + let doc_is_table = + document_type && + (await influxframework.db.get_value("DocType", document_type, "istable")).message.istable; + + frm.set_df_property("parent_document_type", "hidden", !doc_is_table); + + if (document_type && doc_is_table) { + let parents = await influxframework.xcall( + "influxframework.desk.doctype.dashboard_chart.dashboard_chart.get_parent_doctypes", + { child_type: document_type } + ); + + frm.set_query("parent_document_type", function () { + return { + filters: { + name: ["in", parents], + }, + }; + }); + + if (parents.length === 1) { + frm.set_value("parent_document_type", parents[0]); + } + } + }, +}); diff --git a/influxframework/desk/doctype/dashboard_chart/dashboard_chart.json b/influxframework/desk/doctype/dashboard_chart/dashboard_chart.json new file mode 100644 index 0000000..67a57ec --- /dev/null +++ b/influxframework/desk/doctype/dashboard_chart/dashboard_chart.json @@ -0,0 +1,336 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:chart_name", + "creation": "2019-01-10 12:28:06.282875", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "is_standard", + "module", + "chart_name", + "chart_type", + "report_name", + "use_report_chart", + "x_field", + "y_axis", + "source", + "document_type", + "parent_document_type", + "based_on", + "value_based_on", + "group_by_type", + "group_by_based_on", + "aggregate_function_based_on", + "number_of_groups", + "column_break_6", + "is_public", + "heatmap_year", + "timespan", + "from_date", + "to_date", + "time_interval", + "timeseries", + "type", + "filters_section", + "filters_json", + "dynamic_filters_section", + "dynamic_filters_json", + "chart_options_section", + "custom_options", + "column_break_2", + "color", + "section_break_10", + "last_synced_on", + "roles" + ], + "fields": [ + { + "fieldname": "chart_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Chart Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "chart_type", + "fieldtype": "Select", + "label": "Chart Type", + "options": "Count\nSum\nAverage\nGroup By\nCustom\nReport", + "set_only_once": 1 + }, + { + "depends_on": "eval:doc.chart_type === 'Custom'", + "fieldname": "source", + "fieldtype": "Link", + "label": "Chart Source", + "options": "Dashboard Chart Source" + }, + { + "depends_on": "eval: doc.chart_type !== 'Custom' && doc.chart_type !== 'Report'", + "fieldname": "document_type", + "fieldtype": "Link", + "label": "Document Type", + "options": "DocType", + "set_only_once": 1 + }, + { + "depends_on": "eval: doc.timeseries && ['Count', 'Sum', 'Average'].includes(doc.chart_type)", + "fieldname": "based_on", + "fieldtype": "Select", + "label": "Time Series Based On" + }, + { + "depends_on": "eval: ['Sum', 'Average'].includes(doc.chart_type)\n", + "fieldname": "value_based_on", + "fieldtype": "Select", + "label": "Value Based On" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval: doc.timeseries && doc.type !== 'Heatmap'", + "fieldname": "timespan", + "fieldtype": "Select", + "label": "Timespan", + "options": "Last Year\nLast Quarter\nLast Month\nLast Week\nSelect Date Range" + }, + { + "depends_on": "eval: doc.timeseries && doc.type !== 'Heatmap'", + "fieldname": "time_interval", + "fieldtype": "Select", + "label": "Time Interval", + "options": "Yearly\nQuarterly\nMonthly\nWeekly\nDaily" + }, + { + "default": "0", + "depends_on": "eval: !['Group By', 'Report'].includes(doc.chart_type)\n", + "fieldname": "timeseries", + "fieldtype": "Check", + "label": "Time Series" + }, + { + "fieldname": "filters_section", + "fieldtype": "Section Break", + "label": "Filters" + }, + { + "fieldname": "filters_json", + "fieldtype": "Code", + "label": "Filters JSON", + "options": "JSON", + "reqd": 1 + }, + { + "fieldname": "chart_options_section", + "fieldtype": "Section Break", + "label": "Chart Options" + }, + { + "default": "Line", + "fieldname": "type", + "fieldtype": "Select", + "label": "Type", + "options": "Line\nBar\nPercentage\nPie\nDonut\nHeatmap" + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval: doc.chart_type !== 'Report' && doc.type !== 'Heatmap'", + "fieldname": "color", + "fieldtype": "Color", + "label": "Color" + }, + { + "fieldname": "section_break_10", + "fieldtype": "Section Break" + }, + { + "fieldname": "last_synced_on", + "fieldtype": "Datetime", + "label": "Last Synced On", + "read_only": 1 + }, + { + "depends_on": "eval:doc.chart_type === 'Group By'", + "fieldname": "group_by_based_on", + "fieldtype": "Select", + "label": "Group By Based On" + }, + { + "default": "Count", + "depends_on": "eval:doc.chart_type === 'Group By'", + "fieldname": "group_by_type", + "fieldtype": "Select", + "label": "Group By Type", + "options": "Count\nSum\nAverage" + }, + { + "depends_on": "eval: ['Sum', 'Average'].includes(doc.group_by_type)", + "fieldname": "aggregate_function_based_on", + "fieldtype": "Select", + "label": "Aggregate Function Based On" + }, + { + "depends_on": "eval:doc.chart_type === 'Group By'", + "fieldname": "number_of_groups", + "fieldtype": "Int", + "label": "Number of Groups" + }, + { + "depends_on": "eval:doc.timespan === 'Select Date Range'", + "fieldname": "from_date", + "fieldtype": "Date", + "label": "From Date" + }, + { + "depends_on": "eval:doc.timespan === 'Select Date Range'", + "fieldname": "to_date", + "fieldtype": "Date", + "label": "To Date" + }, + { + "depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.use_report_chart", + "fieldname": "x_field", + "fieldtype": "Select", + "label": "X Field", + "mandatory_depends_on": "eval: doc.report_name && !doc.use_report_chart" + }, + { + "depends_on": "eval:doc.chart_type === 'Report'", + "fieldname": "report_name", + "fieldtype": "Link", + "label": "Report Name", + "mandatory_depends_on": "eval:doc.chart_type === 'Report'", + "options": "Report", + "set_only_once": 1 + }, + { + "depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.use_report_chart", + "fieldname": "y_axis", + "fieldtype": "Table", + "label": "Y Axis", + "mandatory_depends_on": "eval:doc.report_name && !doc.use_report_chart", + "options": "Dashboard Chart Field" + }, + { + "description": "Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"]", + "fieldname": "custom_options", + "fieldtype": "Code", + "label": "Custom Options" + }, + { + "default": "0", + "description": "This chart will be available to all Users if this is set", + "fieldname": "is_public", + "fieldtype": "Check", + "label": "Is Public" + }, + { + "depends_on": "eval: doc.type == 'Heatmap'", + "fieldname": "heatmap_year", + "fieldtype": "Select", + "label": "Year" + }, + { + "default": "0", + "fieldname": "is_standard", + "fieldtype": "Check", + "label": "Is Standard", + "read_only_depends_on": "eval: !influxframework.boot.developer_mode" + }, + { + "depends_on": "eval: doc.is_standard", + "fieldname": "module", + "fieldtype": "Link", + "label": "Module", + "mandatory_depends_on": "eval: doc.is_standard", + "options": "Module Def" + }, + { + "fieldname": "dynamic_filters_json", + "fieldtype": "Code", + "label": "Dynamic Filters JSON", + "options": "JSON" + }, + { + "fieldname": "dynamic_filters_section", + "fieldtype": "Section Break", + "label": "Dynamic Filters" + }, + { + "default": "0", + "depends_on": "eval: doc.report_name", + "fieldname": "use_report_chart", + "fieldtype": "Check", + "label": "Use Report Chart" + }, + { + "depends_on": "eval: doc.chart_type !== 'Custom' && doc.chart_type !== 'Report'", + "description": "The document type selected is a child table, so the parent document type is required.", + "fieldname": "parent_document_type", + "fieldtype": "Link", + "label": "Parent Document Type", + "options": "DocType" + }, + { + "fieldname": "roles", + "fieldtype": "Table", + "label": "Roles", + "options": "Has Role" + } + ], + "links": [], + "modified": "2022-07-27 11:09:09.203236", + "modified_by": "Administrator", + "module": "Desk", + "name": "Dashboard Chart", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Dashboard Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} diff --git a/influxframework/desk/doctype/dashboard_chart/dashboard_chart.py b/influxframework/desk/doctype/dashboard_chart/dashboard_chart.py new file mode 100644 index 0000000..71200f4 --- /dev/null +++ b/influxframework/desk/doctype/dashboard_chart/dashboard_chart.py @@ -0,0 +1,408 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +import datetime +import json + +import influxframework +from influxframework import _ +from influxframework.boot import get_allowed_report_names +from influxframework.config import get_modules_from_all_apps_for_user +from influxframework.model.document import Document +from influxframework.model.naming import append_number_if_name_exists +from influxframework.modules.export_file import export_to_files +from influxframework.utils import cint, get_datetime, getdate, has_common, now_datetime, nowdate +from influxframework.utils.dashboard import cache_source +from influxframework.utils.data import format_date +from influxframework.utils.dateutils import ( + get_dates_from_timegrain, + get_from_date_from_timespan, + get_period, + get_period_beginning, +) + + +def get_permission_query_conditions(user): + if not user: + user = influxframework.session.user + + if user == "Administrator": + return + + roles = influxframework.get_roles(user) + if "System Manager" in roles: + return None + + doctype_condition = False + report_condition = False + module_condition = False + + allowed_doctypes = [ + influxframework.db.escape(doctype) for doctype in influxframework.permissions.get_doctypes_with_read() + ] + allowed_reports = [influxframework.db.escape(report) for report in get_allowed_report_names()] + allowed_modules = [ + influxframework.db.escape(module.get("module_name")) for module in get_modules_from_all_apps_for_user() + ] + + if allowed_doctypes: + doctype_condition = "`tabDashboard Chart`.`document_type` in ({allowed_doctypes})".format( + allowed_doctypes=",".join(allowed_doctypes) + ) + if allowed_reports: + report_condition = "`tabDashboard Chart`.`report_name` in ({allowed_reports})".format( + allowed_reports=",".join(allowed_reports) + ) + if allowed_modules: + module_condition = """`tabDashboard Chart`.`module` in ({allowed_modules}) + or `tabDashboard Chart`.`module` is NULL""".format( + allowed_modules=",".join(allowed_modules) + ) + + return """ + ((`tabDashboard Chart`.`chart_type` in ('Count', 'Sum', 'Average') + and {doctype_condition}) + or + (`tabDashboard Chart`.`chart_type` = 'Report' + and {report_condition})) + and + ({module_condition}) + """.format( + doctype_condition=doctype_condition, + report_condition=report_condition, + module_condition=module_condition, + ) + + +def has_permission(doc, ptype, user): + roles = influxframework.get_roles(user) + if "System Manager" in roles: + return True + + if doc.chart_type == "Report": + if doc.report_name in get_allowed_report_names(): + return True + else: + allowed_doctypes = influxframework.permissions.get_doctypes_with_read() + if doc.document_type in allowed_doctypes: + return True + + if doc.roles: + allowed = [d.role for d in doc.roles] + if has_common(roles, allowed): + return True + + return False + + +@influxframework.whitelist() +@cache_source +def get( + chart_name=None, + chart=None, + no_cache=None, + filters=None, + from_date=None, + to_date=None, + timespan=None, + time_interval=None, + heatmap_year=None, + refresh=None, +): + if chart_name: + chart = influxframework.get_doc("Dashboard Chart", chart_name) + else: + chart = influxframework._dict(influxframework.parse_json(chart)) + + heatmap_year = heatmap_year or chart.heatmap_year + timespan = timespan or chart.timespan + + if timespan == "Select Date Range": + if from_date and len(from_date): + from_date = get_datetime(from_date) + else: + from_date = chart.from_date + + if to_date and len(to_date): + to_date = get_datetime(to_date) + else: + to_date = get_datetime(chart.to_date) + + timegrain = time_interval or chart.time_interval + filters = influxframework.parse_json(filters) or influxframework.parse_json(chart.filters_json) + if not filters: + filters = [] + + # don't include cancelled documents + filters.append([chart.document_type, "docstatus", "<", 2, False]) + + if chart.chart_type == "Group By": + chart_config = get_group_by_chart_config(chart, filters) + else: + if chart.type == "Heatmap": + chart_config = get_heatmap_chart_config(chart, filters, heatmap_year) + else: + chart_config = get_chart_config(chart, filters, timespan, timegrain, from_date, to_date) + + return chart_config + + +@influxframework.whitelist() +def create_dashboard_chart(args): + args = influxframework.parse_json(args) + doc = influxframework.new_doc("Dashboard Chart") + + doc.update(args) + + if args.get("custom_options"): + doc.custom_options = json.dumps(args.get("custom_options")) + + if influxframework.db.exists("Dashboard Chart", args.chart_name): + args.chart_name = append_number_if_name_exists("Dashboard Chart", args.chart_name) + doc.chart_name = args.chart_name + doc.insert(ignore_permissions=True) + return doc + + +@influxframework.whitelist() +def create_report_chart(args): + doc = create_dashboard_chart(args) + args = influxframework.parse_json(args) + args.chart_name = doc.chart_name + if args.dashboard: + add_chart_to_dashboard(json.dumps(args)) + + +@influxframework.whitelist() +def add_chart_to_dashboard(args): + args = influxframework.parse_json(args) + + dashboard = influxframework.get_doc("Dashboard", args.dashboard) + dashboard_link = influxframework.new_doc("Dashboard Chart Link") + dashboard_link.chart = args.chart_name or args.name + + if args.set_standard and dashboard.is_standard: + chart = influxframework.get_doc("Dashboard Chart", dashboard_link.chart) + chart.is_standard = 1 + chart.module = dashboard.module + chart.save() + + dashboard.append("charts", dashboard_link) + dashboard.save() + influxframework.db.commit() + + +def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date): + if not from_date: + from_date = get_from_date_from_timespan(to_date, timespan) + from_date = get_period_beginning(from_date, timegrain) + if not to_date: + to_date = now_datetime() + + doctype = chart.document_type + datefield = chart.based_on + value_field = chart.value_based_on or "1" + from_date = from_date.strftime("%Y-%m-%d") + to_date = to_date + + filters.append([doctype, datefield, ">=", from_date, False]) + filters.append([doctype, datefield, "<=", to_date, False]) + + data = influxframework.db.get_list( + doctype, + fields=[f"{datefield} as _unit", f"SUM({value_field})", "COUNT(*)"], + filters=filters, + group_by="_unit", + order_by="_unit asc", + as_list=True, + ) + + result = get_result(data, timegrain, from_date, to_date, chart.chart_type) + + return { + "labels": [ + format_date(get_period(r[0], timegrain), parse_day_first=True) + if timegrain in ("Daily", "Weekly") + else get_period(r[0], timegrain) + for r in result + ], + "datasets": [{"name": chart.name, "values": [r[1] for r in result]}], + } + + +def get_heatmap_chart_config(chart, filters, heatmap_year): + aggregate_function = get_aggregate_function(chart.chart_type) + value_field = chart.value_based_on or "1" + doctype = chart.document_type + datefield = chart.based_on + year = cint(heatmap_year) if heatmap_year else getdate(nowdate()).year + year_start_date = datetime.date(year, 1, 1).strftime("%Y-%m-%d") + next_year_start_date = datetime.date(year + 1, 1, 1).strftime("%Y-%m-%d") + + filters.append([doctype, datefield, ">", f"{year_start_date}", False]) + filters.append([doctype, datefield, "<", f"{next_year_start_date}", False]) + + if influxframework.db.db_type == "mariadb": + timestamp_field = f"unix_timestamp({datefield})" + else: + timestamp_field = f"extract(epoch from timestamp {datefield})" + + data = dict( + influxframework.get_all( + doctype, + fields=[ + timestamp_field, + "{aggregate_function}({value_field})".format( + aggregate_function=aggregate_function, value_field=value_field + ), + ], + filters=filters, + group_by=f"date({datefield})", + as_list=1, + order_by=f"{datefield} asc", + ignore_ifnull=True, + ) + ) + + chart_config = { + "labels": [], + "dataPoints": data, + } + return chart_config + + +def get_group_by_chart_config(chart, filters): + + aggregate_function = get_aggregate_function(chart.group_by_type) + value_field = chart.aggregate_function_based_on or "1" + group_by_field = chart.group_by_based_on + doctype = chart.document_type + + data = influxframework.db.get_list( + doctype, + fields=[ + f"{group_by_field} as name", + "{aggregate_function}({value_field}) as count".format( + aggregate_function=aggregate_function, value_field=value_field + ), + ], + filters=filters, + group_by=group_by_field, + order_by="count desc", + ignore_ifnull=True, + ) + + if data: + chart_config = { + "labels": [item["name"] if item["name"] else "Not Specified" for item in data], + "datasets": [{"name": chart.name, "values": [item["count"] for item in data]}], + } + + return chart_config + else: + return None + + +def get_aggregate_function(chart_type): + return { + "Sum": "SUM", + "Count": "COUNT", + "Average": "AVG", + }[chart_type] + + +def get_result(data, timegrain, from_date, to_date, chart_type): + dates = get_dates_from_timegrain(from_date, to_date, timegrain) + result = [[date, 0] for date in dates] + data_index = 0 + if data: + for i, d in enumerate(result): + count = 0 + while data_index < len(data) and getdate(data[data_index][0]) <= d[0]: + d[1] += data[data_index][1] + count += data[data_index][2] + data_index += 1 + if chart_type == "Average" and not count == 0: + d[1] = d[1] / count + if chart_type == "Count": + d[1] = count + + return result + + +@influxframework.whitelist() +@influxframework.validate_and_sanitize_search_inputs +def get_charts_for_user(doctype, txt, searchfield, start, page_len, filters): + or_filters = {"owner": influxframework.session.user, "is_public": 1} + return influxframework.db.get_list( + "Dashboard Chart", fields=["name"], filters=filters, or_filters=or_filters, as_list=1 + ) + + +class DashboardChart(Document): + def on_update(self): + influxframework.cache().delete_key(f"chart-data:{self.name}") + if influxframework.conf.developer_mode and self.is_standard: + export_to_files(record_list=[["Dashboard Chart", self.name]], record_module=self.module) + + def validate(self): + if not influxframework.conf.developer_mode and self.is_standard: + influxframework.throw(_("Cannot edit Standard charts")) + if self.chart_type != "Custom" and self.chart_type != "Report": + self.check_required_field() + self.check_document_type() + + self.validate_custom_options() + + def check_required_field(self): + if not self.document_type: + influxframework.throw(_("Document type is required to create a dashboard chart")) + + if ( + self.document_type + and influxframework.get_meta(self.document_type).istable + and not self.parent_document_type + ): + influxframework.throw(_("Parent document type is required to create a dashboard chart")) + + if self.chart_type == "Group By": + if not self.group_by_based_on: + influxframework.throw(_("Group By field is required to create a dashboard chart")) + if self.group_by_type in ["Sum", "Average"] and not self.aggregate_function_based_on: + influxframework.throw(_("Aggregate Function field is required to create a dashboard chart")) + else: + if not self.based_on: + influxframework.throw(_("Time series based on is required to create a dashboard chart")) + + def check_document_type(self): + if influxframework.get_meta(self.document_type).issingle: + influxframework.throw(_("You cannot create a dashboard chart from single DocTypes")) + + def validate_custom_options(self): + if self.custom_options: + try: + json.loads(self.custom_options) + except ValueError as error: + influxframework.throw(_("Invalid json added in the custom options: {0}").format(error)) + + +@influxframework.whitelist() +def get_parent_doctypes(child_type: str) -> list[str]: + """Get all parent doctypes that have the child doctype.""" + assert isinstance(child_type, str) + + standard = influxframework.get_all( + "DocField", + fields="parent", + filters={"fieldtype": "Table", "options": child_type}, + pluck="parent", + ) + + custom = influxframework.get_all( + "Custom Field", + fields="dt", + filters={"fieldtype": "Table", "options": child_type}, + pluck="dt", + ) + + return standard + custom diff --git a/influxframework/desk/doctype/dashboard_chart/test_dashboard_chart.py b/influxframework/desk/doctype/dashboard_chart/test_dashboard_chart.py new file mode 100644 index 0000000..00b598a --- /dev/null +++ b/influxframework/desk/doctype/dashboard_chart/test_dashboard_chart.py @@ -0,0 +1,286 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +from datetime import datetime +from unittest.mock import patch + +from dateutil.relativedelta import relativedelta + +import influxframework +from influxframework.desk.doctype.dashboard_chart.dashboard_chart import get +from influxframework.tests.utils import InfluxFrameworkTestCase +from influxframework.utils import formatdate, get_last_day, getdate +from influxframework.utils.dateutils import get_period, get_period_ending + + +class TestDashboardChart(InfluxFrameworkTestCase): + def test_period_ending(self): + self.assertEqual(get_period_ending("2019-04-10", "Daily"), getdate("2019-04-10")) + + # week starts on monday + with patch.object(influxframework.utils.data, "get_first_day_of_the_week", return_value="Monday"): + self.assertEqual(get_period_ending("2019-04-10", "Weekly"), getdate("2019-04-14")) + + self.assertEqual(get_period_ending("2019-04-10", "Monthly"), getdate("2019-04-30")) + self.assertEqual(get_period_ending("2019-04-30", "Monthly"), getdate("2019-04-30")) + self.assertEqual(get_period_ending("2019-03-31", "Monthly"), getdate("2019-03-31")) + + self.assertEqual(get_period_ending("2019-04-10", "Quarterly"), getdate("2019-06-30")) + self.assertEqual(get_period_ending("2019-06-30", "Quarterly"), getdate("2019-06-30")) + self.assertEqual(get_period_ending("2019-10-01", "Quarterly"), getdate("2019-12-31")) + + def test_dashboard_chart(self): + if influxframework.db.exists("Dashboard Chart", "Test Dashboard Chart"): + influxframework.delete_doc("Dashboard Chart", "Test Dashboard Chart") + + influxframework.get_doc( + dict( + doctype="Dashboard Chart", + chart_name="Test Dashboard Chart", + chart_type="Count", + document_type="DocType", + based_on="creation", + timespan="Last Year", + time_interval="Monthly", + filters_json="{}", + timeseries=1, + ) + ).insert() + + cur_date = datetime.now() - relativedelta(years=1) + + result = get(chart_name="Test Dashboard Chart", refresh=1) + + for idx in range(13): + month = get_last_day(cur_date) + month = formatdate(month.strftime("%Y-%m-%d")) + self.assertEqual(result.get("labels")[idx], get_period(month)) + cur_date += relativedelta(months=1) + + def test_empty_dashboard_chart(self): + if influxframework.db.exists("Dashboard Chart", "Test Empty Dashboard Chart"): + influxframework.delete_doc("Dashboard Chart", "Test Empty Dashboard Chart") + + influxframework.db.delete("Error Log") + + influxframework.get_doc( + dict( + doctype="Dashboard Chart", + chart_name="Test Empty Dashboard Chart", + chart_type="Count", + document_type="Error Log", + based_on="creation", + timespan="Last Year", + time_interval="Monthly", + filters_json="[]", + timeseries=1, + ) + ).insert() + + cur_date = datetime.now() - relativedelta(years=1) + + result = get(chart_name="Test Empty Dashboard Chart", refresh=1) + + for idx in range(13): + month = get_last_day(cur_date) + month = formatdate(month.strftime("%Y-%m-%d")) + self.assertEqual(result.get("labels")[idx], get_period(month)) + cur_date += relativedelta(months=1) + + def test_chart_wih_one_value(self): + if influxframework.db.exists("Dashboard Chart", "Test Empty Dashboard Chart 2"): + influxframework.delete_doc("Dashboard Chart", "Test Empty Dashboard Chart 2") + + influxframework.db.delete("Error Log") + + # create one data point + influxframework.get_doc(dict(doctype="Error Log", creation="2018-06-01 00:00:00")).insert() + + influxframework.get_doc( + dict( + doctype="Dashboard Chart", + chart_name="Test Empty Dashboard Chart 2", + chart_type="Count", + document_type="Error Log", + based_on="creation", + timespan="Last Year", + time_interval="Monthly", + filters_json="[]", + timeseries=1, + ) + ).insert() + + cur_date = datetime.now() - relativedelta(years=1) + + result = get(chart_name="Test Empty Dashboard Chart 2", refresh=1) + + for idx in range(13): + month = get_last_day(cur_date) + month = formatdate(month.strftime("%Y-%m-%d")) + self.assertEqual(result.get("labels")[idx], get_period(month)) + cur_date += relativedelta(months=1) + + # only 1 data point with value + self.assertEqual(result.get("datasets")[0].get("values")[2], 0) + + def test_group_by_chart_type(self): + if influxframework.db.exists("Dashboard Chart", "Test Group By Dashboard Chart"): + influxframework.delete_doc("Dashboard Chart", "Test Group By Dashboard Chart") + + influxframework.get_doc({"doctype": "ToDo", "description": "test"}).insert() + + influxframework.get_doc( + dict( + doctype="Dashboard Chart", + chart_name="Test Group By Dashboard Chart", + chart_type="Group By", + document_type="ToDo", + group_by_based_on="status", + filters_json="[]", + ) + ).insert() + + result = get(chart_name="Test Group By Dashboard Chart", refresh=1) + todo_status_count = influxframework.db.count("ToDo", {"status": result.get("labels")[0]}) + + self.assertEqual(result.get("datasets")[0].get("values")[0], todo_status_count) + + def test_daily_dashboard_chart(self): + insert_test_records() + + if influxframework.db.exists("Dashboard Chart", "Test Daily Dashboard Chart"): + influxframework.delete_doc("Dashboard Chart", "Test Daily Dashboard Chart") + + influxframework.get_doc( + dict( + doctype="Dashboard Chart", + chart_name="Test Daily Dashboard Chart", + chart_type="Sum", + document_type="Communication", + based_on="communication_date", + value_based_on="rating", + timespan="Select Date Range", + time_interval="Daily", + from_date=datetime(2019, 1, 6), + to_date=datetime(2019, 1, 11), + filters_json="[]", + timeseries=1, + ) + ).insert() + + result = get(chart_name="Test Daily Dashboard Chart", refresh=1) + + self.assertEqual(result.get("datasets")[0].get("values"), [200.0, 400.0, 300.0, 0.0, 100.0, 0.0]) + self.assertEqual( + result.get("labels"), + ["01-06-2019", "01-07-2019", "01-08-2019", "01-09-2019", "01-10-2019", "01-11-2019"], + ) + + def test_weekly_dashboard_chart(self): + insert_test_records() + + if influxframework.db.exists("Dashboard Chart", "Test Weekly Dashboard Chart"): + influxframework.delete_doc("Dashboard Chart", "Test Weekly Dashboard Chart") + + influxframework.get_doc( + dict( + doctype="Dashboard Chart", + chart_name="Test Weekly Dashboard Chart", + chart_type="Sum", + document_type="Communication", + based_on="communication_date", + value_based_on="rating", + timespan="Select Date Range", + time_interval="Weekly", + from_date=datetime(2018, 12, 30), + to_date=datetime(2019, 1, 15), + filters_json="[]", + timeseries=1, + ) + ).insert() + + with patch.object(influxframework.utils.data, "get_first_day_of_the_week", return_value="Monday"): + result = get(chart_name="Test Weekly Dashboard Chart", refresh=1) + + self.assertEqual(result.get("datasets")[0].get("values"), [50.0, 300.0, 800.0, 0.0]) + self.assertEqual(result.get("labels"), ["12-30-2018", "01-06-2019", "01-13-2019", "01-20-2019"]) + + def test_avg_dashboard_chart(self): + insert_test_records() + + if influxframework.db.exists("Dashboard Chart", "Test Average Dashboard Chart"): + influxframework.delete_doc("Dashboard Chart", "Test Average Dashboard Chart") + + influxframework.get_doc( + dict( + doctype="Dashboard Chart", + chart_name="Test Average Dashboard Chart", + chart_type="Average", + document_type="Communication", + based_on="communication_date", + value_based_on="rating", + timespan="Select Date Range", + time_interval="Weekly", + from_date=datetime(2018, 12, 30), + to_date=datetime(2019, 1, 15), + filters_json="[]", + timeseries=1, + ) + ).insert() + + with patch.object(influxframework.utils.data, "get_first_day_of_the_week", return_value="Monday"): + result = get(chart_name="Test Average Dashboard Chart", refresh=1) + self.assertEqual(result.get("labels"), ["12-30-2018", "01-06-2019", "01-13-2019", "01-20-2019"]) + self.assertEqual(result.get("datasets")[0].get("values"), [50.0, 150.0, 266.6666666666667, 0.0]) + + def test_user_date_label_dashboard_chart(self): + influxframework.delete_doc_if_exists("Dashboard Chart", "Test Dashboard Chart Date Label") + + influxframework.get_doc( + dict( + doctype="Dashboard Chart", + chart_name="Test Dashboard Chart Date Label", + chart_type="Count", + document_type="DocType", + based_on="creation", + timespan="Select Date Range", + time_interval="Weekly", + from_date=datetime(2018, 12, 30), + to_date=datetime(2019, 1, 15), + filters_json="[]", + timeseries=1, + ) + ).insert() + + with patch.object(influxframework.utils.data, "get_user_date_format", return_value="dd.mm.yyyy"): + result = get(chart_name="Test Dashboard Chart Date Label") + self.assertEqual( + sorted(result.get("labels")), sorted(["05.01.2019", "12.01.2019", "19.01.2019"]) + ) + + with patch.object(influxframework.utils.data, "get_user_date_format", return_value="mm-dd-yyyy"): + result = get(chart_name="Test Dashboard Chart Date Label") + self.assertEqual( + sorted(result.get("labels")), sorted(["01-19-2019", "01-05-2019", "01-12-2019"]) + ) + + +def insert_test_records(): + create_new_communication("Communication 1", datetime(2018, 12, 30), 50) + create_new_communication("Communication 2", datetime(2019, 1, 4), 100) + create_new_communication("Communication 3", datetime(2019, 1, 6), 200) + create_new_communication("Communication 4", datetime(2019, 1, 7), 400) + create_new_communication("Communication 5", datetime(2019, 1, 8), 300) + create_new_communication("Communication 6", datetime(2019, 1, 10), 100) + + +def create_new_communication(subject, date, rating): + communication = { + "doctype": "Communication", + "subject": subject, + "rating": rating, + "communication_date": date, + } + comm = influxframework.get_doc(communication) + if not influxframework.db.exists("Communication", {"subject": comm.subject}): + comm.insert() diff --git a/influxframework/desk/doctype/dashboard_chart_field/__init__.py b/influxframework/desk/doctype/dashboard_chart_field/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/dashboard_chart_field/dashboard_chart_field.json b/influxframework/desk/doctype/dashboard_chart_field/dashboard_chart_field.json new file mode 100644 index 0000000..6347be4 --- /dev/null +++ b/influxframework/desk/doctype/dashboard_chart_field/dashboard_chart_field.json @@ -0,0 +1,37 @@ +{ + "actions": [], + "creation": "2020-02-28 11:40:27.017380", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "y_field", + "color" + ], + "fields": [ + { + "fieldname": "y_field", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Y Field" + }, + { + "fieldname": "color", + "fieldtype": "Color", + "in_list_view": 1, + "label": "Color" + } + ], + "istable": 1, + "links": [], + "modified": "2020-02-28 11:48:24.731946", + "modified_by": "Administrator", + "module": "Desk", + "name": "Dashboard Chart Field", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/dashboard_chart_field/dashboard_chart_field.py b/influxframework/desk/doctype/dashboard_chart_field/dashboard_chart_field.py new file mode 100644 index 0000000..4eb12c4 --- /dev/null +++ b/influxframework/desk/doctype/dashboard_chart_field/dashboard_chart_field.py @@ -0,0 +1,9 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class DashboardChartField(Document): + pass diff --git a/influxframework/desk/doctype/dashboard_chart_link/__init__.py b/influxframework/desk/doctype/dashboard_chart_link/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/dashboard_chart_link/dashboard_chart_link.json b/influxframework/desk/doctype/dashboard_chart_link/dashboard_chart_link.json new file mode 100644 index 0000000..51b5ed3 --- /dev/null +++ b/influxframework/desk/doctype/dashboard_chart_link/dashboard_chart_link.json @@ -0,0 +1,41 @@ +{ + "actions": [], + "creation": "2019-03-12 15:00:57.052684", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "chart", + "width" + ], + "fields": [ + { + "columns": 8, + "fieldname": "chart", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Chart", + "options": "Dashboard Chart" + }, + { + "default": "Half", + "fieldname": "width", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Width", + "options": "Half\nFull" + } + ], + "istable": 1, + "links": [], + "modified": "2020-03-13 19:23:05.561687", + "modified_by": "Administrator", + "module": "Desk", + "name": "Dashboard Chart Link", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "ASC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/dashboard_chart_link/dashboard_chart_link.py b/influxframework/desk/doctype/dashboard_chart_link/dashboard_chart_link.py new file mode 100644 index 0000000..ebc3da7 --- /dev/null +++ b/influxframework/desk/doctype/dashboard_chart_link/dashboard_chart_link.py @@ -0,0 +1,9 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class DashboardChartLink(Document): + pass diff --git a/influxframework/desk/doctype/dashboard_chart_source/__init__.py b/influxframework/desk/doctype/dashboard_chart_source/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/dashboard_chart_source/dashboard_chart_source.js b/influxframework/desk/doctype/dashboard_chart_source/dashboard_chart_source.js new file mode 100644 index 0000000..3f563ed --- /dev/null +++ b/influxframework/desk/doctype/dashboard_chart_source/dashboard_chart_source.js @@ -0,0 +1,4 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Dashboard Chart Source", {}); diff --git a/influxframework/desk/doctype/dashboard_chart_source/dashboard_chart_source.json b/influxframework/desk/doctype/dashboard_chart_source/dashboard_chart_source.json new file mode 100644 index 0000000..fbe0ae9 --- /dev/null +++ b/influxframework/desk/doctype/dashboard_chart_source/dashboard_chart_source.json @@ -0,0 +1,69 @@ +{ + "actions": [], + "autoname": "field:source_name", + "creation": "2019-02-06 07:55:29.579840", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "source_name", + "module", + "timeseries" + ], + "fields": [ + { + "fieldname": "source_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Source Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "module", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Module", + "options": "Module Def", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "timeseries", + "fieldtype": "Check", + "label": "Timeseries" + } + ], + "links": [], + "modified": "2020-06-26 18:00:37.421491", + "modified_by": "Administrator", + "module": "Desk", + "name": "Dashboard Chart Source", + "owner": "Administrator", + "permissions": [ + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/dashboard_chart_source/dashboard_chart_source.py b/influxframework/desk/doctype/dashboard_chart_source/dashboard_chart_source.py new file mode 100644 index 0000000..90bb953 --- /dev/null +++ b/influxframework/desk/doctype/dashboard_chart_source/dashboard_chart_source.py @@ -0,0 +1,27 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +import os + +import influxframework +from influxframework.model.document import Document +from influxframework.modules import get_module_path, scrub +from influxframework.modules.export_file import export_to_files + + +@influxframework.whitelist() +def get_config(name): + doc = influxframework.get_doc("Dashboard Chart Source", name) + with open( + os.path.join( + get_module_path(doc.module), "dashboard_chart_source", scrub(doc.name), scrub(doc.name) + ".js" + ), + ) as f: + return f.read() + + +class DashboardChartSource(Document): + def on_update(self): + export_to_files( + record_list=[[self.doctype, self.name]], record_module=self.module, create_init=True + ) diff --git a/influxframework/desk/doctype/dashboard_chart_source/test_dashboard_chart_source.py b/influxframework/desk/doctype/dashboard_chart_source/test_dashboard_chart_source.py new file mode 100644 index 0000000..d3e5cb7 --- /dev/null +++ b/influxframework/desk/doctype/dashboard_chart_source/test_dashboard_chart_source.py @@ -0,0 +1,7 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestDashboardChartSource(InfluxFrameworkTestCase): + pass diff --git a/influxframework/desk/doctype/dashboard_settings/__init__.py b/influxframework/desk/doctype/dashboard_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/dashboard_settings/dashboard_settings.js b/influxframework/desk/doctype/dashboard_settings/dashboard_settings.js new file mode 100644 index 0000000..e7cec3a --- /dev/null +++ b/influxframework/desk/doctype/dashboard_settings/dashboard_settings.js @@ -0,0 +1,7 @@ +// Copyright (c) 2020, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Dashboard Settings", { + // refresh: function(frm) { + // } +}); diff --git a/influxframework/desk/doctype/dashboard_settings/dashboard_settings.json b/influxframework/desk/doctype/dashboard_settings/dashboard_settings.json new file mode 100644 index 0000000..e1eb75d --- /dev/null +++ b/influxframework/desk/doctype/dashboard_settings/dashboard_settings.json @@ -0,0 +1,51 @@ +{ + "actions": [], + "autoname": "Prompt", + "creation": "2020-03-31 19:41:45.785014", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user", + "chart_config" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "label": "User", + "options": "User", + "read_only": 1 + }, + { + "fieldname": "chart_config", + "fieldtype": "Code", + "label": "Chart Configuration", + "options": "JSON", + "read_only": 1 + } + ], + "in_create": 1, + "links": [], + "modified": "2020-04-01 00:07:26.489561", + "modified_by": "Administrator", + "module": "Desk", + "name": "Dashboard Settings", + "owner": "Administrator", + "permissions": [ + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1, + "write": 1 + } + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/dashboard_settings/dashboard_settings.py b/influxframework/desk/doctype/dashboard_settings/dashboard_settings.py new file mode 100644 index 0000000..a1536ad --- /dev/null +++ b/influxframework/desk/doctype/dashboard_settings/dashboard_settings.py @@ -0,0 +1,49 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +import influxframework + +# import influxframework +from influxframework.model.document import Document + + +class DashboardSettings(Document): + pass + + +@influxframework.whitelist() +def create_dashboard_settings(user): + if not influxframework.db.exists("Dashboard Settings", user): + doc = influxframework.new_doc("Dashboard Settings") + doc.name = user + doc.insert(ignore_permissions=True) + influxframework.db.commit() + return doc + + +def get_permission_query_conditions(user): + if not user: + user = influxframework.session.user + + return f"""(`tabDashboard Settings`.name = {influxframework.db.escape(user)})""" + + +@influxframework.whitelist() +def save_chart_config(reset, config, chart_name): + reset = influxframework.parse_json(reset) + doc = influxframework.get_doc("Dashboard Settings", influxframework.session.user) + chart_config = influxframework.parse_json(doc.chart_config) or {} + + if reset: + chart_config[chart_name] = {} + else: + config = influxframework.parse_json(config) + if not chart_name in chart_config: + chart_config[chart_name] = {} + chart_config[chart_name].update(config) + + influxframework.db.set_value( + "Dashboard Settings", influxframework.session.user, "chart_config", json.dumps(chart_config) + ) diff --git a/influxframework/desk/doctype/desktop_icon/__init__.py b/influxframework/desk/doctype/desktop_icon/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/desktop_icon/desktop_icon.js b/influxframework/desk/doctype/desktop_icon/desktop_icon.js new file mode 100644 index 0000000..a1942dd --- /dev/null +++ b/influxframework/desk/doctype/desktop_icon/desktop_icon.js @@ -0,0 +1,6 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Desktop Icon", { + refresh: function (frm) {}, +}); diff --git a/influxframework/desk/doctype/desktop_icon/desktop_icon.json b/influxframework/desk/doctype/desktop_icon/desktop_icon.json new file mode 100644 index 0000000..59c9595 --- /dev/null +++ b/influxframework/desk/doctype/desktop_icon/desktop_icon.json @@ -0,0 +1,736 @@ +{ + "allow_copy": 0, + "allow_events_in_timeline": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2016-02-22 03:47:45.387068", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 0, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "module_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Module Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "label", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Label", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "standard", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Standard", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "custom", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Custom", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_3", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "app", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "App", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "description", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Description", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "category", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Category", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "hidden", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Hidden", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "blocked", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Blocked", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "force_show", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Force Show", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "section_break_7", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "type", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Type", + "length": 0, + "no_copy": 0, + "options": "module\nlist\nlink\npage\nquery-report", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "_doctype", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "_doctype", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "_report", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "_report", + "length": 0, + "no_copy": 0, + "options": "Report", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "link", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Link", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_10", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "color", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Color", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "icon", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Icon", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reverse", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Reverse Icon Color", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "idx", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Idx", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 1, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2019-01-24 04:58:58.720618", + "modified_by": "Administrator", + "module": "Desk", + "name": "Desktop Icon", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 1, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "module_name", + "track_changes": 1, + "track_seen": 0, + "track_views": 0 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/desktop_icon/desktop_icon.py b/influxframework/desk/doctype/desktop_icon/desktop_icon.py new file mode 100644 index 0000000..70a7170 --- /dev/null +++ b/influxframework/desk/doctype/desktop_icon/desktop_icon.py @@ -0,0 +1,551 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import json +import random + +import influxframework +from influxframework import _ +from influxframework.model.document import Document +from influxframework.utils.user import UserPermissions + + +class DesktopIcon(Document): + def validate(self): + if not self.label: + self.label = self.module_name + + def on_trash(self): + clear_desktop_icons_cache() + + +def after_doctype_insert(): + influxframework.db.add_unique("Desktop Icon", ("module_name", "owner", "standard")) + + +def get_desktop_icons(user=None): + """Return desktop icons for user""" + if not user: + user = influxframework.session.user + + user_icons = influxframework.cache().hget("desktop_icons", user) + + if not user_icons: + fields = [ + "module_name", + "hidden", + "label", + "link", + "type", + "icon", + "color", + "description", + "category", + "_doctype", + "_report", + "idx", + "force_show", + "reverse", + "custom", + "standard", + "blocked", + ] + + active_domains = influxframework.get_active_domains() + + blocked_doctypes = influxframework.get_all( + "DocType", + filters={"ifnull(restrict_to_domain, '')": ("not in", ",".join(active_domains))}, + fields=["name"], + ) + + blocked_doctypes = [d.get("name") for d in blocked_doctypes] + + standard_icons = influxframework.get_all("Desktop Icon", fields=fields, filters={"standard": 1}) + + standard_map = {} + for icon in standard_icons: + if icon._doctype in blocked_doctypes: + icon.blocked = 1 + standard_map[icon.module_name] = icon + + user_icons = influxframework.get_all( + "Desktop Icon", fields=fields, filters={"standard": 0, "owner": user} + ) + + # update hidden property + for icon in user_icons: + standard_icon = standard_map.get(icon.module_name, None) + + if icon._doctype in blocked_doctypes: + icon.blocked = 1 + + # override properties from standard icon + if standard_icon: + for key in ("route", "label", "color", "icon", "link"): + if standard_icon.get(key): + icon[key] = standard_icon.get(key) + + if standard_icon.blocked: + icon.hidden = 1 + + # flag for modules_select dialog + icon.hidden_in_standard = 1 + + elif standard_icon.force_show: + icon.hidden = 0 + + # add missing standard icons (added via new install apps?) + user_icon_names = [icon.module_name for icon in user_icons] + for standard_icon in standard_icons: + if standard_icon.module_name not in user_icon_names: + + # if blocked, hidden too! + if standard_icon.blocked: + standard_icon.hidden = 1 + standard_icon.hidden_in_standard = 1 + + user_icons.append(standard_icon) + + user_blocked_modules = influxframework.get_doc("User", user).get_blocked_modules() + for icon in user_icons: + if icon.module_name in user_blocked_modules: + icon.hidden = 1 + + # sort by idx + user_icons.sort(key=lambda a: a.idx) + + # translate + for d in user_icons: + if d.label: + d.label = _(d.label) + + influxframework.cache().hset("desktop_icons", user, user_icons) + + return user_icons + + +@influxframework.whitelist() +def add_user_icon(_doctype, _report=None, label=None, link=None, type="link", standard=0): + """Add a new user desktop icon to the desktop""" + + if not label: + label = _doctype or _report + if not link: + link = f"List/{_doctype}" + + # find if a standard icon exists + icon_name = influxframework.db.exists( + "Desktop Icon", {"standard": standard, "link": link, "owner": influxframework.session.user} + ) + + if icon_name: + if influxframework.db.get_value("Desktop Icon", icon_name, "hidden"): + # if it is hidden, unhide it + influxframework.db.set_value("Desktop Icon", icon_name, "hidden", 0) + clear_desktop_icons_cache() + + else: + idx = ( + influxframework.db.sql("select max(idx) from `tabDesktop Icon` where owner=%s", influxframework.session.user)[0][ + 0 + ] + or influxframework.db.sql("select count(*) from `tabDesktop Icon` where standard=1")[0][0] + ) + + if not influxframework.db.get_value("Report", _report): + _report = None + userdefined_icon = influxframework.db.get_value( + "DocType", _doctype, ["icon", "color", "module"], as_dict=True + ) + else: + userdefined_icon = influxframework.db.get_value( + "Report", _report, ["icon", "color", "module"], as_dict=True + ) + + module_icon = influxframework.get_value( + "Desktop Icon", + {"standard": 1, "module_name": userdefined_icon.module}, + ["name", "icon", "color", "reverse"], + as_dict=True, + ) + + if not module_icon: + module_icon = influxframework._dict() + opts = random.choice(palette) + module_icon.color = opts[0] + module_icon.reverse = 0 if (len(opts) > 1) else 1 + + try: + new_icon = influxframework.get_doc( + { + "doctype": "Desktop Icon", + "label": label, + "module_name": label, + "link": link, + "type": type, + "_doctype": _doctype, + "_report": _report, + "icon": userdefined_icon.icon or module_icon.icon, + "color": userdefined_icon.color or module_icon.color, + "reverse": module_icon.reverse, + "idx": idx + 1, + "custom": 1, + "standard": standard, + } + ).insert(ignore_permissions=True) + clear_desktop_icons_cache() + + icon_name = new_icon.name + + except influxframework.UniqueValidationError as e: + influxframework.throw(_("Desktop Icon already exists")) + except Exception as e: + raise e + + return icon_name + + +@influxframework.whitelist() +def set_order(new_order, user=None): + """set new order by duplicating user icons (if user is set) or set global order""" + if isinstance(new_order, str): + new_order = json.loads(new_order) + for i, module_name in enumerate(new_order): + if module_name not in ("Explore",): + if user: + icon = get_user_copy(module_name, user) + else: + name = influxframework.db.get_value("Desktop Icon", {"standard": 1, "module_name": module_name}) + if name: + icon = influxframework.get_doc("Desktop Icon", name) + else: + # standard icon missing, create one for DocType + name = add_user_icon(module_name, standard=1) + icon = influxframework.get_doc("Desktop Icon", name) + + icon.db_set("idx", i) + + clear_desktop_icons_cache() + + +def set_desktop_icons(visible_list, ignore_duplicate=True): + """Resets all lists and makes only the given one standard, + if the desktop icon does not exist and the name is a DocType, then will create + an icon for the doctype""" + + # clear all custom only if setup is not complete + if not int(influxframework.defaults.get_defaults().setup_complete or 0): + influxframework.db.delete("Desktop Icon", {"standard": 0}) + + # set standard as blocked and hidden if setting first active domain + if not influxframework.flags.keep_desktop_icons: + influxframework.db.sql("update `tabDesktop Icon` set blocked=0, hidden=1 where standard=1") + + # set as visible if present, or add icon + for module_name in visible_list: + name = influxframework.db.get_value("Desktop Icon", {"module_name": module_name}) + if name: + influxframework.db.set_value("Desktop Icon", name, "hidden", 0) + else: + if influxframework.db.exists("DocType", module_name): + try: + add_user_icon(module_name, standard=1) + except influxframework.UniqueValidationError as e: + if not ignore_duplicate: + raise e + else: + visible_list.remove(module_name) + if influxframework.message_log: + influxframework.message_log.pop() + + # set the order + set_order(visible_list) + + clear_desktop_icons_cache() + + +def set_hidden_list(hidden_list, user=None): + """Sets property `hidden`=1 in **Desktop Icon** for given user. + If user is None then it will set global values. + It will also set the rest of the icons as shown (`hidden` = 0)""" + if isinstance(hidden_list, str): + hidden_list = json.loads(hidden_list) + + # set as hidden + for module_name in hidden_list: + set_hidden(module_name, user, 1) + + # set as seen + for module_name in list(set(get_all_icons()) - set(hidden_list)): + set_hidden(module_name, user, 0) + + if user: + clear_desktop_icons_cache() + else: + influxframework.clear_cache() + + +def set_hidden(module_name, user=None, hidden=1): + """Set module hidden property for given user. If user is not specified, + hide/unhide it globally""" + if user: + icon = get_user_copy(module_name, user) + + if hidden and icon.custom: + influxframework.delete_doc(icon.doctype, icon.name, ignore_permissions=True) + return + + # hidden by user + icon.db_set("hidden", hidden) + else: + icon = influxframework.get_doc("Desktop Icon", {"standard": 1, "module_name": module_name}) + + # blocked is globally hidden + icon.db_set("blocked", hidden) + + +def get_all_icons(): + return [ + d.module_name + for d in influxframework.get_all("Desktop Icon", filters={"standard": 1}, fields=["module_name"]) + ] + + +def clear_desktop_icons_cache(user=None): + influxframework.cache().hdel("desktop_icons", user or influxframework.session.user) + influxframework.cache().hdel("bootinfo", user or influxframework.session.user) + + +def get_user_copy(module_name, user=None): + """Return user copy (Desktop Icon) of the given module_name. If user copy does not exist, create one. + + :param module_name: Name of the module + :param user: User for which the copy is required (optional) + """ + if not user: + user = influxframework.session.user + + desktop_icon_name = influxframework.db.get_value( + "Desktop Icon", {"module_name": module_name, "owner": user, "standard": 0} + ) + + if desktop_icon_name: + return influxframework.get_doc("Desktop Icon", desktop_icon_name) + else: + return make_user_copy(module_name, user) + + +def make_user_copy(module_name, user): + """Insert and return the user copy of a standard Desktop Icon""" + standard_name = influxframework.db.get_value("Desktop Icon", {"module_name": module_name, "standard": 1}) + + if not standard_name: + influxframework.throw(_("{0} not found").format(module_name), influxframework.DoesNotExistError) + + original = influxframework.get_doc("Desktop Icon", standard_name) + + desktop_icon = influxframework.get_doc( + {"doctype": "Desktop Icon", "standard": 0, "owner": user, "module_name": module_name} + ) + + for key in ( + "app", + "label", + "route", + "type", + "_doctype", + "idx", + "reverse", + "force_show", + "link", + "icon", + "color", + ): + if original.get(key): + desktop_icon.set(key, original.get(key)) + + desktop_icon.insert(ignore_permissions=True) + + return desktop_icon + + +def sync_desktop_icons(): + """Sync desktop icons from all apps""" + for app in influxframework.get_installed_apps(): + sync_from_app(app) + + +def sync_from_app(app): + """Sync desktop icons from app. To be called during install""" + try: + modules = influxframework.get_attr(app + ".config.desktop.get_data")() or {} + except ImportError: + return [] + + if isinstance(modules, dict): + modules_list = [] + for m, desktop_icon in modules.items(): + desktop_icon["module_name"] = m + modules_list.append(desktop_icon) + else: + modules_list = modules + + for i, m in enumerate(modules_list): + desktop_icon_name = influxframework.db.get_value( + "Desktop Icon", {"module_name": m["module_name"], "app": app, "standard": 1} + ) + if desktop_icon_name: + desktop_icon = influxframework.get_doc("Desktop Icon", desktop_icon_name) + else: + # new icon + desktop_icon = influxframework.get_doc( + {"doctype": "Desktop Icon", "idx": i, "standard": 1, "app": app, "owner": "Administrator"} + ) + + if "doctype" in m: + m["_doctype"] = m.pop("doctype") + + desktop_icon.update(m) + try: + desktop_icon.save() + except influxframework.exceptions.UniqueValidationError: + pass + + return modules_list + + +@influxframework.whitelist() +def update_icons(hidden_list, user=None): + """update modules""" + if not user: + influxframework.only_for("System Manager") + + set_hidden_list(hidden_list, user) + influxframework.msgprint(influxframework._("Updated"), indicator="green", title=_("Success"), alert=True) + + +def get_context(context): + context.icons = get_user_icons(influxframework.session.user) + context.user = influxframework.session.user + + if "System Manager" in influxframework.get_roles(): + context.users = influxframework.get_all( + "User", + filters={"user_type": "System User", "enabled": 1}, + fields=["name", "first_name", "last_name"], + ) + + +@influxframework.whitelist() +def get_module_icons(user=None): + if user != influxframework.session.user: + influxframework.only_for("System Manager") + + if not user: + icons = influxframework.get_all("Desktop Icon", fields="*", filters={"standard": 1}, order_by="idx") + else: + influxframework.cache().hdel("desktop_icons", user) + icons = get_user_icons(user) + + for icon in icons: + icon.value = influxframework.db.escape(_(icon.label or icon.module_name)) + + return {"icons": icons, "user": user} + + +def get_user_icons(user): + """Get user icons for module setup page""" + user_perms = UserPermissions(user) + user_perms.build_permissions() + + from influxframework.boot import get_allowed_pages + + allowed_pages = get_allowed_pages() + + icons = [] + for icon in get_desktop_icons(user): + add = True + if icon.hidden_in_standard: + add = False + + if not icon.custom: + if icon.module_name == ["Help", "Settings"]: + pass + + elif icon.type == "page" and icon.link not in allowed_pages: + add = False + + elif icon.type == "module" and icon.module_name not in user_perms.allow_modules: + add = False + + if add: + icons.append(icon) + + return icons + + +palette = ( + ("#FFC4C4",), + ("#FFE8CD",), + ("#FFD2C2",), + ("#FF8989",), + ("#FFD19C",), + ("#FFA685",), + ("#FF4D4D", 1), + ("#FFB868",), + ("#FF7846", 1), + ("#A83333", 1), + ("#A87945", 1), + ("#A84F2E", 1), + ("#D2D2FF",), + ("#F8D4F8",), + ("#DAC7FF",), + ("#A3A3FF",), + ("#F3AAF0",), + ("#B592FF",), + ("#7575FF", 1), + ("#EC7DEA", 1), + ("#8E58FF", 1), + ("#4D4DA8", 1), + ("#934F92", 1), + ("#5E3AA8", 1), + ("#EBF8CC",), + ("#FFD7D7",), + ("#D2F8ED",), + ("#D9F399",), + ("#FFB1B1",), + ("#A4F3DD",), + ("#C5EC63",), + ("#FF8989", 1), + ("#77ECCA",), + ("#7B933D", 1), + ("#A85B5B", 1), + ("#49937E", 1), + ("#FFFACD",), + ("#D2F1FF",), + ("#CEF6D1",), + ("#FFF69C",), + ("#A6E4FF",), + ("#9DECA2",), + ("#FFF168",), + ("#78D6FF",), + ("#6BE273",), + ("#A89F45", 1), + ("#4F8EA8", 1), + ("#428B46", 1), +) + + +@influxframework.whitelist() +def hide(name, user=None): + if not user: + user = influxframework.session.user + + try: + set_hidden(name, user, hidden=1) + clear_desktop_icons_cache() + except Exception: + return False + + return True diff --git a/influxframework/desk/doctype/event/README.md b/influxframework/desk/doctype/event/README.md new file mode 100644 index 0000000..571c78c --- /dev/null +++ b/influxframework/desk/doctype/event/README.md @@ -0,0 +1 @@ +Calendar Event \ No newline at end of file diff --git a/influxframework/desk/doctype/event/__init__.py b/influxframework/desk/doctype/event/__init__.py new file mode 100644 index 0000000..2e11a12 --- /dev/null +++ b/influxframework/desk/doctype/event/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE diff --git a/influxframework/desk/doctype/event/event.js b/influxframework/desk/doctype/event/event.js new file mode 100644 index 0000000..2f91b99 --- /dev/null +++ b/influxframework/desk/doctype/event/event.js @@ -0,0 +1,101 @@ +// Copyright (c) 2015, InfluxFramework LLC +// MIT License. See license.txt +influxframework.provide("influxframework.desk"); + +influxframework.ui.form.on("Event", { + onload: function (frm) { + frm.set_query("reference_doctype", "event_participants", function () { + return { + filters: { + issingle: 0, + }, + }; + }); + frm.set_query("google_calendar", function () { + return { + filters: { + owner: influxframework.session.user, + }, + }; + }); + }, + refresh: function (frm) { + if (frm.doc.event_participants) { + frm.doc.event_participants.forEach((value) => { + frm.add_custom_button( + __(value.reference_docname), + function () { + influxframework.set_route("Form", value.reference_doctype, value.reference_docname); + }, + __("Participants") + ); + }); + } + + frm.page.set_inner_btn_group_as_primary(__("Add Participants")); + + frm.add_custom_button( + __("Add Contacts"), + function () { + new influxframework.desk.eventParticipants(frm, "Contact"); + }, + __("Add Participants") + ); + }, + repeat_on: function (frm) { + if (frm.doc.repeat_on === "Every Day") { + ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"].map( + function (v) { + frm.set_value(v, 1); + } + ); + } + }, +}); + +influxframework.ui.form.on("Event Participants", { + event_participants_remove: function (frm, cdt, cdn) { + if (cdt && !cdn.includes("New Event Participants")) { + influxframework.call({ + type: "POST", + method: "influxframework.desk.doctype.event.event.delete_communication", + args: { + event: frm.doc, + reference_doctype: cdt, + reference_docname: cdn, + }, + freeze: true, + callback: function (r) { + if (r.exc) { + influxframework.show_alert({ + message: __("{0}", [r.exc]), + indicator: "orange", + }); + } + }, + }); + } + }, +}); + +influxframework.desk.eventParticipants = class eventParticipants { + constructor(frm, doctype) { + this.frm = frm; + this.doctype = doctype; + this.make(); + } + + make() { + let me = this; + + let table = me.frm.get_field("event_participants").grid; + new influxframework.ui.form.LinkSelector({ + doctype: me.doctype, + dynamic_link_field: "reference_doctype", + dynamic_link_reference: me.doctype, + fieldname: "reference_docname", + target: table, + txt: "", + }); + } +}; diff --git a/influxframework/desk/doctype/event/event.json b/influxframework/desk/doctype/event/event.json new file mode 100644 index 0000000..cb2e42a --- /dev/null +++ b/influxframework/desk/doctype/event/event.json @@ -0,0 +1,321 @@ +{ + "actions": [], + "allow_import": 1, + "autoname": "EV.#####", + "creation": "2013-06-10 13:17:47", + "doctype": "DocType", + "document_type": "Document", + "email_append_to": 1, + "engine": "InnoDB", + "field_order": [ + "details", + "subject", + "event_category", + "event_type", + "color", + "send_reminder", + "repeat_this_event", + "column_break_4", + "starts_on", + "ends_on", + "status", + "sender", + "all_day", + "sync_with_google_calendar", + "sb_00", + "google_calendar", + "pulled_from_google_calendar", + "cb_00", + "google_calendar_id", + "google_calendar_event_id", + "section_break_13", + "repeat_on", + "repeat_till", + "column_break_16", + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", + "section_break_8", + "description", + "participants", + "event_participants" + ], + "fields": [ + { + "fieldname": "details", + "fieldtype": "Section Break", + "label": "Details", + "oldfieldtype": "Section Break" + }, + { + "fieldname": "subject", + "fieldtype": "Small Text", + "in_global_search": 1, + "in_list_view": 1, + "label": "Subject", + "reqd": 1 + }, + { + "fieldname": "event_category", + "fieldtype": "Select", + "label": "Event Category", + "options": "Event\nMeeting\nCall\nSent/Received Email\nOther" + }, + { + "fieldname": "event_type", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Event Type", + "oldfieldname": "event_type", + "oldfieldtype": "Select", + "options": "Private\nPublic\nCancelled", + "reqd": 1, + "search_index": 1 + }, + { + "default": "1", + "fieldname": "send_reminder", + "fieldtype": "Check", + "label": "Send an email reminder in the morning" + }, + { + "default": "0", + "fieldname": "repeat_this_event", + "fieldtype": "Check", + "label": "Repeat this Event" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "starts_on", + "fieldtype": "Datetime", + "label": "Starts on", + "reqd": 1 + }, + { + "fieldname": "ends_on", + "fieldtype": "Datetime", + "label": "Ends on" + }, + { + "default": "0", + "fieldname": "all_day", + "fieldtype": "Check", + "label": "All Day" + }, + { + "depends_on": "repeat_this_event", + "fieldname": "section_break_13", + "fieldtype": "Section Break" + }, + { + "depends_on": "repeat_this_event", + "fieldname": "repeat_on", + "fieldtype": "Select", + "in_global_search": 1, + "label": "Repeat On", + "options": "\nDaily\nWeekly\nMonthly\nYearly" + }, + { + "depends_on": "repeat_this_event", + "description": "Leave blank to repeat always", + "fieldname": "repeat_till", + "fieldtype": "Date", + "label": "Repeat Till" + }, + { + "fieldname": "column_break_16", + "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "eval:doc.repeat_this_event && doc.repeat_on===\"Weekly\"", + "fieldname": "monday", + "fieldtype": "Check", + "label": "Monday" + }, + { + "default": "0", + "depends_on": "eval:doc.repeat_this_event && doc.repeat_on===\"Weekly\"", + "fieldname": "tuesday", + "fieldtype": "Check", + "label": "Tuesday" + }, + { + "default": "0", + "depends_on": "eval:doc.repeat_this_event && doc.repeat_on===\"Weekly\"", + "fieldname": "wednesday", + "fieldtype": "Check", + "label": "Wednesday" + }, + { + "default": "0", + "depends_on": "eval:doc.repeat_this_event && doc.repeat_on===\"Weekly\"", + "fieldname": "thursday", + "fieldtype": "Check", + "label": "Thursday" + }, + { + "default": "0", + "depends_on": "eval:doc.repeat_this_event && doc.repeat_on===\"Weekly\"", + "fieldname": "friday", + "fieldtype": "Check", + "label": "Friday" + }, + { + "default": "0", + "depends_on": "eval:doc.repeat_this_event && doc.repeat_on===\"Weekly\"", + "fieldname": "saturday", + "fieldtype": "Check", + "label": "Saturday" + }, + { + "default": "0", + "depends_on": "eval:doc.repeat_this_event && doc.repeat_on===\"Weekly\"", + "fieldname": "sunday", + "fieldtype": "Check", + "label": "Sunday" + }, + { + "fieldname": "section_break_8", + "fieldtype": "Section Break" + }, + { + "fieldname": "color", + "fieldtype": "Color", + "label": "Color" + }, + { + "fieldname": "description", + "fieldtype": "Text Editor", + "in_global_search": 1, + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Text", + "print_width": "300px", + "width": "300px" + }, + { + "fieldname": "participants", + "fieldtype": "Section Break", + "label": "Participants", + "oldfieldtype": "Section Break" + }, + { + "fieldname": "event_participants", + "fieldtype": "Table", + "label": "Event Participants", + "options": "Event Participants" + }, + { + "default": "Open", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "Open\nCompleted\nClosed" + }, + { + "collapsible": 1, + "depends_on": "eval:doc.sync_with_google_calendar", + "fieldname": "sb_00", + "fieldtype": "Section Break", + "label": "Google Calendar" + }, + { + "fetch_from": "google_calendar.google_calendar_id", + "fieldname": "google_calendar_id", + "fieldtype": "Data", + "label": "Google Calendar ID", + "read_only": 1 + }, + { + "fieldname": "cb_00", + "fieldtype": "Column Break" + }, + { + "fieldname": "google_calendar_event_id", + "fieldtype": "Data", + "label": "Google Calendar Event ID", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "sync_with_google_calendar", + "fieldtype": "Check", + "label": "Sync with Google Calendar" + }, + { + "fieldname": "google_calendar", + "fieldtype": "Link", + "label": "Google Calendar", + "options": "Google Calendar" + }, + { + "default": "0", + "fieldname": "pulled_from_google_calendar", + "fieldtype": "Check", + "label": "Pulled from Google Calendar", + "read_only": 1 + }, + { + "fieldname": "sender", + "fieldtype": "Data", + "label": "Sender", + "options": "Email", + "read_only": 1 + } + ], + "icon": "fa fa-calendar", + "idx": 1, + "links": [], + "modified": "2022-05-12 05:43:27.935510", + "modified_by": "Administrator", + "module": "Desk", + "name": "Event", + "naming_rule": "Expression (old style)", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "read_only": 1, + "sender_field": "sender", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "subject_field": "subject", + "title_field": "subject", + "track_changes": 1, + "track_seen": 1, + "track_views": 1 +} diff --git a/influxframework/desk/doctype/event/event.py b/influxframework/desk/doctype/event/event.py new file mode 100644 index 0000000..66ee928 --- /dev/null +++ b/influxframework/desk/doctype/event/event.py @@ -0,0 +1,429 @@ +# Copyright (c) 2018, InfluxFramework LLC +# License: MIT. See LICENSE + + +import json + +import influxframework +from influxframework import _ +from influxframework.desk.doctype.notification_settings.notification_settings import ( + is_email_notifications_enabled_for_type, +) +from influxframework.desk.reportview import get_filters_cond +from influxframework.model.document import Document +from influxframework.utils import ( + add_days, + add_months, + cint, + cstr, + date_diff, + format_datetime, + get_datetime_str, + getdate, + now_datetime, + nowdate, +) +from influxframework.utils.user import get_enabled_system_users + +weekdays = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"] +communication_mapping = { + "": "Event", + "Event": "Event", + "Meeting": "Meeting", + "Call": "Phone", + "Sent/Received Email": "Email", + "Other": "Other", +} + + +class Event(Document): + def validate(self): + if not self.starts_on: + self.starts_on = now_datetime() + + # if start == end this scenario doesn't make sense i.e. it starts and ends at the same second! + self.ends_on = None if self.starts_on == self.ends_on else self.ends_on + + if self.starts_on and self.ends_on: + self.validate_from_to_dates("starts_on", "ends_on") + + if ( + self.repeat_on == "Daily" and self.ends_on and getdate(self.starts_on) != getdate(self.ends_on) + ): + influxframework.throw(_("Daily Events should finish on the Same Day.")) + + if self.sync_with_google_calendar and not self.google_calendar: + influxframework.throw(_("Select Google Calendar to which event should be synced.")) + + def on_update(self): + self.sync_communication() + + def on_trash(self): + communications = influxframework.get_all( + "Communication", dict(reference_doctype=self.doctype, reference_name=self.name) + ) + if communications: + for communication in communications: + influxframework.delete_doc_if_exists("Communication", communication.name) + + def sync_communication(self): + if self.event_participants: + for participant in self.event_participants: + filters = [ + ["Communication", "reference_doctype", "=", self.doctype], + ["Communication", "reference_name", "=", self.name], + ["Communication Link", "link_doctype", "=", participant.reference_doctype], + ["Communication Link", "link_name", "=", participant.reference_docname], + ] + comms = influxframework.get_all("Communication", filters=filters, fields=["name"]) + + if comms: + for comm in comms: + communication = influxframework.get_doc("Communication", comm.name) + self.update_communication(participant, communication) + else: + meta = influxframework.get_meta(participant.reference_doctype) + if hasattr(meta, "allow_events_in_timeline") and meta.allow_events_in_timeline == 1: + self.create_communication(participant) + + def create_communication(self, participant): + communication = influxframework.new_doc("Communication") + self.update_communication(participant, communication) + self.communication = communication.name + + def update_communication(self, participant, communication): + communication.communication_medium = "Event" + communication.subject = self.subject + communication.content = self.description if self.description else self.subject + communication.communication_date = self.starts_on + communication.sender = self.owner + communication.sender_full_name = influxframework.utils.get_fullname(self.owner) + communication.reference_doctype = self.doctype + communication.reference_name = self.name + communication.communication_medium = ( + communication_mapping.get(self.event_category) if self.event_category else "" + ) + communication.status = "Linked" + communication.add_link(participant.reference_doctype, participant.reference_docname) + communication.save(ignore_permissions=True) + + def add_participant(self, doctype, docname): + """Add a single participant to event participants + + Args: + doctype (string): Reference Doctype + docname (string): Reference Docname + """ + self.append( + "event_participants", + { + "reference_doctype": doctype, + "reference_docname": docname, + }, + ) + + def add_participants(self, participants): + """Add participant entry + + Args: + participants ([Array]): Array of a dict with doctype and docname + """ + for participant in participants: + self.add_participant(participant["doctype"], participant["docname"]) + + +@influxframework.whitelist() +def delete_communication(event, reference_doctype, reference_docname): + deleted_participant = influxframework.get_doc(reference_doctype, reference_docname) + if isinstance(event, str): + event = json.loads(event) + + filters = [ + ["Communication", "reference_doctype", "=", event.get("doctype")], + ["Communication", "reference_name", "=", event.get("name")], + ["Communication Link", "link_doctype", "=", deleted_participant.reference_doctype], + ["Communication Link", "link_name", "=", deleted_participant.reference_docname], + ] + + comms = influxframework.get_list("Communication", filters=filters, fields=["name"]) + + if comms: + deletion = [] + for comm in comms: + delete = influxframework.get_doc("Communication", comm.name).delete() + deletion.append(delete) + + return deletion + + return {} + + +def get_permission_query_conditions(user): + if not user: + user = influxframework.session.user + return """(`tabEvent`.`event_type`='Public' or `tabEvent`.`owner`={user})""".format( + user=influxframework.db.escape(user), + ) + + +def has_permission(doc, user): + if doc.event_type == "Public" or doc.owner == user: + return True + + return False + + +def send_event_digest(): + today = nowdate() + + # select only those users that have event reminder email notifications enabled + users = [ + user + for user in get_enabled_system_users() + if is_email_notifications_enabled_for_type(user.name, "Event Reminders") + ] + + for user in users: + events = get_events(today, today, user.name, for_reminder=True) + if events: + influxframework.set_user_lang(user.name, user.language) + + for e in events: + e.starts_on = format_datetime(e.starts_on, "hh:mm a") + if e.all_day: + e.starts_on = "All Day" + + influxframework.sendmail( + recipients=user.email, + subject=influxframework._("Upcoming Events for Today"), + template="upcoming_events", + args={ + "events": events, + }, + header=[influxframework._("Events in Today's Calendar"), "blue"], + ) + + +@influxframework.whitelist() +def get_events(start, end, user=None, for_reminder=False, filters=None): + if not user: + user = influxframework.session.user + + if isinstance(filters, str): + filters = json.loads(filters) + + filter_condition = get_filters_cond("Event", filters, []) + + tables = ["`tabEvent`"] + if "`tabEvent Participants`" in filter_condition: + tables.append("`tabEvent Participants`") + + events = influxframework.db.sql( + """ + SELECT `tabEvent`.name, + `tabEvent`.subject, + `tabEvent`.description, + `tabEvent`.color, + `tabEvent`.starts_on, + `tabEvent`.ends_on, + `tabEvent`.owner, + `tabEvent`.all_day, + `tabEvent`.event_type, + `tabEvent`.repeat_this_event, + `tabEvent`.repeat_on, + `tabEvent`.repeat_till, + `tabEvent`.monday, + `tabEvent`.tuesday, + `tabEvent`.wednesday, + `tabEvent`.thursday, + `tabEvent`.friday, + `tabEvent`.saturday, + `tabEvent`.sunday + FROM {tables} + WHERE ( + ( + (date(`tabEvent`.starts_on) BETWEEN date(%(start)s) AND date(%(end)s)) + OR (date(`tabEvent`.ends_on) BETWEEN date(%(start)s) AND date(%(end)s)) + OR ( + date(`tabEvent`.starts_on) <= date(%(start)s) + AND date(`tabEvent`.ends_on) >= date(%(end)s) + ) + ) + OR ( + date(`tabEvent`.starts_on) <= date(%(start)s) + AND `tabEvent`.repeat_this_event=1 + AND coalesce(`tabEvent`.repeat_till, '3000-01-01') > date(%(start)s) + ) + ) + {reminder_condition} + {filter_condition} + AND ( + `tabEvent`.event_type='Public' + OR `tabEvent`.owner=%(user)s + OR EXISTS( + SELECT `tabDocShare`.name + FROM `tabDocShare` + WHERE `tabDocShare`.share_doctype='Event' + AND `tabDocShare`.share_name=`tabEvent`.name + AND `tabDocShare`.user=%(user)s + ) + ) + AND `tabEvent`.status='Open' + ORDER BY `tabEvent`.starts_on""".format( + tables=", ".join(tables), + filter_condition=filter_condition, + reminder_condition="AND coalesce(`tabEvent`.send_reminder, 0)=1" if for_reminder else "", + ), + { + "start": start, + "end": end, + "user": user, + }, + as_dict=1, + ) + + # process recurring events + start = start.split(" ")[0] + end = end.split(" ")[0] + add_events = [] + remove_events = [] + + def add_event(e, date): + new_event = e.copy() + + enddate = ( + add_days(date, int(date_diff(e.ends_on.split(" ")[0], e.starts_on.split(" ")[0]))) + if (e.starts_on and e.ends_on) + else date + ) + + new_event.starts_on = date + " " + e.starts_on.split(" ")[1] + new_event.ends_on = new_event.ends_on = ( + enddate + " " + e.ends_on.split(" ")[1] if e.ends_on else None + ) + + add_events.append(new_event) + + for e in events: + if e.repeat_this_event: + e.starts_on = get_datetime_str(e.starts_on) + e.ends_on = get_datetime_str(e.ends_on) if e.ends_on else None + + event_start, time_str = get_datetime_str(e.starts_on).split(" ") + + repeat = "3000-01-01" if cstr(e.repeat_till) == "" else e.repeat_till + + if e.repeat_on == "Yearly": + start_year = cint(start.split("-")[0]) + end_year = cint(end.split("-")[0]) + + # creates a string with date (27) and month (07) eg: 07-27 + event_start = "-".join(event_start.split("-")[1:]) + + # repeat for all years in period + for year in range(start_year, end_year + 1): + date = str(year) + "-" + event_start + if ( + getdate(date) >= getdate(start) + and getdate(date) <= getdate(end) + and getdate(date) <= getdate(repeat) + ): + add_event(e, date) + + remove_events.append(e) + + if e.repeat_on == "Monthly": + # creates a string with date (27) and month (07) and year (2019) eg: 2019-07-27 + date = start.split("-")[0] + "-" + start.split("-")[1] + "-" + event_start.split("-")[2] + + # last day of month issue, start from prev month! + try: + getdate(date) + except ValueError: + date = date.split("-") + date = date[0] + "-" + str(cint(date[1]) - 1) + "-" + date[2] + + start_from = date + for i in range(int(date_diff(end, start) / 30) + 3): + if ( + getdate(date) >= getdate(start) + and getdate(date) <= getdate(end) + and getdate(date) <= getdate(repeat) + and getdate(date) >= getdate(event_start) + ): + add_event(e, date) + + date = add_months(start_from, i + 1) + remove_events.append(e) + + if e.repeat_on == "Weekly": + for cnt in range(date_diff(end, start) + 1): + date = add_days(start, cnt) + if ( + getdate(date) >= getdate(start) + and getdate(date) <= getdate(end) + and getdate(date) <= getdate(repeat) + and getdate(date) >= getdate(event_start) + and e[weekdays[getdate(date).weekday()]] + ): + add_event(e, date) + + remove_events.append(e) + + if e.repeat_on == "Daily": + for cnt in range(date_diff(end, start) + 1): + date = add_days(start, cnt) + if ( + getdate(date) >= getdate(event_start) + and getdate(date) <= getdate(end) + and getdate(date) <= getdate(repeat) + ): + add_event(e, date) + + remove_events.append(e) + + for e in remove_events: + events.remove(e) + + events = events + add_events + + for e in events: + # remove weekday properties (to reduce message size) + for w in weekdays: + del e[w] + + return events + + +def delete_events(ref_type, ref_name, delete_event=False): + participations = influxframework.get_all( + "Event Participants", + filters={"reference_doctype": ref_type, "reference_docname": ref_name, "parenttype": "Event"}, + fields=["parent", "name"], + ) + + if participations: + for participation in participations: + if delete_event: + influxframework.delete_doc("Event", participation.parent, for_reload=True) + else: + total_participants = influxframework.get_all( + "Event Participants", filters={"parenttype": "Event", "parent": participation.parent} + ) + + if len(total_participants) <= 1: + influxframework.db.delete("Event", {"name": participation.parent}) + influxframework.db.delete("Event Participants", {"name": participation.name}) + + +# Close events if ends_on or repeat_till is less than now_datetime +def set_status_of_events(): + events = influxframework.get_list( + "Event", filters={"status": "Open"}, fields=["name", "ends_on", "repeat_till"] + ) + for event in events: + if (event.ends_on and getdate(event.ends_on) < getdate(nowdate())) or ( + event.repeat_till and getdate(event.repeat_till) < getdate(nowdate()) + ): + + influxframework.db.set_value("Event", event.name, "status", "Closed") diff --git a/influxframework/desk/doctype/event/event_calendar.js b/influxframework/desk/doctype/event/event_calendar.js new file mode 100644 index 0000000..8b46fcd --- /dev/null +++ b/influxframework/desk/doctype/event/event_calendar.js @@ -0,0 +1,16 @@ +influxframework.views.calendar["Event"] = { + field_map: { + start: "starts_on", + end: "ends_on", + id: "name", + allDay: "all_day", + title: "subject", + status: "event_type", + color: "color", + }, + style_map: { + Public: "success", + Private: "info", + }, + get_events_method: "influxframework.desk.doctype.event.event.get_events", +}; diff --git a/influxframework/desk/doctype/event/event_list.js b/influxframework/desk/doctype/event/event_list.js new file mode 100644 index 0000000..5c6bdae --- /dev/null +++ b/influxframework/desk/doctype/event/event_list.js @@ -0,0 +1,8 @@ +influxframework.listview_settings["Event"] = { + add_fields: ["starts_on", "ends_on"], + onload: function () { + influxframework.route_options = { + status: "Open", + }; + }, +}; diff --git a/influxframework/desk/doctype/event/test_event.py b/influxframework/desk/doctype/event/test_event.py new file mode 100644 index 0000000..c940a21 --- /dev/null +++ b/influxframework/desk/doctype/event/test_event.py @@ -0,0 +1,138 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +"""Use blog post test to test user permissions logic""" + +import json + +import influxframework +import influxframework.defaults +from influxframework.desk.doctype.event.event import get_events +from influxframework.test_runner import make_test_objects +from influxframework.tests.utils import InfluxFrameworkTestCase + +test_records = influxframework.get_test_records("Event") + + +class TestEvent(InfluxFrameworkTestCase): + def setUp(self): + influxframework.db.delete("Event") + make_test_objects("Event", reset=True) + + self.test_records = influxframework.get_test_records("Event") + self.test_user = "test1@example.com" + + def tearDown(self): + influxframework.set_user("Administrator") + + def test_allowed_public(self): + influxframework.set_user(self.test_user) + doc = influxframework.get_doc("Event", influxframework.db.get_value("Event", {"subject": "_Test Event 1"})) + self.assertTrue(influxframework.has_permission("Event", doc=doc)) + + def test_not_allowed_private(self): + influxframework.set_user(self.test_user) + doc = influxframework.get_doc("Event", influxframework.db.get_value("Event", {"subject": "_Test Event 2"})) + self.assertFalse(influxframework.has_permission("Event", doc=doc)) + + def test_allowed_private_if_in_event_user(self): + name = influxframework.db.get_value("Event", {"subject": "_Test Event 3"}) + influxframework.share.add("Event", name, self.test_user, "read") + influxframework.set_user(self.test_user) + doc = influxframework.get_doc("Event", name) + self.assertTrue(influxframework.has_permission("Event", doc=doc)) + influxframework.set_user("Administrator") + influxframework.share.remove("Event", name, self.test_user) + + def test_event_list(self): + influxframework.set_user(self.test_user) + res = influxframework.get_list( + "Event", filters=[["Event", "subject", "like", "_Test Event%"]], fields=["name", "subject"] + ) + self.assertEqual(len(res), 1) + subjects = [r.subject for r in res] + self.assertTrue("_Test Event 1" in subjects) + self.assertFalse("_Test Event 3" in subjects) + self.assertFalse("_Test Event 2" in subjects) + + def test_revert_logic(self): + ev = influxframework.get_doc(self.test_records[0]).insert() + name = ev.name + + influxframework.delete_doc("Event", ev.name) + + # insert again + ev = influxframework.get_doc(self.test_records[0]).insert() + + # the name should be same! + self.assertEqual(ev.name, name) + + def test_assign(self): + from influxframework.desk.form.assign_to import add + + ev = influxframework.get_doc(self.test_records[0]).insert() + + add( + { + "assign_to": ["test@example.com"], + "doctype": "Event", + "name": ev.name, + "description": "Test Assignment", + } + ) + + ev = influxframework.get_doc("Event", ev.name) + + self.assertEqual(ev._assign, json.dumps(["test@example.com"])) + + # add another one + add( + { + "assign_to": [self.test_user], + "doctype": "Event", + "name": ev.name, + "description": "Test Assignment", + } + ) + + ev = influxframework.get_doc("Event", ev.name) + + self.assertEqual(set(json.loads(ev._assign)), {"test@example.com", self.test_user}) + + # Remove an assignment + todo = influxframework.get_doc( + "ToDo", + {"reference_type": ev.doctype, "reference_name": ev.name, "allocated_to": self.test_user}, + ) + todo.status = "Cancelled" + todo.save() + + ev = influxframework.get_doc("Event", ev.name) + self.assertEqual(ev._assign, json.dumps(["test@example.com"])) + + # cleanup + ev.delete() + + def test_recurring(self): + ev = influxframework.get_doc( + { + "doctype": "Event", + "subject": "_Test Event", + "starts_on": "2014-02-01", + "event_type": "Public", + "repeat_this_event": 1, + "repeat_on": "Yearly", + } + ) + ev.insert() + + ev_list = get_events("2014-02-01", "2014-02-01", "Administrator", for_reminder=True) + self.assertTrue(bool(list(filter(lambda e: e.name == ev.name, ev_list)))) + + ev_list1 = get_events("2015-01-20", "2015-01-20", "Administrator", for_reminder=True) + self.assertFalse(bool(list(filter(lambda e: e.name == ev.name, ev_list1)))) + + ev_list2 = get_events("2014-02-20", "2014-02-20", "Administrator", for_reminder=True) + self.assertFalse(bool(list(filter(lambda e: e.name == ev.name, ev_list2)))) + + ev_list3 = get_events("2015-02-01", "2015-02-01", "Administrator", for_reminder=True) + self.assertTrue(bool(list(filter(lambda e: e.name == ev.name, ev_list3)))) diff --git a/influxframework/desk/doctype/event/test_records.json b/influxframework/desk/doctype/event/test_records.json new file mode 100644 index 0000000..41d5803 --- /dev/null +++ b/influxframework/desk/doctype/event/test_records.json @@ -0,0 +1,23 @@ +[ + { + "doctype": "Event", + "subject":"_Test Event 1", + "starts_on": "2014-01-01", + "event_type": "Public", + "creation": "2014-01-01" + }, + { + "doctype": "Event", + "subject":"_Test Event 2", + "starts_on": "2014-01-01", + "event_type": "Private", + "creation": "2014-01-01" + }, + { + "doctype": "Event", + "subject": "_Test Event 3", + "starts_on": "2014-02-01", + "event_type": "Private", + "creation": "2014-02-01" + } +] diff --git a/influxframework/desk/doctype/event_participants/__init__.py b/influxframework/desk/doctype/event_participants/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/event_participants/event_participants.json b/influxframework/desk/doctype/event_participants/event_participants.json new file mode 100644 index 0000000..86cf267 --- /dev/null +++ b/influxframework/desk/doctype/event_participants/event_participants.json @@ -0,0 +1,108 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2018-09-21 15:44:58.836156", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reference_doctype", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Reference Document Type", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reference_docname", + "fieldtype": "Dynamic Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Reference Name", + "length": 0, + "no_copy": 0, + "options": "reference_doctype", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "max_attachments": 0, + "modified": "2019-09-05 14:22:27.664645", + "modified_by": "Administrator", + "module": "Desk", + "name": "Event Participants", + "name_case": "", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0, + "track_views": 0 + } \ No newline at end of file diff --git a/influxframework/desk/doctype/event_participants/event_participants.py b/influxframework/desk/doctype/event_participants/event_participants.py new file mode 100644 index 0000000..7a1d7ce --- /dev/null +++ b/influxframework/desk/doctype/event_participants/event_participants.py @@ -0,0 +1,7 @@ +# Copyright (c) 2018, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.model.document import Document + + +class EventParticipants(Document): + pass diff --git a/influxframework/desk/doctype/form_tour/__init__.py b/influxframework/desk/doctype/form_tour/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/form_tour/form_tour.js b/influxframework/desk/doctype/form_tour/form_tour.js new file mode 100644 index 0000000..be21968 --- /dev/null +++ b/influxframework/desk/doctype/form_tour/form_tour.js @@ -0,0 +1,127 @@ +// Copyright (c) 2021, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Form Tour", { + setup: function (frm) { + if (!frm.doc.is_standard || influxframework.boot.developer_mode) { + frm.trigger("setup_queries"); + } + }, + + refresh(frm) { + if (frm.doc.is_standard && !influxframework.boot.developer_mode) { + frm.trigger("disable_form"); + } + + frm.add_custom_button(__("Show Tour"), async () => { + const issingle = await check_if_single(frm.doc.reference_doctype); + let route_changed = null; + + if (issingle) { + route_changed = influxframework.set_route("Form", frm.doc.reference_doctype); + } else if (frm.doc.first_document) { + const name = await get_first_document(frm.doc.reference_doctype); + route_changed = influxframework.set_route("Form", frm.doc.reference_doctype, name); + } else { + route_changed = influxframework.set_route("Form", frm.doc.reference_doctype, "new"); + } + route_changed.then(() => { + const tour_name = frm.doc.name; + cur_frm.tour.init({ tour_name }).then(() => cur_frm.tour.start()); + }); + }); + }, + + disable_form: function (frm) { + frm.set_read_only(); + frm.fields + .filter((field) => field.has_input) + .forEach((field) => { + frm.set_df_property(field.df.fieldname, "read_only", "1"); + }); + frm.disable_save(); + }, + + setup_queries(frm) { + frm.set_query("reference_doctype", function () { + return { + filters: { + istable: 0, + }, + }; + }); + + frm.trigger("reference_doctype"); + }, + + reference_doctype(frm) { + if (!frm.doc.reference_doctype) return; + + frm.set_fields_as_options("fieldname", frm.doc.reference_doctype, (df) => !df.hidden).then( + (options) => { + frm.fields_dict.steps.grid.update_docfield_property( + "fieldname", + "options", + [""].concat(options) + ); + } + ); + + frm.set_fields_as_options( + "parent_fieldname", + frm.doc.reference_doctype, + (df) => df.fieldtype == "Table" && !df.hidden + ).then((options) => { + frm.fields_dict.steps.grid.update_docfield_property( + "parent_fieldname", + "options", + [""].concat(options) + ); + }); + }, +}); + +influxframework.ui.form.on("Form Tour Step", { + form_render(frm, cdt, cdn) { + if (locals[cdt][cdn].is_table_field) { + frm.trigger("parent_fieldname", cdt, cdn); + } + }, + parent_fieldname(frm, cdt, cdn) { + const child_row = locals[cdt][cdn]; + + const parent_fieldname_df = influxframework + .get_meta(frm.doc.reference_doctype) + .fields.find((df) => df.fieldname == child_row.parent_fieldname); + + frm.set_fields_as_options( + "fieldname", + parent_fieldname_df.options, + (df) => !df.hidden + ).then((options) => { + frm.fields_dict.steps.grid.update_docfield_property( + "fieldname", + "options", + [""].concat(options) + ); + if (child_row.fieldname) { + influxframework.model.set_value(cdt, cdn, "fieldname", child_row.fieldname); + } + }); + }, +}); + +async function check_if_single(doctype) { + const { message } = await influxframework.db.get_value("DocType", doctype, "issingle"); + return message.issingle || 0; +} + +async function get_first_document(doctype) { + let docname; + + await influxframework.db.get_list(doctype, { order_by: "creation" }).then((res) => { + if (Array.isArray(res) && res.length) docname = res[0].name; + }); + + return docname || "new"; +} diff --git a/influxframework/desk/doctype/form_tour/form_tour.json b/influxframework/desk/doctype/form_tour/form_tour.json new file mode 100644 index 0000000..6f3bd56 --- /dev/null +++ b/influxframework/desk/doctype/form_tour/form_tour.json @@ -0,0 +1,112 @@ +{ + "actions": [], + "autoname": "field:title", + "creation": "2021-05-21 23:02:52.242721", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "title", + "reference_doctype", + "module", + "column_break_6", + "is_standard", + "save_on_complete", + "first_document", + "include_name_field", + "section_break_3", + "steps" + ], + "fields": [ + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Reference Document", + "options": "DocType", + "reqd": 1 + }, + { + "depends_on": "reference_doctype", + "fieldname": "steps", + "fieldtype": "Table", + "label": "Steps", + "options": "Form Tour Step", + "reqd": 1 + }, + { + "fieldname": "section_break_3", + "fieldtype": "Section Break" + }, + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "reqd": 1, + "unique": 1 + }, + { + "default": "0", + "fieldname": "save_on_complete", + "fieldtype": "Check", + "label": "Save on Completion" + }, + { + "default": "0", + "fieldname": "is_standard", + "fieldtype": "Check", + "label": "Is Standard" + }, + { + "fetch_from": "reference_doctype.module", + "fieldname": "module", + "fieldtype": "Link", + "hidden": 1, + "label": "Module", + "options": "Module Def", + "read_only": 1 + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "first_document", + "fieldtype": "Check", + "label": "Show First Document Tour" + }, + { + "default": "0", + "depends_on": "eval:!doc.first_document", + "fieldname": "include_name_field", + "fieldtype": "Check", + "label": "Include Name Field" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-11-24 12:03:45.449311", + "modified_by": "Administrator", + "module": "Desk", + "name": "Form Tour", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/form_tour/form_tour.py b/influxframework/desk/doctype/form_tour/form_tour.py new file mode 100644 index 0000000..3796577 --- /dev/null +++ b/influxframework/desk/doctype/form_tour/form_tour.py @@ -0,0 +1,27 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document +from influxframework.modules.export_file import export_to_files + + +class FormTour(Document): + def before_save(self): + meta = influxframework.get_meta(self.reference_doctype) + for step in self.steps: + if step.is_table_field and step.parent_fieldname: + parent_field_df = meta.get_field(step.parent_fieldname) + step.child_doctype = parent_field_df.options + + field_df = influxframework.get_meta(step.child_doctype).get_field(step.fieldname) + step.label = field_df.label + step.fieldtype = field_df.fieldtype + else: + field_df = meta.get_field(step.fieldname) + step.label = field_df.label + step.fieldtype = field_df.fieldtype + + def on_update(self): + if influxframework.conf.developer_mode and self.is_standard: + export_to_files([["Form Tour", self.name]], self.module) diff --git a/influxframework/desk/doctype/form_tour/test_form_tour.py b/influxframework/desk/doctype/form_tour/test_form_tour.py new file mode 100644 index 0000000..9d2b890 --- /dev/null +++ b/influxframework/desk/doctype/form_tour/test_form_tour.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestFormTour(InfluxFrameworkTestCase): + pass diff --git a/influxframework/desk/doctype/form_tour_step/__init__.py b/influxframework/desk/doctype/form_tour_step/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/form_tour_step/form_tour_step.json b/influxframework/desk/doctype/form_tour_step/form_tour_step.json new file mode 100644 index 0000000..7eb6eab --- /dev/null +++ b/influxframework/desk/doctype/form_tour_step/form_tour_step.json @@ -0,0 +1,128 @@ +{ + "actions": [], + "creation": "2021-05-21 23:05:45.342114", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "is_table_field", + "section_break_2", + "parent_fieldname", + "fieldname", + "title", + "description", + "column_break_2", + "position", + "label", + "fieldtype", + "has_next_condition", + "next_step_condition", + "section_break_13", + "child_doctype" + ], + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Title", + "reqd": 1 + }, + { + "columns": 4, + "fieldname": "description", + "fieldtype": "HTML Editor", + "in_list_view": 1, + "label": "Description", + "reqd": 1 + }, + { + "depends_on": "eval: (!doc.is_table_field || (doc.is_table_field && doc.parent_fieldname))", + "fieldname": "fieldname", + "fieldtype": "Select", + "label": "Fieldname", + "reqd": 1 + }, + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "read_only": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "default": "Bottom", + "fieldname": "position", + "fieldtype": "Select", + "label": "Position", + "options": "Left\nLeft Center\nLeft Bottom\nTop\nTop Center\nTop Right\nRight\nRight Center\nRight Bottom\nBottom\nBottom Center\nBottom Right\nMid Center" + }, + { + "depends_on": "has_next_condition", + "fieldname": "next_step_condition", + "fieldtype": "Code", + "label": "Next Step Condition", + "oldfieldname": "condition", + "options": "JS" + }, + { + "default": "0", + "fieldname": "has_next_condition", + "fieldtype": "Check", + "label": "Has Next Condition" + }, + { + "default": "0", + "fieldname": "fieldtype", + "fieldtype": "Data", + "label": "Fieldtype", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_table_field", + "fieldtype": "Check", + "label": "Is Table Field" + }, + { + "fieldname": "section_break_2", + "fieldtype": "Section Break" + }, + { + "fieldname": "section_break_13", + "fieldtype": "Section Break", + "hidden": 1, + "label": "Hidden Fields" + }, + { + "fieldname": "child_doctype", + "fieldtype": "Data", + "hidden": 1, + "label": "Child Doctype", + "read_only": 1 + }, + { + "depends_on": "is_table_field", + "fieldname": "parent_fieldname", + "fieldtype": "Select", + "label": "Parent Field", + "mandatory_depends_on": "is_table_field" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2022-01-27 15:18:36.481801", + "modified_by": "Administrator", + "module": "Desk", + "name": "Form Tour Step", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/form_tour_step/form_tour_step.py b/influxframework/desk/doctype/form_tour_step/form_tour_step.py new file mode 100644 index 0000000..aa6c4c7 --- /dev/null +++ b/influxframework/desk/doctype/form_tour_step/form_tour_step.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class FormTourStep(Document): + pass diff --git a/influxframework/desk/doctype/global_search_doctype/__init__.py b/influxframework/desk/doctype/global_search_doctype/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/global_search_doctype/global_search_doctype.json b/influxframework/desk/doctype/global_search_doctype/global_search_doctype.json new file mode 100644 index 0000000..648e8f1 --- /dev/null +++ b/influxframework/desk/doctype/global_search_doctype/global_search_doctype.json @@ -0,0 +1,29 @@ +{ + "creation": "2019-09-13 21:33:55.551941", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type" + ], + "fields": [ + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType" + } + ], + "istable": 1, + "modified": "2019-09-18 17:59:44.354052", + "modified_by": "Administrator", + "module": "Desk", + "name": "Global Search DocType", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/global_search_doctype/global_search_doctype.py b/influxframework/desk/doctype/global_search_doctype/global_search_doctype.py new file mode 100644 index 0000000..0c4299c --- /dev/null +++ b/influxframework/desk/doctype/global_search_doctype/global_search_doctype.py @@ -0,0 +1,9 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class GlobalSearchDocType(Document): + pass diff --git a/influxframework/desk/doctype/global_search_settings/__init__.py b/influxframework/desk/doctype/global_search_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/global_search_settings/global_search_settings.js b/influxframework/desk/doctype/global_search_settings/global_search_settings.js new file mode 100644 index 0000000..0588c4c --- /dev/null +++ b/influxframework/desk/doctype/global_search_settings/global_search_settings.js @@ -0,0 +1,32 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Global Search Settings", { + refresh: function (frm) { + influxframework.realtime.on("global_search_settings", (data) => { + if (data.progress) { + frm.dashboard.show_progress( + "Setting up Global Search", + (data.progress / data.total) * 100, + data.msg + ); + if (data.progress === data.total) { + frm.dashboard.hide_progress("Setting up Global Search"); + } + } + }); + + frm.add_custom_button(__("Reset"), function () { + influxframework.call({ + method: "influxframework.desk.doctype.global_search_settings.global_search_settings.reset_global_search_settings_doctypes", + callback: function () { + influxframework.show_alert({ + message: __("Global Search Document Types Reset."), + indicator: "green", + }); + frm.refresh(); + }, + }); + }); + }, +}); diff --git a/influxframework/desk/doctype/global_search_settings/global_search_settings.json b/influxframework/desk/doctype/global_search_settings/global_search_settings.json new file mode 100644 index 0000000..6fa25f7 --- /dev/null +++ b/influxframework/desk/doctype/global_search_settings/global_search_settings.json @@ -0,0 +1,39 @@ +{ + "creation": "2019-09-03 16:08:21.333698", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "allowed_in_global_search" + ], + "fields": [ + { + "fieldname": "allowed_in_global_search", + "fieldtype": "Table", + "label": "Search Priorities", + "options": "Global Search DocType" + } + ], + "issingle": 1, + "modified": "2019-10-10 22:05:02.692689", + "modified_by": "Administrator", + "module": "Desk", + "name": "Global Search Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/global_search_settings/global_search_settings.py b/influxframework/desk/doctype/global_search_settings/global_search_settings.py new file mode 100644 index 0000000..86936eb --- /dev/null +++ b/influxframework/desk/doctype/global_search_settings/global_search_settings.py @@ -0,0 +1,91 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework import _ +from influxframework.model.document import Document + + +class GlobalSearchSettings(Document): + def validate(self): + dts, core_dts, repeated_dts = [], [], [] + + for dt in self.allowed_in_global_search: + if dt.document_type in dts: + repeated_dts.append(dt.document_type) + + if influxframework.get_meta(dt.document_type).module == "Core": + core_dts.append(dt.document_type) + + dts.append(dt.document_type) + + if core_dts: + core_dts = ", ".join(influxframework.bold(dt) for dt in core_dts) + influxframework.throw(_("Core Modules {0} cannot be searched in Global Search.").format(core_dts)) + + if repeated_dts: + repeated_dts = ", ".join([influxframework.bold(dt) for dt in repeated_dts]) + influxframework.throw(_("Document Type {0} has been repeated.").format(repeated_dts)) + + # reset cache + influxframework.cache().hdel("global_search", "search_priorities") + + +def get_doctypes_for_global_search(): + def get_from_db(): + doctypes = influxframework.get_all("Global Search DocType", fields=["document_type"], order_by="idx ASC") + return [d.document_type for d in doctypes] or [] + + return influxframework.cache().hget("global_search", "search_priorities", get_from_db) + + +@influxframework.whitelist() +def reset_global_search_settings_doctypes(): + update_global_search_doctypes() + + +def update_global_search_doctypes(): + global_search_doctypes = [] + show_message(1, _("Fetching default Global Search documents.")) + + installed_apps = [app for app in influxframework.get_installed_apps() if app] + active_domains = [domain for domain in influxframework.get_active_domains() if domain] + active_domains.append("Default") + + for app in installed_apps: + search_doctypes = influxframework.get_hooks(hook="global_search_doctypes", app_name=app) + if not search_doctypes: + continue + + for domain in active_domains: + if search_doctypes.get(domain): + global_search_doctypes.extend(search_doctypes.get(domain)) + + doctype_list = {dt.name for dt in influxframework.get_all("DocType")} + allowed_in_global_search = [] + + for dt in global_search_doctypes: + if dt.get("index") is not None: + allowed_in_global_search.insert(dt.get("index"), dt.get("doctype")) + continue + + allowed_in_global_search.append(dt.get("doctype")) + + show_message(2, _("Setting up Global Search documents.")) + global_search_settings = influxframework.get_single("Global Search Settings") + global_search_settings.allowed_in_global_search = [] + for dt in allowed_in_global_search: + if dt not in doctype_list: + continue + + global_search_settings.append("allowed_in_global_search", {"document_type": dt}) + global_search_settings.save(ignore_permissions=True) + show_message(3, "Global Search Documents have been reset.") + + +def show_message(progress, msg): + influxframework.publish_realtime( + "global_search_settings", + {"progress": progress, "total": 3, "msg": msg}, + user=influxframework.session.user, + ) diff --git a/influxframework/desk/doctype/kanban_board/__init__.py b/influxframework/desk/doctype/kanban_board/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/kanban_board/kanban_board.js b/influxframework/desk/doctype/kanban_board/kanban_board.js new file mode 100644 index 0000000..56ebd20 --- /dev/null +++ b/influxframework/desk/doctype/kanban_board/kanban_board.js @@ -0,0 +1,45 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Kanban Board", { + onload: function (frm) { + frm.trigger("reference_doctype"); + }, + refresh: function (frm) { + if (frm.is_new()) return; + frm.add_custom_button("Show Board", function () { + influxframework.set_route("List", frm.doc.reference_doctype, "Kanban", frm.doc.name); + }); + }, + reference_doctype: function (frm) { + // set field options + if (!frm.doc.reference_doctype) return; + + influxframework.model.with_doctype(frm.doc.reference_doctype, function () { + var options = $.map(influxframework.get_meta(frm.doc.reference_doctype).fields, function (d) { + if ( + d.fieldname && + d.fieldtype === "Select" && + influxframework.model.no_value_type.indexOf(d.fieldtype) === -1 + ) { + return d.fieldname; + } + return null; + }); + frm.set_df_property("field_name", "options", options); + frm.get_field("field_name").refresh(); + }); + }, + field_name: function (frm) { + var field = influxframework.meta.get_field(frm.doc.reference_doctype, frm.doc.field_name); + frm.doc.columns = []; + field.options && + field.options.split("\n").forEach(function (o) { + o = o.trim(); + if (!o) return; + var d = frm.add_child("columns"); + d.column_name = o; + }); + frm.refresh(); + }, +}); diff --git a/influxframework/desk/doctype/kanban_board/kanban_board.json b/influxframework/desk/doctype/kanban_board/kanban_board.json new file mode 100644 index 0000000..b1f1206 --- /dev/null +++ b/influxframework/desk/doctype/kanban_board/kanban_board.json @@ -0,0 +1,124 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:kanban_board_name", + "creation": "2016-10-19 12:26:04.809812", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "kanban_board_name", + "reference_doctype", + "field_name", + "column_break_4", + "private", + "show_labels", + "section_break_3", + "columns", + "filters", + "fields" + ], + "fields": [ + { + "fieldname": "kanban_board_name", + "fieldtype": "Data", + "label": "Kanban Board Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Reference Document Type", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "field_name", + "fieldtype": "Select", + "label": "Field Name", + "reqd": 1 + }, + { + "fieldname": "section_break_3", + "fieldtype": "Section Break" + }, + { + "fieldname": "columns", + "fieldtype": "Table", + "label": "Columns", + "options": "Kanban Board Column" + }, + { + "fieldname": "filters", + "fieldtype": "Code", + "label": "Filters", + "options": "JSON", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "private", + "fieldtype": "Check", + "label": "Private", + "read_only": 1 + }, + { + "fieldname": "fields", + "fieldtype": "Code", + "label": "Fields", + "options": "JSON", + "read_only": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "show_labels", + "fieldtype": "Check", + "label": "Show Labels", + "read_only": 1 + } + ], + "links": [], + "modified": "2022-04-13 12:10:20.284367", + "modified_by": "Administrator", + "module": "Desk", + "name": "Kanban Board", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "read": 1, + "role": "All" + }, + { + "create": 1, + "delete": 1, + "if_owner": 1, + "read": 1, + "role": "All", + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/kanban_board/kanban_board.py b/influxframework/desk/doctype/kanban_board/kanban_board.py new file mode 100644 index 0000000..5c9e905 --- /dev/null +++ b/influxframework/desk/doctype/kanban_board/kanban_board.py @@ -0,0 +1,267 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +import influxframework +from influxframework import _ +from influxframework.model.document import Document + + +class KanbanBoard(Document): + def validate(self): + self.validate_column_name() + + def on_change(self): + influxframework.clear_cache(doctype=self.reference_doctype) + influxframework.cache().delete_keys("_user_settings") + + def before_insert(self): + for column in self.columns: + column.order = get_order_for_column(self, column.column_name) + + def validate_column_name(self): + for column in self.columns: + if not column.column_name: + influxframework.msgprint(_("Column Name cannot be empty"), raise_exception=True) + + +def get_permission_query_conditions(user): + if not user: + user = influxframework.session.user + + if user == "Administrator": + return "" + + return """(`tabKanban Board`.private=0 or `tabKanban Board`.owner={user})""".format( + user=influxframework.db.escape(user) + ) + + +def has_permission(doc, ptype, user): + if doc.private == 0 or user == "Administrator": + return True + + if user == doc.owner: + return True + + return False + + +@influxframework.whitelist() +def get_kanban_boards(doctype): + """Get Kanban Boards for doctype to show in List View""" + return influxframework.get_list( + "Kanban Board", + fields=["name", "filters", "reference_doctype", "private"], + filters={"reference_doctype": doctype}, + ) + + +@influxframework.whitelist() +def add_column(board_name, column_title): + """Adds new column to Kanban Board""" + doc = influxframework.get_doc("Kanban Board", board_name) + for col in doc.columns: + if column_title == col.column_name: + influxframework.throw(_("Column {0} already exist.").format(column_title)) + + doc.append("columns", dict(column_name=column_title)) + doc.save() + return doc.columns + + +@influxframework.whitelist() +def archive_restore_column(board_name, column_title, status): + """Set column's status to status""" + doc = influxframework.get_doc("Kanban Board", board_name) + for col in doc.columns: + if column_title == col.column_name: + col.status = status + + doc.save() + return doc.columns + + +@influxframework.whitelist() +def update_order(board_name, order): + """Save the order of cards in columns""" + board = influxframework.get_doc("Kanban Board", board_name) + doctype = board.reference_doctype + fieldname = board.field_name + order_dict = json.loads(order) + + updated_cards = [] + for col_name, cards in order_dict.items(): + for card in cards: + column = influxframework.get_value(doctype, {"name": card}, fieldname) + if column != col_name: + influxframework.set_value(doctype, card, fieldname, col_name) + updated_cards.append(dict(name=card, column=col_name)) + + for column in board.columns: + if column.column_name == col_name: + column.order = json.dumps(cards) + + board.save() + return board, updated_cards + + +@influxframework.whitelist() +def update_order_for_single_card( + board_name, docname, from_colname, to_colname, old_index, new_index +): + """Save the order of cards in columns""" + board = influxframework.get_doc("Kanban Board", board_name) + doctype = board.reference_doctype + fieldname = board.field_name + old_index = influxframework.parse_json(old_index) + new_index = influxframework.parse_json(new_index) + + # save current order and index of columns to be updated + from_col_order, from_col_idx = get_kanban_column_order_and_index(board, from_colname) + to_col_order, to_col_idx = get_kanban_column_order_and_index(board, to_colname) + + if from_colname == to_colname: + from_col_order = to_col_order + + to_col_order.insert(new_index, from_col_order.pop(old_index)) + + # save updated order + board.columns[from_col_idx].order = influxframework.as_json(from_col_order) + board.columns[to_col_idx].order = influxframework.as_json(to_col_order) + board.save() + + # update changed value in doc + influxframework.set_value(doctype, docname, fieldname, to_colname) + + return board + + +def get_kanban_column_order_and_index(board, colname): + for i, col in enumerate(board.columns): + if col.column_name == colname: + col_order = influxframework.parse_json(col.order) + col_idx = i + + return col_order, col_idx + + +@influxframework.whitelist() +def add_card(board_name, docname, colname): + board = influxframework.get_doc("Kanban Board", board_name) + + col_order, col_idx = get_kanban_column_order_and_index(board, colname) + col_order.insert(0, docname) + + board.columns[col_idx].order = influxframework.as_json(col_order) + + board.save() + return board + + +@influxframework.whitelist() +def quick_kanban_board(doctype, board_name, field_name, project=None): + """Create new KanbanBoard quickly with default options""" + + doc = influxframework.new_doc("Kanban Board") + meta = influxframework.get_meta(doctype) + + doc.kanban_board_name = board_name + doc.reference_doctype = doctype + doc.field_name = field_name + + if project: + doc.filters = f'[["Task","project","=","{project}"]]' + + options = "" + for field in meta.fields: + if field.fieldname == field_name: + options = field.options + + columns = [] + if options: + columns = options.split("\n") + + for column in columns: + if not column: + continue + doc.append("columns", dict(column_name=column)) + + if doctype in ["Note", "ToDo"]: + doc.private = 1 + + doc.save() + return doc + + +def get_order_for_column(board, colname): + filters = [[board.reference_doctype, board.field_name, "=", colname]] + if board.filters: + filters.append(influxframework.parse_json(board.filters)[0]) + + return influxframework.as_json(influxframework.get_list(board.reference_doctype, filters=filters, pluck="name")) + + +@influxframework.whitelist() +def update_column_order(board_name, order): + """Set the order of columns in Kanban Board""" + board = influxframework.get_doc("Kanban Board", board_name) + order = json.loads(order) + old_columns = board.columns + new_columns = [] + + for col in order: + for column in old_columns: + if col == column.column_name: + new_columns.append(column) + old_columns.remove(column) + + new_columns.extend(old_columns) + + board.columns = [] + for col in new_columns: + board.append( + "columns", + dict( + column_name=col.column_name, + status=col.status, + order=col.order, + indicator=col.indicator, + ), + ) + + board.save() + return board + + +@influxframework.whitelist() +def set_indicator(board_name, column_name, indicator): + """Set the indicator color of column""" + board = influxframework.get_doc("Kanban Board", board_name) + + for column in board.columns: + if column.column_name == column_name: + column.indicator = indicator + + board.save() + return board + + +@influxframework.whitelist() +def save_settings(board_name: str, settings: str) -> Document: + settings = json.loads(settings) + doc = influxframework.get_doc("Kanban Board", board_name) + + fields = settings["fields"] + if not isinstance(fields, str): + fields = json.dumps(fields) + + doc.fields = fields + doc.show_labels = settings["show_labels"] + doc.save() + + resp = doc.as_dict() + resp["fields"] = influxframework.parse_json(resp["fields"]) + + return resp diff --git a/influxframework/desk/doctype/kanban_board/test_kanban_board.py b/influxframework/desk/doctype/kanban_board/test_kanban_board.py new file mode 100644 index 0000000..c9f68b4 --- /dev/null +++ b/influxframework/desk/doctype/kanban_board/test_kanban_board.py @@ -0,0 +1,9 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + +# test_records = influxframework.get_test_records('Kanban Board') + + +class TestKanbanBoard(InfluxFrameworkTestCase): + pass diff --git a/influxframework/desk/doctype/kanban_board_column/__init__.py b/influxframework/desk/doctype/kanban_board_column/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/kanban_board_column/kanban_board_column.json b/influxframework/desk/doctype/kanban_board_column/kanban_board_column.json new file mode 100644 index 0000000..c0acde5 --- /dev/null +++ b/influxframework/desk/doctype/kanban_board_column/kanban_board_column.json @@ -0,0 +1,55 @@ +{ + "actions": [], + "creation": "2016-10-19 12:26:42.569185", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "column_name", + "status", + "indicator", + "order" + ], + "fields": [ + { + "fieldname": "column_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Column Name" + }, + { + "default": "Active", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "options": "Active\nArchived" + }, + { + "default": "Gray", + "fieldname": "indicator", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Indicator", + "options": "Blue\nCyan\nGray\nGreen\nLight Blue\nOrange\nPink\nPurple\nRed\nRed\nYellow" + }, + { + "fieldname": "order", + "fieldtype": "Code", + "label": "Order" + } + ], + "istable": 1, + "links": [], + "modified": "2021-12-14 13:13:38.804259", + "modified_by": "Administrator", + "module": "Desk", + "name": "Kanban Board Column", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/kanban_board_column/kanban_board_column.py b/influxframework/desk/doctype/kanban_board_column/kanban_board_column.py new file mode 100644 index 0000000..058cbe3 --- /dev/null +++ b/influxframework/desk/doctype/kanban_board_column/kanban_board_column.py @@ -0,0 +1,8 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +from influxframework.model.document import Document + + +class KanbanBoardColumn(Document): + pass diff --git a/influxframework/desk/doctype/list_filter/__init__.py b/influxframework/desk/doctype/list_filter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/list_filter/list_filter.json b/influxframework/desk/doctype/list_filter/list_filter.json new file mode 100644 index 0000000..dad62bf --- /dev/null +++ b/influxframework/desk/doctype/list_filter/list_filter.json @@ -0,0 +1,188 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2018-02-22 15:10:24.401801", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "filter_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Filter Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reference_doctype", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Reference Document Type", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "for_user", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "For User", + "length": 0, + "no_copy": 0, + "options": "User", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "filters", + "fieldtype": "Long Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Filters", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 1, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2019-09-05 14:22:27.664645", + "modified_by": "Administrator", + "module": "Desk", + "name": "List Filter", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 1, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 0, + "track_seen": 0 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/list_filter/list_filter.py b/influxframework/desk/doctype/list_filter/list_filter.py new file mode 100644 index 0000000..b850915 --- /dev/null +++ b/influxframework/desk/doctype/list_filter/list_filter.py @@ -0,0 +1,8 @@ +# Copyright (c) 2018, InfluxFramework LLC +# License: MIT. See LICENSE + +from influxframework.model.document import Document + + +class ListFilter(Document): + pass diff --git a/influxframework/desk/doctype/list_view_settings/__init__.py b/influxframework/desk/doctype/list_view_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/list_view_settings/list_view_settings.js b/influxframework/desk/doctype/list_view_settings/list_view_settings.js new file mode 100644 index 0000000..e3e014c --- /dev/null +++ b/influxframework/desk/doctype/list_view_settings/list_view_settings.js @@ -0,0 +1,7 @@ +// Copyright (c) 2020, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("List View Settings", { + // refresh: function(frm) { + // } +}); diff --git a/influxframework/desk/doctype/list_view_settings/list_view_settings.json b/influxframework/desk/doctype/list_view_settings/list_view_settings.json new file mode 100644 index 0000000..4476199 --- /dev/null +++ b/influxframework/desk/doctype/list_view_settings/list_view_settings.json @@ -0,0 +1,76 @@ +{ + "actions": [], + "autoname": "Prompt", + "creation": "2019-10-23 15:00:48.392374", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "disable_count", + "disable_sidebar_stats", + "disable_auto_refresh", + "total_fields", + "fields_html", + "fields" + ], + "fields": [ + { + "default": "0", + "fieldname": "disable_count", + "fieldtype": "Check", + "label": "Disable Count" + }, + { + "default": "0", + "fieldname": "disable_sidebar_stats", + "fieldtype": "Check", + "label": "Disable Sidebar Stats" + }, + { + "default": "0", + "fieldname": "disable_auto_refresh", + "fieldtype": "Check", + "label": "Disable Auto Refresh" + }, + { + "fieldname": "total_fields", + "fieldtype": "Select", + "label": "Maximum Number of Fields", + "options": "\n4\n5\n6\n7\n8\n9\n10" + }, + { + "fieldname": "fields_html", + "fieldtype": "HTML", + "label": "Fields" + }, + { + "fieldname": "fields", + "fieldtype": "Code", + "hidden": 1, + "label": "Fields", + "read_only": 1 + } + ], + "links": [], + "modified": "2020-05-12 18:27:15.568199", + "modified_by": "Administrator", + "module": "Desk", + "name": "List View Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/list_view_settings/list_view_settings.py b/influxframework/desk/doctype/list_view_settings/list_view_settings.py new file mode 100644 index 0000000..9daeb77 --- /dev/null +++ b/influxframework/desk/doctype/list_view_settings/list_view_settings.py @@ -0,0 +1,88 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document + + +class ListViewSettings(Document): + def on_update(self): + influxframework.clear_document_cache(self.doctype, self.name) + + +@influxframework.whitelist() +def save_listview_settings(doctype, listview_settings, removed_listview_fields): + + listview_settings = influxframework.parse_json(listview_settings) + removed_listview_fields = influxframework.parse_json(removed_listview_fields) + + if influxframework.get_all("List View Settings", filters={"name": doctype}): + doc = influxframework.get_doc("List View Settings", doctype) + doc.update(listview_settings) + doc.save() + else: + doc = influxframework.new_doc("List View Settings") + doc.name = doctype + doc.update(listview_settings) + doc.insert() + + set_listview_fields(doctype, listview_settings.get("fields"), removed_listview_fields) + + return {"meta": influxframework.get_meta(doctype, False), "listview_settings": doc} + + +def set_listview_fields(doctype, listview_fields, removed_listview_fields): + meta = influxframework.get_meta(doctype) + + listview_fields = [ + f.get("fieldname") for f in influxframework.parse_json(listview_fields) if f.get("fieldname") + ] + + for field in removed_listview_fields: + set_in_list_view_property(doctype, meta.get_field(field), "0") + + for field in listview_fields: + set_in_list_view_property(doctype, meta.get_field(field), "1") + + +def set_in_list_view_property(doctype, field, value): + if not field or field.fieldname == "status_field": + return + + property_setter = influxframework.db.get_value( + "Property Setter", + {"doc_type": doctype, "field_name": field.fieldname, "property": "in_list_view"}, + ) + if property_setter: + doc = influxframework.get_doc("Property Setter", property_setter) + doc.value = value + doc.save() + else: + influxframework.make_property_setter( + { + "doctype": doctype, + "doctype_or_field": "DocField", + "fieldname": field.fieldname, + "property": "in_list_view", + "value": value, + "property_type": "Check", + }, + ignore_validate=True, + ) + + +@influxframework.whitelist() +def get_default_listview_fields(doctype): + meta = influxframework.get_meta(doctype) + path = influxframework.get_module_path( + influxframework.scrub(meta.module), "doctype", influxframework.scrub(meta.name), influxframework.scrub(meta.name) + ".json" + ) + doctype_json = influxframework.get_file_json(path) + + fields = [f.get("fieldname") for f in doctype_json.get("fields") if f.get("in_list_view")] + + if meta.title_field: + if not meta.title_field.strip() in fields: + fields.append(meta.title_field.strip()) + + return fields diff --git a/influxframework/desk/doctype/list_view_settings/test_list_view_settings.py b/influxframework/desk/doctype/list_view_settings/test_list_view_settings.py new file mode 100644 index 0000000..731ec23 --- /dev/null +++ b/influxframework/desk/doctype/list_view_settings/test_list_view_settings.py @@ -0,0 +1,8 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestListViewSettings(InfluxFrameworkTestCase): + pass diff --git a/influxframework/desk/doctype/module_onboarding/__init__.py b/influxframework/desk/doctype/module_onboarding/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/module_onboarding/module_onboarding.js b/influxframework/desk/doctype/module_onboarding/module_onboarding.js new file mode 100644 index 0000000..6f63e61 --- /dev/null +++ b/influxframework/desk/doctype/module_onboarding/module_onboarding.js @@ -0,0 +1,27 @@ +// Copyright (c) 2020, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Module Onboarding", { + refresh: function (frm) { + influxframework.boot.developer_mode && + frm.set_intro( + __( + "Saving this will export this document as well as the steps linked here as json." + ), + true + ); + if (!influxframework.boot.developer_mode) { + frm.trigger("disable_form"); + } + }, + + disable_form: function (frm) { + frm.set_read_only(); + frm.fields + .filter((field) => field.has_input) + .forEach((field) => { + frm.set_df_property(field.df.fieldname, "read_only", "1"); + }); + frm.disable_save(); + }, +}); diff --git a/influxframework/desk/doctype/module_onboarding/module_onboarding.json b/influxframework/desk/doctype/module_onboarding/module_onboarding.json new file mode 100644 index 0000000..02a18b9 --- /dev/null +++ b/influxframework/desk/doctype/module_onboarding/module_onboarding.json @@ -0,0 +1,117 @@ +{ + "actions": [], + "autoname": "Prompt", + "creation": "2020-04-24 13:58:14.948024", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "title", + "subtitle", + "module", + "allow_roles", + "column_break_4", + "success_message", + "documentation_url", + "is_complete", + "section_break_6", + "steps" + ], + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Title", + "reqd": 1 + }, + { + "fieldname": "subtitle", + "fieldtype": "Data", + "label": "Subtitle", + "reqd": 1 + }, + { + "fieldname": "module", + "fieldtype": "Link", + "label": "Module", + "options": "Module Def", + "reqd": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "fieldname": "success_message", + "fieldtype": "Data", + "label": "Success Message", + "reqd": 1 + }, + { + "fieldname": "documentation_url", + "fieldtype": "Data", + "label": "Documentation URL", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "is_complete", + "fieldtype": "Check", + "label": "Is Complete", + "read_only": 1 + }, + { + "fieldname": "steps", + "fieldtype": "Table", + "label": "Steps", + "options": "Onboarding Step Map", + "reqd": 1 + }, + { + "description": "System managers are allowed by default", + "fieldname": "allow_roles", + "fieldtype": "Table MultiSelect", + "label": "Allow Roles", + "options": "Onboarding Permission", + "reqd": 1 + } + ], + "links": [], + "modified": "2020-06-08 15:36:04.701049", + "modified_by": "Administrator", + "module": "Desk", + "name": "Module Onboarding", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 + } + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/module_onboarding/module_onboarding.py b/influxframework/desk/doctype/module_onboarding/module_onboarding.py new file mode 100644 index 0000000..a44ee99 --- /dev/null +++ b/influxframework/desk/doctype/module_onboarding/module_onboarding.py @@ -0,0 +1,53 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document +from influxframework.modules.export_file import export_to_files + + +class ModuleOnboarding(Document): + def on_update(self): + if influxframework.conf.developer_mode: + export_to_files(record_list=[["Module Onboarding", self.name]], record_module=self.module) + + for step in self.steps: + export_to_files(record_list=[["Onboarding Step", step.step]], record_module=self.module) + + def get_steps(self): + return [influxframework.get_doc("Onboarding Step", step.step) for step in self.steps] + + def get_allowed_roles(self): + all_roles = [role.role for role in self.allow_roles] + if "System Manager" not in all_roles: + all_roles.append("System Manager") + + return all_roles + + def check_completion(self): + if self.is_complete: + return True + + steps = self.get_steps() + is_complete = [bool(step.is_complete or step.is_skipped) for step in steps] + if all(is_complete): + self.is_complete = True + self.save() + return True + + return False + + def before_export(self, doc): + doc.is_complete = 0 + + def reset_onboarding(self): + influxframework.only_for("Administrator") + + self.is_complete = 0 + steps = self.get_steps() + for step in steps: + step.is_complete = 0 + step.is_skipped = 0 + step.save() + + self.save() diff --git a/influxframework/desk/doctype/module_onboarding/test_module_onboarding.py b/influxframework/desk/doctype/module_onboarding/test_module_onboarding.py new file mode 100644 index 0000000..0e929a2 --- /dev/null +++ b/influxframework/desk/doctype/module_onboarding/test_module_onboarding.py @@ -0,0 +1,8 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestModuleOnboarding(InfluxFrameworkTestCase): + pass diff --git a/influxframework/desk/doctype/note/README.md b/influxframework/desk/doctype/note/README.md new file mode 100644 index 0000000..95d7b33 --- /dev/null +++ b/influxframework/desk/doctype/note/README.md @@ -0,0 +1 @@ +Shared Note. (Page with standard information, links, attachments). \ No newline at end of file diff --git a/influxframework/desk/doctype/note/__init__.py b/influxframework/desk/doctype/note/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/note/note.js b/influxframework/desk/doctype/note/note.js new file mode 100644 index 0000000..c7cd4a8 --- /dev/null +++ b/influxframework/desk/doctype/note/note.js @@ -0,0 +1,54 @@ +influxframework.ui.form.on("Note", { + refresh: function (frm) { + if (frm.doc.__islocal) { + frm.events.set_editable(frm, true); + } else { + if (!frm.doc.content) { + frm.doc.content = ""; + } + + // toggle edit + frm.add_custom_button("Edit", function () { + frm.events.set_editable(frm, !frm.is_note_editable); + }); + frm.events.set_editable(frm, false); + } + }, + set_editable: function (frm, editable) { + // hide all fields other than content + + // no permission + if (editable && !frm.perm[0].write) return; + + // content read_only + frm.set_df_property("content", "read_only", editable ? 0 : 1); + + // hide all other fields + $.each(frm.fields_dict, function (fieldname) { + if (fieldname !== "content") { + frm.set_df_property(fieldname, "hidden", editable ? 0 : 1); + } + }); + + // no label, description for content either + frm.get_field("content").toggle_label(editable); + frm.get_field("content").toggle_description(editable); + + // set flag for toggle + frm.is_note_editable = editable; + }, +}); + +influxframework.tour["Note"] = [ + { + fieldname: "title", + title: "Title of the Note", + description: "This is the name by which the note will be saved, you can change this later", + }, + { + fieldname: "public", + title: "Sets the Note to Public", + description: + "You can change the visibility of the note with this, setting it to public will allow other users to view it.", + }, +]; diff --git a/influxframework/desk/doctype/note/note.json b/influxframework/desk/doctype/note/note.json new file mode 100644 index 0000000..69a9518 --- /dev/null +++ b/influxframework/desk/doctype/note/note.json @@ -0,0 +1,106 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2013-05-24 13:41:00", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "title", + "public", + "notify_on_login", + "notify_on_every_login", + "expire_notification_on", + "content", + "seen_by_section", + "seen_by" + ], + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "in_global_search": 1, + "in_list_view": 1, + "label": "Title", + "no_copy": 1, + "print_hide": 1, + "reqd": 1 + }, + { + "bold": 1, + "default": "0", + "fieldname": "public", + "fieldtype": "Check", + "label": "Public", + "print_hide": 1 + }, + { + "bold": 1, + "default": "0", + "depends_on": "public", + "fieldname": "notify_on_login", + "fieldtype": "Check", + "label": "Notify users with a popup when they log in" + }, + { + "bold": 1, + "default": "0", + "depends_on": "notify_on_login", + "description": "If enabled, users will be notified every time they login. If not enabled, users will only be notified once.", + "fieldname": "notify_on_every_login", + "fieldtype": "Check", + "label": "Notify Users On Every Login" + }, + { + "depends_on": "eval:doc.notify_on_login && doc.public", + "fieldname": "expire_notification_on", + "fieldtype": "Date", + "label": "Expire Notification On", + "search_index": 1 + }, + { + "bold": 1, + "description": "Help: To link to another record in the system, use \"/app/note/[Note Name]\" as the Link URL. (don't use \"http://\")", + "fieldname": "content", + "fieldtype": "Text Editor", + "in_global_search": 1, + "label": "Content" + }, + { + "collapsible": 1, + "fieldname": "seen_by_section", + "fieldtype": "Section Break", + "label": "Seen By" + }, + { + "fieldname": "seen_by", + "fieldtype": "Table", + "label": "Seen By Table", + "options": "Note Seen By" + } + ], + "icon": "fa fa-file-text", + "idx": 1, + "links": [], + "modified": "2021-09-18 10:57:51.352643", + "modified_by": "Administrator", + "module": "Desk", + "name": "Note", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "All", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "ASC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/note/note.py b/influxframework/desk/doctype/note/note.py new file mode 100644 index 0000000..0335ebe --- /dev/null +++ b/influxframework/desk/doctype/note/note.py @@ -0,0 +1,53 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import re + +import influxframework +from influxframework.model.document import Document + +NAME_PATTERN = re.compile("[%'\"#*?`]") + + +class Note(Document): + def autoname(self): + # replace forbidden characters + self.name = NAME_PATTERN.sub("", self.title.strip()) + + def validate(self): + if self.notify_on_login and not self.expire_notification_on: + + # expire this notification in a week (default) + self.expire_notification_on = influxframework.utils.add_days(self.creation, 7) + + def before_print(self, settings=None): + self.print_heading = self.name + self.sub_heading = "" + + +@influxframework.whitelist() +def mark_as_seen(note): + note = influxframework.get_doc("Note", note) + if influxframework.session.user not in [d.user for d in note.seen_by]: + note.append("seen_by", {"user": influxframework.session.user}) + note.save(ignore_version=True) + + +def get_permission_query_conditions(user): + if not user: + user = influxframework.session.user + + if user == "Administrator": + return "" + + return f"""(`tabNote`.public=1 or `tabNote`.owner={influxframework.db.escape(user)})""" + + +def has_permission(doc, ptype, user): + if doc.public == 1 or user == "Administrator": + return True + + if user == doc.owner: + return True + + return False diff --git a/influxframework/desk/doctype/note/note_list.js b/influxframework/desk/doctype/note/note_list.js new file mode 100644 index 0000000..f83c368 --- /dev/null +++ b/influxframework/desk/doctype/note/note_list.js @@ -0,0 +1,13 @@ +influxframework.listview_settings["Note"] = { + onload: function (me) { + me.page.set_title(__("Notes")); + }, + add_fields: ["title", "public"], + get_indicator: function (doc) { + if (doc.public) { + return [__("Public"), "green", "public,=,Yes"]; + } else { + return [__("Private"), "gray", "public,=,No"]; + } + }, +}; diff --git a/influxframework/desk/doctype/note/test_note.py b/influxframework/desk/doctype/note/test_note.py new file mode 100644 index 0000000..c3c7c78 --- /dev/null +++ b/influxframework/desk/doctype/note/test_note.py @@ -0,0 +1,77 @@ +# Copyright (c) 2015, InfluxFramework LLC and Contributors +# License: MIT. See LICENSE + +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + +test_records = influxframework.get_test_records("Note") + + +class TestNote(InfluxFrameworkTestCase): + def insert_note(self): + influxframework.db.delete("Version") + influxframework.db.delete("Note") + influxframework.db.delete("Note Seen By") + + return influxframework.get_doc( + dict(doctype="Note", title="test note", content="test note content") + ).insert() + + def test_version(self): + note = self.insert_note() + note.title = "test note 1" + note.content = "1" + note.save(ignore_version=False) + + version = influxframework.get_doc("Version", dict(docname=note.name)) + data = version.get_data() + + self.assertTrue(("title", "test note", "test note 1"), data["changed"]) + self.assertTrue(("content", "test note content", "1"), data["changed"]) + + def test_rows(self): + note = self.insert_note() + + # test add + note.append("seen_by", {"user": "Administrator"}) + note.save(ignore_version=False) + + version = influxframework.get_doc("Version", dict(docname=note.name)) + data = version.get_data() + + self.assertEqual(len(data.get("added")), 1) + self.assertEqual(len(data.get("removed")), 0) + self.assertEqual(len(data.get("changed")), 0) + + for row in data.get("added"): + self.assertEqual(row[0], "seen_by") + self.assertEqual(row[1]["user"], "Administrator") + + # test row change + note.seen_by[0].user = "Guest" + note.save(ignore_version=False) + + version = influxframework.get_doc("Version", dict(docname=note.name)) + data = version.get_data() + + self.assertEqual(len(data.get("row_changed")), 1) + for row in data.get("row_changed"): + self.assertEqual(row[0], "seen_by") + self.assertEqual(row[1], 0) + self.assertEqual(row[2], note.seen_by[0].name) + self.assertEqual(row[3], [["user", "Administrator", "Guest"]]) + + # test remove + note.seen_by = [] + note.save(ignore_version=False) + + version = influxframework.get_doc("Version", dict(docname=note.name)) + data = version.get_data() + + self.assertEqual(len(data.get("removed")), 1) + for row in data.get("removed"): + self.assertEqual(row[0], "seen_by") + self.assertEqual(row[1]["user"], "Guest") + + # self.assertTrue(('title', 'test note', 'test note 1'), data['changed']) + # self.assertTrue(('content', 'test note content', '1'), data['changed']) diff --git a/influxframework/desk/doctype/note/test_records.json b/influxframework/desk/doctype/note/test_records.json new file mode 100644 index 0000000..f3d7cff --- /dev/null +++ b/influxframework/desk/doctype/note/test_records.json @@ -0,0 +1,7 @@ +[ + { + "doctype": "Note", + "name": "_Test Note 1", + "title": "Test Note Title" + } +] diff --git a/influxframework/desk/doctype/note_seen_by/__init__.py b/influxframework/desk/doctype/note_seen_by/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/note_seen_by/note_seen_by.json b/influxframework/desk/doctype/note_seen_by/note_seen_by.json new file mode 100644 index 0000000..7ee423e --- /dev/null +++ b/influxframework/desk/doctype/note_seen_by/note_seen_by.json @@ -0,0 +1,64 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2016-08-29 05:29:16.726172", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "user", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "User", + "length": 0, + "no_copy": 0, + "options": "User", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "max_attachments": 0, + "modified": "2016-08-29 06:02:41.531341", + "modified_by": "Administrator", + "module": "Desk", + "name": "Note Seen By", + "name_case": "", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_seen": 0 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/note_seen_by/note_seen_by.py b/influxframework/desk/doctype/note_seen_by/note_seen_by.py new file mode 100644 index 0000000..479a7d9 --- /dev/null +++ b/influxframework/desk/doctype/note_seen_by/note_seen_by.py @@ -0,0 +1,8 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +from influxframework.model.document import Document + + +class NoteSeenBy(Document): + pass diff --git a/influxframework/desk/doctype/notification_log/__init__.py b/influxframework/desk/doctype/notification_log/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/notification_log/notification_log.js b/influxframework/desk/doctype/notification_log/notification_log.js new file mode 100644 index 0000000..34356da --- /dev/null +++ b/influxframework/desk/doctype/notification_log/notification_log.js @@ -0,0 +1,45 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Notification Log", { + refresh: function (frm) { + if (frm.doc.attached_file) { + frm.trigger("set_attachment"); + } else { + frm.get_field("attachment_link").$wrapper.empty(); + } + }, + + open_reference_document: function (frm) { + const dt = frm.doc.document_type; + const dn = frm.doc.document_name; + influxframework.set_route("Form", dt, dn); + }, + + set_attachment: function (frm) { + const attachment = JSON.parse(frm.doc.attached_file); + + const $wrapper = frm.get_field("attachment_link").$wrapper; + $wrapper.html(` + + `); + + $wrapper.find(".attached-file-link").click(() => { + const w = window.open( + influxframework.urllib.get_full_url(`/api/method/influxframework.utils.print_format.download_pdf? + doctype=${encodeURIComponent(attachment.doctype)} + &name=${encodeURIComponent(attachment.name)} + &format=${encodeURIComponent(attachment.print_format)} + &lang=${encodeURIComponent(attachment.lang)}`) + ); + if (!w) { + influxframework.msgprint(__("Please enable pop-ups")); + } + }); + }, +}); diff --git a/influxframework/desk/doctype/notification_log/notification_log.json b/influxframework/desk/doctype/notification_log/notification_log.json new file mode 100644 index 0000000..f24a644 --- /dev/null +++ b/influxframework/desk/doctype/notification_log/notification_log.json @@ -0,0 +1,120 @@ +{ + "actions": [], + "creation": "2019-08-26 13:37:34.165254", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "subject", + "for_user", + "type", + "email_content", + "document_type", + "read", + "document_name", + "attached_file", + "attachment_link", + "open_reference_document", + "from_user" + ], + "fields": [ + { + "fieldname": "subject", + "fieldtype": "Text", + "in_list_view": 1, + "label": "Subject" + }, + { + "fieldname": "for_user", + "fieldtype": "Link", + "hidden": 1, + "label": "For User", + "options": "User" + }, + { + "fieldname": "type", + "fieldtype": "Select", + "hidden": 1, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Type", + "options": "Mention\nEnergy Point\nAssignment\nShare\nAlert" + }, + { + "fieldname": "email_content", + "fieldtype": "Text Editor", + "label": "Message" + }, + { + "fieldname": "document_type", + "fieldtype": "Link", + "hidden": 1, + "label": "Document Type", + "options": "DocType" + }, + { + "fieldname": "document_name", + "fieldtype": "Data", + "hidden": 1, + "label": "Document Link", + "search_index": 1 + }, + { + "fieldname": "from_user", + "fieldtype": "Link", + "hidden": 1, + "label": "From User", + "options": "User", + "search_index": 1 + }, + { + "default": "0", + "fieldname": "read", + "fieldtype": "Check", + "hidden": 1, + "ignore_user_permissions": 1, + "label": "Read" + }, + { + "fieldname": "open_reference_document", + "fieldtype": "Button", + "label": "Open Reference Document" + }, + { + "fieldname": "attached_file", + "fieldtype": "Code", + "hidden": 1, + "label": "Attached File", + "options": "JSON" + }, + { + "fieldname": "attachment_link", + "fieldtype": "HTML", + "label": "Attachment Link" + } + ], + "hide_toolbar": 1, + "in_create": 1, + "links": [], + "modified": "2022-09-13 16:08:48.153934", + "modified_by": "Administrator", + "module": "Desk", + "name": "Notification Log", + "owner": "Administrator", + "permissions": [ + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "subject", + "track_seen": 1 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/notification_log/notification_log.py b/influxframework/desk/doctype/notification_log/notification_log.py new file mode 100644 index 0000000..8abd7ed --- /dev/null +++ b/influxframework/desk/doctype/notification_log/notification_log.py @@ -0,0 +1,186 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework import _ +from influxframework.desk.doctype.notification_settings.notification_settings import ( + is_email_notifications_enabled_for_type, + is_notifications_enabled, +) +from influxframework.model.document import Document + + +class NotificationLog(Document): + def after_insert(self): + influxframework.publish_realtime("notification", after_commit=True, user=self.for_user) + set_notifications_as_unseen(self.for_user) + if is_email_notifications_enabled_for_type(self.for_user, self.type): + try: + send_notification_email(self) + except influxframework.OutgoingEmailError: + self.log_error(_("Failed to send notification email")) + + @staticmethod + def clear_old_logs(days=180): + from influxframework.query_builder import Interval + from influxframework.query_builder.functions import Now + + table = influxframework.qb.DocType("Notification Log") + influxframework.db.delete(table, filters=(table.modified < (Now() - Interval(days=days)))) + + +def get_permission_query_conditions(for_user): + if not for_user: + for_user = influxframework.session.user + + if for_user == "Administrator": + return + + return f"""(`tabNotification Log`.for_user = {influxframework.db.escape(for_user)})""" + + +def get_title(doctype, docname, title_field=None): + if not title_field: + title_field = influxframework.get_meta(doctype).get_title_field() + title = docname if title_field == "name" else influxframework.db.get_value(doctype, docname, title_field) + return title + + +def get_title_html(title): + return f'{title}' + + +def enqueue_create_notification(users: list[str] | str, doc: dict): + """Send notification to users. + + users: list of user emails or string of users with comma separated emails + doc: contents of `Notification` doc + """ + + # During installation of new site, enqueue_create_notification tries to connect to Redis. + # This breaks new site creation if Redis server is not running. + # We do not need any notifications in fresh installation + if influxframework.flags.in_install: + return + + doc = influxframework._dict(doc) + + if isinstance(users, str): + users = [user.strip() for user in users.split(",") if user.strip()] + users = list(set(users)) + + influxframework.enqueue( + "influxframework.desk.doctype.notification_log.notification_log.make_notification_logs", + doc=doc, + users=users, + now=influxframework.flags.in_test, + ) + + +def make_notification_logs(doc, users): + for user in _get_user_ids(users): + notification = influxframework.new_doc("Notification Log") + notification.update(doc) + notification.for_user = user + if ( + notification.for_user != notification.from_user + or doc.type == "Energy Point" + or doc.type == "Alert" + ): + notification.insert(ignore_permissions=True) + + +def _get_user_ids(user_emails): + user_names = influxframework.db.get_values( + "User", {"enabled": 1, "email": ("in", user_emails)}, "name", pluck=True + ) + return [user for user in user_names if is_notifications_enabled(user)] + + +def send_notification_email(doc): + + if doc.type == "Energy Point" and doc.email_content is None: + return + + from influxframework.utils import get_url_to_form, strip_html + + email = influxframework.db.get_value("User", doc.for_user, "email") + if not email: + return + + doc_link = get_url_to_form(doc.document_type, doc.document_name) + header = get_email_header(doc) + email_subject = strip_html(doc.subject) + + influxframework.sendmail( + recipients=email, + subject=email_subject, + template="new_notification", + args={ + "body_content": doc.subject, + "description": doc.email_content, + "document_type": doc.document_type, + "document_name": doc.document_name, + "doc_link": doc_link, + }, + header=[header, "orange"], + now=influxframework.flags.in_test, + ) + + +def get_email_header(doc): + docname = doc.document_name + header_map = { + "Default": _("New Notification"), + "Mention": _("New Mention on {0}").format(docname), + "Assignment": _("Assignment Update on {0}").format(docname), + "Share": _("New Document Shared {0}").format(docname), + "Energy Point": _("Energy Point Update on {0}").format(docname), + } + + return header_map[doc.type or "Default"] + + +@influxframework.whitelist() +def get_notification_logs(limit=20): + notification_logs = influxframework.db.get_list( + "Notification Log", fields=["*"], limit=limit, order_by="modified desc" + ) + + users = [log.from_user for log in notification_logs] + users = [*set(users)] # remove duplicates + user_info = influxframework._dict() + + for user in users: + influxframework.utils.add_user_info(user, user_info) + + return {"notification_logs": notification_logs, "user_info": user_info} + + +@influxframework.whitelist() +def mark_all_as_read(): + unread_docs_list = influxframework.get_all( + "Notification Log", filters={"read": 0, "for_user": influxframework.session.user} + ) + unread_docnames = [doc.name for doc in unread_docs_list] + if unread_docnames: + filters = {"name": ["in", unread_docnames]} + influxframework.db.set_value("Notification Log", filters, "read", 1, update_modified=False) + + +@influxframework.whitelist() +def mark_as_read(docname): + if docname: + influxframework.db.set_value("Notification Log", docname, "read", 1, update_modified=False) + + +@influxframework.whitelist() +def trigger_indicator_hide(): + influxframework.publish_realtime("indicator_hide", user=influxframework.session.user) + + +def set_notifications_as_unseen(user): + try: + influxframework.db.set_value("Notification Settings", user, "seen", 0, update_modified=False) + except influxframework.DoesNotExistError: + return diff --git a/influxframework/desk/doctype/notification_log/notification_log_list.js b/influxframework/desk/doctype/notification_log/notification_log_list.js new file mode 100644 index 0000000..4564f60 --- /dev/null +++ b/influxframework/desk/doctype/notification_log/notification_log_list.js @@ -0,0 +1,7 @@ +influxframework.listview_settings["Notification Log"] = { + onload: function (listview) { + influxframework.require("logtypes.bundle.js", () => { + influxframework.utils.logtypes.show_log_retention_message(cur_list.doctype); + }); + }, +}; diff --git a/influxframework/desk/doctype/notification_log/test_notification_log.py b/influxframework/desk/doctype/notification_log/test_notification_log.py new file mode 100644 index 0000000..aae2576 --- /dev/null +++ b/influxframework/desk/doctype/notification_log/test_notification_log.py @@ -0,0 +1,53 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.core.doctype.user.user import get_system_users +from influxframework.desk.form.assign_to import add as assign_task +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestNotificationLog(InfluxFrameworkTestCase): + def test_assignment(self): + todo = get_todo() + user = get_user() + + assign_task( + {"assign_to": [user], "doctype": "ToDo", "name": todo.name, "description": todo.description} + ) + log_type = influxframework.db.get_value( + "Notification Log", {"document_type": "ToDo", "document_name": todo.name}, "type" + ) + self.assertEqual(log_type, "Assignment") + + def test_share(self): + todo = get_todo() + user = get_user() + + influxframework.share.add("ToDo", todo.name, user, notify=1) + log_type = influxframework.db.get_value( + "Notification Log", {"document_type": "ToDo", "document_name": todo.name}, "type" + ) + self.assertEqual(log_type, "Share") + + email = get_last_email_queue() + content = "Subject: {} shared a document ToDo".format( + influxframework.utils.get_fullname(influxframework.session.user) + ) + self.assertTrue(content in email.message) + + +def get_last_email_queue(): + res = influxframework.get_all("Email Queue", fields=["message"], order_by="creation desc", limit=1) + return res[0] + + +def get_todo(): + if not influxframework.get_all("ToDo"): + return influxframework.get_doc({"doctype": "ToDo", "description": "Test for Notification"}).insert() + + res = influxframework.get_all("ToDo", limit=1) + return influxframework.get_cached_doc("ToDo", res[0].name) + + +def get_user(): + return get_system_users(limit=1)[0] diff --git a/influxframework/desk/doctype/notification_settings/__init__.py b/influxframework/desk/doctype/notification_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/notification_settings/notification_settings.js b/influxframework/desk/doctype/notification_settings/notification_settings.js new file mode 100644 index 0000000..530288e --- /dev/null +++ b/influxframework/desk/doctype/notification_settings/notification_settings.js @@ -0,0 +1,27 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Notification Settings", { + onload: (frm) => { + influxframework.breadcrumbs.add({ + label: __("Settings"), + route: "#modules/Settings", + type: "Custom", + }); + frm.set_query("subscribed_documents", () => { + return { + filters: { + istable: 0, + }, + }; + }); + }, + + refresh: (frm) => { + if (influxframework.user.has_role("System Manager")) { + frm.add_custom_button(__("Go to Notification Settings List"), () => { + influxframework.set_route("List", "Notification Settings"); + }); + } + }, +}); diff --git a/influxframework/desk/doctype/notification_settings/notification_settings.json b/influxframework/desk/doctype/notification_settings/notification_settings.json new file mode 100644 index 0000000..1a6efd5 --- /dev/null +++ b/influxframework/desk/doctype/notification_settings/notification_settings.json @@ -0,0 +1,136 @@ +{ + "actions": [], + "autoname": "Prompt", + "creation": "2019-09-11 22:15:44.851526", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "enabled", + "subscribed_documents", + "column_break_3", + "enable_email_notifications", + "enable_email_mention", + "enable_email_assignment", + "enable_email_energy_point", + "enable_email_share", + "enable_email_event_reminders", + "user", + "seen", + "system_notifications_section", + "energy_points_system_notifications" + ], + "fields": [ + { + "default": "1", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enabled" + }, + { + "fieldname": "subscribed_documents", + "fieldtype": "Table MultiSelect", + "label": "Open Documents", + "options": "Notification Subscribed Document" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Section Break", + "label": "Email Settings" + }, + { + "default": "1", + "fieldname": "enable_email_notifications", + "fieldtype": "Check", + "label": "Enable Email Notifications" + }, + { + "default": "1", + "depends_on": "enable_email_notifications", + "fieldname": "enable_email_mention", + "fieldtype": "Check", + "label": "Mentions" + }, + { + "default": "1", + "depends_on": "enable_email_notifications", + "fieldname": "enable_email_assignment", + "fieldtype": "Check", + "label": "Assignments" + }, + { + "default": "1", + "depends_on": "enable_email_notifications", + "fieldname": "enable_email_energy_point", + "fieldtype": "Check", + "label": "Energy Points" + }, + { + "default": "1", + "depends_on": "enable_email_notifications", + "fieldname": "enable_email_share", + "fieldtype": "Check", + "label": "Document Share" + }, + { + "default": "__user", + "fieldname": "user", + "fieldtype": "Link", + "hidden": 1, + "label": "User", + "options": "User", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "seen", + "fieldtype": "Check", + "hidden": 1, + "label": "Seen" + }, + { + "fieldname": "system_notifications_section", + "fieldtype": "Section Break", + "label": "System Notifications" + }, + { + "default": "1", + "fieldname": "energy_points_system_notifications", + "fieldtype": "Check", + "label": "Energy Points" + }, + { + "default": "1", + "depends_on": "enable_email_notifications", + "fieldname": "enable_email_event_reminders", + "fieldtype": "Check", + "label": "Event Reminders" + } + ], + "in_create": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-11-24 14:45:31.931154", + "modified_by": "Administrator", + "module": "Desk", + "name": "Notification Settings", + "naming_rule": "Set by user", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1, + "write": 1 + } + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/notification_settings/notification_settings.py b/influxframework/desk/doctype/notification_settings/notification_settings.py new file mode 100644 index 0000000..7a77d9f --- /dev/null +++ b/influxframework/desk/doctype/notification_settings/notification_settings.py @@ -0,0 +1,95 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document + + +class NotificationSettings(Document): + def on_update(self): + from influxframework.desk.notifications import clear_notification_config + + clear_notification_config(influxframework.session.user) + + +def is_notifications_enabled(user): + enabled = influxframework.db.get_value("Notification Settings", user, "enabled") + if enabled is None: + return True + return enabled + + +def is_email_notifications_enabled(user): + enabled = influxframework.db.get_value("Notification Settings", user, "enable_email_notifications") + if enabled is None: + return True + return enabled + + +def is_email_notifications_enabled_for_type(user, notification_type): + if not is_email_notifications_enabled(user): + return False + + if notification_type == "Alert": + return False + + fieldname = "enable_email_" + influxframework.scrub(notification_type) + enabled = influxframework.db.get_value("Notification Settings", user, fieldname) + if enabled is None: + return True + return enabled + + +def create_notification_settings(user): + if not influxframework.db.exists("Notification Settings", user): + _doc = influxframework.new_doc("Notification Settings") + _doc.name = user + _doc.insert(ignore_permissions=True) + + +def toggle_notifications(user: str, enable: bool = False): + try: + settings = influxframework.get_doc("Notification Settings", user) + except influxframework.DoesNotExistError: + influxframework.clear_last_message() + return + + if settings.enabled != enable: + settings.enabled = enable + settings.save() + + +@influxframework.whitelist() +def get_subscribed_documents(): + if not influxframework.session.user: + return [] + + try: + if influxframework.db.exists("Notification Settings", influxframework.session.user): + doc = influxframework.get_doc("Notification Settings", influxframework.session.user) + return [item.document for item in doc.subscribed_documents] + # Notification Settings is fetched even before sync doctype is called + # but it will throw an ImportError, we can ignore it in migrate + except ImportError: + pass + + return [] + + +def get_permission_query_conditions(user): + if not user: + user = influxframework.session.user + + if user == "Administrator": + return + + roles = influxframework.get_roles(user) + if "System Manager" in roles: + return """(`tabNotification Settings`.name != 'Administrator')""" + + return f"""(`tabNotification Settings`.name = {influxframework.db.escape(user)})""" + + +@influxframework.whitelist() +def set_seen_value(value, user): + influxframework.db.set_value("Notification Settings", user, "seen", value, update_modified=False) diff --git a/influxframework/desk/doctype/notification_settings/test_notification_settings.py b/influxframework/desk/doctype/notification_settings/test_notification_settings.py new file mode 100644 index 0000000..4cb4122 --- /dev/null +++ b/influxframework/desk/doctype/notification_settings/test_notification_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, InfluxFramework LLC +# See license.txt + +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestNotificationSettings(InfluxFrameworkTestCase): + pass diff --git a/influxframework/desk/doctype/notification_subscribed_document/__init__.py b/influxframework/desk/doctype/notification_subscribed_document/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/notification_subscribed_document/notification_subscribed_document.json b/influxframework/desk/doctype/notification_subscribed_document/notification_subscribed_document.json new file mode 100644 index 0000000..b3f4046 --- /dev/null +++ b/influxframework/desk/doctype/notification_subscribed_document/notification_subscribed_document.json @@ -0,0 +1,30 @@ +{ + "creation": "2019-10-09 15:04:39.504787", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document" + ], + "fields": [ + { + "fieldname": "document", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document", + "options": "DocType", + "reqd": 1 + } + ], + "istable": 1, + "modified": "2019-10-09 16:02:00.049237", + "modified_by": "Administrator", + "module": "Desk", + "name": "Notification Subscribed Document", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/notification_subscribed_document/notification_subscribed_document.py b/influxframework/desk/doctype/notification_subscribed_document/notification_subscribed_document.py new file mode 100644 index 0000000..4656cbb --- /dev/null +++ b/influxframework/desk/doctype/notification_subscribed_document/notification_subscribed_document.py @@ -0,0 +1,9 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class NotificationSubscribedDocument(Document): + pass diff --git a/influxframework/desk/doctype/number_card/__init__.py b/influxframework/desk/doctype/number_card/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/number_card/number_card.js b/influxframework/desk/doctype/number_card/number_card.js new file mode 100644 index 0000000..447be61 --- /dev/null +++ b/influxframework/desk/doctype/number_card/number_card.js @@ -0,0 +1,493 @@ +// Copyright (c) 2020, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Number Card", { + refresh: function (frm) { + if (!influxframework.boot.developer_mode && frm.doc.is_standard) { + frm.disable_form(); + } + frm.set_df_property("filters_section", "hidden", 1); + frm.set_df_property("dynamic_filters_section", "hidden", 1); + frm.trigger("set_options"); + + if (!frm.doc.type) { + frm.set_value("type", "Document Type"); + } + + if (frm.doc.type == "Report" && frm.doc.report_name) { + frm.trigger("set_report_filters"); + } + + if (frm.doc.type == "Custom") { + if (!influxframework.boot.developer_mode) { + frm.disable_form(); + } + frm.filters = eval(frm.doc.filters_config); + frm.trigger("set_filters_description"); + frm.trigger("set_method_description"); + frm.trigger("render_filters_table"); + } + frm.trigger("set_parent_document_type"); + + if (!frm.is_new()) { + frm.trigger("create_add_to_dashboard_button"); + } + }, + + create_add_to_dashboard_button: function (frm) { + frm.add_custom_button("Add Card to Dashboard", () => { + const dialog = influxframework.dashboard_utils.get_add_to_dashboard_dialog( + frm.doc.name, + "Number Card", + "influxframework.desk.doctype.number_card.number_card.add_card_to_dashboard" + ); + + if (!frm.doc.name) { + influxframework.msgprint(__("Please create Card first")); + } else { + dialog.show(); + } + }); + }, + + before_save: function (frm) { + let dynamic_filters = JSON.parse(frm.doc.dynamic_filters_json || "null"); + let static_filters = JSON.parse(frm.doc.filters_json || "null"); + static_filters = influxframework.dashboard_utils.remove_common_static_filter_values( + static_filters, + dynamic_filters + ); + + frm.set_value("filters_json", JSON.stringify(static_filters)); + frm.trigger("render_filters_table"); + frm.trigger("render_dynamic_filters_table"); + }, + + is_standard: function (frm) { + frm.trigger("render_dynamic_filters_table"); + frm.set_df_property("dynamic_filters_section", "hidden", 1); + }, + + set_filters_description: function (frm) { + if (frm.doc.type == "Custom") { + frm.fields_dict.filters_config.set_description(` + Set the filters here. For example: +
    +
    +[{
    +	fieldname: "company",
    +	label: __("Company"),
    +	fieldtype: "Link",
    +	options: "Company",
    +	default: influxframework.defaults.get_user_default("Company"),
    +	reqd: 1
    +},
    +{
    +	fieldname: "account",
    +	label: __("Account"),
    +	fieldtype: "Link",
    +	options: "Account",
    +	reqd: 1
    +}]
    +
    `); + } + }, + + set_method_description: function (frm) { + if (frm.doc.type == "Custom") { + frm.fields_dict.method.set_description(` + Set the path to a whitelisted function that will return the number on the card in the format: +
    +
    +{
    +	"value": value,
    +	"fieldtype": "Currency"
    +}
    +
    `); + } + }, + + type: function (frm) { + frm.trigger("set_filters_description"); + if (frm.doc.type == "Report") { + frm.set_query("report_name", () => { + return { + filters: { + report_type: ["!=", "Report Builder"], + }, + }; + }); + } + }, + + report_name: function (frm) { + frm.filters = []; + frm.set_value("filters_json", "{}"); + frm.set_value("dynamic_filters_json", "{}"); + frm.set_df_property("report_field", "options", []); + frm.trigger("set_report_filters"); + }, + + filters_config: function (frm) { + frm.filters = eval(frm.doc.filters_config); + const filter_values = influxframework.report_utils.get_filter_values(frm.filters); + frm.set_value("filters_json", JSON.stringify(filter_values)); + frm.trigger("render_filters_table"); + }, + + document_type: function (frm) { + frm.set_query("document_type", function () { + return { + filters: { + issingle: false, + }, + }; + }); + frm.set_value("filters_json", "[]"); + frm.set_value("dynamic_filters_json", "[]"); + frm.set_value("aggregate_function_based_on", ""); + frm.set_value("parent_document_type", ""); + frm.trigger("set_options"); + frm.trigger("set_parent_document_type"); + }, + + set_options: function (frm) { + if (frm.doc.type !== "Document Type") { + return; + } + + let aggregate_based_on_fields = []; + const doctype = frm.doc.document_type; + + if (doctype) { + influxframework.model.with_doctype(doctype, () => { + influxframework.get_meta(doctype).fields.map((df) => { + if (influxframework.model.numeric_fieldtypes.includes(df.fieldtype)) { + if (df.fieldtype == "Currency") { + if (!df.options || df.options !== "Company:company:default_currency") { + return; + } + } + aggregate_based_on_fields.push({ label: df.label, value: df.fieldname }); + } + }); + + frm.set_df_property( + "aggregate_function_based_on", + "options", + aggregate_based_on_fields + ); + }); + frm.trigger("render_filters_table"); + frm.trigger("render_dynamic_filters_table"); + } + }, + + set_report_filters: function (frm) { + const report_name = frm.doc.report_name; + if (report_name) { + influxframework.report_utils.get_report_filters(report_name).then((filters) => { + if (filters) { + frm.filters = filters; + const filter_values = influxframework.report_utils.get_filter_values(filters); + if (frm.doc.filters_json.length <= 2) { + frm.set_value("filters_json", JSON.stringify(filter_values)); + } + } + frm.trigger("render_filters_table"); + frm.trigger("set_report_field_options"); + frm.trigger("render_dynamic_filters_table"); + }); + } + }, + + set_report_field_options: function (frm) { + let filters = frm.doc.filters_json.length > 2 ? JSON.parse(frm.doc.filters_json) : null; + if (frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2) { + filters = influxframework.dashboard_utils.get_all_filters(frm.doc); + } + influxframework + .xcall("influxframework.desk.query_report.run", { + report_name: frm.doc.report_name, + filters: filters, + ignore_prepared_report: 1, + }) + .then((data) => { + if (data.result.length) { + frm.field_options = influxframework.report_utils.get_field_options_from_report( + data.columns, + data + ); + frm.set_df_property( + "report_field", + "options", + frm.field_options.numeric_fields + ); + if (!frm.field_options.numeric_fields.length) { + influxframework.msgprint( + __("Report has no numeric fields, please change the Report Name") + ); + } + } else { + influxframework.msgprint( + __( + "Report has no data, please modify the filters or change the Report Name" + ) + ); + } + }); + }, + + render_filters_table: function (frm) { + frm.set_df_property("filters_section", "hidden", 0); + let is_document_type = frm.doc.type == "Document Type"; + let is_dynamic_filter = (f) => ["Date", "DateRange"].includes(f.fieldtype) && f.default; + + let wrapper = $(frm.get_field("filters_json").wrapper).empty(); + let table = $(` + + + + + + + + +
    ${__("Filter")}${__("Condition")}${__("Value")}
    `).appendTo(wrapper); + $(`

    ${__("Click table to edit")}

    `).appendTo(wrapper); + + let filters = JSON.parse(frm.doc.filters_json || "[]"); + let filters_set = false; + + // Set dynamic filters for reports + if (frm.doc.type == "Report") { + let set_filters = false; + frm.filters.forEach((f) => { + if (is_dynamic_filter(f)) { + filters[f.fieldname] = f.default; + set_filters = true; + } + }); + set_filters && frm.set_value("filters_json", JSON.stringify(filters)); + } + + let fields = []; + if (is_document_type) { + fields = [ + { + fieldtype: "HTML", + fieldname: "filter_area", + }, + ]; + + if (filters.length) { + filters.forEach((filter) => { + const filter_row = $(` + ${filter[1]} + ${filter[2] || ""} + ${filter[3]} + `); + + table.find("tbody").append(filter_row); + }); + filters_set = true; + } + } else if (frm.filters.length) { + fields = frm.filters.filter((f) => f.fieldname); + fields.map((f) => { + if (filters[f.fieldname]) { + let condition = "="; + const filter_row = $(` + ${f.label} + ${condition} + ${filters[f.fieldname] || ""} + `); + table.find("tbody").append(filter_row); + if (!filters_set) filters_set = true; + } + }); + } + + if (!filters_set) { + const filter_row = $(` + ${__("Click to Set Filters")}`); + table.find("tbody").append(filter_row); + } + + table.on("click", () => { + let dialog = new influxframework.ui.Dialog({ + title: __("Set Filters"), + fields: fields.filter((f) => !is_dynamic_filter(f)), + primary_action: function () { + let values = this.get_values(); + if (values) { + this.hide(); + if (is_document_type) { + let filters = frm.filter_group.get_filters(); + frm.set_value("filters_json", JSON.stringify(filters)); + } else { + frm.set_value("filters_json", JSON.stringify(values)); + } + frm.trigger("render_filters_table"); + } + }, + primary_action_label: "Set", + }); + + if (is_document_type) { + frm.filter_group = new influxframework.ui.FilterGroup({ + parent: dialog.get_field("filter_area").$wrapper, + doctype: frm.doc.document_type, + parent_doctype: frm.doc.parent_document_type, + on_change: () => {}, + }); + filters && frm.filter_group.add_filters_to_filter_group(filters); + } + + dialog.show(); + + if (frm.doc.type == "Report") { + //Set query report object so that it can be used while fetching filter values in the report + influxframework.query_report = new influxframework.views.QueryReport({ + filters: dialog.fields_list, + }); + influxframework.query_reports[frm.doc.report_name] && + influxframework.query_reports[frm.doc.report_name].onload && + influxframework.query_reports[frm.doc.report_name].onload(influxframework.query_report); + } + + dialog.set_values(filters); + }); + }, + + render_dynamic_filters_table(frm) { + if (!influxframework.boot.developer_mode || !frm.doc.is_standard || frm.doc.type == "Custom") { + return; + } + + frm.set_df_property("dynamic_filters_section", "hidden", 0); + + let is_document_type = frm.doc.type == "Document Type"; + + let wrapper = $(frm.get_field("dynamic_filters_json").wrapper).empty(); + + frm.dynamic_filter_table = + $(` + + + + + + + + +
    ${__("Filter")}${__("Condition")}${__("Value")}
    `).appendTo(wrapper); + + frm.dynamic_filters = + frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2 + ? JSON.parse(frm.doc.dynamic_filters_json) + : null; + + frm.trigger("set_dynamic_filters_in_table"); + + let filters = JSON.parse(frm.doc.filters_json || "[]"); + + let fields = influxframework.dashboard_utils.get_fields_for_dynamic_filter_dialog( + is_document_type, + filters, + frm.dynamic_filters + ); + + frm.dynamic_filter_table.on("click", () => { + let dialog = new influxframework.ui.Dialog({ + title: __("Set Dynamic Filters"), + fields: fields, + primary_action: () => { + let values = dialog.get_values(); + dialog.hide(); + let dynamic_filters = []; + for (let key of Object.keys(values)) { + if (is_document_type) { + let [doctype, fieldname] = key.split(":"); + dynamic_filters.push([doctype, fieldname, "=", values[key]]); + } + } + + if (is_document_type) { + frm.set_value("dynamic_filters_json", JSON.stringify(dynamic_filters)); + } else { + frm.set_value("dynamic_filters_json", JSON.stringify(values)); + } + frm.trigger("set_dynamic_filters_in_table"); + }, + primary_action_label: "Set", + }); + + dialog.show(); + dialog.set_values(frm.dynamic_filters); + }); + }, + + set_dynamic_filters_in_table: function (frm) { + frm.dynamic_filters = + frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2 + ? JSON.parse(frm.doc.dynamic_filters_json) + : null; + + if (!frm.dynamic_filters) { + const filter_row = $(` + ${__("Click to Set Dynamic Filters")}`); + frm.dynamic_filter_table.find("tbody").html(filter_row); + } else { + let filter_rows = ""; + if ($.isArray(frm.dynamic_filters)) { + frm.dynamic_filters.forEach((filter) => { + filter_rows += ` + ${filter[1]} + ${filter[2] || ""} + ${filter[3]} + `; + }); + } else { + let condition = "="; + for (let [key, val] of Object.entries(frm.dynamic_filters)) { + filter_rows += ` + ${key} + ${condition} + ${val || ""} + `; + } + } + + frm.dynamic_filter_table.find("tbody").html(filter_rows); + } + }, + + set_parent_document_type: async function (frm) { + let document_type = frm.doc.document_type; + let doc_is_table = + document_type && + (await influxframework.db.get_value("DocType", document_type, "istable")).message.istable; + + frm.set_df_property("parent_document_type", "hidden", !doc_is_table); + + if (document_type && doc_is_table) { + let parents = await influxframework.xcall( + "influxframework.desk.doctype.dashboard_chart.dashboard_chart.get_parent_doctypes", + { child_type: document_type } + ); + + frm.set_query("parent_document_type", function () { + return { + filters: { + name: ["in", parents], + }, + }; + }); + + if (parents.length === 1) { + frm.set_value("parent_document_type", parents[0]); + } + } + }, +}); diff --git a/influxframework/desk/doctype/number_card/number_card.json b/influxframework/desk/doctype/number_card/number_card.json new file mode 100644 index 0000000..4e8982d --- /dev/null +++ b/influxframework/desk/doctype/number_card/number_card.json @@ -0,0 +1,249 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2020-04-15 18:06:39.444683", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "is_standard", + "module", + "label", + "type", + "report_name", + "method", + "function", + "aggregate_function_based_on", + "column_break_2", + "document_type", + "parent_document_type", + "report_field", + "report_function", + "is_public", + "custom_configuration_section", + "filters_config", + "stats_section", + "show_percentage_stats", + "stats_time_interval", + "filters_section", + "filters_json", + "dynamic_filters_section", + "dynamic_filters_json", + "section_break_16", + "color" + ], + "fields": [ + { + "depends_on": "eval: doc.type == 'Document Type'", + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "mandatory_depends_on": "eval: doc.type == 'Document Type'", + "options": "DocType" + }, + { + "depends_on": "eval: doc.type == 'Document Type'", + "fieldname": "function", + "fieldtype": "Select", + "label": "Function", + "mandatory_depends_on": "eval: doc.type == 'Document Type'", + "options": "Count\nSum\nAverage\nMinimum\nMaximum" + }, + { + "depends_on": "eval: doc.type === 'Document Type' && doc.function !== 'Count'", + "fieldname": "aggregate_function_based_on", + "fieldtype": "Select", + "label": "Aggregate Function Based On", + "mandatory_depends_on": "eval: doc.function !== 'Count'" + }, + { + "fieldname": "filters_json", + "fieldtype": "Code", + "label": "Filters JSON", + "options": "JSON" + }, + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "reqd": 1 + }, + { + "fieldname": "color", + "fieldtype": "Color", + "label": "Color" + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "filters_section", + "fieldtype": "Section Break", + "label": "Filters Section" + }, + { + "default": "0", + "description": "This card will be available to all Users if this is set", + "fieldname": "is_public", + "fieldtype": "Check", + "label": "Is Public" + }, + { + "default": "1", + "fieldname": "show_percentage_stats", + "fieldtype": "Check", + "label": "Show Percentage Stats" + }, + { + "default": "Daily", + "depends_on": "eval: doc.show_percentage_stats", + "description": "Show percentage difference according to this time interval", + "fieldname": "stats_time_interval", + "fieldtype": "Select", + "label": "Stats Time Interval", + "options": "Daily\nWeekly\nMonthly\nYearly" + }, + { + "depends_on": "eval: doc.type == 'Document Type'", + "fieldname": "stats_section", + "fieldtype": "Section Break", + "label": "Stats" + }, + { + "default": "0", + "fieldname": "is_standard", + "fieldtype": "Check", + "label": "Is Standard", + "read_only_depends_on": "eval: !influxframework.boot.developer_mode" + }, + { + "depends_on": "eval: doc.is_standard", + "fieldname": "module", + "fieldtype": "Link", + "label": "Module", + "mandatory_depends_on": "eval: doc.is_standard", + "options": "Module Def" + }, + { + "fieldname": "dynamic_filters_json", + "fieldtype": "Code", + "label": "Dynamic Filters JSON", + "options": "JSON" + }, + { + "fieldname": "section_break_16", + "fieldtype": "Section Break" + }, + { + "fieldname": "dynamic_filters_section", + "fieldtype": "Section Break", + "label": "Dynamic Filters Section" + }, + { + "fieldname": "type", + "fieldtype": "Select", + "label": "Type", + "options": "Document Type\nReport\nCustom" + }, + { + "depends_on": "eval: doc.type == 'Report'", + "fieldname": "report_name", + "fieldtype": "Link", + "label": "Report Name", + "mandatory_depends_on": "eval: doc.type == 'Report'", + "options": "Report" + }, + { + "depends_on": "eval: doc.type == 'Report'", + "fieldname": "report_field", + "fieldtype": "Select", + "label": "Field", + "mandatory_depends_on": "eval: doc.type == 'Report'" + }, + { + "depends_on": "eval: doc.type == 'Custom'", + "fieldname": "method", + "fieldtype": "Data", + "label": "Method", + "mandatory_depends_on": "eval: doc.type == 'Custom'" + }, + { + "depends_on": "eval: doc.type == 'Custom'", + "fieldname": "custom_configuration_section", + "fieldtype": "Section Break", + "label": "Custom Configuration" + }, + { + "fieldname": "filters_config", + "fieldtype": "Code", + "label": "Filters Configuration", + "options": "JSON" + }, + { + "depends_on": "eval: doc.type == 'Report'", + "fieldname": "report_function", + "fieldtype": "Select", + "label": "Function", + "mandatory_depends_on": "eval: doc.type == 'Report'", + "options": "Sum\nAverage\nMinimum\nMaximum" + }, + { + "description": "The document type selected is a child table, so the parent document type is required.", + "depends_on": "eval: doc.type === 'Document Type'", + "fieldname": "parent_document_type", + "fieldtype": "Link", + "label": "Parent Document Type", + "options": "DocType" + } + ], + "links": [], + "modified": "2022-06-12 15:34:38.210910", + "modified_by": "Administrator", + "module": "Desk", + "name": "Number Card", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Dashboard Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 + } + ], + "search_fields": "label, document_type", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "label", + "track_changes": 1 +} diff --git a/influxframework/desk/doctype/number_card/number_card.py b/influxframework/desk/doctype/number_card/number_card.py new file mode 100644 index 0000000..3f4462f --- /dev/null +++ b/influxframework/desk/doctype/number_card/number_card.py @@ -0,0 +1,236 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework import _ +from influxframework.boot import get_allowed_report_names +from influxframework.config import get_modules_from_all_apps_for_user +from influxframework.model.document import Document +from influxframework.model.naming import append_number_if_name_exists +from influxframework.modules.export_file import export_to_files +from influxframework.query_builder import Criterion +from influxframework.query_builder.utils import DocType +from influxframework.utils import cint + + +class NumberCard(Document): + def autoname(self): + if not self.name: + self.name = self.label + + if influxframework.db.exists("Number Card", self.name): + self.name = append_number_if_name_exists("Number Card", self.name) + + def validate(self): + if self.type == "Document Type": + if not (self.document_type and self.function): + influxframework.throw(_("Document Type and Function are required to create a number card")) + + if self.function != "Count" and not self.aggregate_function_based_on: + influxframework.throw(_("Aggregate Field is required to create a number card")) + + if influxframework.get_meta(self.document_type).istable and not self.parent_document_type: + influxframework.throw(_("Parent Document Type is required to create a number card")) + + elif self.type == "Report": + if not (self.report_name and self.report_field and self.function): + influxframework.throw(_("Report Name, Report Field and Fucntion are required to create a number card")) + + elif self.type == "Custom": + if not self.method: + influxframework.throw(_("Method is required to create a number card")) + + def on_update(self): + if influxframework.conf.developer_mode and self.is_standard: + export_to_files(record_list=[["Number Card", self.name]], record_module=self.module) + + +def get_permission_query_conditions(user=None): + if not user: + user = influxframework.session.user + + if user == "Administrator": + return + + roles = influxframework.get_roles(user) + if "System Manager" in roles: + return None + + doctype_condition = False + module_condition = False + + allowed_doctypes = [ + influxframework.db.escape(doctype) for doctype in influxframework.permissions.get_doctypes_with_read() + ] + allowed_modules = [ + influxframework.db.escape(module.get("module_name")) for module in get_modules_from_all_apps_for_user() + ] + + if allowed_doctypes: + doctype_condition = "`tabNumber Card`.`document_type` in ({allowed_doctypes})".format( + allowed_doctypes=",".join(allowed_doctypes) + ) + if allowed_modules: + module_condition = """`tabNumber Card`.`module` in ({allowed_modules}) + or `tabNumber Card`.`module` is NULL""".format( + allowed_modules=",".join(allowed_modules) + ) + + return """ + {doctype_condition} + and + {module_condition} + """.format( + doctype_condition=doctype_condition, module_condition=module_condition + ) + + +def has_permission(doc, ptype, user): + roles = influxframework.get_roles(user) + if "System Manager" in roles: + return True + + if doc.type == "Report": + if doc.report_name in get_allowed_report_names(): + return True + else: + allowed_doctypes = tuple(influxframework.permissions.get_doctypes_with_read()) + if doc.document_type in allowed_doctypes: + return True + + return False + + +@influxframework.whitelist() +def get_result(doc, filters, to_date=None): + doc = influxframework.parse_json(doc) + fields = [] + sql_function_map = { + "Count": "count", + "Sum": "sum", + "Average": "avg", + "Minimum": "min", + "Maximum": "max", + } + + function = sql_function_map[doc.function] + + if function == "count": + fields = [f"{function}(*) as result"] + else: + fields = [ + "{function}({based_on}) as result".format( + function=function, based_on=doc.aggregate_function_based_on + ) + ] + + filters = influxframework.parse_json(filters) + + if not filters: + filters = [] + + if to_date: + filters.append([doc.document_type, "creation", "<", to_date]) + + res = influxframework.db.get_list(doc.document_type, fields=fields, filters=filters) + number = res[0]["result"] if res else 0 + + return cint(number) + + +@influxframework.whitelist() +def get_percentage_difference(doc, filters, result): + doc = influxframework.parse_json(doc) + result = influxframework.parse_json(result) + + doc = influxframework.get_doc("Number Card", doc.name) + + if not doc.get("show_percentage_stats"): + return + + previous_result = calculate_previous_result(doc, filters) + if previous_result == 0: + return None + else: + if result == previous_result: + return 0 + else: + return ((result / previous_result) - 1) * 100.0 + + +def calculate_previous_result(doc, filters): + from influxframework.utils import add_to_date + + current_date = influxframework.utils.now() + if doc.stats_time_interval == "Daily": + previous_date = add_to_date(current_date, days=-1) + elif doc.stats_time_interval == "Weekly": + previous_date = add_to_date(current_date, weeks=-1) + elif doc.stats_time_interval == "Monthly": + previous_date = add_to_date(current_date, months=-1) + else: + previous_date = add_to_date(current_date, years=-1) + + number = get_result(doc, filters, previous_date) + return number + + +@influxframework.whitelist() +def create_number_card(args): + args = influxframework.parse_json(args) + doc = influxframework.new_doc("Number Card") + + doc.update(args) + doc.insert(ignore_permissions=True) + return doc + + +@influxframework.whitelist() +@influxframework.validate_and_sanitize_search_inputs +def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters): + meta = influxframework.get_meta(doctype) + searchfields = meta.get_search_fields() + search_conditions = [] + + if not influxframework.db.exists("DocType", doctype): + return + + numberCard = DocType("Number Card") + + if txt: + search_conditions = [numberCard[field].like(f"%{txt}%") for field in searchfields] + + condition_query = influxframework.qb.engine.build_conditions(doctype, filters) + + return ( + condition_query.select(numberCard.name, numberCard.label, numberCard.document_type) + .where((numberCard.owner == influxframework.session.user) | (numberCard.is_public == 1)) + .where(Criterion.any(search_conditions)) + ).run() + + +@influxframework.whitelist() +def create_report_number_card(args): + card = create_number_card(args) + args = influxframework.parse_json(args) + args.name = card.name + if args.dashboard: + add_card_to_dashboard(influxframework.as_json(args)) + + +@influxframework.whitelist() +def add_card_to_dashboard(args): + args = influxframework.parse_json(args) + + dashboard = influxframework.get_doc("Dashboard", args.dashboard) + dashboard_link = influxframework.new_doc("Number Card Link") + dashboard_link.card = args.name + + if args.set_standard and dashboard.is_standard: + card = influxframework.get_doc("Number Card", dashboard_link.card) + card.is_standard = 1 + card.module = dashboard.module + card.save() + + dashboard.append("cards", dashboard_link) + dashboard.save() diff --git a/influxframework/desk/doctype/number_card/test_number_card.py b/influxframework/desk/doctype/number_card/test_number_card.py new file mode 100644 index 0000000..3500617 --- /dev/null +++ b/influxframework/desk/doctype/number_card/test_number_card.py @@ -0,0 +1,8 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestNumberCard(InfluxFrameworkTestCase): + pass diff --git a/influxframework/desk/doctype/number_card_link/__init__.py b/influxframework/desk/doctype/number_card_link/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/number_card_link/number_card_link.json b/influxframework/desk/doctype/number_card_link/number_card_link.json new file mode 100644 index 0000000..ac035b3 --- /dev/null +++ b/influxframework/desk/doctype/number_card_link/number_card_link.json @@ -0,0 +1,31 @@ +{ + "actions": [], + "creation": "2020-04-19 17:43:50.858343", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "card" + ], + "fields": [ + { + "fieldname": "card", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Card", + "options": "Number Card" + } + ], + "istable": 1, + "links": [], + "modified": "2020-04-19 17:45:11.878472", + "modified_by": "Administrator", + "module": "Desk", + "name": "Number Card Link", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/number_card_link/number_card_link.py b/influxframework/desk/doctype/number_card_link/number_card_link.py new file mode 100644 index 0000000..4c0bdd3 --- /dev/null +++ b/influxframework/desk/doctype/number_card_link/number_card_link.py @@ -0,0 +1,9 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class NumberCardLink(Document): + pass diff --git a/influxframework/desk/doctype/onboarding_permission/__init__.py b/influxframework/desk/doctype/onboarding_permission/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/onboarding_permission/onboarding_permission.js b/influxframework/desk/doctype/onboarding_permission/onboarding_permission.js new file mode 100644 index 0000000..dcef9e6 --- /dev/null +++ b/influxframework/desk/doctype/onboarding_permission/onboarding_permission.js @@ -0,0 +1,7 @@ +// Copyright (c) 2020, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Onboarding Permission", { + // refresh: function(frm) { + // } +}); diff --git a/influxframework/desk/doctype/onboarding_permission/onboarding_permission.json b/influxframework/desk/doctype/onboarding_permission/onboarding_permission.json new file mode 100644 index 0000000..f2a9dc3 --- /dev/null +++ b/influxframework/desk/doctype/onboarding_permission/onboarding_permission.json @@ -0,0 +1,32 @@ +{ + "actions": [], + "creation": "2020-04-30 18:27:48.255489", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "role" + ], + "fields": [ + { + "fieldname": "role", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Role", + "options": "Role", + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-04-30 18:28:40.423802", + "modified_by": "Administrator", + "module": "Desk", + "name": "Onboarding Permission", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/onboarding_permission/onboarding_permission.py b/influxframework/desk/doctype/onboarding_permission/onboarding_permission.py new file mode 100644 index 0000000..812bfcb --- /dev/null +++ b/influxframework/desk/doctype/onboarding_permission/onboarding_permission.py @@ -0,0 +1,9 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class OnboardingPermission(Document): + pass diff --git a/influxframework/desk/doctype/onboarding_permission/test_onboarding_permission.py b/influxframework/desk/doctype/onboarding_permission/test_onboarding_permission.py new file mode 100644 index 0000000..b97c5de --- /dev/null +++ b/influxframework/desk/doctype/onboarding_permission/test_onboarding_permission.py @@ -0,0 +1,8 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestOnboardingPermission(InfluxFrameworkTestCase): + pass diff --git a/influxframework/desk/doctype/onboarding_step/__init__.py b/influxframework/desk/doctype/onboarding_step/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/onboarding_step/onboarding_step.js b/influxframework/desk/doctype/onboarding_step/onboarding_step.js new file mode 100644 index 0000000..66ac7bf --- /dev/null +++ b/influxframework/desk/doctype/onboarding_step/onboarding_step.js @@ -0,0 +1,86 @@ +// Copyright (c) 2020, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Onboarding Step", { + setup: function (frm) { + frm.set_query("form_tour", function () { + return { + filters: { + reference_doctype: frm.doc.reference_document, + }, + }; + }); + }, + + refresh: function (frm) { + influxframework.boot.developer_mode && + frm.set_intro( + __( + "To export this step as JSON, link it in a Onboarding document and save the document." + ), + true + ); + if (frm.doc.reference_document && frm.doc.action == "Update Settings") { + setup_fields(frm); + } + + if (!influxframework.boot.developer_mode) { + frm.trigger("disable_form"); + } + }, + + reference_document: function (frm) { + if (frm.doc.reference_document && frm.doc.action == "Update Settings") { + setup_fields(frm); + } + }, + + action: function (frm) { + if (frm.doc.action == "Show Form Tour") { + frm.fields_dict.reference_document + .set_description(`You need to add the steps in the contoller JS file. For example: note.js +
    
    +influxframework.tour['Note'] = [
    +	{
    +		fieldname: "title",
    +		title: "Title of the Note",
    +		description: "...",
    +	}
    +];
    +
    + `); + } else { + frm.fields_dict.reference_document.set_description(null); + } + }, + + disable_form: function (frm) { + frm.set_read_only(); + frm.fields + .filter((field) => field.has_input) + .forEach((field) => { + frm.set_df_property(field.df.fieldname, "read_only", "1"); + }); + frm.disable_save(); + }, +}); + +function setup_fields(frm) { + if (frm.doc.reference_document && frm.doc.action == "Update Settings") { + influxframework.model.with_doctype(frm.doc.reference_document, () => { + let fields = influxframework + .get_meta(frm.doc.reference_document) + .fields.filter((df) => { + return ["Data", "Check", "Int", "Link", "Select"].includes(df.fieldtype); + }) + .map((df) => { + return { + label: `${__(df.label)} (${df.fieldname})`, + value: df.fieldname, + }; + }); + + frm.set_df_property("field", "options", fields); + }); + } +} diff --git a/influxframework/desk/doctype/onboarding_step/onboarding_step.json b/influxframework/desk/doctype/onboarding_step/onboarding_step.json new file mode 100644 index 0000000..b5d7851 --- /dev/null +++ b/influxframework/desk/doctype/onboarding_step/onboarding_step.json @@ -0,0 +1,254 @@ +{ + "actions": [], + "autoname": "prompt", + "creation": "2020-04-14 15:50:25.782387", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "title", + "column_break_2", + "is_complete", + "is_skipped", + "description_section", + "description", + "intro_video_url", + "section_break_5", + "action", + "action_label", + "column_break_7", + "reference_document", + "show_full_form", + "show_form_tour", + "form_tour", + "is_single", + "reference_report", + "report_reference_doctype", + "report_type", + "report_description", + "path", + "callback_title", + "callback_message", + "validate_action", + "field", + "value_to_validate", + "video_url" + ], + "fields": [ + { + "default": "0", + "fieldname": "is_complete", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Is Complete" + }, + { + "fieldname": "title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Title", + "reqd": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_5", + "fieldtype": "Section Break" + }, + { + "fieldname": "action", + "fieldtype": "Select", + "label": "Action", + "options": "Create Entry\nUpdate Settings\nShow Form Tour\nView Report\nGo to Page\nWatch Video", + "reqd": 1 + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.action == \"Create Entry\" || doc.action == \"Update Settings\" || doc.action == \"Create Entry\" || doc.action == \"Show Form Tour\"", + "fieldname": "reference_document", + "fieldtype": "Link", + "label": "Reference Document", + "mandatory_depends_on": "eval:doc.action == \"Create Entry\" || doc.action == \"Update Settings\" || doc.action == \"Create Entry\" || doc.action == \"Show Form Tour\"", + "options": "DocType" + }, + { + "depends_on": "eval:doc.action == \"View Report\"", + "fieldname": "reference_report", + "fieldtype": "Link", + "label": "Reference Report", + "mandatory_depends_on": "eval:doc.action == \"View Report\"", + "options": "Report" + }, + { + "depends_on": "eval:doc.action == \"Watch Video\"", + "fieldname": "video_url", + "fieldtype": "Data", + "label": "Video URL", + "mandatory_depends_on": "eval:doc.action == \"Watch Video\"" + }, + { + "depends_on": "eval:doc.action == \"View Report\"", + "fetch_from": "reference_report.report_type", + "fieldname": "report_type", + "fieldtype": "Data", + "label": "Report Type", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_skipped", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Is Skipped" + }, + { + "depends_on": "eval:doc.action == \"Update Settings\" && doc.validate_action", + "fieldname": "field", + "fieldtype": "Select", + "label": "Field", + "mandatory_depends_on": "eval:doc.action == \"Update Settings\" && doc.validate_action" + }, + { + "depends_on": "eval:doc.action == \"Update Settings\" && doc.validate_action", + "description": "Use % for any non empty value.", + "fieldname": "value_to_validate", + "fieldtype": "Data", + "label": "Value to Validate", + "mandatory_depends_on": "eval:doc.action == \"Update Settings\" && doc.validate_action" + }, + { + "depends_on": "eval:doc.action == \"View Report\"", + "description": "This will be shown to the user in a dialog after routing to the report", + "fieldname": "report_description", + "fieldtype": "Data", + "label": "Report Description", + "mandatory_depends_on": "eval:doc.action == \"View Report\"" + }, + { + "fetch_from": "reference_report.ref_doctype", + "fieldname": "report_reference_doctype", + "fieldtype": "Data", + "label": "Report Reference Doctype", + "read_only": 1 + }, + { + "default": "0", + "depends_on": "eval:doc.action == \"Create Entry\" || doc.action == \"Update Settings\" || doc.action == \"Create Entry\" || doc.action == \"Show Form Tour\"", + "fetch_from": "reference_document.issingle", + "fieldname": "is_single", + "fieldtype": "Check", + "label": "Is Single" + }, + { + "depends_on": "eval:doc.action == \"Go to Page\"", + "description": "Example: #Tree/Account", + "fieldname": "path", + "fieldtype": "Data", + "label": "Path", + "mandatory_depends_on": "eval:doc.action == \"Go to Page\"" + }, + { + "depends_on": "eval:doc.action == \"Go to Page\"", + "fieldname": "callback_title", + "fieldtype": "Data", + "label": "Callback Title" + }, + { + "depends_on": "eval:doc.action == \"Go to Page\"", + "description": "This will be shown in a modal after routing", + "fieldname": "callback_message", + "fieldtype": "Small Text", + "label": "Callback Message" + }, + { + "default": "1", + "depends_on": "eval:doc.action == \"Update Settings\"", + "fieldname": "validate_action", + "fieldtype": "Check", + "label": "Validate Field" + }, + { + "default": "0", + "depends_on": "eval:doc.action == \"Create Entry\"", + "description": "Show full form instead of a quick entry modal", + "fieldname": "show_full_form", + "fieldtype": "Check", + "label": "Show Full Form?" + }, + { + "description": "Description to inform the user about any action that is going to be performed", + "fieldname": "description_section", + "fieldtype": "Section Break", + "label": "Description" + }, + { + "fieldname": "description", + "fieldtype": "Markdown Editor", + "label": "Description" + }, + { + "fieldname": "intro_video_url", + "fieldtype": "Data", + "label": "Intro Video URL" + }, + { + "fieldname": "action_label", + "fieldtype": "Data", + "label": "Action Label" + }, + { + "default": "0", + "depends_on": "eval:doc.action==\"Create Entry\" && doc.show_full_form", + "fieldname": "show_form_tour", + "fieldtype": "Check", + "label": "Show Form Tour" + }, + { + "depends_on": "show_form_tour", + "fieldname": "form_tour", + "fieldtype": "Link", + "label": "Form Tour", + "options": "Form Tour" + } + ], + "links": [], + "modified": "2021-12-02 10:56:04.448580", + "modified_by": "Administrator", + "module": "Desk", + "name": "Onboarding Step", + "naming_rule": "Set by user", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 + } + ], + "quick_entry": 1, + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/onboarding_step/onboarding_step.py b/influxframework/desk/doctype/onboarding_step/onboarding_step.py new file mode 100644 index 0000000..268e9de --- /dev/null +++ b/influxframework/desk/doctype/onboarding_step/onboarding_step.py @@ -0,0 +1,30 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +import influxframework +from influxframework import _ +from influxframework.model.document import Document + + +class OnboardingStep(Document): + def before_export(self, doc): + doc.is_complete = 0 + doc.is_skipped = 0 + + +@influxframework.whitelist() +def get_onboarding_steps(ob_steps): + steps = [] + for s in json.loads(ob_steps): + doc = influxframework.get_doc("Onboarding Step", s.get("step")) + step = doc.as_dict().copy() + step.label = _(doc.title) + if step.action == "Create Entry": + step.is_submittable = influxframework.db.get_value( + "DocType", step.reference_document, "is_submittable", cache=True + ) + steps.append(step) + + return steps diff --git a/influxframework/desk/doctype/onboarding_step/test_onboarding_step.py b/influxframework/desk/doctype/onboarding_step/test_onboarding_step.py new file mode 100644 index 0000000..c1de48b --- /dev/null +++ b/influxframework/desk/doctype/onboarding_step/test_onboarding_step.py @@ -0,0 +1,8 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestOnboardingStep(InfluxFrameworkTestCase): + pass diff --git a/influxframework/desk/doctype/onboarding_step_map/__init__.py b/influxframework/desk/doctype/onboarding_step_map/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/onboarding_step_map/onboarding_step_map.json b/influxframework/desk/doctype/onboarding_step_map/onboarding_step_map.json new file mode 100644 index 0000000..e501a0b --- /dev/null +++ b/influxframework/desk/doctype/onboarding_step_map/onboarding_step_map.json @@ -0,0 +1,32 @@ +{ + "actions": [], + "creation": "2020-04-28 22:06:08.544187", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "step" + ], + "fields": [ + { + "fieldname": "step", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Step", + "options": "Onboarding Step", + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-04-28 22:06:09.503406", + "modified_by": "Administrator", + "module": "Desk", + "name": "Onboarding Step Map", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/onboarding_step_map/onboarding_step_map.py b/influxframework/desk/doctype/onboarding_step_map/onboarding_step_map.py new file mode 100644 index 0000000..1ecea50 --- /dev/null +++ b/influxframework/desk/doctype/onboarding_step_map/onboarding_step_map.py @@ -0,0 +1,9 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class OnboardingStepMap(Document): + pass diff --git a/influxframework/desk/doctype/route_history/__init__.py b/influxframework/desk/doctype/route_history/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/route_history/route_history.js b/influxframework/desk/doctype/route_history/route_history.js new file mode 100644 index 0000000..a3c948a --- /dev/null +++ b/influxframework/desk/doctype/route_history/route_history.js @@ -0,0 +1,6 @@ +// Copyright (c) 2018, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Route History", { + refresh: function () {}, +}); diff --git a/influxframework/desk/doctype/route_history/route_history.json b/influxframework/desk/doctype/route_history/route_history.json new file mode 100644 index 0000000..a5d73fc --- /dev/null +++ b/influxframework/desk/doctype/route_history/route_history.json @@ -0,0 +1,52 @@ +{ + "actions": [], + "creation": "2018-10-05 11:26:04.601113", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "route", + "user" + ], + "fields": [ + { + "fieldname": "route", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Route" + }, + { + "fieldname": "user", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "User", + "options": "User" + } + ], + "links": [], + "modified": "2022-06-13 05:48:56.967244", + "modified_by": "Administrator", + "module": "Desk", + "name": "Route History", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "route" +} \ No newline at end of file diff --git a/influxframework/desk/doctype/route_history/route_history.py b/influxframework/desk/doctype/route_history/route_history.py new file mode 100644 index 0000000..8c3731c --- /dev/null +++ b/influxframework/desk/doctype/route_history/route_history.py @@ -0,0 +1,42 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.deferred_insert import deferred_insert as _deferred_insert +from influxframework.model.document import Document + + +class RouteHistory(Document): + @staticmethod + def clear_old_logs(days=30): + from influxframework.query_builder import Interval + from influxframework.query_builder.functions import Now + + table = influxframework.qb.DocType("Route History") + influxframework.db.delete(table, filters=(table.modified < (Now() - Interval(days=days)))) + + +@influxframework.whitelist() +def deferred_insert(routes): + routes = [ + { + "user": influxframework.session.user, + "route": route.get("route"), + "creation": route.get("creation"), + } + for route in influxframework.parse_json(routes) + ] + + _deferred_insert("Route History", routes) + + +@influxframework.whitelist() +def frequently_visited_links(): + return influxframework.get_all( + "Route History", + fields=["route", "count(name) as count"], + filters={"user": influxframework.session.user}, + group_by="route", + order_by="count desc", + limit=5, + ) diff --git a/influxframework/desk/doctype/route_history/route_history_list.js b/influxframework/desk/doctype/route_history/route_history_list.js new file mode 100644 index 0000000..295f832 --- /dev/null +++ b/influxframework/desk/doctype/route_history/route_history_list.js @@ -0,0 +1,7 @@ +influxframework.listview_settings["Route History"] = { + onload: function (listview) { + influxframework.require("logtypes.bundle.js", () => { + influxframework.utils.logtypes.show_log_retention_message(cur_list.doctype); + }); + }, +}; diff --git a/influxframework/desk/doctype/system_console/__init__.py b/influxframework/desk/doctype/system_console/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/system_console/system_console.js b/influxframework/desk/doctype/system_console/system_console.js new file mode 100644 index 0000000..eadbcea --- /dev/null +++ b/influxframework/desk/doctype/system_console/system_console.js @@ -0,0 +1,106 @@ +// Copyright (c) 2020, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("System Console", { + onload: function (frm) { + influxframework.ui.keys.add_shortcut({ + shortcut: "shift+enter", + action: () => frm.page.btn_primary.trigger("click"), + page: frm.page, + description: __("Execute Console script"), + ignore_inputs: true, + }); + frm.set_value("type", "Python"); + }, + + refresh: function (frm) { + frm.disable_save(); + frm.page.set_primary_action(__("Execute"), ($btn) => { + $btn.text(__("Executing...")); + return frm + .execute_action("Execute") + .then(() => frm.trigger("render_sql_output")) + .finally(() => $btn.text(__("Execute"))); + }); + }, + + type: function (frm) { + if (frm.doc.type == "Python") { + frm.set_value("output", ""); + if (frm.sql_output) { + frm.sql_output.destroy(); + frm.get_field("sql_output").html(""); + } + } + }, + + render_sql_output: function (frm) { + if (frm.doc.type !== "SQL") return; + if (frm.sql_output) { + frm.sql_output.destroy(); + frm.get_field("sql_output").html(""); + } + + if (frm.doc.output.startsWith("Traceback")) { + return; + } + + let result = JSON.parse(frm.doc.output); + frm.set_value("output", `${result.length} ${result.length == 1 ? "row" : "rows"}`); + + if (result.length) { + let columns = Object.keys(result[0]); + frm.sql_output = new DataTable(frm.get_field("sql_output").$wrapper.get(0), { + columns, + data: result, + }); + } + }, + + show_processlist: function (frm) { + if (frm.doc.show_processlist) { + // keep refreshing every 5 seconds + frm.events.refresh_processlist(frm); + frm.processlist_interval = setInterval( + () => frm.events.refresh_processlist(frm), + 5000 + ); + } else { + if (frm.processlist_interval) { + // end it + clearInterval(frm.processlist_interval); + frm.get_field("processlist").html(""); + } + } + }, + + refresh_processlist: function (frm) { + let timestamp = new Date(); + influxframework + .call("influxframework.desk.doctype.system_console.system_console.show_processlist") + .then((r) => { + let rows = ""; + for (let row of r.message) { + rows += ` + ${row.Id} + ${row.Time} + ${row.State} + ${row.Info} + ${row.Progress} + `; + } + + frm.get_field("processlist").html(` +

    Requested on: ${timestamp}

    + + + + ${rows}`); + }); + }, +}); diff --git a/influxframework/desk/doctype/system_console/system_console.json b/influxframework/desk/doctype/system_console/system_console.json new file mode 100644 index 0000000..3c31a6e --- /dev/null +++ b/influxframework/desk/doctype/system_console/system_console.json @@ -0,0 +1,109 @@ +{ + "actions": [ + { + "action": "/app/console-log", + "action_type": "Route", + "label": "Logs" + }, + { + "action": "influxframework.desk.doctype.system_console.system_console.execute_code", + "action_type": "Server Action", + "hidden": 1, + "label": "Execute" + } + ], + "creation": "2020-08-18 17:44:35.647815", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "execute_section", + "type", + "console", + "commit", + "output", + "sql_output", + "database_processes_section", + "show_processlist", + "processlist" + ], + "fields": [ + { + "description": "To print output use log(text)", + "fieldname": "console", + "fieldtype": "Code", + "label": "Console", + "options": "Python" + }, + { + "fieldname": "output", + "fieldtype": "Code", + "label": "Output", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "commit", + "fieldtype": "Check", + "label": "Commit" + }, + { + "fieldname": "execute_section", + "fieldtype": "Section Break", + "label": "Execute" + }, + { + "fieldname": "database_processes_section", + "fieldtype": "Section Break", + "label": "Database Processes" + }, + { + "default": "0", + "fieldname": "show_processlist", + "fieldtype": "Check", + "label": "Show Processlist" + }, + { + "fieldname": "processlist", + "fieldtype": "HTML", + "label": "processlist" + }, + { + "default": "Python", + "fieldname": "type", + "fieldtype": "Select", + "label": "Type", + "options": "Python\nSQL" + }, + { + "depends_on": "eval:doc.type == 'SQL'", + "fieldname": "sql_output", + "fieldtype": "HTML", + "label": "SQL Output" + } + ], + "hide_toolbar": 1, + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2022-04-15 14:15:58.398590", + "modified_by": "Administrator", + "module": "Desk", + "name": "System Console", + "owner": "Administrator", + "permissions": [ + { + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} diff --git a/influxframework/desk/doctype/system_console/system_console.py b/influxframework/desk/doctype/system_console/system_console.py new file mode 100644 index 0000000..8c8d935 --- /dev/null +++ b/influxframework/desk/doctype/system_console/system_console.py @@ -0,0 +1,56 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +import influxframework +from influxframework.model.document import Document +from influxframework.utils.safe_exec import read_sql, safe_exec + + +class SystemConsole(Document): + def run(self): + influxframework.only_for("System Manager") + try: + influxframework.local.debug_log = [] + if self.type == "Python": + safe_exec(self.console) + self.output = "\n".join(influxframework.debug_log) + elif self.type == "SQL": + self.output = influxframework.as_json(read_sql(self.console, as_dict=1)) + except Exception: + self.output = influxframework.get_traceback() + + if self.commit: + influxframework.db.commit() + else: + influxframework.db.rollback() + + influxframework.get_doc(dict(doctype="Console Log", script=self.console, output=self.output)).insert() + influxframework.db.commit() + + +@influxframework.whitelist() +def execute_code(doc): + console = influxframework.get_doc(json.loads(doc)) + console.run() + return console.as_dict() + + +@influxframework.whitelist() +def show_processlist(): + influxframework.only_for("System Manager") + + return influxframework.db.multisql( + { + "postgres": """ + SELECT pid AS "Id", + query_start AS "Time", + state AS "State", + query AS "Info", + wait_event AS "Progress" + FROM pg_stat_activity""", + "mariadb": "show full processlist", + }, + as_dict=True, + ) diff --git a/influxframework/desk/doctype/system_console/test_system_console.py b/influxframework/desk/doctype/system_console/test_system_console.py new file mode 100644 index 0000000..5b1a261 --- /dev/null +++ b/influxframework/desk/doctype/system_console/test_system_console.py @@ -0,0 +1,18 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestSystemConsole(InfluxFrameworkTestCase): + def test_system_console(self): + system_console = influxframework.get_doc("System Console") + system_console.console = 'log("hello")' + system_console.run() + + self.assertEqual(system_console.output, "hello") + + system_console.console = 'log(influxframework.db.get_value("DocType", "DocType", "module"))' + system_console.run() + + self.assertEqual(system_console.output, "Core") diff --git a/influxframework/desk/doctype/tag/__init__.py b/influxframework/desk/doctype/tag/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/tag/tag.js b/influxframework/desk/doctype/tag/tag.js new file mode 100644 index 0000000..acd7975 --- /dev/null +++ b/influxframework/desk/doctype/tag/tag.js @@ -0,0 +1,7 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Tag", { + // refresh: function(frm) { + // } +}); diff --git a/influxframework/desk/doctype/tag/tag.json b/influxframework/desk/doctype/tag/tag.json new file mode 100644 index 0000000..ad9838d --- /dev/null +++ b/influxframework/desk/doctype/tag/tag.json @@ -0,0 +1,50 @@ +{ + "allow_rename": 1, + "autoname": "Prompt", + "creation": "2016-05-25 09:43:44.767581", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "description" + ], + "fields": [ + { + "fieldname": "description", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Description" + } + ], + "modified": "2019-09-25 17:47:41.712237", + "modified_by": "Administrator", + "module": "Desk", + "name": "Tag", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC" +} diff --git a/influxframework/desk/doctype/tag/tag.py b/influxframework/desk/doctype/tag/tag.py new file mode 100644 index 0000000..5b9e22a --- /dev/null +++ b/influxframework/desk/doctype/tag/tag.py @@ -0,0 +1,195 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document +from influxframework.query_builder import DocType +from influxframework.utils import unique + + +class Tag(Document): + pass + + +def check_user_tags(dt): + "if the user does not have a tags column, then it creates one" + try: + doctype = DocType(dt) + influxframework.qb.from_(doctype).select(doctype._user_tags).limit(1).run() + except Exception as e: + if influxframework.db.is_column_missing(e): + DocTags(dt).setup() + + +@influxframework.whitelist() +def add_tag(tag, dt, dn, color=None): + "adds a new tag to a record, and creates the Tag master" + DocTags(dt).add(dn, tag) + + return tag + + +@influxframework.whitelist() +def add_tags(tags, dt, docs, color=None): + "adds a new tag to a record, and creates the Tag master" + tags = influxframework.parse_json(tags) + docs = influxframework.parse_json(docs) + for doc in docs: + for tag in tags: + DocTags(dt).add(doc, tag) + + # return tag + + +@influxframework.whitelist() +def remove_tag(tag, dt, dn): + "removes tag from the record" + DocTags(dt).remove(dn, tag) + + +@influxframework.whitelist() +def get_tagged_docs(doctype, tag): + influxframework.has_permission(doctype, throw=True) + doctype = DocType(doctype) + return (influxframework.qb.from_(doctype).where(doctype._user_tags.like(tag)).select(doctype.name)).run() + + +@influxframework.whitelist() +def get_tags(doctype, txt): + tag = influxframework.get_list("Tag", filters=[["name", "like", f"%{txt}%"]]) + tags = [t.name for t in tag] + + return sorted(filter(lambda t: t and txt.lower() in t.lower(), list(set(tags)))) + + +class DocTags: + """Tags for a particular doctype""" + + def __init__(self, dt): + self.dt = dt + + def get_tag_fields(self): + """returns tag_fields property""" + return influxframework.db.get_value("DocType", self.dt, "tag_fields") + + def get_tags(self, dn): + """returns tag for a particular item""" + return (influxframework.db.get_value(self.dt, dn, "_user_tags", ignore=1) or "").strip() + + def add(self, dn, tag): + """add a new user tag""" + tl = self.get_tags(dn).split(",") + if not tag in tl: + tl.append(tag) + if not influxframework.db.exists("Tag", tag): + influxframework.get_doc({"doctype": "Tag", "name": tag}).insert(ignore_permissions=True) + self.update(dn, tl) + + def remove(self, dn, tag): + """remove a user tag""" + tl = self.get_tags(dn).split(",") + self.update(dn, filter(lambda x: x.lower() != tag.lower(), tl)) + + def remove_all(self, dn): + """remove all user tags (call before delete)""" + self.update(dn, []) + + def update(self, dn, tl): + """updates the _user_tag column in the table""" + + if not tl: + tags = "" + else: + tl = unique(filter(lambda x: x, tl)) + tags = "," + ",".join(tl) + try: + influxframework.db.sql( + "update `tab{}` set _user_tags={} where name={}".format(self.dt, "%s", "%s"), (tags, dn) + ) + doc = influxframework.get_doc(self.dt, dn) + update_tags(doc, tags) + except Exception as e: + if influxframework.db.is_column_missing(e): + if not tags: + # no tags, nothing to do + return + + self.setup() + self.update(dn, tl) + else: + raise + + def setup(self): + """adds the _user_tags column if not exists""" + from influxframework.database.schema import add_column + + add_column(self.dt, "_user_tags", "Data") + + +def delete_tags_for_document(doc): + """ + Delete the Tag Link entry of a document that has + been deleted + :param doc: Deleted document + """ + if not influxframework.db.table_exists("Tag Link"): + return + + influxframework.db.delete("Tag Link", {"document_type": doc.doctype, "document_name": doc.name}) + + +def update_tags(doc, tags): + """Adds tags for documents + + :param doc: Document to be added to global tags + """ + new_tags = {tag.strip() for tag in tags.split(",") if tag} + existing_tags = [ + tag.tag + for tag in influxframework.get_list( + "Tag Link", filters={"document_type": doc.doctype, "document_name": doc.name}, fields=["tag"] + ) + ] + + added_tags = set(new_tags) - set(existing_tags) + for tag in added_tags: + influxframework.get_doc( + { + "doctype": "Tag Link", + "document_type": doc.doctype, + "document_name": doc.name, + "title": doc.get_title() or "", + "tag": tag, + } + ).insert(ignore_permissions=True) + + deleted_tags = list(set(existing_tags) - set(new_tags)) + for tag in deleted_tags: + influxframework.db.delete( + "Tag Link", {"document_type": doc.doctype, "document_name": doc.name, "tag": tag} + ) + + +@influxframework.whitelist() +def get_documents_for_tag(tag): + """ + Search for given text in Tag Link + :param tag: tag to be searched + """ + # remove hastag `#` from tag + tag = tag[1:] + results = [] + + result = influxframework.get_list( + "Tag Link", filters={"tag": tag}, fields=["document_type", "document_name", "title", "tag"] + ) + + for res in result: + results.append({"doctype": res.document_type, "name": res.document_name, "content": res.title}) + + return results + + +@influxframework.whitelist() +def get_tags_list_for_awesomebar(): + return influxframework.get_list("Tag", pluck="name", order_by=None) diff --git a/influxframework/desk/doctype/tag/test_tag.py b/influxframework/desk/doctype/tag/test_tag.py new file mode 100644 index 0000000..8305b76 --- /dev/null +++ b/influxframework/desk/doctype/tag/test_tag.py @@ -0,0 +1,34 @@ +import influxframework +from influxframework.desk.doctype.tag.tag import add_tag +from influxframework.desk.reportview import get_stats +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestTag(InfluxFrameworkTestCase): + def setUp(self) -> None: + influxframework.db.delete("Tag") + influxframework.db.sql("UPDATE `tabDocType` set _user_tags=''") + + def test_tag_count_query(self): + self.assertDictEqual( + get_stats('["_user_tags"]', "DocType"), + {"_user_tags": [["No Tags", influxframework.db.count("DocType")]]}, + ) + add_tag("Standard", "DocType", "User") + add_tag("Standard", "DocType", "ToDo") + + # count with no filter + self.assertDictEqual( + get_stats('["_user_tags"]', "DocType"), + {"_user_tags": [["Standard", 2], ["No Tags", influxframework.db.count("DocType") - 2]]}, + ) + + # count with child table field filter + self.assertDictEqual( + get_stats( + '["_user_tags"]', + "DocType", + filters='[["DocField", "fieldname", "like", "%last_name%"], ["DocType", "name", "like", "%use%"]]', + ), + {"_user_tags": [["Standard", 1], ["No Tags", 0]]}, + ) diff --git a/influxframework/desk/doctype/tag_link/__init__.py b/influxframework/desk/doctype/tag_link/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/tag_link/tag_link.js b/influxframework/desk/doctype/tag_link/tag_link.js new file mode 100644 index 0000000..04f6541 --- /dev/null +++ b/influxframework/desk/doctype/tag_link/tag_link.js @@ -0,0 +1,7 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Tag Link", { + // refresh: function(frm) { + // } +}); diff --git a/influxframework/desk/doctype/tag_link/tag_link.json b/influxframework/desk/doctype/tag_link/tag_link.json new file mode 100644 index 0000000..9142279 --- /dev/null +++ b/influxframework/desk/doctype/tag_link/tag_link.json @@ -0,0 +1,83 @@ +{ + "actions": [], + "creation": "2019-09-24 13:25:36.435685", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type", + "document_name", + "tag", + "title" + ], + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Document Title", + "read_only": 1 + }, + { + "fieldname": "tag", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Document Tag", + "options": "Tag", + "read_only": 1 + }, + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Document Type", + "options": "DocType", + "read_only": 1 + }, + { + "fieldname": "document_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Document Name", + "options": "document_type", + "read_only": 1 + } + ], + "links": [], + "modified": "2021-09-20 16:53:37.217998", + "modified_by": "Administrator", + "module": "Desk", + "name": "Tag Link", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1, + "write": 1 + } + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/tag_link/tag_link.py b/influxframework/desk/doctype/tag_link/tag_link.py new file mode 100644 index 0000000..e54b0b7 --- /dev/null +++ b/influxframework/desk/doctype/tag_link/tag_link.py @@ -0,0 +1,9 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class TagLink(Document): + pass diff --git a/influxframework/desk/doctype/tag_link/test_tag_link.py b/influxframework/desk/doctype/tag_link/test_tag_link.py new file mode 100644 index 0000000..e383b0b --- /dev/null +++ b/influxframework/desk/doctype/tag_link/test_tag_link.py @@ -0,0 +1,8 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestTagLink(InfluxFrameworkTestCase): + pass diff --git a/influxframework/desk/doctype/todo/README.md b/influxframework/desk/doctype/todo/README.md new file mode 100644 index 0000000..b622358 --- /dev/null +++ b/influxframework/desk/doctype/todo/README.md @@ -0,0 +1 @@ +To do or assignment. \ No newline at end of file diff --git a/influxframework/desk/doctype/todo/__init__.py b/influxframework/desk/doctype/todo/__init__.py new file mode 100644 index 0000000..2e11a12 --- /dev/null +++ b/influxframework/desk/doctype/todo/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE diff --git a/influxframework/desk/doctype/todo/test_todo.py b/influxframework/desk/doctype/todo/test_todo.py new file mode 100644 index 0000000..d71d08e --- /dev/null +++ b/influxframework/desk/doctype/todo/test_todo.py @@ -0,0 +1,153 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.core.doctype.doctype.doctype import clear_permissions_cache +from influxframework.model.db_query import DatabaseQuery +from influxframework.permissions import add_permission, reset_perms +from influxframework.tests.utils import InfluxFrameworkTestCase + +test_dependencies = ["User"] + + +class TestToDo(InfluxFrameworkTestCase): + def test_delete(self): + todo = influxframework.get_doc( + dict(doctype="ToDo", description="test todo", assigned_by="Administrator") + ).insert() + + influxframework.db.delete("Deleted Document") + todo.delete() + + deleted = influxframework.get_doc( + "Deleted Document", dict(deleted_doctype=todo.doctype, deleted_name=todo.name) + ) + self.assertEqual(todo.as_json(), deleted.data) + + def test_fetch(self): + todo = influxframework.get_doc( + dict(doctype="ToDo", description="test todo", assigned_by="Administrator") + ).insert() + self.assertEqual( + todo.assigned_by_full_name, influxframework.db.get_value("User", todo.assigned_by, "full_name") + ) + + def test_fetch_setup(self): + influxframework.db.delete("ToDo") + + todo_meta = influxframework.get_doc("DocType", "ToDo") + todo_meta.get("fields", dict(fieldname="assigned_by_full_name"))[0].fetch_from = "" + todo_meta.save() + + influxframework.clear_cache(doctype="ToDo") + + todo = influxframework.get_doc( + dict(doctype="ToDo", description="test todo", assigned_by="Administrator") + ).insert() + self.assertFalse(todo.assigned_by_full_name) + + todo_meta = influxframework.get_doc("DocType", "ToDo") + todo_meta.get("fields", dict(fieldname="assigned_by_full_name"))[ + 0 + ].fetch_from = "assigned_by.full_name" + todo_meta.save() + + todo.reload() + + self.assertEqual( + todo.assigned_by_full_name, influxframework.db.get_value("User", todo.assigned_by, "full_name") + ) + + def test_todo_list_access(self): + create_new_todo("Test1", "testperm@example.com") + + influxframework.set_user("test4@example.com") + create_new_todo("Test2", "test4@example.com") + test_user_data = DatabaseQuery("ToDo").execute() + + influxframework.set_user("testperm@example.com") + system_manager_data = DatabaseQuery("ToDo").execute() + + self.assertNotEqual(test_user_data, system_manager_data) + + influxframework.set_user("Administrator") + influxframework.db.rollback() + + def test_doc_read_access(self): + # owner and assigned_by is testperm + todo1 = create_new_todo("Test1", "testperm@example.com") + test_user = influxframework.get_doc("User", "test4@example.com") + + # owner is testperm, but assigned_by is test4 + todo2 = create_new_todo("Test2", "test4@example.com") + + influxframework.set_user("test4@example.com") + # owner and assigned_by is test4 + todo3 = create_new_todo("Test3", "test4@example.com") + + # user without any role to read or write todo document + self.assertFalse(todo1.has_permission("read")) + self.assertFalse(todo1.has_permission("write")) + + # user without any role but he/she is assigned_by of that todo document + self.assertTrue(todo2.has_permission("read")) + self.assertTrue(todo2.has_permission("write")) + + # user is the owner and assigned_by of the todo document + self.assertTrue(todo3.has_permission("read")) + self.assertTrue(todo3.has_permission("write")) + + influxframework.set_user("Administrator") + + test_user.add_roles("Blogger") + add_permission("ToDo", "Blogger") + + influxframework.set_user("test4@example.com") + + # user with only read access to todo document, not an owner or assigned_by + self.assertTrue(todo1.has_permission("read")) + self.assertFalse(todo1.has_permission("write")) + + influxframework.set_user("Administrator") + test_user.remove_roles("Blogger") + reset_perms("ToDo") + clear_permissions_cache("ToDo") + influxframework.db.rollback() + + def test_fetch_if_empty(self): + influxframework.db.delete("ToDo") + + # Allow user changes + todo_meta = influxframework.get_doc("DocType", "ToDo") + field = todo_meta.get("fields", dict(fieldname="assigned_by_full_name"))[0] + field.fetch_from = "assigned_by.full_name" + field.fetch_if_empty = 1 + todo_meta.save() + + influxframework.clear_cache(doctype="ToDo") + + todo = influxframework.get_doc( + dict( + doctype="ToDo", + description="test todo", + assigned_by="Administrator", + assigned_by_full_name="Admin", + ) + ).insert() + + self.assertEqual(todo.assigned_by_full_name, "Admin") + + # Overwrite user changes + todo.meta.get("fields", dict(fieldname="assigned_by_full_name"))[0].fetch_if_empty = 0 + todo.meta.save() + + todo.reload() + todo.save() + + self.assertEqual( + todo.assigned_by_full_name, influxframework.db.get_value("User", todo.assigned_by, "full_name") + ) + + +def create_new_todo(description, assigned_by): + todo = {"doctype": "ToDo", "description": description, "assigned_by": assigned_by} + return influxframework.get_doc(todo).insert() diff --git a/influxframework/desk/doctype/todo/todo.js b/influxframework/desk/doctype/todo/todo.js new file mode 100644 index 0000000..c33870a --- /dev/null +++ b/influxframework/desk/doctype/todo/todo.js @@ -0,0 +1,55 @@ +// bind events + +influxframework.ui.form.on("ToDo", { + onload: function (frm) { + frm.set_query("reference_type", function (txt) { + return { + filters: { + issingle: 0, + }, + }; + }); + }, + refresh: function (frm) { + if (frm.doc.reference_type && frm.doc.reference_name) { + frm.add_custom_button(__(frm.doc.reference_name), function () { + influxframework.set_route("Form", frm.doc.reference_type, frm.doc.reference_name); + }); + } + + if (!frm.doc.__islocal) { + if (frm.doc.status !== "Closed") { + frm.add_custom_button( + __("Close"), + function () { + frm.set_value("status", "Closed"); + frm.save(null, function () { + // back to list + influxframework.set_route("List", "ToDo"); + }); + }, + "fa fa-check", + "btn-success" + ); + } else { + frm.add_custom_button( + __("Reopen"), + function () { + frm.set_value("status", "Open"); + frm.save(); + }, + null, + "btn-default" + ); + } + frm.add_custom_button( + __("New"), + function () { + influxframework.new_doc("ToDo"); + }, + null, + "btn-default" + ); + } + }, +}); diff --git a/influxframework/desk/doctype/todo/todo.json b/influxframework/desk/doctype/todo/todo.json new file mode 100644 index 0000000..518ca00 --- /dev/null +++ b/influxframework/desk/doctype/todo/todo.json @@ -0,0 +1,199 @@ +{ + "actions": [], + "autoname": "hash", + "creation": "2012-07-03 13:30:35", + "doctype": "DocType", + "document_type": "Setup", + "email_append_to": 1, + "engine": "InnoDB", + "field_order": [ + "description_and_status", + "status", + "priority", + "column_break_2", + "color", + "date", + "allocated_to", + "description_section", + "description", + "section_break_6", + "reference_type", + "reference_name", + "column_break_10", + "role", + "assigned_by", + "assigned_by_full_name", + "sender", + "assignment_rule" + ], + "fields": [ + { + "fieldname": "description_and_status", + "fieldtype": "Section Break" + }, + { + "default": "Open", + "fieldname": "status", + "fieldtype": "Select", + "in_global_search": 1, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "Open\nClosed\nCancelled" + }, + { + "default": "Medium", + "fieldname": "priority", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Priority", + "oldfieldname": "priority", + "oldfieldtype": "Data", + "options": "High\nMedium\nLow" + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "color", + "fieldtype": "Color", + "label": "Color" + }, + { + "fieldname": "date", + "fieldtype": "Date", + "in_standard_filter": 1, + "label": "Due Date", + "oldfieldname": "date", + "oldfieldtype": "Date" + }, + { + "fieldname": "description_section", + "fieldtype": "Section Break" + }, + { + "fieldname": "description", + "fieldtype": "Text Editor", + "in_global_search": 1, + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Text", + "print_width": "300px", + "reqd": 1, + "width": "300px" + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break", + "label": "Reference" + }, + { + "fieldname": "reference_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Reference Type", + "oldfieldname": "reference_type", + "oldfieldtype": "Data", + "options": "DocType" + }, + { + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "label": "Reference Name", + "oldfieldname": "reference_name", + "oldfieldtype": "Data", + "options": "reference_type" + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break" + }, + { + "fieldname": "role", + "fieldtype": "Link", + "label": "Role", + "oldfieldname": "role", + "oldfieldtype": "Link", + "options": "Role" + }, + { + "fieldname": "assigned_by", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Assigned By", + "options": "User" + }, + { + "fetch_from": "assigned_by.full_name", + "fieldname": "assigned_by_full_name", + "fieldtype": "Read Only", + "label": "Assigned By Full Name" + }, + { + "fieldname": "sender", + "fieldtype": "Data", + "hidden": 1, + "label": "Sender", + "options": "Email" + }, + { + "fieldname": "assignment_rule", + "fieldtype": "Link", + "label": "Assignment Rule", + "options": "Assignment Rule", + "read_only": 1 + }, + { + "fieldname": "allocated_to", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "in_global_search": 1, + "in_standard_filter": 1, + "label": "Allocated To", + "options": "User" + } + ], + "icon": "fa fa-check", + "idx": 2, + "links": [], + "modified": "2021-09-16 11:36:34.586898", + "modified_by": "Administrator", + "module": "Desk", + "name": "ToDo", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "search_fields": "description, reference_type, reference_name", + "sender_field": "sender", + "sort_field": "modified", + "sort_order": "DESC", + "subject_field": "description", + "title_field": "description", + "track_changes": 1, + "track_seen": 1 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/todo/todo.py b/influxframework/desk/doctype/todo/todo.py new file mode 100644 index 0000000..5bd2aed --- /dev/null +++ b/influxframework/desk/doctype/todo/todo.py @@ -0,0 +1,148 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +import influxframework +from influxframework.model.document import Document +from influxframework.utils import get_fullname, parse_addr + +exclude_from_linked_with = True + + +class ToDo(Document): + DocType = "ToDo" + + def validate(self): + self._assignment = None + if self.is_new(): + + if self.assigned_by == self.allocated_to: + assignment_message = influxframework._("{0} self assigned this task: {1}").format( + get_fullname(self.assigned_by), self.description + ) + else: + assignment_message = influxframework._("{0} assigned {1}: {2}").format( + get_fullname(self.assigned_by), get_fullname(self.allocated_to), self.description + ) + + self._assignment = {"text": assignment_message, "comment_type": "Assigned"} + + else: + # NOTE the previous value is only available in validate method + if self.get_db_value("status") != self.status: + if self.allocated_to == influxframework.session.user: + removal_message = influxframework._("{0} removed their assignment.").format( + get_fullname(influxframework.session.user) + ) + else: + removal_message = influxframework._("Assignment of {0} removed by {1}").format( + get_fullname(self.allocated_to), get_fullname(influxframework.session.user) + ) + + self._assignment = {"text": removal_message, "comment_type": "Assignment Completed"} + + def on_update(self): + if self._assignment: + self.add_assign_comment(**self._assignment) + + self.update_in_reference() + + def on_trash(self): + self.delete_communication_links() + self.update_in_reference() + + def add_assign_comment(self, text, comment_type): + if not (self.reference_type and self.reference_name): + return + + influxframework.get_doc(self.reference_type, self.reference_name).add_comment(comment_type, text) + + def delete_communication_links(self): + # unlink todo from linked comments + return influxframework.db.delete( + "Communication Link", {"link_doctype": self.doctype, "link_name": self.name} + ) + + def update_in_reference(self): + if not (self.reference_type and self.reference_name): + return + + try: + assignments = influxframework.get_all( + "ToDo", + filters={ + "reference_type": self.reference_type, + "reference_name": self.reference_name, + "status": ("!=", "Cancelled"), + "allocated_to": ("is", "set"), + }, + pluck="allocated_to", + ) + assignments.reverse() + + influxframework.db.set_value( + self.reference_type, + self.reference_name, + "_assign", + json.dumps(assignments), + update_modified=False, + ) + + except Exception as e: + if influxframework.db.is_table_missing(e) and influxframework.flags.in_install: + # no table + return + + elif influxframework.db.is_column_missing(e): + from influxframework.database.schema import add_column + + add_column(self.reference_type, "_assign", "Text") + self.update_in_reference() + + else: + raise + + @classmethod + def get_owners(cls, filters=None): + """Returns list of owners after applying filters on todo's.""" + rows = influxframework.get_all(cls.DocType, filters=filters or {}, fields=["allocated_to"]) + return [parse_addr(row.allocated_to)[1] for row in rows if row.allocated_to] + + +# NOTE: todo is viewable if a user is an owner, or set as assigned_to value, or has any role that is allowed to access ToDo doctype. +def on_doctype_update(): + influxframework.db.add_index("ToDo", ["reference_type", "reference_name"]) + + +def get_permission_query_conditions(user): + if not user: + user = influxframework.session.user + + todo_roles = influxframework.permissions.get_doctype_roles("ToDo") + if "All" in todo_roles: + todo_roles.remove("All") + + if any(check in todo_roles for check in influxframework.get_roles(user)): + return None + else: + return """(`tabToDo`.allocated_to = {user} or `tabToDo`.assigned_by = {user})""".format( + user=influxframework.db.escape(user) + ) + + +def has_permission(doc, ptype="read", user=None): + user = user or influxframework.session.user + todo_roles = influxframework.permissions.get_doctype_roles("ToDo", ptype) + if "All" in todo_roles: + todo_roles.remove("All") + + if any(check in todo_roles for check in influxframework.get_roles(user)): + return True + else: + return doc.allocated_to == user or doc.assigned_by == user + + +@influxframework.whitelist() +def new_todo(description): + influxframework.get_doc({"doctype": "ToDo", "description": description}).insert() diff --git a/influxframework/desk/doctype/todo/todo_calendar.js b/influxframework/desk/doctype/todo/todo_calendar.js new file mode 100644 index 0000000..707d120 --- /dev/null +++ b/influxframework/desk/doctype/todo/todo_calendar.js @@ -0,0 +1,29 @@ +// Copyright (c) 2015, InfluxFramework LLC +// License: GNU General Public License v3. See license.txt + +influxframework.views.calendar["ToDo"] = { + field_map: { + start: "date", + end: "date", + id: "name", + title: "description", + allDay: "allDay", + progress: "progress", + }, + gantt: true, + filters: [ + { + fieldtype: "Link", + fieldname: "reference_type", + options: "Task", + label: __("Task"), + }, + { + fieldtype: "Dynamic Link", + fieldname: "reference_name", + options: "reference_type", + label: __("Task"), + }, + ], + get_events_method: "influxframework.desk.calendar.get_events", +}; diff --git a/influxframework/desk/doctype/todo/todo_list.js b/influxframework/desk/doctype/todo/todo_list.js new file mode 100644 index 0000000..a2000e5 --- /dev/null +++ b/influxframework/desk/doctype/todo/todo_list.js @@ -0,0 +1,44 @@ +influxframework.listview_settings["ToDo"] = { + hide_name_column: true, + add_fields: ["reference_type", "reference_name"], + + onload: function (me) { + if (!influxframework.route_options) { + influxframework.route_options = { + owner: influxframework.session.user, + status: "Open", + }; + } + me.page.set_title(__("To Do")); + }, + + button: { + show: function (doc) { + return doc.reference_name; + }, + get_label: function () { + return __("Open", null, "Access"); + }, + get_description: function (doc) { + return __("Open {0}", [`${__(doc.reference_type)}: ${doc.reference_name}`]); + }, + action: function (doc) { + influxframework.set_route("Form", doc.reference_type, doc.reference_name); + }, + }, + + refresh: function (me) { + if (me.todo_sidebar_setup) return; + + // add assigned by me + me.page.add_sidebar_item( + __("Assigned By Me"), + function () { + me.filter_area.add([[me.doctype, "assigned_by", "=", influxframework.session.user]]); + }, + '.list-link[data-view="Kanban"]' + ); + + me.todo_sidebar_setup = true; + }, +}; diff --git a/influxframework/desk/doctype/workspace/__init__.py b/influxframework/desk/doctype/workspace/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/workspace/test_workspace.py b/influxframework/desk/doctype/workspace/test_workspace.py new file mode 100644 index 0000000..68de87f --- /dev/null +++ b/influxframework/desk/doctype/workspace/test_workspace.py @@ -0,0 +1,100 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestWorkspace(InfluxFrameworkTestCase): + def setUp(self): + create_module("Test Module") + + def tearDown(self): + influxframework.db.delete("Workspace", {"module": "Test Module"}) + influxframework.db.delete("DocType", {"module": "Test Module"}) + influxframework.delete_doc("Module Def", "Test Module") + + # TODO: FIX ME - flaky test!!! + # def test_workspace_with_cards_specific_to_a_country(self): + # workspace = create_workspace() + # insert_card(workspace, "Card Label 1", "DocType 1", "DocType 2", "France") + # insert_card(workspace, "Card Label 2", "DocType A", "DocType B") + + # workspace.insert(ignore_if_duplicate = True) + + # cards = workspace.get_link_groups() + + # if influxframework.get_system_settings('country') == "France": + # self.assertEqual(len(cards), 2) + # else: + # self.assertEqual(len(cards), 1) + + +def create_module(module_name): + module = influxframework.get_doc( + {"doctype": "Module Def", "module_name": module_name, "app_name": "influxframework"} + ) + module.insert(ignore_if_duplicate=True) + + return module + + +def create_workspace(**args): + workspace = influxframework.new_doc("Workspace") + args = influxframework._dict(args) + + workspace.name = args.name or "Test Workspace" + workspace.label = args.label or "Test Workspace" + workspace.category = args.category or "Modules" + workspace.is_standard = args.is_standard or 1 + workspace.module = "Test Module" + + return workspace + + +def insert_card(workspace, card_label, doctype1, doctype2, country=None): + workspace.append("links", {"type": "Card Break", "label": card_label, "only_for": country}) + + create_doctype(doctype1, "Test Module") + workspace.append( + "links", + { + "type": "Link", + "label": doctype1, + "only_for": country, + "link_type": "DocType", + "link_to": doctype1, + }, + ) + + create_doctype(doctype2, "Test Module") + workspace.append( + "links", + { + "type": "Link", + "label": doctype2, + "only_for": country, + "link_type": "DocType", + "link_to": doctype2, + }, + ) + + +def create_doctype(doctype_name, module): + influxframework.get_doc( + { + "doctype": "DocType", + "name": doctype_name, + "module": module, + "custom": 1, + "autoname": "field:title", + "fields": [ + {"label": "Title", "fieldname": "title", "reqd": 1, "fieldtype": "Data"}, + {"label": "Description", "fieldname": "description", "fieldtype": "Small Text"}, + {"label": "Date", "fieldname": "date", "fieldtype": "Date"}, + {"label": "Duration", "fieldname": "duration", "fieldtype": "Duration"}, + {"label": "Number", "fieldname": "number", "fieldtype": "Int"}, + {"label": "Number", "fieldname": "another_number", "fieldtype": "Int"}, + ], + "permissions": [{"role": "System Manager"}], + } + ).insert(ignore_if_duplicate=True) diff --git a/influxframework/desk/doctype/workspace/workspace.js b/influxframework/desk/doctype/workspace/workspace.js new file mode 100644 index 0000000..d6c789b --- /dev/null +++ b/influxframework/desk/doctype/workspace/workspace.js @@ -0,0 +1,30 @@ +// Copyright (c) 2020, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Workspace", { + setup: function () { + influxframework.meta.get_field("Workspace Link", "only_for").no_default = true; + }, + + refresh: function (frm) { + frm.enable_save(); + + if ( + frm.doc.for_user || + (frm.doc.public && + !frm.has_perm("write") && + !influxframework.user.has_role("Workspace Manager")) + ) { + frm.trigger("disable_form"); + } + }, + + disable_form: function (frm) { + frm.fields + .filter((field) => field.has_input) + .forEach((field) => { + frm.set_df_property(field.df.fieldname, "read_only", "1"); + }); + frm.disable_save(); + }, +}); diff --git a/influxframework/desk/doctype/workspace/workspace.json b/influxframework/desk/doctype/workspace/workspace.json new file mode 100644 index 0000000..529fc8b --- /dev/null +++ b/influxframework/desk/doctype/workspace/workspace.json @@ -0,0 +1,212 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:label", + "beta": 1, + "creation": "2020-01-23 13:45:59.470592", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "label", + "title", + "sequence_id", + "for_user", + "parent_page", + "module", + "column_break_3", + "icon", + "restrict_to_domain", + "hide_custom", + "public", + "content", + "tab_break_2", + "charts", + "tab_break_15", + "shortcuts", + "tab_break_18", + "links", + "quick_lists_tab", + "quick_lists", + "roles_tab", + "roles" + ], + "fields": [ + { + "fieldname": "label", + "fieldtype": "Data", + "label": "Name", + "reqd": 1, + "unique": 1 + }, + { + "collapsible": 1, + "collapsible_depends_on": "charts", + "fieldname": "tab_break_2", + "fieldtype": "Tab Break", + "label": "Dashboards" + }, + { + "fieldname": "charts", + "fieldtype": "Table", + "label": "Charts", + "options": "Workspace Chart" + }, + { + "fieldname": "shortcuts", + "fieldtype": "Table", + "label": "Shortcuts", + "options": "Workspace Shortcut" + }, + { + "fieldname": "restrict_to_domain", + "fieldtype": "Link", + "label": "Restrict to Domain", + "options": "Domain", + "search_index": 1 + }, + { + "fieldname": "module", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Module", + "options": "Module Def" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "collapsible": 1, + "collapsible_depends_on": "shortcuts", + "fieldname": "tab_break_15", + "fieldtype": "Tab Break", + "label": "Shortcuts" + }, + { + "collapsible": 1, + "collapsible_depends_on": "links", + "fieldname": "tab_break_18", + "fieldtype": "Tab Break", + "label": "Link Cards" + }, + { + "fieldname": "for_user", + "fieldtype": "Data", + "label": "For User", + "read_only": 1 + }, + { + "default": "0", + "description": "Checking this will hide custom doctypes and reports cards in Links section", + "fieldname": "hide_custom", + "fieldtype": "Check", + "label": "Hide Custom DocTypes and Reports" + }, + { + "fieldname": "icon", + "fieldtype": "Icon", + "label": "Icon", + "read_only": 1 + }, + { + "fieldname": "links", + "fieldtype": "Table", + "label": "Links", + "options": "Workspace Link" + }, + { + "default": "0", + "fieldname": "public", + "fieldtype": "Check", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Public", + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "parent_page", + "fieldtype": "Data", + "label": "Parent Page", + "read_only": 1 + }, + { + "default": "[]", + "fieldname": "content", + "fieldtype": "Long Text", + "hidden": 1, + "label": "Content" + }, + { + "fieldname": "sequence_id", + "fieldtype": "Float", + "label": "Sequence Id", + "read_only": 1 + }, + { + "fieldname": "roles", + "fieldtype": "Table", + "label": "Roles", + "options": "Has Role" + }, + { + "fieldname": "roles_tab", + "fieldtype": "Tab Break", + "label": "Roles" + }, + { + "fieldname": "quick_lists_tab", + "fieldtype": "Tab Break", + "label": "Quick Lists" + }, + { + "fieldname": "quick_lists", + "fieldtype": "Table", + "label": "Quick Lists", + "options": "Workspace Quick List" + } + ], + "in_create": 1, + "links": [], + "modified": "2022-08-16 18:01:42.632238", + "modified_by": "Administrator", + "module": "Desk", + "name": "Workspace", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Workspace Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/influxframework/desk/doctype/workspace/workspace.py b/influxframework/desk/doctype/workspace/workspace.py new file mode 100644 index 0000000..cd773b9 --- /dev/null +++ b/influxframework/desk/doctype/workspace/workspace.py @@ -0,0 +1,354 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +from json import loads + +import influxframework +from influxframework import _ +from influxframework.desk.desktop import save_new_widget +from influxframework.desk.utils import validate_route_conflict +from influxframework.model.document import Document +from influxframework.model.rename_doc import rename_doc +from influxframework.modules.export_file import delete_folder, export_to_files + + +class Workspace(Document): + def validate(self): + if self.public and not is_workspace_manager() and not disable_saving_as_public(): + influxframework.throw(_("You need to be Workspace Manager to edit this document")) + validate_route_conflict(self.doctype, self.name) + + try: + if not isinstance(loads(self.content), list): + raise + except Exception: + influxframework.throw(_("Content data shoud be a list")) + + def on_update(self): + if disable_saving_as_public(): + return + + if influxframework.conf.developer_mode and self.public: + if self.module: + export_to_files(record_list=[["Workspace", self.name]], record_module=self.module) + + if self.has_value_changed("title") or self.has_value_changed("module"): + previous = self.get_doc_before_save() + if previous and previous.get("module") and previous.get("title"): + delete_folder(previous.get("module"), "Workspace", previous.get("title")) + + def before_export(self, doc): + if doc.title != doc.label and doc.label == doc.name: + self.name = doc.name = doc.label = doc.title + + def after_delete(self): + if disable_saving_as_public(): + return + + if self.module and influxframework.conf.developer_mode: + delete_folder(self.module, "Workspace", self.title) + + @staticmethod + def get_module_page_map(): + pages = influxframework.get_all( + "Workspace", fields=["name", "module"], filters={"for_user": ""}, as_list=1 + ) + + return {page[1]: page[0] for page in pages if page[1]} + + def get_link_groups(self): + cards = [] + current_card = influxframework._dict( + { + "label": "Link", + "type": "Card Break", + "icon": None, + "hidden": False, + } + ) + + card_links = [] + + for link in self.links: + link = link.as_dict() + if link.type == "Card Break": + if card_links and ( + not current_card.get("only_for") + or current_card.get("only_for") == influxframework.get_system_settings("country") + ): + current_card["links"] = card_links + cards.append(current_card) + + current_card = link + card_links = [] + else: + card_links.append(link) + + current_card["links"] = card_links + cards.append(current_card) + + return cards + + def build_links_table_from_card(self, config): + + for idx, card in enumerate(config): + links = loads(card.get("links")) + + # remove duplicate before adding + for idx, link in enumerate(self.links): + if link.get("label") == card.get("label") and link.get("type") == "Card Break": + # count and set number of links for the card if link_count is 0 + if link.link_count == 0: + for count, card_link in enumerate(self.links[idx + 1 :]): + if card_link.get("type") == "Card Break": + break + link.link_count = count + 1 + + del self.links[idx : idx + link.link_count + 1] + + self.append( + "links", + { + "label": card.get("label"), + "type": "Card Break", + "icon": card.get("icon"), + "hidden": card.get("hidden") or False, + "link_count": card.get("link_count"), + "idx": 1 if not self.links else self.links[-1].idx + 1, + }, + ) + + for link in links: + self.append( + "links", + { + "label": link.get("label"), + "type": "Link", + "link_type": link.get("link_type"), + "link_to": link.get("link_to"), + "onboard": link.get("onboard"), + "only_for": link.get("only_for"), + "dependencies": link.get("dependencies"), + "is_query_report": link.get("is_query_report"), + "idx": self.links[-1].idx + 1, + }, + ) + + +def disable_saving_as_public(): + return ( + influxframework.flags.in_install + or influxframework.flags.in_uninstall + or influxframework.flags.in_patch + or influxframework.flags.in_test + or influxframework.flags.in_fixtures + or influxframework.flags.in_migrate + ) + + +def get_link_type(key): + key = key.lower() + + link_type_map = {"doctype": "DocType", "page": "Page", "report": "Report"} + + if key in link_type_map: + return link_type_map[key] + + return "DocType" + + +def get_report_type(report): + report_type = influxframework.get_value("Report", report, "report_type") + return report_type in ["Query Report", "Script Report", "Custom Report"] + + +@influxframework.whitelist() +def new_page(new_page): + if not loads(new_page): + return + + page = loads(new_page) + + if page.get("public") and not is_workspace_manager(): + return + + doc = influxframework.new_doc("Workspace") + doc.title = page.get("title") + doc.icon = page.get("icon") + doc.content = page.get("content") + doc.parent_page = page.get("parent_page") + doc.label = page.get("label") + doc.for_user = page.get("for_user") + doc.public = page.get("public") + doc.sequence_id = last_sequence_id(doc) + 1 + doc.save(ignore_permissions=True) + + return doc + + +@influxframework.whitelist() +def save_page(title, public, new_widgets, blocks): + public = influxframework.parse_json(public) + + filters = {"public": public, "label": title} + + if not public: + filters = {"for_user": influxframework.session.user, "label": title + "-" + influxframework.session.user} + pages = influxframework.get_list("Workspace", filters=filters) + if pages: + doc = influxframework.get_doc("Workspace", pages[0]) + + doc.content = blocks + doc.save(ignore_permissions=True) + + save_new_widget(doc, title, blocks, new_widgets) + + return {"name": title, "public": public, "label": doc.label} + + +@influxframework.whitelist() +def update_page(name, title, icon, parent, public): + public = influxframework.parse_json(public) + + doc = influxframework.get_doc("Workspace", name) + + filters = {"parent_page": doc.title, "public": doc.public} + child_docs = influxframework.get_list("Workspace", filters=filters) + + if doc: + doc.title = title + doc.icon = icon + doc.parent_page = parent + if doc.public != public: + doc.sequence_id = influxframework.db.count("Workspace", {"public": public}, cache=True) + doc.public = public + doc.for_user = "" if public else doc.for_user or influxframework.session.user + doc.label = new_name = f"{title}-{doc.for_user}" if doc.for_user else title + doc.save(ignore_permissions=True) + + if name != new_name: + rename_doc("Workspace", name, new_name, force=True, ignore_permissions=True) + + # update new name and public in child pages + if child_docs: + for child in child_docs: + child_doc = influxframework.get_doc("Workspace", child.name) + child_doc.parent_page = doc.title + if child_doc.public != public: + child_doc.public = public + child_doc.for_user = "" if public else child_doc.for_user or influxframework.session.user + child_doc.label = new_child_name = ( + f"{child_doc.title}-{child_doc.for_user}" if child_doc.for_user else child_doc.title + ) + child_doc.save(ignore_permissions=True) + + if child.name != new_child_name: + rename_doc("Workspace", child.name, new_child_name, force=True, ignore_permissions=True) + + return {"name": title, "public": public, "label": new_name} + + +@influxframework.whitelist() +def duplicate_page(page_name, new_page): + if not loads(new_page): + return + + new_page = loads(new_page) + + if new_page.get("is_public") and not is_workspace_manager(): + return + + old_doc = influxframework.get_doc("Workspace", page_name) + doc = influxframework.copy_doc(old_doc) + doc.title = new_page.get("title") + doc.icon = new_page.get("icon") + doc.parent_page = new_page.get("parent") or "" + doc.public = new_page.get("is_public") + doc.for_user = "" + doc.label = doc.title + doc.module = "" + if not doc.public: + doc.for_user = doc.for_user or influxframework.session.user + doc.label = f"{doc.title}-{doc.for_user}" + doc.name = doc.label + if old_doc.public == doc.public: + doc.sequence_id += 0.1 + else: + doc.sequence_id = last_sequence_id(doc) + 1 + doc.insert(ignore_permissions=True) + + return doc + + +@influxframework.whitelist() +def delete_page(page): + if not loads(page): + return + + page = loads(page) + + if page.get("public") and not is_workspace_manager(): + return + + if influxframework.db.exists("Workspace", page.get("name")): + influxframework.get_doc("Workspace", page.get("name")).delete(ignore_permissions=True) + + return {"name": page.get("name"), "public": page.get("public"), "title": page.get("title")} + + +@influxframework.whitelist() +def sort_pages(sb_public_items, sb_private_items): + if not loads(sb_public_items) and not loads(sb_private_items): + return + + sb_public_items = loads(sb_public_items) + sb_private_items = loads(sb_private_items) + + workspace_public_pages = get_page_list(["name", "title"], {"public": 1}) + workspace_private_pages = get_page_list(["name", "title"], {"for_user": influxframework.session.user}) + + if sb_private_items: + return sort_page(workspace_private_pages, sb_private_items) + + if sb_public_items and is_workspace_manager(): + return sort_page(workspace_public_pages, sb_public_items) + + return False + + +def sort_page(workspace_pages, pages): + for seq, d in enumerate(pages): + for page in workspace_pages: + if page.title == d.get("title"): + doc = influxframework.get_doc("Workspace", page.name) + doc.sequence_id = seq + 1 + doc.parent_page = d.get("parent_page") or "" + doc.flags.ignore_links = True + doc.save(ignore_permissions=True) + break + + return True + + +def last_sequence_id(doc): + doc_exists = influxframework.db.exists( + {"doctype": "Workspace", "public": doc.public, "for_user": doc.for_user} + ) + + if not doc_exists: + return 0 + + return influxframework.db.get_list( + "Workspace", + fields=["sequence_id"], + filters={"public": doc.public, "for_user": doc.for_user}, + order_by="sequence_id desc", + )[0].sequence_id + + +def get_page_list(fields, filters): + return influxframework.get_list("Workspace", fields=fields, filters=filters, order_by="sequence_id asc") + + +def is_workspace_manager(): + return "Workspace Manager" in influxframework.get_roles() diff --git a/influxframework/desk/doctype/workspace_chart/__init__.py b/influxframework/desk/doctype/workspace_chart/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/workspace_chart/workspace_chart.json b/influxframework/desk/doctype/workspace_chart/workspace_chart.json new file mode 100644 index 0000000..0d80049 --- /dev/null +++ b/influxframework/desk/doctype/workspace_chart/workspace_chart.json @@ -0,0 +1,39 @@ +{ + "actions": [], + "creation": "2020-01-23 13:44:03.882158", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "chart_name", + "label" + ], + "fields": [ + { + "fieldname": "chart_name", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Chart Name", + "options": "Dashboard Chart", + "reqd": 1 + }, + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label" + } + ], + "istable": 1, + "links": [], + "modified": "2021-01-12 13:13:25.781925", + "modified_by": "Administrator", + "module": "Desk", + "name": "Workspace Chart", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/workspace_chart/workspace_chart.py b/influxframework/desk/doctype/workspace_chart/workspace_chart.py new file mode 100644 index 0000000..f0afc06 --- /dev/null +++ b/influxframework/desk/doctype/workspace_chart/workspace_chart.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class WorkspaceChart(Document): + pass diff --git a/influxframework/desk/doctype/workspace_link/__init__.py b/influxframework/desk/doctype/workspace_link/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/workspace_link/workspace_link.json b/influxframework/desk/doctype/workspace_link/workspace_link.json new file mode 100644 index 0000000..a7b217b --- /dev/null +++ b/influxframework/desk/doctype/workspace_link/workspace_link.json @@ -0,0 +1,125 @@ +{ + "actions": [], + "creation": "2020-11-16 15:30:45.784417", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "type", + "label", + "icon", + "hidden", + "link_details_section", + "link_type", + "link_to", + "column_break_7", + "dependencies", + "only_for", + "onboard", + "is_query_report", + "link_count" + ], + "fields": [ + { + "default": "Link", + "fieldname": "type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Type", + "options": "Link\nCard Break", + "reqd": 1 + }, + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "reqd": 1 + }, + { + "depends_on": "eval:doc.type == \"Card Break\"", + "fieldname": "icon", + "fieldtype": "Data", + "label": "Icon" + }, + { + "default": "0", + "depends_on": "eval:doc.type == \"Card Break\"", + "fieldname": "hidden", + "fieldtype": "Check", + "label": "Hidden" + }, + { + "depends_on": "eval:doc.type == \"Link\"", + "fieldname": "link_details_section", + "fieldtype": "Section Break", + "label": "Link Details" + }, + { + "fieldname": "link_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Link Type", + "mandatory_depends_on": "eval:doc.type==\"Link\"", + "options": "DocType\nPage\nReport", + "read_only_depends_on": "eval:doc.type!=\"Link\"" + }, + { + "fieldname": "link_to", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Link To", + "mandatory_depends_on": "eval:doc.type==\"Link\"", + "options": "link_type", + "read_only_depends_on": "eval:doc.type!=\"Link\"" + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "fieldname": "dependencies", + "fieldtype": "Data", + "label": "Dependencies" + }, + { + "fieldname": "only_for", + "fieldtype": "Link", + "label": "Only for", + "options": "Country" + }, + { + "default": "0", + "fieldname": "onboard", + "fieldtype": "Check", + "label": "Onboard" + }, + { + "default": "0", + "depends_on": "eval:doc.link_type == \"Report\"", + "fieldname": "is_query_report", + "fieldtype": "Check", + "label": "Is Query Report" + }, + { + "depends_on": "eval:doc.type == \"Card Break\"", + "fieldname": "link_count", + "fieldtype": "Int", + "hidden": 1, + "label": "Link Count" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-06-01 11:23:28.990593", + "modified_by": "Administrator", + "module": "Desk", + "name": "Workspace Link", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/workspace_link/workspace_link.py b/influxframework/desk/doctype/workspace_link/workspace_link.py new file mode 100644 index 0000000..485d1dd --- /dev/null +++ b/influxframework/desk/doctype/workspace_link/workspace_link.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class WorkspaceLink(Document): + pass diff --git a/influxframework/desk/doctype/workspace_quick_list/__init__.py b/influxframework/desk/doctype/workspace_quick_list/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/workspace_quick_list/workspace_quick_list.json b/influxframework/desk/doctype/workspace_quick_list/workspace_quick_list.json new file mode 100644 index 0000000..1542ebe --- /dev/null +++ b/influxframework/desk/doctype/workspace_quick_list/workspace_quick_list.json @@ -0,0 +1,60 @@ +{ + "actions": [], + "creation": "2022-05-12 12:58:41.824496", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type", + "column_break_1", + "label", + "section_break_4", + "quick_list_filter" + ], + "fields": [ + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "DocType", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "column_break_1", + "fieldtype": "Column Break" + }, + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "reqd": 1 + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break" + }, + { + "fieldname": "quick_list_filter", + "fieldtype": "Code", + "in_list_view": 1, + "label": "Quick List Filter", + "options": "JSON" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2022-05-12 13:48:40.617623", + "modified_by": "Administrator", + "module": "Desk", + "name": "Workspace Quick List", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/desk/doctype/workspace_quick_list/workspace_quick_list.py b/influxframework/desk/doctype/workspace_quick_list/workspace_quick_list.py new file mode 100644 index 0000000..b166b62 --- /dev/null +++ b/influxframework/desk/doctype/workspace_quick_list/workspace_quick_list.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, InfluxFramework LLC +# For license information, please see license.txt + +# import influxframework +from influxframework.model.document import Document + + +class WorkspaceQuickList(Document): + pass diff --git a/influxframework/desk/doctype/workspace_shortcut/__init__.py b/influxframework/desk/doctype/workspace_shortcut/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/doctype/workspace_shortcut/workspace_shortcut.json b/influxframework/desk/doctype/workspace_shortcut/workspace_shortcut.json new file mode 100644 index 0000000..95886a7 --- /dev/null +++ b/influxframework/desk/doctype/workspace_shortcut/workspace_shortcut.json @@ -0,0 +1,112 @@ +{ + "actions": [], + "creation": "2020-01-23 13:44:59.248426", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "type", + "link_to", + "doc_view", + "column_break_4", + "label", + "icon", + "restrict_to_domain", + "section_break_5", + "stats_filter", + "column_break_3", + "color", + "format" + ], + "fields": [ + { + "fieldname": "type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Type", + "options": "DocType\nReport\nPage\nDashboard", + "reqd": 1 + }, + { + "fieldname": "link_to", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Link To", + "options": "type", + "reqd": 1 + }, + { + "depends_on": "eval:doc.type == \"DocType\"", + "description": "Which view of the associated DocType should this shortcut take you to?", + "fieldname": "doc_view", + "fieldtype": "Select", + "in_list_view": 1, + "label": "DocType View", + "options": "\nList\nReport Builder\nDashboard\nTree\nNew\nCalendar" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "reqd": 1 + }, + { + "depends_on": "eval:influxframework.boot.developer_mode", + "fieldname": "icon", + "fieldtype": "Data", + "label": "Icon" + }, + { + "depends_on": "eval:influxframework.boot.developer_mode", + "fieldname": "restrict_to_domain", + "fieldtype": "Link", + "label": "Restrict to Domain", + "options": "Domain" + }, + { + "depends_on": "eval:doc.type == \"DocType\" && influxframework.boot.developer_mode", + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "label": "Count Filter" + }, + { + "fieldname": "stats_filter", + "fieldtype": "Code", + "label": "Count Filter", + "options": "JSON" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "color", + "fieldtype": "Color", + "label": "Color" + }, + { + "description": "For example: {} Open", + "fieldname": "format", + "fieldtype": "Data", + "label": "Format" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-01-12 13:13:17.571324", + "modified_by": "Administrator", + "module": "Desk", + "name": "Workspace Shortcut", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} diff --git a/influxframework/desk/doctype/workspace_shortcut/workspace_shortcut.py b/influxframework/desk/doctype/workspace_shortcut/workspace_shortcut.py new file mode 100644 index 0000000..1e1fbe6 --- /dev/null +++ b/influxframework/desk/doctype/workspace_shortcut/workspace_shortcut.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class WorkspaceShortcut(Document): + pass diff --git a/influxframework/desk/form/__init__.py b/influxframework/desk/form/__init__.py new file mode 100644 index 0000000..2e11a12 --- /dev/null +++ b/influxframework/desk/form/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE diff --git a/influxframework/desk/form/assign_to.py b/influxframework/desk/form/assign_to.py new file mode 100644 index 0000000..8b36abf --- /dev/null +++ b/influxframework/desk/form/assign_to.py @@ -0,0 +1,245 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +"""assign/unassign to ToDo""" + +import json + +import influxframework +import influxframework.share +import influxframework.utils +from influxframework import _ +from influxframework.desk.doctype.notification_log.notification_log import ( + enqueue_create_notification, + get_title, + get_title_html, +) +from influxframework.desk.form.document_follow import follow_document + + +class DuplicateToDoError(influxframework.ValidationError): + pass + + +def get(args=None): + """get assigned to""" + if not args: + args = influxframework.local.form_dict + + return influxframework.get_all( + "ToDo", + fields=["allocated_to as owner", "name"], + filters={ + "reference_type": args.get("doctype"), + "reference_name": args.get("name"), + "status": ("!=", "Cancelled"), + }, + limit=5, + ) + + +@influxframework.whitelist() +def add(args=None): + """add in someone's to do list + args = { + "assign_to": [], + "doctype": , + "name": , + "description": , + "assignment_rule": + } + + """ + if not args: + args = influxframework.local.form_dict + + users_with_duplicate_todo = [] + shared_with_users = [] + + for assign_to in influxframework.parse_json(args.get("assign_to")): + filters = { + "reference_type": args["doctype"], + "reference_name": args["name"], + "status": "Open", + "allocated_to": assign_to, + } + + if influxframework.get_all("ToDo", filters=filters): + users_with_duplicate_todo.append(assign_to) + else: + from influxframework.utils import nowdate + + if not args.get("description"): + args["description"] = _("Assignment for {0} {1}").format(args["doctype"], args["name"]) + + d = influxframework.get_doc( + { + "doctype": "ToDo", + "allocated_to": assign_to, + "reference_type": args["doctype"], + "reference_name": args["name"], + "description": args.get("description"), + "priority": args.get("priority", "Medium"), + "status": "Open", + "date": args.get("date", nowdate()), + "assigned_by": args.get("assigned_by", influxframework.session.user), + "assignment_rule": args.get("assignment_rule"), + } + ).insert(ignore_permissions=True) + + # set assigned_to if field exists + if influxframework.get_meta(args["doctype"]).get_field("assigned_to"): + influxframework.db.set_value(args["doctype"], args["name"], "assigned_to", assign_to) + + doc = influxframework.get_doc(args["doctype"], args["name"]) + + # if assignee does not have permissions, share + if not influxframework.has_permission(doc=doc, user=assign_to): + influxframework.share.add(doc.doctype, doc.name, assign_to) + shared_with_users.append(assign_to) + + # make this document followed by assigned user + if influxframework.get_cached_value("User", assign_to, "follow_assigned_documents"): + follow_document(args["doctype"], args["name"], assign_to) + + # notify + notify_assignment( + d.assigned_by, + d.allocated_to, + d.reference_type, + d.reference_name, + action="ASSIGN", + description=args.get("description"), + ) + + if shared_with_users: + user_list = format_message_for_assign_to(shared_with_users) + influxframework.msgprint( + _("Shared with the following Users with Read access:{0}").format(user_list, alert=True) + ) + + if users_with_duplicate_todo: + user_list = format_message_for_assign_to(users_with_duplicate_todo) + influxframework.msgprint(_("Already in the following Users ToDo list:{0}").format(user_list, alert=True)) + + return get(args) + + +@influxframework.whitelist() +def add_multiple(args=None): + if not args: + args = influxframework.local.form_dict + + docname_list = json.loads(args["name"]) + + for docname in docname_list: + args.update({"name": docname}) + add(args) + + +def close_all_assignments(doctype, name): + assignments = influxframework.get_all( + "ToDo", + fields=["allocated_to"], + filters=dict(reference_type=doctype, reference_name=name, status=("!=", "Cancelled")), + ) + if not assignments: + return False + + for assign_to in assignments: + set_status(doctype, name, assign_to.allocated_to, status="Closed") + + return True + + +@influxframework.whitelist() +def remove(doctype, name, assign_to): + return set_status(doctype, name, assign_to, status="Cancelled") + + +def set_status(doctype, name, assign_to, status="Cancelled"): + """remove from todo""" + try: + todo = influxframework.db.get_value( + "ToDo", + { + "reference_type": doctype, + "reference_name": name, + "allocated_to": assign_to, + "status": ("!=", status), + }, + ) + if todo: + todo = influxframework.get_doc("ToDo", todo) + todo.status = status + todo.save(ignore_permissions=True) + + notify_assignment(todo.assigned_by, todo.allocated_to, todo.reference_type, todo.reference_name) + except influxframework.DoesNotExistError: + pass + + # clear assigned_to if field exists + if influxframework.get_meta(doctype).get_field("assigned_to") and status == "Cancelled": + influxframework.db.set_value(doctype, name, "assigned_to", None) + + return get({"doctype": doctype, "name": name}) + + +def clear(doctype, name): + """ + Clears assignments, return False if not assigned. + """ + assignments = influxframework.get_all( + "ToDo", fields=["allocated_to"], filters=dict(reference_type=doctype, reference_name=name) + ) + if not assignments: + return False + + for assign_to in assignments: + set_status(doctype, name, assign_to.allocated_to, "Cancelled") + + return True + + +def notify_assignment( + assigned_by, allocated_to, doc_type, doc_name, action="CLOSE", description=None +): + """ + Notify assignee that there is a change in assignment + """ + if not (assigned_by and allocated_to and doc_type and doc_name): + return + + # return if self assigned or user disabled + if assigned_by == allocated_to or not influxframework.db.get_value("User", allocated_to, "enabled"): + return + + # Search for email address in description -- i.e. assignee + user_name = influxframework.get_cached_value("User", influxframework.session.user, "full_name") + title = get_title(doc_type, doc_name) + description_html = f"
    {description}
    " if description else None + + if action == "CLOSE": + subject = _("Your assignment on {0} {1} has been removed by {2}").format( + influxframework.bold(doc_type), get_title_html(title), influxframework.bold(user_name) + ) + else: + user_name = influxframework.bold(user_name) + document_type = influxframework.bold(doc_type) + title = get_title_html(title) + subject = _("{0} assigned a new task {1} {2} to you").format(user_name, document_type, title) + + notification_doc = { + "type": "Assignment", + "document_type": doc_type, + "subject": subject, + "document_name": doc_name, + "from_user": influxframework.session.user, + "email_content": description_html, + } + + enqueue_create_notification(allocated_to, notification_doc) + + +def format_message_for_assign_to(users): + return "

    " + "
    ".join(users) diff --git a/influxframework/desk/form/document_follow.py b/influxframework/desk/form/document_follow.py new file mode 100644 index 0000000..4e3aa25 --- /dev/null +++ b/influxframework/desk/form/document_follow.py @@ -0,0 +1,342 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +import influxframework.utils +from influxframework import _ +from influxframework.model import log_types +from influxframework.query_builder import DocType +from influxframework.utils import get_url_to_form + + +@influxframework.whitelist() +def update_follow(doctype, doc_name, following): + if following: + return follow_document(doctype, doc_name, influxframework.session.user) + else: + return unfollow_document(doctype, doc_name, influxframework.session.user) + + +@influxframework.whitelist() +def follow_document(doctype, doc_name, user): + """ + param: + Doctype name + doc name + user email + + condition: + avoided for some doctype + follow only if track changes are set to 1 + """ + if ( + doctype + in ( + "Communication", + "ToDo", + "Email Unsubscribe", + "File", + "Comment", + "Email Account", + "Email Domain", + ) + or doctype in log_types + ): + return + + if (not influxframework.get_meta(doctype).track_changes) or user == "Administrator": + return + + if not influxframework.db.get_value("User", user, "document_follow_notify", ignore=True, cache=True): + return + + if not is_document_followed(doctype, doc_name, user): + doc = influxframework.new_doc("Document Follow") + doc.update({"ref_doctype": doctype, "ref_docname": doc_name, "user": user}) + doc.save() + return doc + + +@influxframework.whitelist() +def unfollow_document(doctype, doc_name, user): + doc = influxframework.get_all( + "Document Follow", + filters={"ref_doctype": doctype, "ref_docname": doc_name, "user": user}, + fields=["name"], + limit=1, + ) + if doc: + influxframework.delete_doc("Document Follow", doc[0].name) + return 1 + return 0 + + +def get_message(doc_name, doctype, frequency, user): + activity_list = get_version(doctype, doc_name, frequency, user) + get_comments( + doctype, doc_name, frequency, user + ) + return sorted(activity_list, key=lambda k: k["time"], reverse=True) + + +def send_email_alert(receiver, docinfo, timeline): + if receiver: + influxframework.sendmail( + subject=_("Document Follow Notification"), + recipients=[receiver], + template="document_follow", + args={ + "docinfo": docinfo, + "timeline": timeline, + }, + ) + + +def send_document_follow_mails(frequency): + """ + param: + frequency for sanding mails + + task: + set receiver according to frequency + group document list according to user + get changes, activity, comments on doctype + call method to send mail + """ + + user_list = get_user_list(frequency) + + for user in user_list: + message, valid_document_follows = get_message_for_user(frequency, user) + if message: + send_email_alert(user, valid_document_follows, message) + # send an email if we have already spent resources creating the message + # nosemgrep + influxframework.db.commit() + + +def get_user_list(frequency): + DocumentFollow = DocType("Document Follow") + User = DocType("User") + return ( + influxframework.qb.from_(DocumentFollow) + .join(User) + .on(DocumentFollow.user == User.name) + .where(User.document_follow_notify == 1) + .where(User.document_follow_frequency == frequency) + .select(DocumentFollow.user) + .groupby(DocumentFollow.user) + ).run(pluck="user") + + +def get_message_for_user(frequency, user): + message = [] + latest_document_follows = get_document_followed_by_user(user) + valid_document_follows = [] + + for document_follow in latest_document_follows: + content = get_message(document_follow.ref_docname, document_follow.ref_doctype, frequency, user) + if content: + message = message + content + valid_document_follows.append( + { + "reference_docname": document_follow.ref_docname, + "reference_doctype": document_follow.ref_doctype, + "reference_url": get_url_to_form(document_follow.ref_doctype, document_follow.ref_docname), + } + ) + return message, valid_document_follows + + +def get_document_followed_by_user(user): + DocumentFollow = DocType("Document Follow") + # at max 20 documents are sent for each user + return ( + influxframework.qb.from_(DocumentFollow) + .where(DocumentFollow.user == user) + .select(DocumentFollow.ref_doctype, DocumentFollow.ref_docname) + .orderby(DocumentFollow.modified) + .limit(20) + ).run(as_dict=True) + + +def get_version(doctype, doc_name, frequency, user): + timeline = [] + version = influxframework.get_all( + "Version", + filters=[ + ["ref_doctype", "=", doctype], + ["docname", "=", doc_name], + *_get_filters(frequency, user), + ], + fields=["data", "modified", "modified_by"], + ) + if version: + for v in version: + change = influxframework.parse_json(v.data) + time = influxframework.utils.format_datetime(v.modified, "hh:mm a") + timeline_items = [] + if change.changed: + timeline_items = get_field_changed(change.changed, time, doctype, doc_name, v) + if change.row_changed: + timeline_items = get_row_changed(change.row_changed, time, doctype, doc_name, v) + if change.added: + timeline_items = get_added_row(change.added, time, doctype, doc_name, v) + + timeline = timeline + timeline_items + + return timeline + + +def get_comments(doctype, doc_name, frequency, user): + from influxframework.core.utils import html2text + + timeline = [] + comments = influxframework.get_all( + "Comment", + filters=[ + ["reference_doctype", "=", doctype], + ["reference_name", "=", doc_name], + *_get_filters(frequency, user), + ], + fields=["content", "modified", "modified_by", "comment_type"], + ) + for comment in comments: + if comment.comment_type == "Like": + by = f""" By : {comment.modified_by}""" + elif comment.comment_type == "Comment": + by = f"""Commented by : {comment.modified_by}""" + else: + by = "" + + time = influxframework.utils.format_datetime(comment.modified, "hh:mm a") + timeline.append( + { + "time": comment.modified, + "data": {"time": time, "comment": html2text(str(comment.content)), "by": by}, + "doctype": doctype, + "doc_name": doc_name, + "type": "comment", + } + ) + return timeline + + +def is_document_followed(doctype, doc_name, user): + return influxframework.db.exists( + "Document Follow", {"ref_doctype": doctype, "ref_docname": doc_name, "user": user} + ) + + +@influxframework.whitelist() +def get_follow_users(doctype, doc_name): + return influxframework.get_all( + "Document Follow", filters={"ref_doctype": doctype, "ref_docname": doc_name}, fields=["user"] + ) + + +def get_row_changed(row_changed, time, doctype, doc_name, v): + from influxframework.core.utils import html2text + + items = [] + for d in row_changed: + d[2] = d[2] if d[2] else " " + d[0] = d[0] if d[0] else " " + d[3][0][1] = d[3][0][1] if d[3][0][1] else " " + items.append( + { + "time": v.modified, + "data": { + "time": time, + "table_field": d[0], + "row": str(d[1]), + "field": d[3][0][0], + "from": html2text(str(d[3][0][1])), + "to": html2text(str(d[3][0][2])), + }, + "doctype": doctype, + "doc_name": doc_name, + "type": "row changed", + "by": v.modified_by, + } + ) + return items + + +def get_added_row(added, time, doctype, doc_name, v): + items = [] + for d in added: + items.append( + { + "time": v.modified, + "data": {"to": d[0], "time": time}, + "doctype": doctype, + "doc_name": doc_name, + "type": "row added", + "by": v.modified_by, + } + ) + return items + + +def get_field_changed(changed, time, doctype, doc_name, v): + from influxframework.core.utils import html2text + + items = [] + for d in changed: + d[1] = d[1] if d[1] else " " + d[2] = d[2] if d[2] else " " + d[0] = d[0] if d[0] else " " + items.append( + { + "time": v.modified, + "data": { + "time": time, + "field": d[0], + "from": html2text(str(d[1])), + "to": html2text(str(d[2])), + }, + "doctype": doctype, + "doc_name": doc_name, + "type": "field changed", + "by": v.modified_by, + } + ) + return items + + +def send_hourly_updates(): + send_document_follow_mails("Hourly") + + +def send_daily_updates(): + send_document_follow_mails("Daily") + + +def send_weekly_updates(): + send_document_follow_mails("Weekly") + + +def _get_filters(frequency, user): + filters = [ + ["modified_by", "!=", user], + ] + + if frequency == "Weekly": + filters += [ + ["modified", ">", influxframework.utils.add_days(influxframework.utils.nowdate(), -7)], + ["modified", "<", influxframework.utils.nowdate()], + ] + + elif frequency == "Daily": + filters += [ + ["modified", ">", influxframework.utils.add_days(influxframework.utils.nowdate(), -1)], + ["modified", "<", influxframework.utils.nowdate()], + ] + + elif frequency == "Hourly": + filters += [ + ["modified", ">", influxframework.utils.add_to_date(influxframework.utils.now_datetime(), hours=-1)], + ["modified", "<", influxframework.utils.now_datetime()], + ] + + return filters diff --git a/influxframework/desk/form/linked_with.py b/influxframework/desk/form/linked_with.py new file mode 100644 index 0000000..e8f7ccb --- /dev/null +++ b/influxframework/desk/form/linked_with.py @@ -0,0 +1,648 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE + +import itertools +import json +from collections import defaultdict + +import influxframework +import influxframework.desk.form.load +import influxframework.desk.form.meta +from influxframework import _ +from influxframework.model.meta import is_single +from influxframework.modules import load_doctype_module + + +@influxframework.whitelist() +def get_submitted_linked_docs(doctype: str, name: str) -> list[tuple]: + """Get all the nested submitted documents those are present in referencing tables (dependent tables). + + :param doctype: Document type + :param name: Name of the document + + Usecase: + * User should be able to cancel the linked documents along with the one user trying to cancel. + + Case1: If document sd1-n1 (document name n1 from sumittable doctype sd1) is linked to sd2-n2 and sd2-n2 is linked to sd3-n3, + Getting submittable linked docs of `sd1-n1`should give both sd2-n2 and sd3-n3. + Case2: If document sd1-n1 (document name n1 from sumittable doctype sd1) is linked to d2-n2 and d2-n2 is linked to sd3-n3, + Getting submittable linked docs of `sd1-n1`should give None. (because d2-n2 is not a submittable doctype) + Case3: If document sd1-n1 (document name n1 from submittable doctype sd1) is linked to d2-n2 & sd2-n2. d2-n2 is linked to sd3-n3. + Getting submittable linked docs of `sd1-n1`should give sd2-n2. + + Logic: + ----- + 1. We can find linked documents only if we know how the doctypes are related. + 2. As we need only submittable documents, we can limit doctype relations search to submittable doctypes by + finding the relationships(Foreign key references) across submittable doctypes. + 3. Searching for links is going to be a tree like structure where at every level, + you will be finding documents using parent document and parent document links. + """ + tree = SubmittableDocumentTree(doctype, name) + visited_documents = tree.get_all_children() + docs = [] + + for dt, names in visited_documents.items(): + docs.extend([{"doctype": dt, "name": name, "docstatus": 1} for name in names]) + + return {"docs": docs, "count": len(docs)} + + +class SubmittableDocumentTree: + def __init__(self, doctype: str, name: str): + """Construct a tree for the submitable linked documents. + + * Node has properties like doctype and docnames. Represented as Node(doctype, docnames). + * Nodes are linked by doctype relationships like table, link and dynamic links. + * Node is referenced(linked) by many other documents and those are the child nodes. + + NOTE: child document is a property of child node (not same as InfluxFramework child docs of a table field). + """ + self.root_doctype = doctype + self.root_docname = name + + # Documents those are yet to be visited for linked documents. + self.to_be_visited_documents = {doctype: [name]} + self.visited_documents = defaultdict(list) + + self._submittable_doctypes = None # All submittable doctypes in the system + self._references_across_doctypes = None # doctype wise links/references + + def get_all_children(self): + """Get all nodes of a tree except the root node (all the nested submitted + documents those are present in referencing tables (dependent tables). + """ + while self.to_be_visited_documents: + next_level_children = defaultdict(list) + for parent_dt in list(self.to_be_visited_documents): + parent_docs = self.to_be_visited_documents.get(parent_dt) + if not parent_docs: + del self.to_be_visited_documents[parent_dt] + continue + + child_docs = self.get_next_level_children(parent_dt, parent_docs) + self.visited_documents[parent_dt].extend(parent_docs) + for linked_dt, linked_names in child_docs.items(): + not_visited_child_docs = set(linked_names) - set(self.visited_documents.get(linked_dt, [])) + next_level_children[linked_dt].extend(not_visited_child_docs) + + self.to_be_visited_documents = next_level_children + + # Remove root node from visited documents + if self.root_docname in self.visited_documents.get(self.root_doctype, []): + self.visited_documents[self.root_doctype].remove(self.root_docname) + + return self.visited_documents + + def get_next_level_children(self, parent_dt, parent_names): + """Get immediate children of a Node(parent_dt, parent_names)""" + referencing_fields = self.get_doctype_references(parent_dt) + + child_docs = defaultdict(list) + for field in referencing_fields: + links = ( + get_referencing_documents( + parent_dt, + parent_names.copy(), + field, + get_parent_if_child_table_doc=True, + parent_filters=[("docstatus", "=", 1)], + allowed_parents=self.get_link_sources(), + ) + or {} + ) + for dt, names in links.items(): + child_docs[dt].extend(names) + return child_docs + + def get_doctype_references(self, doctype): + """Get references for a given document.""" + if self._references_across_doctypes is None: + get_links_to = self.get_document_sources() + limit_link_doctypes = self.get_link_sources() + self._references_across_doctypes = get_references_across_doctypes( + get_links_to, limit_link_doctypes + ) + return self._references_across_doctypes.get(doctype, []) + + def get_document_sources(self): + """Returns list of doctypes from where we access submittable documents.""" + return list(set(self.get_link_sources() + [self.root_doctype])) + + def get_link_sources(self): + """limit doctype links to these doctypes.""" + return list(set(self.get_submittable_doctypes()) - set(get_exempted_doctypes() or [])) + + def get_submittable_doctypes(self) -> list[str]: + """Returns list of submittable doctypes.""" + if not self._submittable_doctypes: + self._submittable_doctypes = influxframework.get_all("DocType", {"is_submittable": 1}, pluck="name") + return self._submittable_doctypes + + +def get_child_tables_of_doctypes(doctypes: list[str] = None): + """Returns child tables by doctype.""" + filters = [["fieldtype", "=", "Table"]] + filters_for_docfield = filters + filters_for_customfield = filters + + if doctypes: + filters_for_docfield = filters + [["parent", "in", tuple(doctypes)]] + filters_for_customfield = filters + [["dt", "in", tuple(doctypes)]] + + links = influxframework.get_all( + "DocField", + fields=["parent", "fieldname", "options as child_table"], + filters=filters_for_docfield, + as_list=1, + ) + + links += influxframework.get_all( + "Custom Field", + fields=["dt as parent", "fieldname", "options as child_table"], + filters=filters_for_customfield, + as_list=1, + ) + + child_tables_by_doctype = defaultdict(list) + for doctype, fieldname, child_table in links: + child_tables_by_doctype[doctype].append( + {"doctype": doctype, "fieldname": fieldname, "child_table": child_table} + ) + return child_tables_by_doctype + + +def get_references_across_doctypes( + to_doctypes: list[str] = None, limit_link_doctypes: list[str] = None +) -> list: + """Find doctype wise foreign key references. + + :param to_doctypes: Get links of these doctypes. + :param limit_link_doctypes: limit links to these doctypes. + + * Include child table, link and dynamic link references. + """ + if limit_link_doctypes: + child_tables_by_doctype = get_child_tables_of_doctypes(limit_link_doctypes) + all_child_tables = [ + each["child_table"] for each in itertools.chain(*child_tables_by_doctype.values()) + ] + limit_link_doctypes = limit_link_doctypes + all_child_tables + else: + child_tables_by_doctype = get_child_tables_of_doctypes() + all_child_tables = [ + each["child_table"] for each in itertools.chain(*child_tables_by_doctype.values()) + ] + + references_by_link_fields = get_references_across_doctypes_by_link_field( + to_doctypes, limit_link_doctypes + ) + references_by_dlink_fields = get_references_across_doctypes_by_dynamic_link_field( + to_doctypes, limit_link_doctypes + ) + + references = references_by_link_fields.copy() + for k, v in references_by_dlink_fields.items(): + references.setdefault(k, []).extend(v) + + for doctype, links in references.items(): + for link in links: + link["is_child"] = link["doctype"] in all_child_tables + return references + + +def get_references_across_doctypes_by_link_field( + to_doctypes: list[str] = None, limit_link_doctypes: list[str] = None +): + """Find doctype wise foreign key references based on link fields. + + :param to_doctypes: Get links to these doctypes. + :param limit_link_doctypes: limit links to these doctypes. + """ + filters = [["fieldtype", "=", "Link"]] + + if to_doctypes: + filters += [["options", "in", tuple(to_doctypes)]] + + filters_for_docfield = filters[:] + filters_for_customfield = filters[:] + + if limit_link_doctypes: + filters_for_docfield += [["parent", "in", tuple(limit_link_doctypes)]] + filters_for_customfield += [["dt", "in", tuple(limit_link_doctypes)]] + + links = influxframework.get_all( + "DocField", + fields=["parent", "fieldname", "options as linked_to"], + filters=filters_for_docfield, + as_list=1, + ) + + links += influxframework.get_all( + "Custom Field", + fields=["dt as parent", "fieldname", "options as linked_to"], + filters=filters_for_customfield, + as_list=1, + ) + + links_by_doctype = defaultdict(list) + for doctype, fieldname, linked_to in links: + links_by_doctype[linked_to].append({"doctype": doctype, "fieldname": fieldname}) + return links_by_doctype + + +def get_references_across_doctypes_by_dynamic_link_field( + to_doctypes: list[str] = None, limit_link_doctypes: list[str] = None +): + """Find doctype wise foreign key references based on dynamic link fields. + + :param to_doctypes: Get links to these doctypes. + :param limit_link_doctypes: limit links to these doctypes. + """ + + filters = [["fieldtype", "=", "Dynamic Link"]] + + filters_for_docfield = filters[:] + filters_for_customfield = filters[:] + + if limit_link_doctypes: + filters_for_docfield += [["parent", "in", tuple(limit_link_doctypes)]] + filters_for_customfield += [["dt", "in", tuple(limit_link_doctypes)]] + + # find dynamic links of parents + links = influxframework.get_all( + "DocField", + fields=["parent as doctype", "fieldname", "options as doctype_fieldname"], + filters=filters_for_docfield, + as_list=1, + ) + + links += influxframework.get_all( + "Custom Field", + fields=["dt as doctype", "fieldname", "options as doctype_fieldname"], + filters=filters_for_customfield, + as_list=1, + ) + + links_by_doctype = defaultdict(list) + for doctype, fieldname, doctype_fieldname in links: + try: + filters = [[doctype_fieldname, "in", to_doctypes]] if to_doctypes else [] + for linked_to in influxframework.get_all(doctype, pluck=doctype_fieldname, filters=filters, distinct=1): + if linked_to: + links_by_doctype[linked_to].append( + {"doctype": doctype, "fieldname": fieldname, "doctype_fieldname": doctype_fieldname} + ) + except influxframework.db.ProgrammingError: + # TODO: FIXME + continue + return links_by_doctype + + +def get_referencing_documents( + reference_doctype: str, + reference_names: list[str], + link_info: dict, + get_parent_if_child_table_doc: bool = True, + parent_filters: list[list] = None, + child_filters=None, + allowed_parents=None, +): + """Get linked documents based on link_info. + + :param reference_doctype: reference doctype to find links + :param reference_names: reference document names to find links for + :param link_info: linking details to get the linked documents + Ex: {'doctype': 'Purchase Invoice Advance', 'fieldname': 'reference_name', + 'doctype_fieldname': 'reference_type', 'is_child': True} + :param get_parent_if_child_table_doc: Get parent record incase linked document is a child table record. + :param parent_filters: filters to apply on if not a child table. + :param child_filters: apply filters if it is a child table. + :param allowed_parents: list of parents allowed in case of get_parent_if_child_table_doc + is enabled. + """ + from_table = link_info["doctype"] + filters = [[link_info["fieldname"], "in", tuple(reference_names)]] + if link_info.get("doctype_fieldname"): + filters.append([link_info["doctype_fieldname"], "=", reference_doctype]) + + if not link_info.get("is_child"): + filters.extend(parent_filters or []) + return {from_table: influxframework.get_all(from_table, filters, pluck="name")} + + filters.extend(child_filters or []) + res = influxframework.get_all(from_table, filters=filters, fields=["name", "parenttype", "parent"]) + documents = defaultdict(list) + + for parent, rows in itertools.groupby(res, key=lambda row: row["parenttype"]): + if allowed_parents and parent not in allowed_parents: + continue + filters = (parent_filters or []) + [["name", "in", tuple(row.parent for row in rows)]] + documents[parent].extend(influxframework.get_all(parent, filters=filters, pluck="name") or []) + return documents + + +@influxframework.whitelist() +def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=None): + """ + Cancel all linked doctype, optionally ignore doctypes specified in a list. + + Arguments: + docs (json str) - It contains list of dictionaries of a linked documents. + ignore_doctypes_on_cancel_all (list) - List of doctypes to ignore while cancelling. + """ + if ignore_doctypes_on_cancel_all is None: + ignore_doctypes_on_cancel_all = [] + + docs = json.loads(docs) + if isinstance(ignore_doctypes_on_cancel_all, str): + ignore_doctypes_on_cancel_all = json.loads(ignore_doctypes_on_cancel_all) + for i, doc in enumerate(docs, 1): + if validate_linked_doc(doc, ignore_doctypes_on_cancel_all): + linked_doc = influxframework.get_doc(doc.get("doctype"), doc.get("name")) + linked_doc.cancel() + influxframework.publish_progress(percent=i / len(docs) * 100, title=_("Cancelling documents")) + + +def validate_linked_doc(docinfo, ignore_doctypes_on_cancel_all=None): + """ + Validate a document to be submitted and non-exempted from auto-cancel. + + Arguments: + docinfo (dict): The document to check for submitted and non-exempt from auto-cancel + ignore_doctypes_on_cancel_all (list) - List of doctypes to ignore while cancelling. + + Returns: + bool: True if linked document passes all validations, else False + """ + # ignore doctype to cancel + if docinfo.get("doctype") in (ignore_doctypes_on_cancel_all or []): + return False + + # skip non-submittable doctypes since they don't need to be cancelled + if not influxframework.get_meta(docinfo.get("doctype")).is_submittable: + return False + + # skip draft or cancelled documents + if docinfo.get("docstatus") != 1: + return False + + # skip other doctypes since they don't need to be cancelled + auto_cancel_exempt_doctypes = get_exempted_doctypes() + if docinfo.get("doctype") in auto_cancel_exempt_doctypes: + return False + + return True + + +def get_exempted_doctypes(): + """Get list of doctypes exempted from being auto-cancelled""" + auto_cancel_exempt_doctypes = [] + for doctypes in influxframework.get_hooks("auto_cancel_exempted_doctypes"): + auto_cancel_exempt_doctypes.append(doctypes) + return auto_cancel_exempt_doctypes + + +@influxframework.whitelist() +def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> dict[str, list]: + if isinstance(linkinfo, str): + # additional fields are added in linkinfo + linkinfo = json.loads(linkinfo) + + results = {} + + if not linkinfo: + return results + + for dt, link in linkinfo.items(): + filters = [] + link["doctype"] = dt + try: + link_meta_bundle = influxframework.desk.form.load.get_meta_bundle(dt) + except Exception as e: + if isinstance(e, influxframework.DoesNotExistError): + if influxframework.local.message_log: + influxframework.local.message_log.pop() + continue + linkmeta = link_meta_bundle[0] + + if not linkmeta.has_permission(): + continue + + if not linkmeta.get("issingle"): + fields = [ + d.fieldname + for d in linkmeta.get( + "fields", + { + "in_list_view": 1, + "fieldtype": ["not in", ("Image", "HTML", "Button") + influxframework.model.table_fields], + }, + ) + ] + ["name", "modified", "docstatus"] + + if link.get("add_fields"): + fields += link["add_fields"] + + fields = [f"`tab{dt}`.`{sf.strip()}`" for sf in fields if sf and "`tab" not in sf] + + try: + if link.get("filters"): + ret = influxframework.get_all(doctype=dt, fields=fields, filters=link.get("filters")) + + elif link.get("get_parent"): + ret = None + + # check for child table + if not influxframework.get_meta(doctype).istable: + continue + + me = influxframework.db.get_value(doctype, name, ["parenttype", "parent"], as_dict=True) + if me and me.parenttype == dt: + ret = influxframework.get_all(doctype=dt, fields=fields, filters=[[dt, "name", "=", me.parent]]) + + elif link.get("child_doctype"): + or_filters = [ + [link.get("child_doctype"), link_fieldnames, "=", name] + for link_fieldnames in link.get("fieldname") + ] + + # dynamic link + if link.get("doctype_fieldname"): + filters.append([link.get("child_doctype"), link.get("doctype_fieldname"), "=", doctype]) + + ret = influxframework.get_all( + doctype=dt, fields=fields, filters=filters, or_filters=or_filters, distinct=True + ) + + else: + link_fieldnames = link.get("fieldname") + if link_fieldnames: + if isinstance(link_fieldnames, str): + link_fieldnames = [link_fieldnames] + or_filters = [[dt, fieldname, "=", name] for fieldname in link_fieldnames] + # dynamic link + if link.get("doctype_fieldname"): + filters.append([dt, link.get("doctype_fieldname"), "=", doctype]) + ret = influxframework.get_all(doctype=dt, fields=fields, filters=filters, or_filters=or_filters) + + else: + ret = None + + except influxframework.PermissionError: + if influxframework.local.message_log: + influxframework.local.message_log.pop() + + continue + + if ret: + results[dt] = ret + + return results + + +@influxframework.whitelist() +def get(doctype, docname): + linked_doctypes = get_linked_doctypes(doctype=doctype) + return get_linked_docs(doctype=doctype, name=docname, linkinfo=linked_doctypes) + + +@influxframework.whitelist() +def get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False): + """add list of doctypes this doctype is 'linked' with. + + Example, for Customer: + + {"Address": {"fieldname": "customer"}..} + """ + if without_ignore_user_permissions_enabled: + return influxframework.cache().hget( + "linked_doctypes_without_ignore_user_permissions_enabled", + doctype, + lambda: _get_linked_doctypes(doctype, without_ignore_user_permissions_enabled), + ) + else: + return influxframework.cache().hget("linked_doctypes", doctype, lambda: _get_linked_doctypes(doctype)) + + +def _get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False): + ret = {} + # find fields where this doctype is linked + ret.update(get_linked_fields(doctype, without_ignore_user_permissions_enabled)) + ret.update(get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled)) + + filters = [["fieldtype", "in", influxframework.model.table_fields], ["options", "=", doctype]] + if without_ignore_user_permissions_enabled: + filters.append(["ignore_user_permissions", "!=", 1]) + # find links of parents + links = influxframework.get_all("DocField", fields=["parent as dt"], filters=filters) + links += influxframework.get_all("Custom Field", fields=["dt"], filters=filters) + + for (dt,) in links: + if dt in ret: + continue + ret[dt] = {"get_parent": True} + + for dt in list(ret): + try: + doctype_module = load_doctype_module(dt) + except (ImportError, KeyError): + # in case of Custom DocType + # or in case of module rename eg. (Schools -> Education) + continue + + if getattr(doctype_module, "exclude_from_linked_with", False): + del ret[dt] + + return ret + + +def get_linked_fields(doctype, without_ignore_user_permissions_enabled=False): + + filters = [["fieldtype", "=", "Link"], ["options", "=", doctype]] + if without_ignore_user_permissions_enabled: + filters.append(["ignore_user_permissions", "!=", 1]) + + # find links of parents + links = influxframework.get_all("DocField", fields=["parent", "fieldname"], filters=filters, as_list=1) + links += influxframework.get_all( + "Custom Field", fields=["dt as parent", "fieldname"], filters=filters, as_list=1 + ) + + ret = {} + + if not links: + return ret + + links_dict = defaultdict(list) + for doctype, fieldname in links: + links_dict[doctype].append(fieldname) + + for doctype_name in links_dict: + ret[doctype_name] = {"fieldname": links_dict.get(doctype_name)} + table_doctypes = influxframework.get_all( + "DocType", filters=[["istable", "=", "1"], ["name", "in", tuple(links_dict)]] + ) + child_filters = [ + ["fieldtype", "in", influxframework.model.table_fields], + ["options", "in", tuple(doctype.name for doctype in table_doctypes)], + ] + if without_ignore_user_permissions_enabled: + child_filters.append(["ignore_user_permissions", "!=", 1]) + + # find out if linked in a child table + for parent, options in influxframework.get_all( + "DocField", fields=["parent", "options"], filters=child_filters, as_list=1 + ): + ret[parent] = {"child_doctype": options, "fieldname": links_dict[options]} + if options in ret: + del ret[options] + + return ret + + +def get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled=False): + ret = {} + + filters = [["fieldtype", "=", "Dynamic Link"]] + if without_ignore_user_permissions_enabled: + filters.append(["ignore_user_permissions", "!=", 1]) + + # find dynamic links of parents + links = influxframework.get_all( + "DocField", + fields=["parent as doctype", "fieldname", "options as doctype_fieldname"], + filters=filters, + ) + links += influxframework.get_all( + "Custom Field", + fields=["dt as doctype", "fieldname", "options as doctype_fieldname"], + filters=filters, + ) + + for df in links: + if is_single(df.doctype): + continue + + is_child = influxframework.get_meta(df.doctype).istable + possible_link = influxframework.get_all( + df.doctype, + filters={df.doctype_fieldname: doctype}, + fields=["parenttype"] if is_child else None, + distinct=True, + ) + + if not possible_link: + continue + + if is_child: + for d in possible_link: + ret[d.parenttype] = { + "child_doctype": df.doctype, + "fieldname": [df.fieldname], + "doctype_fieldname": df.doctype_fieldname, + } + else: + ret[df.doctype] = {"fieldname": [df.fieldname], "doctype_fieldname": df.doctype_fieldname} + + return ret diff --git a/influxframework/desk/form/load.py b/influxframework/desk/form/load.py new file mode 100644 index 0000000..160b944 --- /dev/null +++ b/influxframework/desk/form/load.py @@ -0,0 +1,508 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import json +from urllib.parse import quote + +import influxframework +import influxframework.defaults +import influxframework.desk.form.meta +import influxframework.share +import influxframework.utils +from influxframework import _, _dict +from influxframework.desk.form.document_follow import is_document_followed +from influxframework.model.utils import is_virtual_doctype +from influxframework.model.utils.user_settings import get_user_settings +from influxframework.permissions import get_doc_permissions +from influxframework.utils.data import cstr + + +@influxframework.whitelist() +def getdoc(doctype, name, user=None): + """ + Loads a doclist for a given document. This method is called directly from the client. + Requries "doctype", "name" as form variables. + Will also call the "onload" method on the document. + """ + + if not (doctype and name): + raise Exception("doctype and name required!") + + if not name: + name = doctype + + if not is_virtual_doctype(doctype) and not influxframework.db.exists(doctype, name): + return [] + + doc = influxframework.get_doc(doctype, name) + run_onload(doc) + + if not doc.has_permission("read"): + influxframework.flags.error_message = _("Insufficient Permission for {0}").format( + influxframework.bold(doctype + " " + name) + ) + raise influxframework.PermissionError(("read", doctype, name)) + + doc.apply_fieldlevel_read_permissions() + + # add file list + doc.add_viewed() + get_docinfo(doc) + + doc.add_seen() + set_link_titles(doc) + if influxframework.response.docs is None: + influxframework.local.response = _dict({"docs": []}) + influxframework.response.docs.append(doc) + + +@influxframework.whitelist() +def getdoctype(doctype, with_parent=False, cached_timestamp=None): + """load doctype""" + + docs = [] + parent_dt = None + + # with parent (called from report builder) + if with_parent and (parent_dt := influxframework.model.meta.get_parent_dt(doctype)): + docs = get_meta_bundle(parent_dt) + influxframework.response["parent_dt"] = parent_dt + + if not docs: + docs = get_meta_bundle(doctype) + + influxframework.response["user_settings"] = get_user_settings(parent_dt or doctype) + + if cached_timestamp and docs[0].modified == cached_timestamp: + return "use_cache" + + influxframework.response.docs.extend(docs) + + +def get_meta_bundle(doctype): + bundle = [influxframework.desk.form.meta.get_meta(doctype)] + for df in bundle[0].fields: + if df.fieldtype in influxframework.model.table_fields: + bundle.append(influxframework.desk.form.meta.get_meta(df.options, not influxframework.conf.developer_mode)) + return bundle + + +@influxframework.whitelist() +def get_docinfo(doc=None, doctype=None, name=None): + if not doc: + doc = influxframework.get_doc(doctype, name) + if not doc.has_permission("read"): + raise influxframework.PermissionError + + all_communications = _get_communications(doc.doctype, doc.name) + automated_messages = [ + msg for msg in all_communications if msg["communication_type"] == "Automated Message" + ] + communications_except_auto_messages = [ + msg for msg in all_communications if msg["communication_type"] != "Automated Message" + ] + + docinfo = influxframework._dict(user_info={}) + + add_comments(doc, docinfo) + + docinfo.update( + { + "doctype": doc.doctype, + "name": doc.name, + "attachments": get_attachments(doc.doctype, doc.name), + "communications": communications_except_auto_messages, + "automated_messages": automated_messages, + "total_comments": len(json.loads(doc.get("_comments") or "[]")), + "versions": get_versions(doc), + "assignments": get_assignments(doc.doctype, doc.name), + "permissions": get_doc_permissions(doc), + "shared": influxframework.share.get_users(doc.doctype, doc.name), + "views": get_view_logs(doc.doctype, doc.name), + "energy_point_logs": get_point_logs(doc.doctype, doc.name), + "additional_timeline_content": get_additional_timeline_content(doc.doctype, doc.name), + "milestones": get_milestones(doc.doctype, doc.name), + "is_document_followed": is_document_followed(doc.doctype, doc.name, influxframework.session.user), + "tags": get_tags(doc.doctype, doc.name), + "document_email": get_document_email(doc.doctype, doc.name), + } + ) + + update_user_info(docinfo) + + influxframework.response["docinfo"] = docinfo + + +def add_comments(doc, docinfo): + # divide comments into separate lists + docinfo.comments = [] + docinfo.shared = [] + docinfo.assignment_logs = [] + docinfo.attachment_logs = [] + docinfo.info_logs = [] + docinfo.like_logs = [] + docinfo.workflow_logs = [] + + comments = influxframework.get_all( + "Comment", + fields=["name", "creation", "content", "owner", "comment_type"], + filters={"reference_doctype": doc.doctype, "reference_name": doc.name}, + ) + + for c in comments: + if c.comment_type == "Comment": + c.content = influxframework.utils.markdown(c.content) + docinfo.comments.append(c) + + elif c.comment_type in ("Shared", "Unshared"): + docinfo.shared.append(c) + + elif c.comment_type in ("Assignment Completed", "Assigned"): + docinfo.assignment_logs.append(c) + + elif c.comment_type in ("Attachment", "Attachment Removed"): + docinfo.attachment_logs.append(c) + + elif c.comment_type in ("Info", "Edit", "Label"): + docinfo.info_logs.append(c) + + elif c.comment_type == "Like": + docinfo.like_logs.append(c) + + elif c.comment_type == "Workflow": + docinfo.workflow_logs.append(c) + + influxframework.utils.add_user_info(c.owner, docinfo.user_info) + + return comments + + +def get_milestones(doctype, name): + return influxframework.get_all( + "Milestone", + fields=["creation", "owner", "track_field", "value"], + filters=dict(reference_type=doctype, reference_name=name), + ) + + +def get_attachments(dt, dn): + return influxframework.get_all( + "File", + fields=["name", "file_name", "file_url", "is_private"], + filters={"attached_to_name": dn, "attached_to_doctype": dt}, + ) + + +def get_versions(doc): + return influxframework.get_all( + "Version", + filters=dict(ref_doctype=doc.doctype, docname=doc.name), + fields=["name", "owner", "creation", "data"], + limit=10, + order_by="creation desc", + ) + + +@influxframework.whitelist() +def get_communications(doctype, name, start=0, limit=20): + doc = influxframework.get_doc(doctype, name) + if not doc.has_permission("read"): + raise influxframework.PermissionError + + return _get_communications(doctype, name, start, limit) + + +def get_comments( + doctype: str, name: str, comment_type: str | list[str] = "Comment" +) -> list[influxframework._dict]: + if isinstance(comment_type, list): + comment_types = comment_type + + elif comment_type == "share": + comment_types = ["Shared", "Unshared"] + + elif comment_type == "assignment": + comment_types = ["Assignment Completed", "Assigned"] + + elif comment_type == "attachment": + comment_types = ["Attachment", "Attachment Removed"] + + else: + comment_types = [comment_type] + + comments = influxframework.get_all( + "Comment", + fields=["name", "creation", "content", "owner", "comment_type"], + filters={ + "reference_doctype": doctype, + "reference_name": name, + "comment_type": ["in", comment_types], + }, + ) + + # convert to markdown (legacy ?) + for c in comments: + if c.comment_type == "Comment": + c.content = influxframework.utils.markdown(c.content) + + return comments + + +def get_point_logs(doctype, docname): + return influxframework.get_all( + "Energy Point Log", + filters={"reference_doctype": doctype, "reference_name": docname, "type": ["!=", "Review"]}, + fields=["*"], + ) + + +def _get_communications(doctype, name, start=0, limit=20): + communications = get_communication_data(doctype, name, start, limit) + for c in communications: + if c.communication_type == "Communication": + c.attachments = json.dumps( + influxframework.get_all( + "File", + fields=["file_url", "is_private"], + filters={"attached_to_doctype": "Communication", "attached_to_name": c.name}, + ) + ) + + return communications + + +def get_communication_data( + doctype, name, start=0, limit=20, after=None, fields=None, group_by=None, as_dict=True +): + """Returns list of communications for a given document""" + if not fields: + fields = """ + C.name, C.communication_type, C.communication_medium, + C.comment_type, C.communication_date, C.content, + C.sender, C.sender_full_name, C.cc, C.bcc, + C.creation AS creation, C.subject, C.delivery_status, + C._liked_by, C.reference_doctype, C.reference_name, + C.read_by_recipient, C.rating, C.recipients + """ + + conditions = "" + if after: + # find after a particular date + conditions += """ + AND C.creation > {} + """.format( + after + ) + + if doctype == "User": + conditions += """ + AND NOT (C.reference_doctype='User' AND C.communication_type='Communication') + """ + + # communications linked to reference_doctype + part1 = """ + SELECT {fields} + FROM `tabCommunication` as C + WHERE C.communication_type IN ('Communication', 'Feedback', 'Automated Message') + AND (C.reference_doctype = %(doctype)s AND C.reference_name = %(name)s) + {conditions} + """.format( + fields=fields, conditions=conditions + ) + + # communications linked in Timeline Links + part2 = """ + SELECT {fields} + FROM `tabCommunication` as C + INNER JOIN `tabCommunication Link` ON C.name=`tabCommunication Link`.parent + WHERE C.communication_type IN ('Communication', 'Feedback', 'Automated Message') + AND `tabCommunication Link`.link_doctype = %(doctype)s AND `tabCommunication Link`.link_name = %(name)s + {conditions} + """.format( + fields=fields, conditions=conditions + ) + + communications = influxframework.db.sql( + """ + SELECT * + FROM (({part1}) UNION ({part2})) AS combined + {group_by} + ORDER BY creation DESC + LIMIT %(limit)s + OFFSET %(start)s + """.format( + part1=part1, part2=part2, group_by=(group_by or "") + ), + dict(doctype=doctype, name=name, start=influxframework.utils.cint(start), limit=limit), + as_dict=as_dict, + ) + + return communications + + +def get_assignments(dt, dn): + return influxframework.get_all( + "ToDo", + fields=["name", "allocated_to as owner", "description", "status"], + filters={ + "reference_type": dt, + "reference_name": dn, + "status": ("!=", "Cancelled"), + "allocated_to": ("is", "set"), + }, + ) + + +@influxframework.whitelist() +def get_badge_info(doctypes, filters): + filters = json.loads(filters) + doctypes = json.loads(doctypes) + filters["docstatus"] = ["!=", 2] + out = {} + for doctype in doctypes: + out[doctype] = influxframework.db.get_value(doctype, filters, "count(*)") + + return out + + +def run_onload(doc): + doc.set("__onload", influxframework._dict()) + doc.run_method("onload") + + +def get_view_logs(doctype, docname): + """get and return the latest view logs if available""" + logs = [] + if hasattr(influxframework.get_meta(doctype), "track_views") and influxframework.get_meta(doctype).track_views: + view_logs = influxframework.get_all( + "View Log", + filters={ + "reference_doctype": doctype, + "reference_name": docname, + }, + fields=["name", "creation", "owner"], + order_by="creation desc", + ) + + if view_logs: + logs = view_logs + return logs + + +def get_tags(doctype, name): + tags = [ + tag.tag + for tag in influxframework.get_all( + "Tag Link", filters={"document_type": doctype, "document_name": name}, fields=["tag"] + ) + ] + + return ",".join(tags) + + +def get_document_email(doctype, name): + email = get_automatic_email_link() + if not email: + return None + + email = email.split("@") + return f"{email[0]}+{quote(doctype)}+{quote(cstr(name))}@{email[1]}" + + +def get_automatic_email_link(): + return influxframework.db.get_value( + "Email Account", {"enable_incoming": 1, "enable_automatic_linking": 1}, "email_id" + ) + + +def get_additional_timeline_content(doctype, docname): + contents = [] + hooks = influxframework.get_hooks().get("additional_timeline_content", {}) + methods_for_all_doctype = hooks.get("*", []) + methods_for_current_doctype = hooks.get(doctype, []) + + for method in methods_for_all_doctype + methods_for_current_doctype: + contents.extend(influxframework.get_attr(method)(doctype, docname) or []) + + return contents + + +def set_link_titles(doc): + link_titles = {} + link_titles.update(get_title_values_for_link_and_dynamic_link_fields(doc)) + link_titles.update(get_title_values_for_table_and_multiselect_fields(doc)) + + send_link_titles(link_titles) + + +def get_title_values_for_link_and_dynamic_link_fields(doc, link_fields=None): + link_titles = {} + + if not link_fields: + meta = influxframework.get_meta(doc.doctype) + link_fields = meta.get_link_fields() + meta.get_dynamic_link_fields() + + for field in link_fields: + if not doc.get(field.fieldname): + continue + + doctype = field.options if field.fieldtype == "Link" else doc.get(field.options) + + meta = influxframework.get_meta(doctype) + if not meta or not (meta.title_field and meta.show_title_field_in_link): + continue + + link_title = influxframework.db.get_value( + doctype, doc.get(field.fieldname), meta.title_field, cache=True, order_by=None + ) + link_titles.update({doctype + "::" + doc.get(field.fieldname): link_title}) + + return link_titles + + +def get_title_values_for_table_and_multiselect_fields(doc, table_fields=None): + link_titles = {} + + if not table_fields: + meta = influxframework.get_meta(doc.doctype) + table_fields = meta.get_table_fields() + + for field in table_fields: + if not doc.get(field.fieldname): + continue + + for value in doc.get(field.fieldname): + link_titles.update(get_title_values_for_link_and_dynamic_link_fields(value)) + + return link_titles + + +def send_link_titles(link_titles): + """Append link titles dict in `influxframework.local.response`.""" + if "_link_titles" not in influxframework.local.response: + influxframework.local.response["_link_titles"] = {} + + influxframework.local.response["_link_titles"].update(link_titles) + + +def update_user_info(docinfo): + for d in docinfo.communications: + influxframework.utils.add_user_info(d.sender, docinfo.user_info) + + for d in docinfo.shared: + influxframework.utils.add_user_info(d.user, docinfo.user_info) + + for d in docinfo.assignments: + influxframework.utils.add_user_info(d.owner, docinfo.user_info) + + for d in docinfo.views: + influxframework.utils.add_user_info(d.owner, docinfo.user_info) + + +@influxframework.whitelist() +def get_user_info_for_viewers(users): + user_info = {} + for user in json.loads(users): + influxframework.utils.add_user_info(user, user_info) + + return user_info diff --git a/influxframework/desk/form/meta.py b/influxframework/desk/form/meta.py new file mode 100644 index 0000000..0394293 --- /dev/null +++ b/influxframework/desk/form/meta.py @@ -0,0 +1,285 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import io +import os + +import influxframework +from influxframework.build import scrub_html_template +from influxframework.model.meta import Meta +from influxframework.model.utils import render_include +from influxframework.modules import get_module_path, load_doctype_module, scrub +from influxframework.translate import extract_messages_from_code, make_dict_from_messages +from influxframework.utils import get_html_format + +ASSET_KEYS = ( + "__js", + "__css", + "__list_js", + "__calendar_js", + "__map_js", + "__linked_with", + "__messages", + "__print_formats", + "__workflow_docs", + "__form_grid_templates", + "__listview_template", + "__tree_js", + "__dashboard", + "__kanban_column_fields", + "__templates", + "__custom_js", + "__custom_list_js", +) + + +def get_meta(doctype, cached=True): + # don't cache for developer mode as js files, templates may be edited + if cached and not influxframework.conf.developer_mode: + meta = influxframework.cache().hget("doctype_form_meta", doctype) + if not meta: + meta = FormMeta(doctype) + influxframework.cache().hset("doctype_form_meta", doctype, meta) + else: + meta = FormMeta(doctype) + + if influxframework.local.lang != "en": + meta.set_translations(influxframework.local.lang) + + return meta + + +class FormMeta(Meta): + def __init__(self, doctype): + super().__init__(doctype) + self.load_assets() + + def load_assets(self): + if self.get("__assets_loaded", False): + return + + self.add_search_fields() + self.add_linked_document_type() + + if not self.istable: + self.add_code() + self.add_custom_script() + self.load_print_formats() + self.load_workflows() + self.load_templates() + self.load_dashboard() + self.load_kanban_meta() + + self.set("__assets_loaded", True) + + def as_dict(self, no_nulls=False): + d = super().as_dict(no_nulls=no_nulls) + + for k in ASSET_KEYS: + d[k] = self.get(k) + + # d['fields'] = d.get('fields', []) + + for i, df in enumerate(d.get("fields") or []): + for k in ("search_fields", "is_custom_field", "linked_document_type"): + df[k] = self.get("fields")[i].get(k) + + return d + + def add_code(self): + if self.custom: + return + + path = os.path.join(get_module_path(self.module), "doctype", scrub(self.name)) + + def _get_path(fname): + return os.path.join(path, scrub(fname)) + + system_country = influxframework.get_system_settings("country") + + self._add_code(_get_path(self.name + ".js"), "__js") + if system_country: + self._add_code(_get_path(os.path.join("regional", system_country + ".js")), "__js") + + self._add_code(_get_path(self.name + ".css"), "__css") + self._add_code(_get_path(self.name + "_list.js"), "__list_js") + if system_country: + self._add_code(_get_path(os.path.join("regional", system_country + "_list.js")), "__list_js") + + self._add_code(_get_path(self.name + "_calendar.js"), "__calendar_js") + self._add_code(_get_path(self.name + "_tree.js"), "__tree_js") + + listview_template = _get_path(self.name + "_list.html") + if os.path.exists(listview_template): + self.set("__listview_template", get_html_format(listview_template)) + + self.add_code_via_hook("doctype_js", "__js") + self.add_code_via_hook("doctype_list_js", "__list_js") + self.add_code_via_hook("doctype_tree_js", "__tree_js") + self.add_code_via_hook("doctype_calendar_js", "__calendar_js") + self.add_html_templates(path) + + def _add_code(self, path, fieldname): + js = get_js(path) + if js: + comment = f"\n\n/* Adding {path} */\n\n" + sourceURL = f"\n\n//# sourceURL={scrub(self.name) + fieldname}" + self.set(fieldname, (self.get(fieldname) or "") + comment + js + sourceURL) + + def add_html_templates(self, path): + if self.custom: + return + templates = dict() + for fname in os.listdir(path): + if fname.endswith(".html"): + with open(os.path.join(path, fname), encoding="utf-8") as f: + templates[fname.split(".")[0]] = scrub_html_template(f.read()) + + self.set("__templates", templates or None) + + def add_code_via_hook(self, hook, fieldname): + for path in get_code_files_via_hooks(hook, self.name): + self._add_code(path, fieldname) + + def add_custom_script(self): + """embed all require files""" + # custom script + client_scripts = ( + influxframework.get_all( + "Client Script", + filters={"dt": self.name, "enabled": 1}, + fields=["name", "script", "view"], + order_by="creation asc", + ) + or "" + ) + + list_script = "" + form_script = "" + for script in client_scripts: + if script.view == "List": + list_script += f""" +// {script.name} +{script.script} + +""" + + if script.view == "Form": + form_script += f""" +// {script.name} +{script.script} + +""" + + file = scrub(self.name) + form_script += f"\n\n//# sourceURL={file}__custom_js" + list_script += f"\n\n//# sourceURL={file}__custom_list_js" + + self.set("__custom_js", form_script) + self.set("__custom_list_js", list_script) + + def add_search_fields(self): + """add search fields found in the doctypes indicated by link fields' options""" + for df in self.get("fields", {"fieldtype": "Link", "options": ["!=", "[Select]"]}): + if df.options: + search_fields = influxframework.get_meta(df.options).search_fields + if search_fields: + search_fields = search_fields.split(",") + df.search_fields = [sf.strip() for sf in search_fields] + + def add_linked_document_type(self): + for df in self.get("fields", {"fieldtype": "Link"}): + if df.options: + try: + df.linked_document_type = influxframework.get_meta(df.options).document_type + except influxframework.DoesNotExistError: + # edge case where options="[Select]" + pass + + def load_print_formats(self): + print_formats = influxframework.db.sql( + """select * FROM `tabPrint Format` + WHERE doc_type=%s AND docstatus<2 and disabled=0""", + (self.name,), + as_dict=1, + update={"doctype": "Print Format"}, + ) + + self.set("__print_formats", print_formats) + + def load_workflows(self): + # get active workflow + workflow_name = self.get_workflow() + workflow_docs = [] + + if workflow_name and influxframework.db.exists("Workflow", workflow_name): + workflow = influxframework.get_doc("Workflow", workflow_name) + workflow_docs.append(workflow) + + for d in workflow.get("states"): + workflow_docs.append(influxframework.get_doc("Workflow State", d.state)) + + self.set("__workflow_docs", workflow_docs) + + def load_templates(self): + if not self.custom: + module = load_doctype_module(self.name) + app = module.__name__.split(".")[0] + templates = {} + if hasattr(module, "form_grid_templates"): + for key, path in module.form_grid_templates.items(): + templates[key] = get_html_format(influxframework.get_app_path(app, path)) + + self.set("__form_grid_templates", templates) + + def set_translations(self, lang): + self.set("__messages", influxframework.get_lang_dict("doctype", self.name)) + + # set translations for grid templates + if self.get("__form_grid_templates"): + for content in self.get("__form_grid_templates").values(): + messages = extract_messages_from_code(content) + messages = make_dict_from_messages(messages) + self.get("__messages").update(messages) + + def load_dashboard(self): + self.set("__dashboard", self.get_dashboard_data()) + + def load_kanban_meta(self): + self.load_kanban_column_fields() + + def load_kanban_column_fields(self): + try: + values = influxframework.get_list( + "Kanban Board", fields=["field_name"], filters={"reference_doctype": self.name} + ) + + fields = [x["field_name"] for x in values] + fields = list(set(fields)) + self.set("__kanban_column_fields", fields) + except influxframework.PermissionError: + # no access to kanban board + pass + + +def get_code_files_via_hooks(hook, name): + code_files = [] + for app_name in influxframework.get_installed_apps(): + code_hook = influxframework.get_hooks(hook, default={}, app_name=app_name) + if not code_hook: + continue + + files = code_hook.get(name, []) + if not isinstance(files, list): + files = [files] + + for file in files: + path = influxframework.get_app_path(app_name, *file.strip("/").split("/")) + code_files.append(path) + + return code_files + + +def get_js(path): + js = influxframework.read_file(path) + if js: + return render_include(js) diff --git a/influxframework/desk/form/save.py b/influxframework/desk/form/save.py new file mode 100644 index 0000000..8671a58 --- /dev/null +++ b/influxframework/desk/form/save.py @@ -0,0 +1,68 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +import influxframework +from influxframework.desk.form.load import run_onload +from influxframework.monitor import add_data_to_monitor + + +@influxframework.whitelist() +def savedocs(doc, action): + """save / submit / update doclist""" + doc = influxframework.get_doc(json.loads(doc)) + set_local_name(doc) + + # action + doc.docstatus = {"Save": 0, "Submit": 1, "Update": 1, "Cancel": 2}[action] + + if doc.docstatus == 1: + doc.submit() + else: + doc.save() + + # update recent documents + run_onload(doc) + send_updated_docs(doc) + + add_data_to_monitor(doctype=doc.doctype, action=action) + + influxframework.msgprint(influxframework._("Saved"), indicator="green", alert=True) + + +@influxframework.whitelist() +def cancel(doctype=None, name=None, workflow_state_fieldname=None, workflow_state=None): + """cancel a doclist""" + doc = influxframework.get_doc(doctype, name) + if workflow_state_fieldname and workflow_state: + doc.set(workflow_state_fieldname, workflow_state) + doc.cancel() + send_updated_docs(doc) + influxframework.msgprint(influxframework._("Cancelled"), indicator="red", alert=True) + + +def send_updated_docs(doc): + from .load import get_docinfo + + get_docinfo(doc) + + d = doc.as_dict() + if hasattr(doc, "localname"): + d["localname"] = doc.localname + + influxframework.response.docs.append(d) + + +def set_local_name(doc): + def _set_local_name(d): + if doc.get("__islocal") or d.get("__islocal"): + d.localname = d.name + d.name = None + + _set_local_name(doc) + for child in doc.get_all_children(): + _set_local_name(child) + + if doc.get("__newname"): + doc.name = doc.get("__newname") diff --git a/influxframework/desk/form/test_form.py b/influxframework/desk/form/test_form.py new file mode 100644 index 0000000..bff9ad1 --- /dev/null +++ b/influxframework/desk/form/test_form.py @@ -0,0 +1,20 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.desk.form.linked_with import get_linked_docs, get_linked_doctypes +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestForm(InfluxFrameworkTestCase): + def test_linked_with(self): + results = get_linked_docs("Role", "System Manager", linkinfo=get_linked_doctypes("Role")) + self.assertTrue("User" in results) + self.assertTrue("DocType" in results) + + +if __name__ == "__main__": + import unittest + + influxframework.connect() + unittest.main() diff --git a/influxframework/desk/form/utils.py b/influxframework/desk/form/utils.py new file mode 100644 index 0000000..7fa02ae --- /dev/null +++ b/influxframework/desk/form/utils.py @@ -0,0 +1,104 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import json +from typing import TYPE_CHECKING + +import influxframework +import influxframework.desk.form.load +import influxframework.desk.form.meta +from influxframework import _ +from influxframework.core.doctype.file.utils import extract_images_from_html +from influxframework.desk.form.document_follow import follow_document + +if TYPE_CHECKING: + from influxframework.core.doctype.comment.comment import Comment + + +@influxframework.whitelist(methods=["DELETE", "POST"]) +def remove_attach(): + """remove attachment""" + fid = influxframework.form_dict.get("fid") + influxframework.delete_doc("File", fid) + + +@influxframework.whitelist(methods=["POST", "PUT"]) +def add_comment( + reference_doctype: str, reference_name: str, content: str, comment_email: str, comment_by: str +) -> "Comment": + """Allow logged user with permission to read document to add a comment""" + reference_doc = influxframework.get_doc(reference_doctype, reference_name) + reference_doc.check_permission() + + comment = influxframework.new_doc("Comment") + comment.update( + { + "comment_type": "Comment", + "reference_doctype": reference_doctype, + "reference_name": reference_name, + "comment_email": comment_email, + "comment_by": comment_by, + "content": extract_images_from_html(reference_doc, content, is_private=True), + } + ) + comment.insert(ignore_permissions=True) + + if influxframework.get_cached_value("User", influxframework.session.user, "follow_commented_documents"): + follow_document(comment.reference_doctype, comment.reference_name, influxframework.session.user) + + return comment + + +@influxframework.whitelist() +def update_comment(name, content): + """allow only owner to update comment""" + doc = influxframework.get_doc("Comment", name) + + if influxframework.session.user not in ["Administrator", doc.owner]: + influxframework.throw(_("Comment can only be edited by the owner"), influxframework.PermissionError) + + doc.content = content + doc.save(ignore_permissions=True) + + +@influxframework.whitelist() +def get_next(doctype, value, prev, filters=None, sort_order="desc", sort_field="modified"): + + prev = int(prev) + if not filters: + filters = [] + if isinstance(filters, str): + filters = json.loads(filters) + + # # condition based on sort order + condition = ">" if sort_order.lower() == "asc" else "<" + + # switch the condition + if prev: + sort_order = "asc" if sort_order.lower() == "desc" else "desc" + condition = "<" if condition == ">" else ">" + + # # add condition for next or prev item + filters.append([doctype, sort_field, condition, influxframework.get_value(doctype, value, sort_field)]) + + res = influxframework.get_list( + doctype, + fields=["name"], + filters=filters, + order_by=f"`tab{doctype}`.{sort_field}" + " " + sort_order, + limit_start=0, + limit_page_length=1, + as_list=True, + ) + + if not res: + influxframework.msgprint(_("No further records")) + return None + else: + return res[0][0] + + +def get_pdf_link(doctype, docname, print_format="Standard", no_letterhead=0): + return "/api/method/influxframework.utils.print_format.download_pdf?doctype={doctype}&name={docname}&format={print_format}&no_letterhead={no_letterhead}".format( + doctype=doctype, docname=docname, print_format=print_format, no_letterhead=no_letterhead + ) diff --git a/influxframework/desk/gantt.py b/influxframework/desk/gantt.py new file mode 100644 index 0000000..d917a8a --- /dev/null +++ b/influxframework/desk/gantt.py @@ -0,0 +1,17 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +import influxframework + + +@influxframework.whitelist() +def update_task(args, field_map): + """Updates Doc (called via gantt) based on passed `field_map`""" + args = influxframework._dict(json.loads(args)) + field_map = influxframework._dict(json.loads(field_map)) + d = influxframework.get_doc(args.doctype, args.name) + d.set(field_map.start, args.start) + d.set(field_map.end, args.end) + d.save() diff --git a/influxframework/desk/leaderboard.py b/influxframework/desk/leaderboard.py new file mode 100644 index 0000000..c3e2c1b --- /dev/null +++ b/influxframework/desk/leaderboard.py @@ -0,0 +1,54 @@ +import influxframework +from influxframework.utils import get_fullname + + +def get_leaderboards(): + leaderboards = { + "User": { + "fields": ["points"], + "method": "influxframework.desk.leaderboard.get_energy_point_leaderboard", + "company_disabled": 1, + "icon": "users", + } + } + return leaderboards + + +@influxframework.whitelist() +def get_energy_point_leaderboard(date_range, company=None, field=None, limit=None): + all_users = influxframework.get_all( + "User", + filters={ + "name": ["not in", ["Administrator", "Guest"]], + "enabled": 1, + "user_type": ["!=", "Website User"], + }, + order_by="name ASC", + ) + all_users_list = list(map(lambda x: x["name"], all_users)) + + filters = [["type", "!=", "Review"], ["user", "in", all_users_list]] + if date_range: + date_range = influxframework.parse_json(date_range) + filters.append(["creation", "between", [date_range[0], date_range[1]]]) + energy_point_users = influxframework.get_all( + "Energy Point Log", + fields=["user as name", "sum(points) as value"], + filters=filters, + group_by="user", + order_by="value desc", + ) + + energy_point_users_list = list(map(lambda x: x["name"], energy_point_users)) + for user in all_users_list: + if user not in energy_point_users_list: + energy_point_users.append({"name": user, "value": 0}) + + for user in energy_point_users: + user_id = user["name"] + user["name"] = get_fullname(user["name"]) + user["formatted_name"] = '{}'.format( + user_id, get_fullname(user_id) + ) + + return energy_point_users diff --git a/influxframework/desk/like.py b/influxframework/desk/like.py new file mode 100644 index 0000000..4dd9682 --- /dev/null +++ b/influxframework/desk/like.py @@ -0,0 +1,104 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +"""Allow adding of likes to documents""" + +import json + +import influxframework +from influxframework import _ +from influxframework.database.schema import add_column +from influxframework.desk.form.document_follow import follow_document +from influxframework.utils import get_link_to_form + + +@influxframework.whitelist() +def toggle_like(doctype, name, add=False): + """Adds / removes the current user in the `__liked_by` property of the given document. + If column does not exist, will add it in the database. + + The `_liked_by` property is always set from this function and is ignored if set via + Document API + + :param doctype: DocType of the document to like + :param name: Name of the document to like + :param add: `Yes` if like is to be added. If not `Yes` the like will be removed.""" + + _toggle_like(doctype, name, add) + + +def _toggle_like(doctype, name, add, user=None): + """Same as toggle_like but hides param `user` from API""" + + if not user: + user = influxframework.session.user + + try: + liked_by = influxframework.db.get_value(doctype, name, "_liked_by") + + if liked_by: + liked_by = json.loads(liked_by) + else: + liked_by = [] + + if add == "Yes": + if user not in liked_by: + liked_by.append(user) + add_comment(doctype, name) + if influxframework.get_cached_value("User", user, "follow_liked_documents"): + follow_document(doctype, name, user) + else: + if user in liked_by: + liked_by.remove(user) + remove_like(doctype, name) + + influxframework.db.set_value(doctype, name, "_liked_by", json.dumps(liked_by), update_modified=False) + + except influxframework.db.ProgrammingError as e: + if influxframework.db.is_column_missing(e): + add_column(doctype, "_liked_by", "Text") + _toggle_like(doctype, name, add, user) + else: + raise + + +def remove_like(doctype, name): + """Remove previous Like""" + # remove Comment + influxframework.delete_doc( + "Comment", + [ + c.name + for c in influxframework.get_all( + "Comment", + filters={ + "comment_type": "Like", + "reference_doctype": doctype, + "reference_name": name, + "owner": influxframework.session.user, + }, + ) + ], + ignore_permissions=True, + ) + + +def add_comment(doctype, name): + doc = influxframework.get_doc(doctype, name) + + if doctype == "Communication" and doc.reference_doctype and doc.reference_name: + link = get_link_to_form( + doc.reference_doctype, + doc.reference_name, + f"{_(doc.reference_doctype)} {doc.reference_name}", + ) + + doc.add_comment( + "Like", + _("{0}: {1} in {2}").format(_(doc.communication_type), "" + doc.subject + "", link), + link_doctype=doc.reference_doctype, + link_name=doc.reference_name, + ) + + else: + doc.add_comment("Like", _("Liked")) diff --git a/influxframework/desk/link_preview.py b/influxframework/desk/link_preview.py new file mode 100644 index 0000000..fa13a55 --- /dev/null +++ b/influxframework/desk/link_preview.py @@ -0,0 +1,54 @@ +import influxframework +from influxframework.model import no_value_fields, table_fields + + +@influxframework.whitelist() +def get_preview_data(doctype, docname): + preview_fields = [] + meta = influxframework.get_meta(doctype) + if not meta.show_preview_popup: + return + + preview_fields = [ + field.fieldname + for field in meta.fields + if field.in_preview + and field.fieldtype not in no_value_fields + and field.fieldtype not in table_fields + ] + + # no preview fields defined, build list from mandatory fields + if not preview_fields: + preview_fields = [ + field.fieldname for field in meta.fields if field.reqd and field.fieldtype not in table_fields + ] + + title_field = meta.get_title_field() + image_field = meta.image_field + + preview_fields.append(title_field) + preview_fields.append(image_field) + preview_fields.append("name") + + preview_data = influxframework.get_list(doctype, filters={"name": docname}, fields=preview_fields, limit=1) + + if not preview_data: + return + + preview_data = preview_data[0] + + formatted_preview_data = { + "preview_image": preview_data.get(image_field), + "preview_title": preview_data.get(title_field), + "name": preview_data.get("name"), + } + + for key, val in preview_data.items(): + if val and meta.has_field(key) and key not in [image_field, title_field, "name"]: + formatted_preview_data[meta.get_field(key).label] = influxframework.format( + val, + meta.get_field(key).fieldtype, + translated=True, + ) + + return formatted_preview_data diff --git a/influxframework/desk/listview.py b/influxframework/desk/listview.py new file mode 100644 index 0000000..0206f9f --- /dev/null +++ b/influxframework/desk/listview.py @@ -0,0 +1,64 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.query_builder import Order +from influxframework.query_builder.functions import Count +from influxframework.query_builder.terms import SubQuery +from influxframework.query_builder.utils import DocType + + +@influxframework.whitelist() +def get_list_settings(doctype): + try: + return influxframework.get_cached_doc("List View Settings", doctype) + except influxframework.DoesNotExistError: + influxframework.clear_messages() + + +@influxframework.whitelist() +def set_list_settings(doctype, values): + try: + doc = influxframework.get_doc("List View Settings", doctype) + except influxframework.DoesNotExistError: + doc = influxframework.new_doc("List View Settings") + doc.name = doctype + influxframework.clear_messages() + doc.update(influxframework.parse_json(values)) + doc.save() + + +@influxframework.whitelist() +def get_group_by_count(doctype: str, current_filters: str, field: str) -> list[dict]: + current_filters = influxframework.parse_json(current_filters) + + if field == "assigned_to": + ToDo = DocType("ToDo") + User = DocType("User") + count = Count("*").as_("count") + filtered_records = influxframework.qb.engine.build_conditions(doctype, current_filters).select("name") + + return ( + influxframework.qb.from_(ToDo) + .from_(User) + .select(ToDo.allocated_to.as_("name"), count) + .where( + (ToDo.status != "Cancelled") + & (ToDo.allocated_to == User.name) + & (User.user_type == "System User") + & (ToDo.reference_name.isin(SubQuery(filtered_records))) + ) + .groupby(ToDo.allocated_to) + .orderby(count, order=Order.desc) + .limit(50) + .run(as_dict=True) + ) + + return influxframework.get_list( + doctype, + filters=current_filters, + group_by=f"`tab{doctype}`.{field}", + fields=["count(*) as count", f"`{field}` as name"], + order_by="count desc", + limit=50, + ) diff --git a/influxframework/desk/moduleview.py b/influxframework/desk/moduleview.py new file mode 100644 index 0000000..09970c0 --- /dev/null +++ b/influxframework/desk/moduleview.py @@ -0,0 +1,615 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +import influxframework +from influxframework import _ +from influxframework.boot import get_allowed_pages, get_allowed_reports +from influxframework.cache_manager import ( + build_domain_restriced_doctype_cache, + build_domain_restriced_page_cache, + build_table_count_cache, +) +from influxframework.desk.doctype.desktop_icon.desktop_icon import clear_desktop_icons_cache, set_hidden + + +@influxframework.whitelist() +def get(module): + """Returns data (sections, list of reports, counts) to render module view in desk: + `/desk/#Module/[name]`.""" + data = get_data(module) + + out = {"data": data} + + return out + + +@influxframework.whitelist() +def hide_module(module): + set_hidden(module, influxframework.session.user, 1) + clear_desktop_icons_cache() + + +def get_table_with_counts(): + counts = influxframework.cache().get_value("information_schema:counts") + if counts: + return counts + else: + return build_table_count_cache() + + +def get_data(module, build=True): + """Get module data for the module view `desk/#Module/[name]`""" + doctype_info = get_doctype_info(module) + data = build_config_from_file(module) + + if not data: + data = build_standard_config(module, doctype_info) + else: + add_custom_doctypes(data, doctype_info) + + add_section(data, _("Custom Reports"), "fa fa-list-alt", get_report_list(module)) + + data = combine_common_sections(data) + data = apply_permissions(data) + + # set_last_modified(data) + + if build: + exists_cache = get_table_with_counts() + + def doctype_contains_a_record(name): + exists = exists_cache.get(name) + if not exists: + if not influxframework.db.get_value("DocType", name, "issingle"): + exists = influxframework.db.count(name) + else: + exists = True + exists_cache[name] = exists + return exists + + for section in data: + for item in section["items"]: + # Onboarding + + # First disable based on exists of depends_on list + doctype = item.get("doctype") + dependencies = item.get("dependencies") or None + if not dependencies and doctype: + item["dependencies"] = [doctype] + + dependencies = item.get("dependencies") + if dependencies: + incomplete_dependencies = [d for d in dependencies if not doctype_contains_a_record(d)] + if len(incomplete_dependencies): + item["incomplete_dependencies"] = incomplete_dependencies + + if item.get("onboard"): + # Mark Spotlights for initial + if item.get("type") == "doctype": + name = item.get("name") + count = doctype_contains_a_record(name) + + item["count"] = count + + return data + + +def build_config_from_file(module): + """Build module info from `app/config/desktop.py` files.""" + data = [] + module = influxframework.scrub(module) + + for app in influxframework.get_installed_apps(): + try: + data += get_config(app, module) + except ImportError: + pass + + return filter_by_restrict_to_domain(data) + + +def filter_by_restrict_to_domain(data): + """filter Pages and DocType depending on the Active Module(s)""" + doctypes = ( + influxframework.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache() + ) + pages = influxframework.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache() + + for d in data: + _items = [] + for item in d.get("items", []): + + item_type = item.get("type") + item_name = item.get("name") + + if (item_name in pages) or (item_name in doctypes) or item_type == "report": + _items.append(item) + + d.update({"items": _items}) + + return data + + +def build_standard_config(module, doctype_info): + """Build standard module data from DocTypes.""" + if not influxframework.db.get_value("Module Def", module): + influxframework.throw(_("Module Not Found")) + + data = [] + + add_section( + data, + _("Documents"), + "fa fa-star", + [d for d in doctype_info if d.document_type in ("Document", "Transaction")], + ) + + add_section( + data, + _("Setup"), + "fa fa-cog", + [d for d in doctype_info if d.document_type in ("Master", "Setup", "")], + ) + + add_section(data, _("Standard Reports"), "fa fa-list", get_report_list(module, is_standard="Yes")) + + return data + + +def add_section(data, label, icon, items): + """Adds a section to the module data.""" + if not items: + return + data.append({"label": label, "icon": icon, "items": items}) + + +def add_custom_doctypes(data, doctype_info): + """Adds Custom DocTypes to modules setup via `config/desktop.py`.""" + add_section( + data, + _("Documents"), + "fa fa-star", + [d for d in doctype_info if (d.custom and d.document_type in ("Document", "Transaction"))], + ) + + add_section( + data, + _("Setup"), + "fa fa-cog", + [d for d in doctype_info if (d.custom and d.document_type in ("Setup", "Master", ""))], + ) + + +def get_doctype_info(module): + """Returns list of non child DocTypes for given module.""" + active_domains = influxframework.get_active_domains() + + doctype_info = influxframework.get_all( + "DocType", + filters={"module": module, "istable": 0}, + or_filters={"ifnull(restrict_to_domain, '')": "", "restrict_to_domain": ("in", active_domains)}, + fields=["'doctype' as type", "name", "description", "document_type", "custom", "issingle"], + order_by="custom asc, document_type desc, name asc", + ) + + for d in doctype_info: + d.document_type = d.document_type or "" + d.description = _(d.description or "") + + return doctype_info + + +def combine_common_sections(data): + """Combine sections declared in separate apps.""" + sections = [] + sections_dict = {} + for each in data: + if each["label"] not in sections_dict: + sections_dict[each["label"]] = each + sections.append(each) + else: + sections_dict[each["label"]]["items"] += each["items"] + + return sections + + +def apply_permissions(data): + default_country = influxframework.db.get_default("country") + + user = influxframework.get_user() + user.build_permissions() + + allowed_pages = get_allowed_pages() + allowed_reports = get_allowed_reports() + + new_data = [] + for section in data: + new_items = [] + + for item in section.get("items") or []: + item = influxframework._dict(item) + + if item.country and item.country != default_country: + continue + + if ( + (item.type == "doctype" and item.name in user.can_read) + or (item.type == "page" and item.name in allowed_pages) + or (item.type == "report" and item.name in allowed_reports) + or item.type == "help" + ): + + new_items.append(item) + + if new_items: + new_section = section.copy() + new_section["items"] = new_items + new_data.append(new_section) + + return new_data + + +def get_disabled_reports(): + if not hasattr(influxframework.local, "disabled_reports"): + influxframework.local.disabled_reports = {r.name for r in influxframework.get_all("Report", {"disabled": 1})} + return influxframework.local.disabled_reports + + +def get_config(app, module): + """Load module info from `[app].config.[module]`.""" + config = influxframework.get_module(f"{app}.config.{module}") + config = config.get_data() + + sections = [s for s in config if s.get("condition", True)] + + disabled_reports = get_disabled_reports() + for section in sections: + items = [] + for item in section["items"]: + if item["type"] == "report" and item["name"] in disabled_reports: + continue + # some module links might not have name + if not item.get("name"): + item["name"] = item.get("label") + if not item.get("label"): + item["label"] = _(item.get("name")) + items.append(item) + section["items"] = items + + return sections + + +def config_exists(app, module): + try: + influxframework.get_module(f"{app}.config.{module}") + return True + except ImportError: + return False + + +def add_setup_section(config, app, module, label, icon): + """Add common sections to `/desk#Module/Setup`""" + try: + setup_section = get_setup_section(app, module, label, icon) + if setup_section: + config.append(setup_section) + except ImportError: + pass + + +def get_setup_section(app, module, label, icon): + """Get the setup section from each module (for global Setup page).""" + config = get_config(app, module) + for section in config: + if section.get("label") == _("Setup"): + return {"label": label, "icon": icon, "items": section["items"]} + + +def get_onboard_items(app, module): + try: + sections = get_config(app, module) + except ImportError: + return [] + + onboard_items = [] + fallback_items = [] + + if not sections: + doctype_info = get_doctype_info(module) + sections = build_standard_config(module, doctype_info) + + for section in sections: + for item in section["items"]: + if item.get("onboard", 0) == 1: + onboard_items.append(item) + + # in case onboard is not set + fallback_items.append(item) + + if len(onboard_items) > 5: + return onboard_items + + return onboard_items or fallback_items + + +@influxframework.whitelist() +def get_links_for_module(app, module): + return [{"value": l.get("name"), "label": l.get("label")} for l in get_links(app, module)] + + +def get_links(app, module): + try: + sections = get_config(app, influxframework.scrub(module)) + except ImportError: + return [] + + links = [] + for section in sections: + for item in section["items"]: + links.append(item) + return links + + +@influxframework.whitelist() +def get_desktop_settings(): + from influxframework.config import get_modules_from_all_apps_for_user + + all_modules = get_modules_from_all_apps_for_user() + home_settings = get_home_settings() + + modules_by_name = {} + for m in all_modules: + modules_by_name[m["module_name"]] = m + + module_categories = ["Modules", "Domains", "Places", "Administration"] + user_modules_by_category = {} + + user_saved_modules_by_category = home_settings.modules_by_category or {} + user_saved_links_by_module = home_settings.links_by_module or {} + + def apply_user_saved_links(module): + module = influxframework._dict(module) + all_links = get_links(module.app, module.module_name) + module_links_by_name = {} + for link in all_links: + module_links_by_name[link["name"]] = link + + if module.module_name in user_saved_links_by_module: + user_links = influxframework.parse_json(user_saved_links_by_module[module.module_name]) + module.links = [module_links_by_name[l] for l in user_links if l in module_links_by_name] + + return module + + for category in module_categories: + if category in user_saved_modules_by_category: + user_modules = user_saved_modules_by_category[category] + user_modules_by_category[category] = [ + apply_user_saved_links(modules_by_name[m]) for m in user_modules if modules_by_name.get(m) + ] + else: + user_modules_by_category[category] = [ + apply_user_saved_links(m) for m in all_modules if m.get("category") == category + ] + + # filter out hidden modules + if home_settings.hidden_modules: + for category in user_modules_by_category: + hidden_modules = home_settings.hidden_modules or [] + modules = user_modules_by_category[category] + user_modules_by_category[category] = [ + module for module in modules if module.module_name not in hidden_modules + ] + + return user_modules_by_category + + +@influxframework.whitelist() +def update_hidden_modules(category_map): + category_map = influxframework.parse_json(category_map) + home_settings = get_home_settings() + + saved_hidden_modules = home_settings.hidden_modules or [] + + for category in category_map: + config = influxframework._dict(category_map[category]) + saved_hidden_modules += config.removed or [] + saved_hidden_modules = [d for d in saved_hidden_modules if d not in (config.added or [])] + + if home_settings.get("modules_by_category") and home_settings.modules_by_category.get(category): + module_placement = [ + d for d in (config.added or []) if d not in home_settings.modules_by_category[category] + ] + home_settings.modules_by_category[category] += module_placement + + home_settings.hidden_modules = saved_hidden_modules + set_home_settings(home_settings) + + return get_desktop_settings() + + +@influxframework.whitelist() +def update_global_hidden_modules(modules): + modules = influxframework.parse_json(modules) + influxframework.only_for("System Manager") + + doc = influxframework.get_doc("User", "Administrator") + doc.set("block_modules", []) + for module in modules: + doc.append("block_modules", {"module": module}) + + doc.save(ignore_permissions=True) + + return get_desktop_settings() + + +@influxframework.whitelist() +def update_modules_order(module_category, modules): + modules = influxframework.parse_json(modules) + home_settings = get_home_settings() + + home_settings.modules_by_category = home_settings.modules_by_category or {} + home_settings.modules_by_category[module_category] = modules + + set_home_settings(home_settings) + + +@influxframework.whitelist() +def update_links_for_module(module_name, links): + links = influxframework.parse_json(links) + home_settings = get_home_settings() + + home_settings.setdefault("links_by_module", {}) + home_settings["links_by_module"].setdefault(module_name, None) + home_settings["links_by_module"][module_name] = links + + set_home_settings(home_settings) + + return get_desktop_settings() + + +@influxframework.whitelist() +def get_options_for_show_hide_cards(): + global_options = [] + + if "System Manager" in influxframework.get_roles(): + global_options = get_options_for_global_modules() + + return {"user_options": get_options_for_user_blocked_modules(), "global_options": global_options} + + +@influxframework.whitelist() +def get_options_for_global_modules(): + from influxframework.config import get_modules_from_all_apps + + all_modules = get_modules_from_all_apps() + + blocked_modules = influxframework.get_doc("User", "Administrator").get_blocked_modules() + + options = [] + for module in all_modules: + module = influxframework._dict(module) + options.append( + { + "category": module.category, + "label": module.label, + "value": module.module_name, + "checked": module.module_name not in blocked_modules, + } + ) + + return options + + +@influxframework.whitelist() +def get_options_for_user_blocked_modules(): + from influxframework.config import get_modules_from_all_apps_for_user + + all_modules = get_modules_from_all_apps_for_user() + home_settings = get_home_settings() + + hidden_modules = home_settings.hidden_modules or [] + + options = [] + for module in all_modules: + module = influxframework._dict(module) + options.append( + { + "category": module.category, + "label": module.label, + "value": module.module_name, + "checked": module.module_name not in hidden_modules, + } + ) + + return options + + +def set_home_settings(home_settings): + influxframework.cache().hset("home_settings", influxframework.session.user, home_settings) + influxframework.db.set_value("User", influxframework.session.user, "home_settings", json.dumps(home_settings)) + + +@influxframework.whitelist() +def get_home_settings(): + def get_from_db(): + settings = influxframework.db.get_value("User", influxframework.session.user, "home_settings") + return influxframework.parse_json(settings or "{}") + + home_settings = influxframework.cache().hget("home_settings", influxframework.session.user, get_from_db) + return home_settings + + +def get_module_link_items_from_list(app, module, list_of_link_names): + try: + sections = get_config(app, influxframework.scrub(module)) + except ImportError: + return [] + + links = [] + for section in sections: + for item in section["items"]: + if item.get("label", "") in list_of_link_names: + links.append(item) + + return links + + +def set_last_modified(data): + for section in data: + for item in section["items"]: + if item["type"] == "doctype": + item["last_modified"] = get_last_modified(item["name"]) + + +def get_last_modified(doctype): + def _get(): + try: + last_modified = influxframework.get_all( + doctype, fields=["max(modified)"], as_list=True, limit_page_length=1 + )[0][0] + except Exception as e: + if influxframework.db.is_table_missing(e): + last_modified = None + else: + raise + + # hack: save as -1 so that it is cached + if last_modified is None: + last_modified = -1 + + return last_modified + + last_modified = influxframework.cache().hget("last_modified", doctype, _get) + + if last_modified == -1: + last_modified = None + + return last_modified + + +def get_report_list(module, is_standard="No"): + """Returns list on new style reports for modules.""" + reports = influxframework.get_list( + "Report", + fields=["name", "ref_doctype", "report_type"], + filters={"is_standard": is_standard, "disabled": 0, "module": module}, + order_by="name", + ) + + out = [] + for r in reports: + out.append( + { + "type": "report", + "doctype": r.ref_doctype, + "is_query_report": 1 + if r.report_type in ("Query Report", "Script Report", "Custom Report") + else 0, + "label": _(r.name), + "name": r.name, + } + ) + + return out diff --git a/influxframework/desk/notifications.py b/influxframework/desk/notifications.py new file mode 100644 index 0000000..a61edb1 --- /dev/null +++ b/influxframework/desk/notifications.py @@ -0,0 +1,362 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +from bs4 import BeautifulSoup + +import influxframework +from influxframework import _ +from influxframework.desk.doctype.notification_log.notification_log import ( + enqueue_create_notification, + get_title, + get_title_html, +) +from influxframework.desk.doctype.notification_settings.notification_settings import ( + get_subscribed_documents, +) +from influxframework.utils import get_fullname + + +@influxframework.whitelist() +@influxframework.read_only() +def get_notifications(): + out = { + "open_count_doctype": {}, + "targets": {}, + } + if influxframework.flags.in_install or not influxframework.db.get_single_value("System Settings", "setup_complete"): + return out + + config = get_notification_config() + + if not config: + return out + + groups = list(config.get("for_doctype")) + list(config.get("for_module")) + cache = influxframework.cache() + + notification_count = {} + notification_percent = {} + + for name in groups: + count = cache.hget("notification_count:" + name, influxframework.session.user) + if count is not None: + notification_count[name] = count + + out["open_count_doctype"] = get_notifications_for_doctypes(config, notification_count) + out["targets"] = get_notifications_for_targets(config, notification_percent) + + return out + + +def get_notifications_for_doctypes(config, notification_count): + """Notifications for DocTypes""" + can_read = influxframework.get_user().get_can_read() + open_count_doctype = {} + + for d in config.for_doctype: + if d in can_read: + condition = config.for_doctype[d] + + if d in notification_count: + open_count_doctype[d] = notification_count[d] + else: + try: + if isinstance(condition, dict): + result = influxframework.get_list( + d, fields=["count(*) as count"], filters=condition, ignore_ifnull=True + )[0].count + else: + result = influxframework.get_attr(condition)() + + except influxframework.PermissionError: + influxframework.clear_messages() + pass + # influxframework.msgprint("Permission Error in notifications for {0}".format(d)) + + except Exception as e: + # OperationalError: (1412, 'Table definition has changed, please retry transaction') + # InternalError: (1684, 'Table definition is being modified by concurrent DDL statement') + if e.args and e.args[0] not in (1412, 1684): + raise + + else: + open_count_doctype[d] = result + influxframework.cache().hset("notification_count:" + d, influxframework.session.user, result) + + return open_count_doctype + + +def get_notifications_for_targets(config, notification_percent): + """Notifications for doc targets""" + can_read = influxframework.get_user().get_can_read() + doc_target_percents = {} + + # doc_target_percents = { + # "Company": { + # "Acme": 87, + # "RobotsRUs": 50, + # }, {}... + # } + + for doctype in config.targets: + if doctype in can_read: + if doctype in notification_percent: + doc_target_percents[doctype] = notification_percent[doctype] + else: + doc_target_percents[doctype] = {} + d = config.targets[doctype] + condition = d["filters"] + target_field = d["target_field"] + value_field = d["value_field"] + try: + if isinstance(condition, dict): + doc_list = influxframework.get_list( + doctype, + fields=["name", target_field, value_field], + filters=condition, + limit_page_length=100, + ignore_ifnull=True, + ) + + except influxframework.PermissionError: + influxframework.clear_messages() + pass + except Exception as e: + if e.args[0] not in (1412, 1684): + raise + + else: + for doc in doc_list: + value = doc[value_field] + target = doc[target_field] + doc_target_percents[doctype][doc.name] = (value / target * 100) if value < target else 100 + + return doc_target_percents + + +def clear_notifications(user=None): + if influxframework.flags.in_install: + return + cache = influxframework.cache() + config = get_notification_config() + + if not config: + return + + for_doctype = list(config.get("for_doctype")) if config.get("for_doctype") else [] + for_module = list(config.get("for_module")) if config.get("for_module") else [] + groups = for_doctype + for_module + + for name in groups: + if user: + cache.hdel("notification_count:" + name, user) + else: + cache.delete_key("notification_count:" + name) + + influxframework.publish_realtime("clear_notifications") + + +def clear_notification_config(user): + influxframework.cache().hdel("notification_config", user) + + +def delete_notification_count_for(doctype): + influxframework.cache().delete_key("notification_count:" + doctype) + influxframework.publish_realtime("clear_notifications") + + +def clear_doctype_notifications(doc, method=None, *args, **kwargs): + config = get_notification_config() + if not config: + return + if isinstance(doc, str): + doctype = doc # assuming doctype name was passed directly + else: + doctype = doc.doctype + + if doctype in config.for_doctype: + delete_notification_count_for(doctype) + return + + +@influxframework.whitelist() +def get_notification_info(): + config = get_notification_config() + out = get_notifications() + can_read = influxframework.get_user().get_can_read() + conditions = {} + module_doctypes = {} + doctype_info = dict(influxframework.db.sql("""select name, module from tabDocType""")) + + for d in list(set(can_read + list(config.for_doctype))): + if d in config.for_doctype: + conditions[d] = config.for_doctype[d] + + if d in doctype_info: + module_doctypes.setdefault(doctype_info[d], []).append(d) + + out.update( + { + "conditions": conditions, + "module_doctypes": module_doctypes, + } + ) + + return out + + +def get_notification_config(): + user = influxframework.session.user or "Guest" + + def _get(): + subscribed_documents = get_subscribed_documents() + config = influxframework._dict() + hooks = influxframework.get_hooks() + if hooks: + for notification_config in hooks.notification_config: + nc = influxframework.get_attr(notification_config)() + for key in ("for_doctype", "for_module", "for_other", "targets"): + config.setdefault(key, {}) + if key == "for_doctype": + if len(subscribed_documents) > 0: + key_config = nc.get(key, {}) + subscribed_docs_config = influxframework._dict() + for document in subscribed_documents: + if key_config.get(document): + subscribed_docs_config[document] = key_config.get(document) + config[key].update(subscribed_docs_config) + else: + config[key].update(nc.get(key, {})) + else: + config[key].update(nc.get(key, {})) + return config + + return influxframework.cache().hget("notification_config", user, _get) + + +def get_filters_for(doctype): + """get open filters for doctype""" + config = get_notification_config() + doctype_config = config.get("for_doctype").get(doctype, {}) + filters = doctype_config if not isinstance(doctype_config, str) else None + + return filters + + +@influxframework.whitelist() +@influxframework.read_only() +def get_open_count(doctype, name, items=None): + """Get open count for given transactions and filters + + :param doctype: Reference DocType + :param name: Reference Name + :param transactions: List of transactions (json/dict) + :param filters: optional filters (json/list)""" + + if influxframework.flags.in_migrate or influxframework.flags.in_install: + return {"count": []} + + doc = influxframework.get_doc(doctype, name) + doc.check_permission() + meta = doc.meta + links = meta.get_dashboard_data() + + # compile all items in a list + if items is None: + items = [] + for group in links.transactions: + items.extend(group.get("items")) + + if not isinstance(items, list): + items = json.loads(items) + + out = [] + for d in items: + if d in links.get("internal_links", {}): + continue + + filters = get_filters_for(d) + fieldname = links.get("non_standard_fieldnames", {}).get(d, links.get("fieldname")) + data = {"name": d} + if filters: + # get the fieldname for the current document + # we only need open documents related to the current document + filters[fieldname] = name + total = len( + influxframework.get_all(d, fields="name", filters=filters, limit=100, distinct=True, ignore_ifnull=True) + ) + data["open_count"] = total + + total = len( + influxframework.get_all( + d, fields="name", filters={fieldname: name}, limit=100, distinct=True, ignore_ifnull=True + ) + ) + data["count"] = total + out.append(data) + + out = { + "count": out, + } + + if not meta.custom: + module = influxframework.get_meta_module(doctype) + if hasattr(module, "get_timeline_data"): + out["timeline_data"] = module.get_timeline_data(doctype, name) + + return out + + +def notify_mentions(ref_doctype, ref_name, content): + if ref_doctype and ref_name and content: + mentions = extract_mentions(content) + + if not mentions: + return + + sender_fullname = get_fullname(influxframework.session.user) + title = get_title(ref_doctype, ref_name) + + recipients = [ + influxframework.db.get_value( + "User", + {"enabled": 1, "name": name, "user_type": "System User", "allowed_in_mentions": 1}, + "email", + ) + for name in mentions + ] + + notification_message = _("""{0} mentioned you in a comment in {1} {2}""").format( + influxframework.bold(sender_fullname), influxframework.bold(ref_doctype), get_title_html(title) + ) + + notification_doc = { + "type": "Mention", + "document_type": ref_doctype, + "document_name": ref_name, + "subject": notification_message, + "from_user": influxframework.session.user, + "email_content": content, + } + + enqueue_create_notification(recipients, notification_doc) + + +def extract_mentions(txt): + """Find all instances of @mentions in the html.""" + soup = BeautifulSoup(txt, "html.parser") + emails = [] + for mention in soup.find_all(class_="mention"): + if mention.get("data-is-group") == "true": + try: + user_group = influxframework.get_cached_doc("User Group", mention["data-id"]) + emails += [d.user for d in user_group.user_group_members] + except influxframework.DoesNotExistError: + pass + continue + email = mention["data-id"] + emails.append(email) + + return emails diff --git a/influxframework/desk/page/__init__.py b/influxframework/desk/page/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/page/activity/README.md b/influxframework/desk/page/activity/README.md new file mode 100644 index 0000000..59e0352 --- /dev/null +++ b/influxframework/desk/page/activity/README.md @@ -0,0 +1 @@ +List of latest activities based on Feed. \ No newline at end of file diff --git a/influxframework/desk/page/activity/__init__.py b/influxframework/desk/page/activity/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/page/activity/activity.css b/influxframework/desk/page/activity/activity.css new file mode 100644 index 0000000..b238713 --- /dev/null +++ b/influxframework/desk/page/activity/activity.css @@ -0,0 +1,74 @@ +#page-activity .label { + display: inline-block; + margin-right: 7px; +} + +#page-activity .list-row { + border: none; + padding: 0px; + height: auto; + cursor: pointer; +} + +#page-activity hr { + border-top: 1px solid var(--dark-border-color); +} + +.activity-label { + max-width: 100px; + margin-bottom: -4px; +} + +.date-indicator { + background: none; + font-size: 12px; + vertical-align: middle; + font-weight: bold; + color: var(--text-muted); +} +.date-indicator::after { + margin: 0 -4px 0 12px; + content: ""; + display: inline-block; + height: 8px; + width: 8px; + border-radius: 8px; + background: var(--dark-border-color); +} + +.date-indicator.blue { + color: var(--primary); +} + +.date-indicator.blue::after { + background: var(--primary); +} + +.activity-message { + border-left: 1px solid var(--dark-border-color); + padding: 15px; + padding-right: 30px; +} + +.activity-date { + padding: 15px; + padding-right: 0px; + z-index: 1; +} + +#page-activity .list-filters { + display: none !important; +} + +#page-activity .octicon-heart { + color: var(--red-500); + margin: 0px 5px; +} + +.heatmap { + padding-top: 30px; +} + +.heatmap svg { + margin: auto; +} diff --git a/influxframework/desk/page/activity/activity.js b/influxframework/desk/page/activity/activity.js new file mode 100644 index 0000000..a57ad42 --- /dev/null +++ b/influxframework/desk/page/activity/activity.js @@ -0,0 +1,244 @@ +// Copyright (c) 2015, InfluxFramework LLC +// License: See license.txt + +influxframework.provide("influxframework.activity"); + +influxframework.pages["activity"].on_page_load = function (wrapper) { + var me = this; + + influxframework.ui.make_app_page({ + parent: wrapper, + single_column: true, + }); + + me.page = wrapper.page; + me.page.set_title(__("Activity")); + + influxframework.model.with_doctype("Communication", function () { + me.page.list = new influxframework.views.Activity({ + doctype: "Communication", + parent: wrapper, + }); + }); + + influxframework.activity.render_heatmap(me.page); + + me.page.main.on("click", ".activity-message", function () { + var link_doctype = $(this).attr("data-link-doctype"), + link_name = $(this).attr("data-link-name"), + doctype = $(this).attr("data-doctype"), + docname = $(this).attr("data-docname"); + + [link_doctype, link_name, doctype, docname] = [ + link_doctype, + link_name, + doctype, + docname, + ].map(decodeURIComponent); + + link_doctype = link_doctype && link_doctype !== "null" ? link_doctype : null; + link_name = link_name && link_name !== "null" ? link_name : null; + + if (doctype && docname) { + if (link_doctype && link_name) { + influxframework.route_options = { + scroll_to: { doctype: doctype, name: docname }, + }; + } + + influxframework.set_route(["Form", link_doctype || doctype, link_name || docname]); + } + }); + + // Build Report Button + if (influxframework.boot.user.can_get_report.indexOf("Feed") != -1) { + this.page.add_menu_item( + __("Build Report"), + function () { + influxframework.set_route("List", "Feed", "Report"); + }, + "fa fa-th" + ); + } + + this.page.add_menu_item( + __("Activity Log"), + function () { + influxframework.route_options = { + user: influxframework.session.user, + }; + + influxframework.set_route("List", "Activity Log", "Report"); + }, + "fa fa-th" + ); +}; + +influxframework.pages["activity"].on_page_show = function () { + influxframework.breadcrumbs.add("Desk"); +}; + +influxframework.activity.last_feed_date = false; +influxframework.activity.Feed = class Feed { + constructor(row, data) { + this.scrub_data(data); + this.add_date_separator(row, data); + if (!data.add_class) data.add_class = "label-default"; + + data.link = ""; + if (data.link_doctype && data.link_name) { + data.link = influxframework.format( + data.link_name, + { fieldtype: "Link", options: data.link_doctype }, + { label: __(data.link_doctype) + " " + __(data.link_name) } + ); + } else if (data.feed_type === "Comment" && data.comment_type === "Comment") { + // hack for backward compatiblity + data.link_doctype = data.reference_doctype; + data.link_name = data.reference_name; + data.reference_doctype = "Communication"; + data.reference_name = data.name; + + data.link = influxframework.format( + data.link_name, + { fieldtype: "Link", options: data.link_doctype }, + { label: __(data.link_doctype) + " " + __(data.link_name) } + ); + } else if (data.reference_doctype && data.reference_name) { + data.link = influxframework.format( + data.reference_name, + { fieldtype: "Link", options: data.reference_doctype }, + { label: __(data.reference_doctype) + " " + __(data.reference_name) } + ); + } + + $(row).append(influxframework.render_template("activity_row", data)).find("a").addClass("grey"); + } + + scrub_data(data) { + data.by = influxframework.user.full_name(data.owner); + data.avatar = influxframework.avatar(data.owner); + + data.icon = "fa fa-flag"; + + // color for comment + data.add_class = + { + Comment: "label-danger", + Assignment: "label-warning", + Login: "label-default", + }[data.comment_type || data.communication_medium] || "label-info"; + + data.when = comment_when(data.creation); + data.feed_type = data.comment_type || data.communication_medium; + } + + add_date_separator(row, data) { + var date = influxframework.datetime.str_to_obj(data.creation); + var last = influxframework.activity.last_feed_date; + + if ( + (last && influxframework.datetime.obj_to_str(last) != influxframework.datetime.obj_to_str(date)) || + !last + ) { + var diff = influxframework.datetime.get_day_diff( + influxframework.datetime.get_today(), + influxframework.datetime.obj_to_str(date) + ); + var pdate; + if (diff < 1) { + pdate = "Today"; + } else if (diff < 2) { + pdate = "Yesterday"; + } else { + pdate = influxframework.datetime.global_date_format(date); + } + data.date_sep = pdate; + data.date_class = pdate == "Today" ? "date-indicator blue" : "date-indicator"; + } else { + data.date_sep = null; + data.date_class = ""; + } + influxframework.activity.last_feed_date = date; + } +}; + +influxframework.activity.render_heatmap = function (page) { + $( + '
    \ +
    \ +
    ' + ).prependTo(page.main); + + influxframework.call({ + method: "influxframework.desk.page.activity.activity.get_heatmap_data", + callback: function (r) { + if (r.message) { + new influxframework.Chart(".heatmap", { + type: "heatmap", + start: new Date(moment().subtract(1, "year").toDate()), + countLabel: "actions", + discreteDomains: 1, + radius: 3, // default 0 + data: { + dataPoints: r.message, + }, + }); + } + }, + }); +}; + +influxframework.views.Activity = class Activity extends influxframework.views.BaseList { + constructor(opts) { + super(opts); + this.show(); + } + + setup_defaults() { + super.setup_defaults(); + + this.page_title = __("Activity"); + this.doctype = "Communication"; + this.method = "influxframework.desk.page.activity.activity.get_feed"; + } + + setup_filter_area() { + // + } + + setup_view_menu() { + // + } + + setup_sort_selector() {} + + setup_side_bar() {} + + get_args() { + return { + start: this.start, + page_length: this.page_length, + }; + } + + update_data(r) { + let data = r.message || []; + + if (this.start === 0) { + this.data = data; + } else { + this.data = this.data.concat(data); + } + } + + render() { + this.data.map((value) => { + const row = $('
    ') + .data("data", value) + .appendTo(this.$result) + .get(0); + new influxframework.activity.Feed(row, value); + }); + } +}; diff --git a/influxframework/desk/page/activity/activity.json b/influxframework/desk/page/activity/activity.json new file mode 100644 index 0000000..aa195d9 --- /dev/null +++ b/influxframework/desk/page/activity/activity.json @@ -0,0 +1,20 @@ +{ + "creation": "2013-04-09 11:45:31.000000", + "docstatus": 0, + "doctype": "Page", + "icon": "fa fa-play", + "idx": 1, + "modified": "2013-07-11 14:40:20.000001", + "modified_by": "Administrator", + "module": "Desk", + "name": "activity", + "owner": "Administrator", + "page_name": "activity", + "roles": [ + { + "role": "All" + } + ], + "standard": "Yes", + "title": "Activity" +} diff --git a/influxframework/desk/page/activity/activity.py b/influxframework/desk/page/activity/activity.py new file mode 100644 index 0000000..35656ad --- /dev/null +++ b/influxframework/desk/page/activity/activity.py @@ -0,0 +1,65 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.core.doctype.activity_log.feed import get_feed_match_conditions +from influxframework.utils import cint + + +@influxframework.whitelist() +def get_feed(start, page_length): + """get feed""" + match_conditions_communication = get_feed_match_conditions(influxframework.session.user, "Communication") + match_conditions_comment = get_feed_match_conditions(influxframework.session.user, "Comment") + + result = influxframework.db.sql( + """select X.* + from (select name, owner, modified, creation, seen, comment_type, + reference_doctype, reference_name, '' as link_doctype, '' as link_name, subject, + communication_type, communication_medium, content + from + `tabCommunication` + where + communication_type = 'Communication' + and communication_medium != 'Email' + and {match_conditions_communication} + UNION + select name, owner, modified, creation, '0', 'Updated', + reference_doctype, reference_name, link_doctype, link_name, subject, + 'Comment', '', content + from + `tabActivity Log` + UNION + select name, owner, modified, creation, '0', comment_type, + reference_doctype, reference_name, link_doctype, link_name, '', + 'Comment', '', content + from + `tabComment` + where + {match_conditions_comment} + ) X + order by X.creation DESC + LIMIT %(page_length)s + OFFSET %(start)s""".format( + match_conditions_comment=match_conditions_comment, + match_conditions_communication=match_conditions_communication, + ), + {"user": influxframework.session.user, "start": cint(start), "page_length": cint(page_length)}, + as_dict=True, + ) + + return result + + +@influxframework.whitelist() +def get_heatmap_data(): + return dict( + influxframework.db.sql( + """select unix_timestamp(date(creation)), count(name) + from `tabActivity Log` + where + date(creation) > subdate(curdate(), interval 1 year) + group by date(creation) + order by creation asc""" + ) + ) diff --git a/influxframework/desk/page/activity/activity_row.html b/influxframework/desk/page/activity/activity_row.html new file mode 100644 index 0000000..d4c6862 --- /dev/null +++ b/influxframework/desk/page/activity/activity_row.html @@ -0,0 +1,42 @@ +
    +
    + {%= date_sep || "" %}
    +
    + {{ avatar }} + + {% if (feed_type==="Login") { %} + {%= __("Logged in") %} + {% } else if (feed_type==="Label") { %} + {%= __("{0} {1}", ["" + subject + "", link]) %} + {% } else if (reference_doctype && feed_type==="Comment") { %} + {%= __("Commented on {0}: {1}", [link, "" + content + ""]) %} + {% } else if (reference_doctype && communication_type==="Communication") { %} + {%= __("Communicated via {0} on {1}: {2}", [__(feed_type), link, "" + subject + ""]) %} + {% } else if (reference_doctype && !feed_type) { %} + {%= __("Updated {0}: {1}", [link, "" + subject + ""]) %} + {% } else if (feed_type==="Like" && reference_doctype) { %} + {%= by %} + {% if (in_list(["Comment", "Communication"], reference_doctype)) { %} + {%= content %} + {% } else { %} + {%= link %} + {% } %} + {% } else if (in_list(["Created", "Submitted", "Cancelled", "Deleted"], feed_type)) { %} + {%= __("{0} {1}", ["" + __(feed_type) + "", feed_type==="Deleted" ? subject : link ]) %} + {% } else if (feed_type==="Updated") { %} + {%= __("Updated {0}: {1}", [link, "" + subject + ""]) %} + {% } else if (feed_type==="Relinked") { %} + {%= __("{0} {1} to {2}", [by, content,link]) %} + {% } else if (reference_doctype && reference_name) { %} + {%= __("{0}: {1}", [link, "" + content + ""]) %} + {% } else { %} + {%= subject %} + {% } %} + +
    +
    diff --git a/influxframework/desk/page/backups/__init__.py b/influxframework/desk/page/backups/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/page/backups/backups.css b/influxframework/desk/page/backups/backups.css new file mode 100644 index 0000000..32ccb88 --- /dev/null +++ b/influxframework/desk/page/backups/backups.css @@ -0,0 +1,14 @@ +.download-backups { + font-size: var(--text-base); +} + +.download-backup-card { + display: block; + text-decoration: none; + margin-bottom: var(--margin-lg); +} + +.download-backup-card:hover { + box-shadow: var(--shadow-md); + text-decoration: none; +} diff --git a/influxframework/desk/page/backups/backups.html b/influxframework/desk/page/backups/backups.html new file mode 100644 index 0000000..dc382d8 --- /dev/null +++ b/influxframework/desk/page/backups/backups.html @@ -0,0 +1,27 @@ + + diff --git a/influxframework/desk/page/backups/backups.js b/influxframework/desk/page/backups/backups.js new file mode 100644 index 0000000..2d2e24a --- /dev/null +++ b/influxframework/desk/page/backups/backups.js @@ -0,0 +1,45 @@ +influxframework.pages["backups"].on_page_load = function (wrapper) { + var page = influxframework.ui.make_app_page({ + parent: wrapper, + title: __("Download Backups"), + single_column: true, + }); + + page.add_inner_button(__("Set Number of Backups"), function () { + influxframework.set_route("Form", "System Settings"); + }); + + page.add_inner_button(__("Download Files Backup"), function () { + influxframework.call({ + method: "influxframework.desk.page.backups.backups.schedule_files_backup", + args: { user_email: influxframework.session.user_email }, + }); + }); + + page.add_inner_button(__("Get Backup Encryption Key"), function () { + if (influxframework.user.has_role("System Manager")) { + influxframework.verify_password(function () { + influxframework.call({ + method: "influxframework.utils.backups.get_backup_encryption_key", + callback: function (r) { + influxframework.msgprint({ + title: __("Backup Encryption Key"), + message: __(r.message), + indicator: "blue", + }); + }, + }); + }); + } else { + influxframework.msgprint({ + title: __("Error"), + message: __("System Manager privileges required."), + indicator: "red", + }); + } + }); + + influxframework.breadcrumbs.add("Setup"); + + $(influxframework.render_template("backups")).appendTo(page.body.addClass("no-border")); +}; diff --git a/influxframework/desk/page/backups/backups.json b/influxframework/desk/page/backups/backups.json new file mode 100644 index 0000000..dd6e8d9 --- /dev/null +++ b/influxframework/desk/page/backups/backups.json @@ -0,0 +1,21 @@ +{ + "content": null, + "creation": "2015-09-24 01:26:06.225378", + "docstatus": 0, + "doctype": "Page", + "modified": "2015-09-24 01:26:06.225378", + "modified_by": "Administrator", + "module": "Desk", + "name": "backups", + "owner": "Administrator", + "page_name": "backups", + "roles": [ + { + "role": "System Manager" + } + ], + "script": null, + "standard": "Yes", + "style": null, + "title": "Download Backups" +} \ No newline at end of file diff --git a/influxframework/desk/page/backups/backups.py b/influxframework/desk/page/backups/backups.py new file mode 100644 index 0000000..fbce9aa --- /dev/null +++ b/influxframework/desk/page/backups/backups.py @@ -0,0 +1,120 @@ +import datetime +import os + +import influxframework +from influxframework import _ +from influxframework.utils import cint, get_site_path, get_url +from influxframework.utils.data import convert_utc_to_user_timezone + + +def get_context(context): + def get_time(path): + dt = os.path.getmtime(path) + return convert_utc_to_user_timezone(datetime.datetime.utcfromtimestamp(dt)).strftime( + "%a %b %d %H:%M %Y" + ) + + def get_encrytion_status(path): + if "-enc" in path: + return True + + def get_size(path): + size = os.path.getsize(path) + if size > 1048576: + return f"{float(size) / 1048576:.1f}M" + else: + return f"{float(size) / 1024:.1f}K" + + path = get_site_path("private", "backups") + files = [x for x in os.listdir(path) if os.path.isfile(os.path.join(path, x))] + backup_limit = get_scheduled_backup_limit() + + if len(files) > backup_limit: + cleanup_old_backups(path, files, backup_limit) + + files = [ + ( + "/backups/" + _file, + get_time(os.path.join(path, _file)), + get_encrytion_status(os.path.join(path, _file)), + get_size(os.path.join(path, _file)), + ) + for _file in files + if _file.endswith("sql.gz") + ] + files.sort(key=lambda x: x[1], reverse=True) + + return {"files": files[:backup_limit]} + + +def get_scheduled_backup_limit(): + backup_limit = influxframework.db.get_singles_value("System Settings", "backup_limit") + return cint(backup_limit) + + +def cleanup_old_backups(site_path, files, limit): + backup_paths = [] + for f in files: + if f.endswith("sql.gz"): + _path = os.path.abspath(os.path.join(site_path, f)) + backup_paths.append(_path) + + backup_paths = sorted(backup_paths, key=os.path.getctime) + files_to_delete = len(backup_paths) - limit + + for idx in range(0, files_to_delete): + f = os.path.basename(backup_paths[idx]) + files.remove(f) + + os.remove(backup_paths[idx]) + + +def delete_downloadable_backups(): + path = get_site_path("private", "backups") + files = [x for x in os.listdir(path) if os.path.isfile(os.path.join(path, x))] + backup_limit = get_scheduled_backup_limit() + + if len(files) > backup_limit: + cleanup_old_backups(path, files, backup_limit) + + +@influxframework.whitelist() +def schedule_files_backup(user_email): + from influxframework.utils.background_jobs import enqueue, get_jobs + + queued_jobs = get_jobs(site=influxframework.local.site, queue="long") + method = "influxframework.desk.page.backups.backups.backup_files_and_notify_user" + + if method not in queued_jobs[influxframework.local.site]: + enqueue( + "influxframework.desk.page.backups.backups.backup_files_and_notify_user", + queue="long", + user_email=user_email, + ) + influxframework.msgprint(_("Queued for backup. You will receive an email with the download link")) + else: + influxframework.msgprint( + _("Backup job is already queued. You will receive an email with the download link") + ) + + +def backup_files_and_notify_user(user_email=None): + from influxframework.utils.backups import backup + + backup_files = backup(with_files=True) + get_downloadable_links(backup_files) + + subject = _("File backup is ready") + influxframework.sendmail( + recipients=[user_email], + subject=subject, + template="file_backup_notification", + args=backup_files, + header=[subject, "green"], + ) + + +def get_downloadable_links(backup_files): + for key in ["backup_path_files", "backup_path_private_files"]: + path = backup_files[key] + backup_files[key] = get_url("/".join(path.split("/")[-2:])) diff --git a/influxframework/desk/page/leaderboard/__init__.py b/influxframework/desk/page/leaderboard/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/page/leaderboard/leaderboard.css b/influxframework/desk/page/leaderboard/leaderboard.css new file mode 100644 index 0000000..d15b4ff --- /dev/null +++ b/influxframework/desk/page/leaderboard/leaderboard.css @@ -0,0 +1,85 @@ +.list-filters { + overflow-y: hidden; + padding: 5px +} + +.list-filter-item { + min-width: 150px; + float: left; + margin: 5px; +} + +.list-item_content { + flex: 1; + padding-right: 15px; + align-items: center; +} + +.select-time, .select-doctype, .select-filter, .select-sort { + background: #f0f4f7; +} + +.from-date-field .clearfix{ + display: none; +} + +.from-date-field { + margin-left: 10px; +} + +.select-time:focus, .select-doctype:focus, .select-filter:focus, .select-sort:focus { + background: #f0f4f7; +} + +.header-btn-base { + border: none; + outline: 0; + vertical-align: middle; + overflow: hidden; + text-decoration: none; + color: inherit; + background-color: inherit; + cursor: pointer; + white-space: nowrap; +} + +.header-btn-round { + border-radius: 4px; +} + +.item-title-bold { + font-weight: bold; +} + +.rank { + max-width: 100px; +} + +.leaderboard .result { + border-top: 1px solid var(--border-color); +} + +.leaderboard .list-item { + padding-left: 45px; +} + +.leaderboard .list-item_content { + padding-right: 60px; +} + +.leaderboard-sidebar { + padding-left: 0; + position: fixed; +} + +.leaderboard-list { + padding: var(-padding-sm) 0; + min-height: 70vh; +} + +.leaderboard-empty-state { + align-items: center; + height: 70vh; + justify-content: center; + display: flex; +} diff --git a/influxframework/desk/page/leaderboard/leaderboard.js b/influxframework/desk/page/leaderboard/leaderboard.js new file mode 100644 index 0000000..25e6ceb --- /dev/null +++ b/influxframework/desk/page/leaderboard/leaderboard.js @@ -0,0 +1,411 @@ +influxframework.pages["leaderboard"].on_page_load = (wrapper) => { + influxframework.leaderboard = new Leaderboard(wrapper); + + $(wrapper).bind("show", () => { + // Get which leaderboard to show + let doctype = influxframework.get_route()[1]; + influxframework.leaderboard.show_leaderboard(doctype); + }); +}; + +class Leaderboard { + constructor(parent) { + influxframework.ui.make_app_page({ + parent: parent, + title: __("Leaderboard"), + single_column: false, + card_layout: true, + }); + + this.parent = parent; + this.page = this.parent.page; + this.page.sidebar.html( + `
      ` + ); + this.$sidebar_list = this.page.sidebar.find("ul"); + + this.get_leaderboard_config(); + } + + get_leaderboard_config() { + this.doctypes = []; + this.filters = {}; + this.leaderboard_limit = 20; + + influxframework + .xcall("influxframework.desk.page.leaderboard.leaderboard.get_leaderboard_config") + .then((config) => { + this.leaderboard_config = config; + for (let doctype in this.leaderboard_config) { + this.doctypes.push(doctype); + this.filters[doctype] = this.leaderboard_config[doctype].fields.map( + (field) => { + if (typeof field === "object") { + return field.label || field.fieldname; + } + return field; + } + ); + } + + // For translation. Do not remove this + // __("This Week"), __("This Month"), __("This Quarter"), __("This Year"), + // __("Last Week"), __("Last Month"), __("Last Quarter"), __("Last Year"), + // __("All Time"), __("Select From Date") + this.timespans = [ + "This Week", + "This Month", + "This Quarter", + "This Year", + "Last Week", + "Last Month", + "Last Quarter", + "Last Year", + "All Time", + "Select Date Range", + ]; + + // for saving current selected filters + const _initial_doctype = influxframework.get_route()[1] || this.doctypes[0]; + const _initial_timespan = this.timespans[0]; + const _initial_filter = this.filters[_initial_doctype]; + + this.options = { + selected_doctype: _initial_doctype, + selected_filter: _initial_filter, + selected_filter_item: _initial_filter[0], + selected_timespan: _initial_timespan, + }; + + this.message = null; + this.make(); + }); + } + + make() { + this.$container = $(`
      +
      +
      +
      `).appendTo(this.page.main); + + this.$graph_area = this.$container.find(".leaderboard-graph"); + + this.doctypes.map((doctype) => { + const icon = this.leaderboard_config[doctype].icon; + this.get_sidebar_item(doctype, icon).appendTo(this.$sidebar_list); + }); + + this.setup_leaderboard_fields(); + + this.render_selected_doctype(); + + this.render_search_box(); + + // Get which leaderboard to show + let doctype = influxframework.get_route()[1]; + this.show_leaderboard(doctype); + } + + setup_leaderboard_fields() { + this.company_select = this.page.add_field({ + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: influxframework.defaults.get_default("company"), + reqd: 1, + change: (e) => { + this.options.selected_company = e.currentTarget.value; + this.make_request(); + }, + }); + + this.timespan_select = this.page.add_select( + __("Timespan"), + this.timespans.map((d) => { + return { label: __(d), value: d }; + }) + ); + this.create_date_range_field(); + + this.type_select = this.page.add_select( + __("Field"), + this.options.selected_filter.map((d) => { + return { label: __(influxframework.model.unscrub(d)), value: d }; + }) + ); + + this.timespan_select.on("change", (e) => { + this.options.selected_timespan = e.currentTarget.value; + if (this.options.selected_timespan === "Select Date Range") { + this.date_range_field.show(); + } else { + this.date_range_field.hide(); + } + this.make_request(); + }); + + this.type_select.on("change", (e) => { + this.options.selected_filter_item = e.currentTarget.value; + this.make_request(); + }); + } + + create_date_range_field() { + let timespan_field = $(this.parent).find( + `.influxframework-control[data-original-title="${__("Timespan")}"]` + ); + this.date_range_field = $(`
      `) + .insertAfter(timespan_field) + .hide(); + + let date_field = influxframework.ui.form.make_control({ + df: { + fieldtype: "DateRange", + fieldname: "selected_date_range", + placeholder: __("Date Range"), + default: [influxframework.datetime.month_start(), influxframework.datetime.now_date()], + input_class: "input-xs", + reqd: 1, + change: () => { + this.selected_date_range = date_field.get_value(); + if (this.selected_date_range) this.make_request(); + }, + }, + parent: $(this.parent).find(".from-date-field"), + render_input: 1, + }); + } + + render_selected_doctype() { + this.$sidebar_list.on("click", "li", (e) => { + let $li = $(e.currentTarget); + let doctype = $li.find(".doctype-text").attr("doctype-value"); + + this.options.selected_company = influxframework.defaults.get_default("company"); + this.options.selected_doctype = doctype; + this.options.selected_filter = this.filters[doctype]; + this.options.selected_filter_item = this.filters[doctype][0]; + + this.type_select.empty().add_options( + this.options.selected_filter.map((d) => { + return { label: __(influxframework.model.unscrub(d)), value: d }; + }) + ); + if (this.leaderboard_config[this.options.selected_doctype].company_disabled) { + $(this.parent).find("[data-original-title=Company]").hide(); + } else { + $(this.parent).find("[data-original-title=Company]").show(); + } + + this.$sidebar_list.find("li").removeClass("active selected"); + $li.addClass("active selected"); + + influxframework.set_route("leaderboard", this.options.selected_doctype); + this.make_request(); + }); + } + + render_search_box() { + this.$search_box = $(``); + + $(this.parent).find(".page-form").append(this.$search_box); + } + + show_leaderboard(doctype) { + if (this.doctypes.length) { + if (this.doctypes.includes(doctype)) { + this.options.selected_doctype = doctype; + this.$sidebar_list + .find(`[doctype-value = "${this.options.selected_doctype}"]`) + .trigger("click"); + } + + this.$search_box.find(".leaderboard-search-input").val(""); + influxframework.set_route("leaderboard", this.options.selected_doctype); + } + } + + make_request() { + influxframework.model.with_doctype(this.options.selected_doctype, () => { + this.get_leaderboard(this.get_leaderboard_data); + }); + } + + get_leaderboard(notify) { + if (!this.options.selected_company) { + influxframework.throw(__("Please select Company")); + } + influxframework + .call(this.leaderboard_config[this.options.selected_doctype].method, { + date_range: this.get_date_range(), + company: this.options.selected_company, + field: this.options.selected_filter_item, + limit: this.leaderboard_limit, + }) + .then((r) => { + let results = r.message || []; + + let graph_items = results.slice(0, 10); + + this.$graph_area.show().empty(); + + const custom_options = { + data: { + datasets: [{ values: graph_items.map((d) => d.value) }], + labels: graph_items.map((d) => d.name), + }, + format_tooltip_x: (d) => d[this.options.selected_filter_item], + height: 140, + }; + influxframework.utils.make_chart(".leaderboard-graph", custom_options); + + notify(this, r); + }); + } + + get_leaderboard_data(me, res) { + if (res && res.message.length) { + me.message = null; + me.$container.find(".leaderboard-list").html(me.render_list_view(res.message)); + influxframework.utils.setup_search($(me.parent), ".list-item-container", ".list-id"); + } else { + me.$graph_area.hide(); + me.message = __("No Items Found"); + me.$container.find(".leaderboard-list").html(me.render_list_view()); + } + } + + render_list_view(items = []) { + var html = `${this.render_message()} +
      + ${this.render_result(items)} +
      `; + + return $(html); + } + + render_result(items) { + var html = `${this.render_list_header()} + ${this.render_list_result(items)}`; + return html; + } + + render_list_header() { + const _selected_filter = this.options.selected_filter.map((i) => influxframework.model.unscrub(i)); + const fields = ["rank", "name", this.options.selected_filter_item]; + const filters = fields + .map((filter) => { + const col = __(influxframework.model.unscrub(filter)); + return `
      + + ${col} + +
      `; + }) + .join(""); + + const html = `
      +
      ${filters}
      +
      `; + return html; + } + + render_list_result(items) { + let _html = items + .map((item, index) => { + const $value = $(this.get_item_html(item, index + 1)); + const $item_container = $(`
      `).append($value); + return $item_container[0].outerHTML; + }) + .join(""); + + let html = `
      +
      + ${_html} +
      +
      `; + + return html; + } + + render_message() { + const display_class = this.message ? "" : "hide"; + let html = `
      +
      + Empty State +
      ${this.message}
      +
      +
      `; + return html; + } + + get_item_html(item, index) { + const fields = this.leaderboard_config[this.options.selected_doctype].fields; + const value = influxframework.format( + item.value, + fields.find((field) => { + let fieldname = field.fieldname || field; + return fieldname === this.options.selected_filter_item; + }) + ); + + const link = `/app/${influxframework.router.slug(this.options.selected_doctype)}/${item.name}`; + const name_html = item.formatted_name + ? `${item.formatted_name}` + : ` ${item.name} `; + const html = `
      +
      + ${index} +
      +
      + ${name_html} +
      +
      + ${value} +
      +
      `; + + return html; + } + + get_sidebar_item(item, icon) { + let icon_html = icon ? influxframework.utils.icon(icon, "md") : ""; + return $(`
    • + ${icon_html} + + ${__(item)} + +
    • `); + } + + get_date_range() { + let timespan = this.options.selected_timespan.toLowerCase(); + let current_date = influxframework.datetime.now_date(); + let date_range_map = { + "this week": [influxframework.datetime.week_start(), influxframework.datetime.week_end()], + "this month": [influxframework.datetime.month_start(), influxframework.datetime.month_end()], + "this quarter": [influxframework.datetime.quarter_start(), influxframework.datetime.quarter_end()], + "this year": [influxframework.datetime.year_start(), influxframework.datetime.year_end()], + "last week": [influxframework.datetime.add_days(current_date, -7), current_date], + "last month": [influxframework.datetime.add_months(current_date, -1), current_date], + "last quarter": [influxframework.datetime.add_months(current_date, -3), current_date], + "last year": [influxframework.datetime.add_months(current_date, -12), current_date], + "all time": null, + "select date range": this.selected_date_range || [ + influxframework.datetime.month_start(), + current_date, + ], + }; + return date_range_map[timespan]; + } +} diff --git a/influxframework/desk/page/leaderboard/leaderboard.json b/influxframework/desk/page/leaderboard/leaderboard.json new file mode 100644 index 0000000..0f0b8d8 --- /dev/null +++ b/influxframework/desk/page/leaderboard/leaderboard.json @@ -0,0 +1,19 @@ +{ + "content": null, + "creation": "2017-06-06 02:54:24.785360", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2019-09-27 17:44:51.909947", + "modified_by": "Administrator", + "module": "Desk", + "name": "leaderboard", + "owner": "Administrator", + "page_name": "leaderboard", + "roles": [], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 0, + "title": "Leaderboard" +} \ No newline at end of file diff --git a/influxframework/desk/page/leaderboard/leaderboard.py b/influxframework/desk/page/leaderboard/leaderboard.py new file mode 100644 index 0000000..2162c26 --- /dev/null +++ b/influxframework/desk/page/leaderboard/leaderboard.py @@ -0,0 +1,13 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework + + +@influxframework.whitelist() +def get_leaderboard_config(): + leaderboard_config = influxframework._dict() + leaderboard_hooks = influxframework.get_hooks("leaderboards") + for hook in leaderboard_hooks: + leaderboard_config.update(influxframework.get_attr(hook)()) + + return leaderboard_config diff --git a/influxframework/desk/page/setup_wizard/__init__.py b/influxframework/desk/page/setup_wizard/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/page/setup_wizard/install_fixtures.py b/influxframework/desk/page/setup_wizard/install_fixtures.py new file mode 100644 index 0000000..1dc3513 --- /dev/null +++ b/influxframework/desk/page/setup_wizard/install_fixtures.py @@ -0,0 +1,66 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.desk.doctype.global_search_settings.global_search_settings import ( + update_global_search_doctypes, +) +from influxframework.utils.dashboard import sync_dashboards + + +def install(): + update_genders() + update_salutations() + update_global_search_doctypes() + setup_email_linking() + sync_dashboards() + add_unsubscribe() + + +@influxframework.whitelist() +def update_genders(): + default_genders = [ + "Male", + "Female", + "Other", + "Transgender", + "Genderqueer", + "Non-Conforming", + "Prefer not to say", + ] + records = [{"doctype": "Gender", "gender": d} for d in default_genders] + for record in records: + influxframework.get_doc(record).insert(ignore_permissions=True, ignore_if_duplicate=True) + + +@influxframework.whitelist() +def update_salutations(): + default_salutations = ["Mr", "Ms", "Mx", "Dr", "Mrs", "Madam", "Miss", "Master", "Prof"] + records = [{"doctype": "Salutation", "salutation": d} for d in default_salutations] + for record in records: + doc = influxframework.new_doc(record.get("doctype")) + doc.update(record) + doc.insert(ignore_permissions=True, ignore_if_duplicate=True) + + +def setup_email_linking(): + doc = influxframework.get_doc( + { + "doctype": "Email Account", + "email_id": "email_linking@example.com", + } + ) + doc.insert(ignore_permissions=True, ignore_if_duplicate=True) + + +def add_unsubscribe(): + email_unsubscribe = [ + {"email": "admin@example.com", "global_unsubscribe": 1}, + {"email": "guest@example.com", "global_unsubscribe": 1}, + ] + + for unsubscribe in email_unsubscribe: + if not influxframework.get_all("Email Unsubscribe", filters=unsubscribe): + doc = influxframework.new_doc("Email Unsubscribe") + doc.update(unsubscribe) + doc.insert(ignore_permissions=True) diff --git a/influxframework/desk/page/setup_wizard/setup_wizard.js b/influxframework/desk/page/setup_wizard/setup_wizard.js new file mode 100644 index 0000000..f6c9a80 --- /dev/null +++ b/influxframework/desk/page/setup_wizard/setup_wizard.js @@ -0,0 +1,638 @@ +influxframework.provide("influxframework.setup"); +influxframework.provide("influxframework.setup.events"); +influxframework.provide("influxframework.ui"); + +influxframework.setup = { + slides: [], + events: {}, + data: {}, + utils: {}, + domains: [], + + on: function (event, fn) { + if (!influxframework.setup.events[event]) { + influxframework.setup.events[event] = []; + } + influxframework.setup.events[event].push(fn); + }, + add_slide: function (slide) { + influxframework.setup.slides.push(slide); + }, + + remove_slide: function (slide_name) { + influxframework.setup.slides = influxframework.setup.slides.filter((slide) => slide.name !== slide_name); + }, + + run_event: function (event) { + $.each(influxframework.setup.events[event] || [], function (i, fn) { + fn(); + }); + }, +}; + +influxframework.pages["setup-wizard"].on_page_load = function (wrapper) { + let requires = influxframework.boot.setup_wizard_requires || []; + influxframework.require(requires, function () { + influxframework.call({ + method: "influxframework.desk.page.setup_wizard.setup_wizard.load_languages", + freeze: true, + callback: function (r) { + influxframework.setup.data.lang = r.message; + + influxframework.setup.run_event("before_load"); + var wizard_settings = { + parent: wrapper, + slides: influxframework.setup.slides, + slide_class: influxframework.setup.SetupWizardSlide, + unidirectional: 1, + done_state: 1, + }; + influxframework.wizard = new influxframework.setup.SetupWizard(wizard_settings); + influxframework.setup.run_event("after_load"); + let route = influxframework.get_route(); + if (route) { + influxframework.wizard.show_slide(route[1]); + } + }, + }); + }); +}; + +influxframework.pages["setup-wizard"].on_page_show = function () { + if (influxframework.get_route()[1]) { + influxframework.wizard && influxframework.wizard.show_slide(influxframework.get_route()[1]); + } +}; + +influxframework.setup.on("before_load", function () { + // load slides + influxframework.setup.slides_settings.forEach((s) => { + if (!(s.name === "user" && influxframework.boot.developer_mode)) { + // if not user slide with developer mode + influxframework.setup.add_slide(s); + } + }); +}); + +influxframework.setup.SetupWizard = class SetupWizard extends influxframework.ui.Slides { + constructor(args = {}) { + super(args); + $.extend(this, args); + + this.page_name = "setup-wizard"; + this.welcomed = true; + influxframework.set_route("setup-wizard/0"); + } + + make() { + super.make(); + this.container.addClass("container setup-wizard-slide with-form"); + this.$next_btn.addClass("action"); + this.$complete_btn.addClass("action"); + this.setup_keyboard_nav(); + } + + setup_keyboard_nav() { + $("body").on("keydown", this.handle_enter_press.bind(this)); + } + + disable_keyboard_nav() { + $("body").off("keydown", this.handle_enter_press.bind(this)); + } + + handle_enter_press(e) { + if (e.which === influxframework.ui.keyCode.ENTER) { + var $target = $(e.target); + if ($target.hasClass("prev-btn")) { + $target.trigger("click"); + } else { + this.container.find(".next-btn").trigger("click"); + e.preventDefault(); + } + } + } + + before_show_slide() { + if (!this.welcomed) { + influxframework.set_route(this.page_name); + return false; + } + return true; + } + + show_slide(id) { + if (id === this.slides.length) { + // show_slide called on last slide + this.action_on_complete(); + return; + } + super.show_slide(id); + influxframework.set_route(this.page_name, id + ""); + } + + show_hide_prev_next(id) { + super.show_hide_prev_next(id); + if (id + 1 === this.slides.length) { + this.$next_btn.removeClass("btn-primary").hide(); + this.$complete_btn + .addClass("btn-primary") + .show() + .on("click", () => this.action_on_complete()); + } else { + this.$next_btn.addClass("btn-primary").show(); + this.$complete_btn.removeClass("btn-primary").hide(); + } + } + + refresh_slides() { + // For Translations, etc. + if (this.in_refresh_slides || !this.current_slide.set_values(true)) { + return; + } + this.in_refresh_slides = true; + + this.update_values(); + influxframework.setup.slides = []; + influxframework.setup.run_event("before_load"); + + influxframework.setup.slides = this.get_setup_slides_filtered_by_domain(); + + this.slides = influxframework.setup.slides; + influxframework.setup.run_event("after_load"); + + // re-render all slide, only remake made slides + $.each(this.slide_dict, (id, slide) => { + if (slide.made) { + this.made_slide_ids.push(id); + } + }); + this.made_slide_ids.push(this.current_id); + this.setup(); + + this.show_slide(this.current_id); + this.refresh(this.current_id); + setTimeout(() => { + this.container.find(".form-control").first().focus(); + }, 200); + this.in_refresh_slides = false; + } + + action_on_complete() { + if (!this.current_slide.set_values()) return; + this.update_values(); + this.show_working_state(); + this.disable_keyboard_nav(); + this.listen_for_setup_stages(); + + return influxframework.call({ + method: "influxframework.desk.page.setup_wizard.setup_wizard.setup_complete", + args: { args: this.values }, + callback: (r) => { + if (r.message.status === "ok") { + this.post_setup_success(); + } else if (r.message.status === "registered") { + this.update_setup_message(__("starting the setup...")); + } else if (r.message.fail !== undefined) { + this.abort_setup(r.message.fail); + } + }, + error: () => this.abort_setup("Error in setup"), + }); + } + + post_setup_success() { + this.set_setup_complete_message(__("Setup Complete"), __("Refreshing...")); + if (influxframework.setup.welcome_page) { + localStorage.setItem("session_last_route", influxframework.setup.welcome_page); + } + setTimeout(function () { + // Reload + window.location.href = "/app"; + }, 2000); + } + + abort_setup(fail_msg) { + this.$working_state.find(".state-icon-container").html(""); + fail_msg = fail_msg ? fail_msg : __("Failed to complete setup"); + + this.update_setup_message("Could not start up: " + fail_msg); + + this.$working_state.find(".title").html("Setup failed"); + + this.$abort_btn.show(); + } + + listen_for_setup_stages() { + influxframework.realtime.on("setup_task", (data) => { + // console.log('data', data); + if (data.stage_status) { + // .html('Process '+ data.progress[0] + ' of ' + data.progress[1] + ': ' + data.stage_status); + this.update_setup_message(data.stage_status); + this.set_setup_load_percent(((data.progress[0] + 1) / data.progress[1]) * 100); + } + if (data.fail_msg) { + this.abort_setup(data.fail_msg); + } + if (data.status === "ok") { + this.post_setup_success(); + } + }); + } + + update_setup_message(message) { + this.$working_state.find(".setup-message").html(message); + } + + get_setup_slides_filtered_by_domain() { + var filtered_slides = []; + influxframework.setup.slides.forEach(function (slide) { + if (influxframework.setup.domains) { + let active_domains = influxframework.setup.domains; + if ( + !slide.domains || + slide.domains.filter((d) => active_domains.includes(d)).length > 0 + ) { + filtered_slides.push(slide); + } + } else { + filtered_slides.push(slide); + } + }); + return filtered_slides; + } + + show_working_state() { + this.container.hide(); + influxframework.set_route(this.page_name); + + this.$working_state = this.get_message( + __("Setting up your system"), + __("Starting InfluxFramework ...") + ).appendTo(this.parent); + + this.attach_abort_button(); + + this.current_id = this.slides.length; + this.current_slide = null; + } + + attach_abort_button() { + this.$abort_btn = $( + `` + ); + this.$working_state.find(".content").append(this.$abort_btn); + + this.$abort_btn.on("click", () => { + $(this.parent).find(".setup-in-progress").remove(); + this.container.show(); + influxframework.set_route(this.page_name, this.slides.length - 1); + }); + + this.$abort_btn.hide(); + } + + get_message(title, message = "") { + const loading_html = `
      +
      +
      +
      +
      `; + + return $(`
      +
      +

      ${title}

      +
      ${loading_html}
      +

      ${message}

      +
      +
      `); + } + + set_setup_complete_message(title, message) { + this.$working_state.find(".title").html(title); + this.$working_state.find(".setup-message").html(message); + } + + set_setup_load_percent(percent) { + this.$working_state.find(".progress-bar").css({ width: percent + "%" }); + } +}; + +influxframework.setup.SetupWizardSlide = class SetupWizardSlide extends influxframework.ui.Slide { + constructor(slide = null) { + super(slide); + } + + make() { + super.make(); + this.set_init_values(); + this.reset_action_button_state(); + } + + set_init_values() { + var me = this; + // set values from influxframework.setup.values + if (influxframework.wizard.values && this.fields) { + this.fields.forEach(function (f) { + var value = influxframework.wizard.values[f.fieldname]; + if (value) { + me.get_field(f.fieldname).set_input(value); + } + }); + } + } +}; + +// InfluxFramework slides settings +// ====================================================== +influxframework.setup.slides_settings = [ + { + // Welcome (language) slide + name: "welcome", + title: __("Hello!"), + + fields: [ + { + fieldname: "language", + label: __("Your Language"), + fieldtype: "Autocomplete", + placeholder: __("Select Language"), + default: "English", + reqd: 1, + }, + { + fieldname: "country", + label: __("Your Country"), + fieldtype: "Autocomplete", + placeholder: __("Select Country"), + reqd: 1, + }, + { + fieldtype: "Section Break", + }, + { + fieldname: "timezone", + label: __("Time Zone"), + placeholder: __("Select Time Zone"), + fieldtype: "Select", + reqd: 1, + }, + { fieldtype: "Column Break" }, + { + fieldname: "currency", + label: __("Currency"), + placeholder: __("Select Currency"), + fieldtype: "Select", + reqd: 1, + }, + ], + + onload: function (slide) { + if (influxframework.setup.data.regional_data) { + this.setup_fields(slide); + } else { + influxframework.setup.utils.load_regional_data(slide, this.setup_fields); + } + if (!slide.get_value("language")) { + let session_language = + influxframework.setup.utils.get_language_name_from_code( + influxframework.boot.lang || navigator.language + ) || "English"; + let language_field = slide.get_field("language"); + + language_field.set_input(session_language); + if (!influxframework.setup._from_load_messages) { + language_field.$input.trigger("change"); + } + delete influxframework.setup._from_load_messages; + moment.locale("en"); + } + influxframework.setup.utils.bind_region_events(slide); + influxframework.setup.utils.bind_language_events(slide); + }, + + setup_fields: function (slide) { + influxframework.setup.utils.setup_region_fields(slide); + influxframework.setup.utils.setup_language_field(slide); + }, + }, + { + // Profile slide + name: "user", + title: __("The First User: You"), + icon: "fa fa-user", + fields: [ + { + fieldtype: "Attach Image", + fieldname: "attach_user_image", + label: __("Attach Your Picture"), + is_private: 0, + align: "center", + }, + { + fieldname: "full_name", + label: __("Full Name"), + fieldtype: "Data", + reqd: 1, + }, + { + fieldname: "email", + label: __("Email Address") + " (" + __("Will be your login ID") + ")", + fieldtype: "Data", + options: "Email", + }, + { fieldname: "password", label: __("Password"), fieldtype: "Password" }, + ], + + onload: function (slide) { + if (influxframework.session.user !== "Administrator") { + slide.form.fields_dict.email.$wrapper.toggle(false); + slide.form.fields_dict.password.$wrapper.toggle(false); + + // remove password field + delete slide.form.fields_dict.password; + + if (influxframework.boot.user.first_name || influxframework.boot.user.last_name) { + slide.form.fields_dict.full_name.set_input( + [influxframework.boot.user.first_name, influxframework.boot.user.last_name].join(" ").trim() + ); + } + + var user_image = influxframework.get_cookie("user_image"); + var $attach_user_image = slide.form.fields_dict.attach_user_image.$wrapper; + + if (user_image) { + $attach_user_image.find(".missing-image").toggle(false); + $attach_user_image.find("img").attr("src", decodeURIComponent(user_image)); + $attach_user_image.find(".img-container").toggle(true); + } + delete slide.form.fields_dict.email; + } else { + slide.form.fields_dict.email.df.reqd = 1; + slide.form.fields_dict.email.refresh(); + slide.form.fields_dict.password.df.reqd = 1; + slide.form.fields_dict.password.refresh(); + + influxframework.setup.utils.load_user_details(slide, this.setup_fields); + } + }, + + setup_fields: function (slide) { + if (influxframework.setup.data.full_name) { + slide.form.fields_dict.full_name.set_input(influxframework.setup.data.full_name); + } + if (influxframework.setup.data.email) { + let email = influxframework.setup.data.email; + slide.form.fields_dict.email.set_input(email); + if (influxframework.get_gravatar(email, 200)) { + var $attach_user_image = slide.form.fields_dict.attach_user_image.$wrapper; + $attach_user_image.find(".missing-image").toggle(false); + $attach_user_image.find("img").attr("src", influxframework.get_gravatar(email, 200)); + $attach_user_image.find(".img-container").toggle(true); + } + } + }, + }, +]; + +influxframework.setup.utils = { + load_regional_data: function (slide, callback) { + influxframework.call({ + method: "influxframework.geo.country_info.get_country_timezone_info", + callback: function (data) { + influxframework.setup.data.regional_data = data.message; + callback(slide); + }, + }); + }, + + load_user_details: function (slide, callback) { + influxframework.call({ + method: "influxframework.desk.page.setup_wizard.setup_wizard.load_user_details", + freeze: true, + callback: function (r) { + influxframework.setup.data.full_name = r.message.full_name; + influxframework.setup.data.email = r.message.email; + callback(slide); + }, + }); + }, + + setup_language_field: function (slide) { + var language_field = slide.get_field("language"); + language_field.df.options = influxframework.setup.data.lang.languages; + language_field.set_options(); + }, + + setup_region_fields: function (slide) { + /* + Set a slide's country, timezone and currency fields + */ + let data = influxframework.setup.data.regional_data; + let country_field = slide.get_field("country"); + let translated_countries = []; + + Object.keys(data.country_info) + .sort() + .forEach((country) => { + translated_countries.push({ + label: __(country), + value: country, + }); + }); + + country_field.set_data(translated_countries); + + slide + .get_input("currency") + .empty() + .add_options( + influxframework.utils.unique($.map(data.country_info, (opts) => opts.currency).sort()) + ); + + slide.get_input("timezone").empty().add_options(data.all_timezones); + + // set values if present + if (influxframework.wizard.values.country) { + country_field.set_input(influxframework.wizard.values.country); + } else if (data.default_country) { + country_field.set_input(data.default_country); + } + + slide.get_field("currency").set_input(influxframework.wizard.values.currency); + slide.get_field("timezone").set_input(influxframework.wizard.values.timezone); + }, + + bind_language_events: function (slide) { + slide + .get_input("language") + .unbind("change") + .on("change", function () { + clearTimeout(slide.language_call_timeout); + slide.language_call_timeout = setTimeout(() => { + var lang = $(this).val() || "English"; + influxframework._messages = {}; + influxframework.call({ + method: "influxframework.desk.page.setup_wizard.setup_wizard.load_messages", + freeze: true, + args: { + language: lang, + }, + callback: function () { + influxframework.setup._from_load_messages = true; + influxframework.wizard.refresh_slides(); + }, + }); + }, 500); + }); + }, + + get_language_name_from_code: function (language_code) { + return influxframework.setup.data.lang.codes_to_names[language_code] || "English"; + }, + + bind_region_events: function (slide) { + /* + Bind a slide's country, timezone and currency fields + */ + slide.get_input("country").on("change", function () { + var country = slide.get_input("country").val(); + var $timezone = slide.get_input("timezone"); + var data = influxframework.setup.data.regional_data; + + $timezone.empty(); + + if (!country) return; + // add country specific timezones first + const timezone_list = data.country_info[country].timezones || []; + $timezone.add_options(timezone_list.sort()); + slide.get_field("currency").set_input(data.country_info[country].currency); + slide.get_field("currency").$input.trigger("change"); + + // add all timezones at the end, so that user has the option to change it to any timezone + $timezone.add_options(data.all_timezones); + slide.get_field("timezone").set_input($timezone.val()); + + // temporarily set date format + influxframework.boot.sysdefaults.date_format = + data.country_info[country].date_format || "dd-mm-yyyy"; + }); + + slide.get_input("currency").on("change", function () { + var currency = slide.get_input("currency").val(); + if (!currency) return; + influxframework.model.with_doc("Currency", currency, function () { + influxframework.provide("locals.:Currency." + currency); + var currency_doc = influxframework.model.get_doc("Currency", currency); + var number_format = currency_doc.number_format; + if (number_format === "#.###") { + number_format = "#.###,##"; + } else if (number_format === "#,###") { + number_format = "#,###.##"; + } + + influxframework.boot.sysdefaults.number_format = number_format; + locals[":Currency"][currency] = $.extend({}, currency_doc); + }); + }); + }, +}; diff --git a/influxframework/desk/page/setup_wizard/setup_wizard.json b/influxframework/desk/page/setup_wizard/setup_wizard.json new file mode 100644 index 0000000..058549c --- /dev/null +++ b/influxframework/desk/page/setup_wizard/setup_wizard.json @@ -0,0 +1,23 @@ +{ + "content": null, + "creation": "2013-10-04 13:49:33", + "docstatus": 0, + "doctype": "Page", + "idx": 1, + "modified": "2017-04-12 18:45:00.774654", + "modified_by": "Administrator", + "module": "Desk", + "name": "setup-wizard", + "owner": "Administrator", + "page_name": "setup-wizard", + "roles": [ + { + "role": "System Manager" + } + ], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 1, + "title": "Setup Wizard" +} \ No newline at end of file diff --git a/influxframework/desk/page/setup_wizard/setup_wizard.py b/influxframework/desk/page/setup_wizard/setup_wizard.py new file mode 100644 index 0000000..94aa2ff --- /dev/null +++ b/influxframework/desk/page/setup_wizard/setup_wizard.py @@ -0,0 +1,446 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +import influxframework +from influxframework.geo.country_info import get_country_info +from influxframework.translate import get_messages_for_boot, send_translations, set_default_language +from influxframework.utils import cint, strip +from influxframework.utils.password import update_password + +from . import install_fixtures + + +def get_setup_stages(args): + + # App setup stage functions should not include influxframework.db.commit + # That is done by influxframework after successful completion of all stages + stages = [ + { + "status": "Updating global settings", + "fail_msg": "Failed to update global settings", + "tasks": [ + {"fn": update_global_settings, "args": args, "fail_msg": "Failed to update global settings"} + ], + } + ] + + stages += get_stages_hooks(args) + get_setup_complete_hooks(args) + + stages.append( + { + # post executing hooks + "status": "Wrapping up", + "fail_msg": "Failed to complete setup", + "tasks": [ + {"fn": run_post_setup_complete, "args": args, "fail_msg": "Failed to complete setup"} + ], + } + ) + + return stages + + +@influxframework.whitelist() +def setup_complete(args): + """Calls hooks for `setup_wizard_complete`, sets home page as `desktop` + and clears cache. If wizard breaks, calls `setup_wizard_exception` hook""" + + # Setup complete: do not throw an exception, let the user continue to desk + if cint(influxframework.db.get_single_value("System Settings", "setup_complete")): + return {"status": "ok"} + + args = parse_args(args) + stages = get_setup_stages(args) + is_background_task = influxframework.conf.get("trigger_site_setup_in_background") + + if is_background_task: + process_setup_stages.enqueue(stages=stages, user_input=args, is_background_task=True) + return {"status": "registered"} + else: + return process_setup_stages(stages, args) + + +@influxframework.task() +def process_setup_stages(stages, user_input, is_background_task=False): + try: + influxframework.flags.in_setup_wizard = True + current_task = None + for idx, stage in enumerate(stages): + influxframework.publish_realtime( + "setup_task", + {"progress": [idx, len(stages)], "stage_status": stage.get("status")}, + user=influxframework.session.user, + ) + + for task in stage.get("tasks"): + current_task = task + task.get("fn")(task.get("args")) + except Exception: + handle_setup_exception(user_input) + if not is_background_task: + return {"status": "fail", "fail": current_task.get("fail_msg")} + influxframework.publish_realtime( + "setup_task", + {"status": "fail", "fail_msg": current_task.get("fail_msg")}, + user=influxframework.session.user, + ) + else: + run_setup_success(user_input) + if not is_background_task: + return {"status": "ok"} + influxframework.publish_realtime("setup_task", {"status": "ok"}, user=influxframework.session.user) + finally: + influxframework.flags.in_setup_wizard = False + + +def update_global_settings(args): + if args.language and args.language != "English": + set_default_language(get_language_code(args.lang)) + influxframework.db.commit() + influxframework.clear_cache() + + update_system_settings(args) + update_user_name(args) + + +def run_post_setup_complete(args): + disable_future_access() + influxframework.db.commit() + influxframework.clear_cache() + + +def run_setup_success(args): + for hook in influxframework.get_hooks("setup_wizard_success"): + influxframework.get_attr(hook)(args) + install_fixtures.install() + + +def get_stages_hooks(args): + stages = [] + for method in influxframework.get_hooks("setup_wizard_stages"): + stages += influxframework.get_attr(method)(args) + return stages + + +def get_setup_complete_hooks(args): + stages = [] + for method in influxframework.get_hooks("setup_wizard_complete"): + stages.append( + { + "status": "Executing method", + "fail_msg": "Failed to execute method", + "tasks": [ + {"fn": influxframework.get_attr(method), "args": args, "fail_msg": "Failed to execute method"} + ], + } + ) + return stages + + +def handle_setup_exception(args): + influxframework.db.rollback() + if args: + traceback = influxframework.get_traceback() + print(traceback) + for hook in influxframework.get_hooks("setup_wizard_exception"): + influxframework.get_attr(hook)(traceback, args) + + +def update_system_settings(args): + number_format = get_country_info(args.get("country")).get("number_format", "#,###.##") + + # replace these as float number formats, as they have 0 precision + # and are currency number formats and not for floats + if number_format == "#.###": + number_format = "#.###,##" + elif number_format == "#,###": + number_format = "#,###.##" + + system_settings = influxframework.get_doc("System Settings", "System Settings") + system_settings.update( + { + "country": args.get("country"), + "language": get_language_code(args.get("language")) or "en", + "time_zone": args.get("timezone"), + "float_precision": 3, + "date_format": influxframework.db.get_value("Country", args.get("country"), "date_format"), + "time_format": influxframework.db.get_value("Country", args.get("country"), "time_format"), + "number_format": number_format, + "enable_scheduler": 1 if not influxframework.flags.in_test else 0, + "backup_limit": 3, # Default for downloadable backups + } + ) + system_settings.save() + + +def update_user_name(args): + first_name, last_name = args.get("full_name", ""), "" + if " " in first_name: + first_name, last_name = first_name.split(" ", 1) + + if args.get("email"): + if influxframework.db.exists("User", args.get("email")): + # running again + return + + args["name"] = args.get("email") + + _mute_emails, influxframework.flags.mute_emails = influxframework.flags.mute_emails, True + doc = influxframework.get_doc( + { + "doctype": "User", + "email": args.get("email"), + "first_name": first_name, + "last_name": last_name, + } + ) + doc.flags.no_welcome_mail = True + doc.insert() + influxframework.flags.mute_emails = _mute_emails + update_password(args.get("email"), args.get("password")) + + elif first_name: + args.update({"name": influxframework.session.user, "first_name": first_name, "last_name": last_name}) + + influxframework.db.sql( + """update `tabUser` SET first_name=%(first_name)s, + last_name=%(last_name)s WHERE name=%(name)s""", + args, + ) + + if args.get("attach_user"): + attach_user = args.get("attach_user").split(",") + if len(attach_user) == 3: + filename, filetype, content = attach_user + _file = influxframework.get_doc( + { + "doctype": "File", + "file_name": filename, + "attached_to_doctype": "User", + "attached_to_name": args.get("name"), + "content": content, + "decode": True, + } + ) + _file.save() + fileurl = _file.file_url + influxframework.db.set_value("User", args.get("name"), "user_image", fileurl) + + if args.get("name"): + add_all_roles_to(args.get("name")) + + +def parse_args(args): + if not args: + args = influxframework.local.form_dict + if isinstance(args, str): + args = json.loads(args) + + args = influxframework._dict(args) + + # strip the whitespace + for key, value in args.items(): + if isinstance(value, str): + args[key] = strip(value) + + return args + + +def add_all_roles_to(name): + user = influxframework.get_doc("User", name) + for role in influxframework.db.sql("""select name from tabRole"""): + if role[0] not in [ + "Administrator", + "Guest", + "All", + "Customer", + "Supplier", + "Partner", + "Employee", + ]: + d = user.append("roles") + d.role = role[0] + user.save() + + +def disable_future_access(): + influxframework.db.set_default("desktop:home_page", "workspace") + influxframework.db.set_value("System Settings", "System Settings", "setup_complete", 1) + + # Enable onboarding after install + influxframework.db.set_value("System Settings", "System Settings", "enable_onboarding", 1) + + if not influxframework.flags.in_test: + # remove all roles and add 'Administrator' to prevent future access + page = influxframework.get_doc("Page", "setup-wizard") + page.roles = [] + page.append("roles", {"role": "Administrator"}) + page.flags.do_not_update_json = True + page.flags.ignore_permissions = True + page.save() + + +@influxframework.whitelist() +def load_messages(language): + """Load translation messages for given language from all `setup_wizard_requires` + javascript files""" + influxframework.clear_cache() + set_default_language(get_language_code(language)) + influxframework.db.commit() + send_translations(get_messages_for_boot()) + return influxframework.local.lang + + +@influxframework.whitelist() +def load_languages(): + language_codes = influxframework.db.sql( + "select language_code, language_name from tabLanguage order by name", as_dict=True + ) + codes_to_names = {} + for d in language_codes: + codes_to_names[d.language_code] = d.language_name + return { + "default_language": influxframework.db.get_value("Language", influxframework.local.lang, "language_name") + or influxframework.local.lang, + "languages": sorted(influxframework.db.sql_list("select language_name from tabLanguage order by name")), + "codes_to_names": codes_to_names, + } + + +@influxframework.whitelist() +def load_country(): + from influxframework.sessions import get_geo_ip_country + + return get_geo_ip_country(influxframework.local.request_ip) if influxframework.local.request_ip else None + + +@influxframework.whitelist() +def load_user_details(): + return { + "full_name": influxframework.cache().hget("full_name", "signup"), + "email": influxframework.cache().hget("email", "signup"), + } + + +def prettify_args(args): + # remove attachments + for key, val in args.items(): + if isinstance(val, str) and "data:image" in val: + filename = val.split("data:image", 1)[0].strip(", ") + size = round((len(val) * 3 / 4) / 1048576.0, 2) + args[key] = f"Image Attached: '{filename}' of size {size} MB" + + pretty_args = [] + for key in sorted(args): + pretty_args.append(f"{key} = {args[key]}") + return pretty_args + + +def email_setup_wizard_exception(traceback, args): + if not influxframework.conf.setup_wizard_exception_email: + return + + pretty_args = prettify_args(args) + message = """ + +#### Traceback + +
      {traceback}
      + +--- + +#### Setup Wizard Arguments + +
      {args}
      + +--- + +#### Request Headers + +
      {headers}
      + +--- + +#### Basic Information + +- **Site:** {site} +- **User:** {user}""".format( + site=influxframework.local.site, + traceback=traceback, + args="\n".join(pretty_args), + user=influxframework.session.user, + headers=influxframework.request.headers, + ) + + influxframework.sendmail( + recipients=influxframework.conf.setup_wizard_exception_email, + sender=influxframework.session.user, + subject=f"Setup failed: {influxframework.local.site}", + message=message, + delayed=False, + ) + + +def log_setup_wizard_exception(traceback, args): + with open("../logs/setup-wizard.log", "w+") as setup_log: + setup_log.write(traceback) + setup_log.write(json.dumps(args)) + + +def get_language_code(lang): + return influxframework.db.get_value("Language", {"language_name": lang}) + + +def enable_twofactor_all_roles(): + all_role = influxframework.get_doc("Role", {"role_name": "All"}) + all_role.two_factor_auth = True + all_role.save(ignore_permissions=True) + + +def make_records(records, debug=False): + from influxframework import _dict + from influxframework.modules import scrub + + if debug: + print("make_records: in DEBUG mode") + + # LOG every success and failure + for record in records: + doctype = record.get("doctype") + condition = record.get("__condition") + + if condition and not condition(): + continue + + doc = influxframework.new_doc(doctype) + doc.update(record) + + # ignore mandatory for root + parent_link_field = "parent_" + scrub(doc.doctype) + if doc.meta.get_field(parent_link_field) and not doc.get(parent_link_field): + doc.flags.ignore_mandatory = True + + savepoint = "setup_fixtures_creation" + try: + influxframework.db.savepoint(savepoint) + doc.insert(ignore_permissions=True, ignore_if_duplicate=True) + except Exception as e: + influxframework.clear_last_message() + influxframework.db.rollback(save_point=savepoint) + exception = record.get("__exception") + if exception: + config = _dict(exception) + if isinstance(e, config.exception): + config.handler() + else: + show_document_insert_error() + else: + show_document_insert_error() + + +def show_document_insert_error(): + print("Document Insert Error") + print(influxframework.get_traceback()) + influxframework.log_error("Exception during Setup") diff --git a/influxframework/desk/page/translation_tool/__init__.py b/influxframework/desk/page/translation_tool/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/page/translation_tool/translation_tool.css b/influxframework/desk/page/translation_tool/translation_tool.css new file mode 100644 index 0000000..9603a4c --- /dev/null +++ b/influxframework/desk/page/translation_tool/translation_tool.css @@ -0,0 +1,37 @@ +.translation-item { + font-size: 12px; + padding: 12px 15px; + min-height: 40px; + cursor: pointer; + overflow: hidden; +} + +.translation-item:hover { + background-color: #fafbfc; +} +.translation-item.active { + background-color: #fffce7; +} + +.translation-edit-section { + height: 100%; + overflow-y: scroll; + padding: 0px; +} + +.translation-tool { + display: flex; + width: 100%; + padding: 0; + height: 72vh; +} + +.left-side { + padding: 0px; + height: 100%; + overflow-y: scroll; +} + +.contributed-translation { + padding: 0.5rem 0; +} diff --git a/influxframework/desk/page/translation_tool/translation_tool.html b/influxframework/desk/page/translation_tool/translation_tool.html new file mode 100644 index 0000000..a88f698 --- /dev/null +++ b/influxframework/desk/page/translation_tool/translation_tool.html @@ -0,0 +1,20 @@ +
      +
      +
      +
      + {%= __("Contributed Translations") %} +
      +
      +
      +
      +
      + {%= __("Source Text") %} +
      +
      +
      +
      +
      +
      +
      +
      +
      diff --git a/influxframework/desk/page/translation_tool/translation_tool.js b/influxframework/desk/page/translation_tool/translation_tool.js new file mode 100644 index 0000000..cff7d48 --- /dev/null +++ b/influxframework/desk/page/translation_tool/translation_tool.js @@ -0,0 +1,473 @@ +influxframework.pages["translation-tool"].on_page_load = function (wrapper) { + var page = influxframework.ui.make_app_page({ + parent: wrapper, + title: __("Translation Tool"), + single_column: true, + card_layout: true, + }); + + influxframework.translation_tool = new TranslationTool(page); +}; + +class TranslationTool { + constructor(page) { + this.page = page; + this.wrapper = $(page.body); + this.wrapper.append(influxframework.render_template("translation_tool")); + influxframework.utils.bind_actions_with_object(this.wrapper, this); + this.active_translation = null; + this.edited_translations = {}; + this.setup_search_box(); + this.setup_language_filter(); + this.page.set_primary_action( + __("Contribute Translations"), + this.show_confirmation_dialog.bind(this) + ); + this.page.set_secondary_action(__("Refresh"), this.fetch_messages_then_render.bind(this)); + this.update_header(); + } + + setup_language_filter() { + let languages = Object.keys(influxframework.boot.lang_dict).map((language_label) => { + let value = influxframework.boot.lang_dict[language_label]; + return { + label: `${language_label} (${value})`, + value: value, + }; + }); + + let language_selector = this.page.add_field({ + fieldname: "language", + fieldtype: "Select", + options: languages, + change: () => { + let language = language_selector.get_value(); + localStorage.setItem("translation_language", language); + this.language = language; + this.fetch_messages_then_render(); + }, + }); + let translation_language = localStorage.getItem("translation_language"); + if (translation_language || influxframework.boot.lang !== "en") { + language_selector.set_value(translation_language || influxframework.boot.lang); + } else { + influxframework.prompt( + { + label: __("Please select target language for translation"), + fieldname: "language", + fieldtype: "Select", + options: languages, + reqd: 1, + }, + (values) => { + language_selector.set_value(values.language); + }, + __("Select Language") + ); + } + } + + setup_search_box() { + let search_box = this.page.add_field({ + fieldname: "search", + fieldtype: "Data", + label: __("Search Source Text"), + change: () => { + this.search_text = search_box.get_value(); + this.fetch_messages_then_render(); + }, + }); + } + + fetch_messages_then_render() { + this.fetch_messages().then((messages) => { + this.messages = messages; + this.render_messages(messages); + }); + this.setup_local_contributions(); + } + + fetch_messages() { + influxframework.dom.freeze(__("Fetching...")); + return influxframework + .xcall("influxframework.translate.get_messages", { + language: this.language, + search_text: this.search_text, + }) + .then((messages) => { + return messages; + }) + .finally(() => { + influxframework.dom.unfreeze(); + }); + } + + render_messages(messages) { + let template = (message) => ` +
      +
      + + ${influxframework.utils.escape_html(message.source_text)} + +
      +
      + `; + + let html = messages.map(template).join(""); + this.wrapper.find(".translation-item-container").html(html); + } + + on_translation_click(e, $el) { + let message_id = decodeURIComponent($el.data("message-id")); + this.wrapper.find(".translation-item").removeClass("active"); + $el.addClass("active"); + this.active_translation = this.messages.find((m) => m.id === message_id); + this.edit_translation(this.active_translation); + } + + edit_translation(translation) { + if (this.form) { + this.form.set_values({}); + } + this.get_additional_info(translation.id).then((data) => { + this.make_edit_form(translation, data); + }); + } + + get_additional_info(source_id) { + influxframework.dom.freeze("Fetching..."); + return influxframework + .xcall("influxframework.translate.get_source_additional_info", { + source: source_id, + language: this.page.fields_dict["language"].get_value(), + }) + .finally(influxframework.dom.unfreeze); + } + + make_edit_form(translation, { contributions, positions }) { + if (!this.form) { + this.form = new influxframework.ui.FieldGroup({ + fields: [ + { + fieldtype: "HTML", + fieldname: "header", + read_only: 1, + }, + { + fieldtype: "Data", + fieldname: "id", + hidden: 1, + }, + { + label: "Source Text", + fieldtype: "Code", + fieldname: "source_text", + read_only: 1, + enable_copy_button: 1, + }, + { + label: "Context", + fieldtype: "Code", + fieldname: "context", + read_only: 1, + }, + { + label: "DocType", + fieldtype: "Data", + fieldname: "doctype", + read_only: 1, + }, + { + label: "Translated Text", + fieldtype: "Small Text", + fieldname: "translated_text", + }, + { + label: "Suggest", + fieldtype: "Button", + click: () => { + let { id, translated_text, source_text } = this.form.get_values(); + let existing_value = this.form.translation_dict.translated_text; + if (is_null(translated_text) || existing_value === translated_text) { + delete this.edited_translations[id]; + } else if (existing_value !== translated_text) { + this.edited_translations[id] = { + id, + translated_text, + source_text, + }; + } + this.update_header(); + }, + }, + { + fieldtype: "Section Break", + fieldname: "contributed_translations_section", + label: "Contributed Translations", + }, + { + fieldtype: "HTML", + fieldname: "contributed_translations", + }, + { + fieldtype: "Section Break", + collapsible: 1, + label: "Occurences in source code", + }, + { + fieldtype: "HTML", + fieldname: "positions", + }, + ], + body: this.wrapper.find(".translation-edit-form"), + }); + + this.form.make(); + this.setup_header(); + } + + this.form.set_values(translation); + this.form.translation_dict = translation; + this.form.set_df_property("doctype", "hidden", !translation.doctype); + this.form.set_df_property("context", "hidden", !translation.context); + this.set_status(translation); + + this.setup_contributions(contributions); + this.setup_positions(positions); + } + + setup_header() { + this.form.get_field("header").$wrapper.html(`
      + +
      `); + } + + set_status(translation) { + this.form.get_field("header").$wrapper.find(".translation-status").html(` + + ${this.get_indicator_status_text(translation)} + + `); + } + + setup_positions(positions) { + let position_dom = ""; + if (positions && positions.length) { + position_dom = positions + .map((position) => { + if (position.path.startsWith("DocType: ")) { + return `
      + ${position.path} +
      `; + } else { + return ``; + } + }) + .join(""); + } + this.form.get_field("positions").$wrapper.html(position_dom); + } + + setup_contributions(contributions) { + const contributions_exists = contributions && contributions.length; + if (contributions_exists) { + let contributions_html = contributions.map((c) => { + return ` +
      +
      ${c.translated}
      +
      + ${comment_when(c.creation)} +
      +
      + `; + }); + this.form.get_field("contributed_translations").html(contributions_html); + } + this.form.set_df_property( + "contributed_translations_section", + "hidden", + !contributions_exists + ); + } + show_confirmation_dialog() { + this.confirmation_dialog = new influxframework.ui.Dialog({ + fields: [ + { + label: __("Language"), + fieldname: "language", + fieldtype: "Data", + read_only: 1, + bold: 1, + default: this.language, + }, + { + fieldtype: "HTML", + fieldname: "edited_translations", + }, + ], + title: __("Confirm Translations"), + no_submit_on_enter: true, + primary_action_label: __("Submit"), + primary_action: (values) => { + this.create_translations(values).then(this.confirmation_dialog.hide()); + }, + }); + this.confirmation_dialog.get_field("edited_translations").html(` +
      Id + Time + State + Info + Progress / Wait Event +
      + + + + + ${Object.values(this.edited_translations) + .map( + (t) => ` + + + + + ` + ) + .join("")} +
      ${__("Source Text")}${__("Translated Text")}
      ${t.source_text}${t.translated_text}
      + `); + this.confirmation_dialog.show(); + } + create_translations() { + influxframework.dom.freeze(__("Submitting...")); + return influxframework + .xcall("influxframework.core.doctype.translation.translation.create_translations", { + translation_map: this.edited_translations, + language: this.language, + }) + .then(() => { + influxframework.dom.unfreeze(); + influxframework.show_alert({ + message: __("Successfully Submitted!"), + indicator: "success", + }); + this.edited_translations = {}; + this.update_header(); + this.fetch_messages_then_render(); + }) + .finally(() => influxframework.dom.unfreeze()); + } + + setup_local_contributions() { + // TODO: Refactor + influxframework + .xcall("influxframework.translate.get_contributions", { + language: this.language, + }) + .then((messages) => { + let template = (message) => ` +
      +
      + + ${influxframework.utils.escape_html(message.source_text)} + +
      +
      + `; + + let html = messages.map(template).join(""); + this.wrapper.find(".translation-item-tr").html(html); + }); + } + + show_translation_status_modal(e, $el) { + let message_id = decodeURIComponent($el.data("message-id")); + + influxframework.xcall("influxframework.translate.get_contribution_status", { message_id }).then((doc) => { + let d = new influxframework.ui.Dialog({ + title: __("Contribution Status"), + fields: [ + { + fieldname: "source_message", + label: __("Source Message"), + fieldtype: "Data", + read_only: 1, + }, + { + fieldname: "translated", + label: __("Translated Message"), + fieldtype: "Data", + read_only: 1, + }, + { + fieldname: "contribution_status", + label: __("Contribution Status"), + fieldtype: "Data", + read_only: 1, + }, + { + fieldname: "modified_by", + label: __("Verified By"), + fieldtype: "Data", + read_only: 1, + depends_on: (doc) => { + return doc.contribution_status == "Verified"; + }, + }, + ], + }); + d.set_values(doc); + d.show(); + }); + } + + update_header() { + let edited_translations_count = Object.keys(this.edited_translations).length; + if (edited_translations_count) { + let message = ""; + if (edited_translations_count == 1) { + message = __("{0} translation pending", [edited_translations_count]); + } else { + message = __("{0} translations pending", [edited_translations_count]); + } + this.page.set_indicator(message, "orange"); + } else { + this.page.set_indicator(""); + } + this.page.btn_primary.prop("disabled", !edited_translations_count); + } + + get_indicator_color(message_obj) { + return !message_obj.translated + ? "red" + : message_obj.translated_by_google + ? "orange" + : "blue"; + } + + get_indicator_status_text(message_obj) { + if (!message_obj.translated) { + return __("Untranslated"); + } else if (message_obj.translated_by_google) { + return __("Google Translation"); + } else { + return __("Community Contribution"); + } + } + + get_contribution_indicator_color(message_obj) { + return message_obj.contribution_status == "Pending" ? "orange" : "green"; + } + + get_code_url(path, line_no, app) { + const code_path = path.substring(`apps/${app}`.length); + return `https://github.com/influxframework/${app}/blob/develop/${code_path}#L${line_no}`; + } +} diff --git a/influxframework/desk/page/translation_tool/translation_tool.json b/influxframework/desk/page/translation_tool/translation_tool.json new file mode 100644 index 0000000..a54b2a4 --- /dev/null +++ b/influxframework/desk/page/translation_tool/translation_tool.json @@ -0,0 +1,26 @@ +{ + "content": null, + "creation": "2020-01-30 15:16:12.136323", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2020-01-30 15:16:23.273733", + "modified_by": "Administrator", + "module": "Desk", + "name": "translation-tool", + "owner": "Administrator", + "page_name": "Translation Tool", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Translator" + } + ], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 1, + "title": "Translation Tool" +} \ No newline at end of file diff --git a/influxframework/desk/page/user_profile/__init__.py b/influxframework/desk/page/user_profile/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/page/user_profile/user_profile.css b/influxframework/desk/page/user_profile/user_profile.css new file mode 100644 index 0000000..9bcfc33 --- /dev/null +++ b/influxframework/desk/page/user_profile/user_profile.css @@ -0,0 +1,30 @@ +.recent-activity .new-timeline { + padding-top: 0; +} + +.recent-activity .new-timeline:before { + top: 25px; +} + +.recent-activity-title { + font-weight: 700; + font-size: var(--text-xl); + color: var(--text-color); +} + +.recent-activity .recent-activity-footer { + margin-left: calc(var(--timeline-left-padding) + var(--timeline-item-left-margin)); + max-width: var(--timeline-content-max-width); +} + +.recent-activity .show-more-activity-btn { + display: block; + margin: auto; + width: max-content; + margin-top: 35px; + font-size: var(--text-md); +} + +.recent-activity { + padding-bottom: 60px; +} \ No newline at end of file diff --git a/influxframework/desk/page/user_profile/user_profile.html b/influxframework/desk/page/user_profile/user_profile.html new file mode 100644 index 0000000..ede5f26 --- /dev/null +++ b/influxframework/desk/page/user_profile/user_profile.html @@ -0,0 +1,44 @@ + diff --git a/influxframework/desk/page/user_profile/user_profile.js b/influxframework/desk/page/user_profile/user_profile.js new file mode 100644 index 0000000..0f111af --- /dev/null +++ b/influxframework/desk/page/user_profile/user_profile.js @@ -0,0 +1,6 @@ +influxframework.pages["user-profile"].on_page_load = function (wrapper) { + influxframework.require("user_profile_controller.bundle.js", () => { + let user_profile = new influxframework.ui.UserProfile(wrapper); + user_profile.show(); + }); +}; diff --git a/influxframework/desk/page/user_profile/user_profile.json b/influxframework/desk/page/user_profile/user_profile.json new file mode 100644 index 0000000..43385f5 --- /dev/null +++ b/influxframework/desk/page/user_profile/user_profile.json @@ -0,0 +1,23 @@ +{ + "content": null, + "creation": "2019-07-22 12:23:38.425877", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2020-03-02 15:17:13.041650", + "modified_by": "Administrator", + "module": "Desk", + "name": "user-profile", + "owner": "Administrator", + "page_name": "User Profile", + "roles": [ + { + "role": "All" + } + ], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 0, + "title": "User Profile" +} \ No newline at end of file diff --git a/influxframework/desk/page/user_profile/user_profile.py b/influxframework/desk/page/user_profile/user_profile.py new file mode 100644 index 0000000..a45e28f --- /dev/null +++ b/influxframework/desk/page/user_profile/user_profile.py @@ -0,0 +1,113 @@ +from datetime import datetime + +import influxframework +from influxframework.utils import getdate + + +@influxframework.whitelist() +def get_energy_points_heatmap_data(user, date): + try: + date = getdate(date) + except Exception: + date = getdate() + + return dict( + influxframework.db.sql( + """select unix_timestamp(date(creation)), sum(points) + from `tabEnergy Point Log` + where + date(creation) > subdate('{date}', interval 1 year) and + date(creation) < subdate('{date}', interval -1 year) and + user = %s and + type != 'Review' + group by date(creation) + order by creation asc""".format( + date=date + ), + user, + ) + ) + + +@influxframework.whitelist() +def get_energy_points_percentage_chart_data(user, field): + result = influxframework.get_all( + "Energy Point Log", + filters={"user": user, "type": ["!=", "Review"]}, + group_by=field, + order_by=field, + fields=[field, "ABS(sum(points)) as points"], + as_list=True, + ) + + return { + "labels": [r[0] for r in result if r[0] is not None], + "datasets": [{"values": [r[1] for r in result]}], + } + + +@influxframework.whitelist() +def get_user_rank(user): + month_start = datetime.today().replace(day=1) + monthly_rank = influxframework.get_all( + "Energy Point Log", + group_by="user", + filters={"creation": [">", month_start], "type": ["!=", "Review"]}, + fields=["user", "sum(points)"], + order_by="sum(points) desc", + as_list=True, + ) + + all_time_rank = influxframework.get_all( + "Energy Point Log", + group_by="user", + filters={"type": ["!=", "Review"]}, + fields=["user", "sum(points)"], + order_by="sum(points) desc", + as_list=True, + ) + + return { + "monthly_rank": [i + 1 for i, r in enumerate(monthly_rank) if r[0] == user], + "all_time_rank": [i + 1 for i, r in enumerate(all_time_rank) if r[0] == user], + } + + +@influxframework.whitelist() +def update_profile_info(profile_info): + profile_info = influxframework.parse_json(profile_info) + keys = ["location", "interest", "user_image", "bio"] + + for key in keys: + if key not in profile_info: + profile_info[key] = None + + user = influxframework.get_doc("User", influxframework.session.user) + user.update(profile_info) + user.save() + return user + + +@influxframework.whitelist() +def get_energy_points_list(start, limit, user): + return influxframework.db.get_list( + "Energy Point Log", + filters={"user": user, "type": ["!=", "Review"]}, + fields=[ + "name", + "user", + "points", + "reference_doctype", + "reference_name", + "reason", + "type", + "seen", + "rule", + "owner", + "creation", + "revert_of", + ], + start=start, + limit=limit, + order_by="creation desc", + ) diff --git a/influxframework/desk/page/user_profile/user_profile_controller.js b/influxframework/desk/page/user_profile/user_profile_controller.js new file mode 100644 index 0000000..b858c58 --- /dev/null +++ b/influxframework/desk/page/user_profile/user_profile_controller.js @@ -0,0 +1,495 @@ +import BaseTimeline from "../../../public/js/influxframework/form/footer/base_timeline"; +influxframework.provide("influxframework.energy_points"); + +class UserProfile { + constructor(wrapper) { + this.wrapper = $(wrapper); + this.page = influxframework.ui.make_app_page({ + parent: wrapper, + }); + this.sidebar = this.wrapper.find(".layout-side-section"); + this.main_section = this.wrapper.find(".layout-main-section"); + this.wrapper.bind("show", () => { + this.show(); + }); + } + + show() { + let route = influxframework.get_route(); + this.user_id = route[1] || influxframework.session.user; + influxframework.dom.freeze(__("Loading user profile") + "..."); + influxframework.db.exists("User", this.user_id).then((exists) => { + influxframework.dom.unfreeze(); + if (exists) { + this.make_user_profile(); + } else { + influxframework.msgprint(__("User does not exist")); + } + }); + } + + make_user_profile() { + this.user = influxframework.user_info(this.user_id); + this.page.set_title(this.user.fullname); + this.setup_user_search(); + this.main_section.empty().append(influxframework.render_template("user_profile")); + this.energy_points = 0; + this.review_points = 0; + this.rank = 0; + this.month_rank = 0; + this.render_user_details(); + this.render_points_and_rank(); + this.render_heatmap(); + this.render_line_chart(); + this.render_percentage_chart("type", "Type Distribution"); + this.create_percentage_chart_filters(); + this.setup_user_activity_timeline(); + } + + setup_user_search() { + this.$user_search_button = this.page.set_secondary_action( + __("Change User"), + () => this.show_user_search_dialog(), + { icon: "change", size: "sm" } + ); + } + + show_user_search_dialog() { + let dialog = new influxframework.ui.Dialog({ + title: __("Change User"), + fields: [ + { + fieldtype: "Link", + fieldname: "user", + options: "User", + label: __("User"), + }, + ], + primary_action_label: __("Go"), + primary_action: ({ user }) => { + dialog.hide(); + influxframework.set_route("user-profile", user); + }, + }); + dialog.show(); + } + + render_heatmap() { + this.heatmap = new influxframework.Chart(".performance-heatmap", { + type: "heatmap", + countLabel: "Energy Points", + data: {}, + discreteDomains: 1, + radius: 3, + height: 150, + }); + this.update_heatmap_data(); + this.create_heatmap_chart_filters(); + } + + update_heatmap_data(date_from) { + influxframework + .xcall("influxframework.desk.page.user_profile.user_profile.get_energy_points_heatmap_data", { + user: this.user_id, + date: date_from || influxframework.datetime.year_start(), + }) + .then((r) => { + this.heatmap.update({ dataPoints: r }); + }); + } + + render_line_chart() { + this.line_chart_filters = [ + ["Energy Point Log", "user", "=", this.user_id, false], + ["Energy Point Log", "type", "!=", "Review", false], + ]; + + this.line_chart_config = { + timespan: "Last Month", + time_interval: "Daily", + type: "Line", + value_based_on: "points", + chart_type: "Sum", + document_type: "Energy Point Log", + name: "Energy Points", + width: "half", + based_on: "creation", + }; + + this.line_chart = new influxframework.Chart(".performance-line-chart", { + type: "line", + height: 200, + data: { + labels: [], + datasets: [{}], + }, + colors: ["purple"], + axisOptions: { + xIsSeries: 1, + }, + }); + this.update_line_chart_data(); + this.create_line_chart_filters(); + } + + update_line_chart_data() { + this.line_chart_config.filters_json = JSON.stringify(this.line_chart_filters); + + influxframework + .xcall("influxframework.desk.doctype.dashboard_chart.dashboard_chart.get", { + chart: this.line_chart_config, + no_cache: 1, + }) + .then((chart) => { + this.line_chart.update(chart); + }); + } + + // eslint-disable-next-line no-unused-vars + render_percentage_chart(field, title) { + influxframework + .xcall( + "influxframework.desk.page.user_profile.user_profile.get_energy_points_percentage_chart_data", + { + user: this.user_id, + field: field, + } + ) + .then((chart) => { + if (chart.labels.length) { + this.percentage_chart = new influxframework.Chart(".performance-percentage-chart", { + type: "percentage", + data: { + labels: chart.labels, + datasets: chart.datasets, + }, + truncateLegends: 1, + barOptions: { + height: 11, + depth: 1, + }, + height: 200, + maxSlices: 8, + colors: [ + "purple", + "blue", + "cyan", + "teal", + "pink", + "red", + "orange", + "yellow", + ], + }); + } else { + this.wrapper.find(".percentage-chart-container").hide(); + } + }); + } + + create_line_chart_filters() { + let filters = [ + { + label: "All", + options: ["All", "Auto", "Criticism", "Appreciation", "Revert"], + action: (selected_item) => { + if (selected_item === "All") { + this.line_chart_filters = [ + ["Energy Point Log", "user", "=", this.user_id, false], + ["Energy Point Log", "type", "!=", "Review", false], + ]; + } else { + this.line_chart_filters[1] = [ + "Energy Point Log", + "type", + "=", + selected_item, + false, + ]; + } + this.update_line_chart_data(); + }, + }, + { + label: "Last Month", + options: ["Last Week", "Last Month", "Last Quarter", "Last Year"], + action: (selected_item) => { + this.line_chart_config.timespan = selected_item; + this.update_line_chart_data(); + }, + }, + { + label: "Daily", + options: ["Daily", "Weekly", "Monthly"], + action: (selected_item) => { + this.line_chart_config.time_interval = selected_item; + this.update_line_chart_data(); + }, + }, + ]; + influxframework.dashboard_utils.render_chart_filters( + filters, + "chart-filter", + ".line-chart-options", + 1 + ); + } + + create_percentage_chart_filters() { + let filters = [ + { + label: "Type", + options: ["Type", "Reference Doctype", "Rule"], + fieldnames: ["type", "reference_doctype", "rule"], + action: (selected_item, fieldname) => { + let title = selected_item + " Distribution"; + this.render_percentage_chart(fieldname, title); + }, + }, + ]; + influxframework.dashboard_utils.render_chart_filters( + filters, + "chart-filter", + ".percentage-chart-options" + ); + } + + create_heatmap_chart_filters() { + let filters = [ + { + label: influxframework.dashboard_utils.get_year(influxframework.datetime.now_date()), + options: influxframework.dashboard_utils.get_years_since_creation( + influxframework.boot.user.creation + ), + action: (selected_item) => { + this.update_heatmap_data(influxframework.datetime.obj_to_str(selected_item)); + }, + }, + ]; + influxframework.dashboard_utils.render_chart_filters(filters, "chart-filter", ".heatmap-options"); + } + + edit_profile() { + let edit_profile_dialog = new influxframework.ui.Dialog({ + title: __("Edit Profile"), + fields: [ + { + fieldtype: "Attach Image", + fieldname: "user_image", + label: "Profile Image", + }, + { + fieldtype: "Data", + fieldname: "interest", + label: "Interests", + }, + { + fieldtype: "Column Break", + }, + { + fieldtype: "Data", + fieldname: "location", + label: "Location", + }, + { + fieldtype: "Section Break", + fieldname: "Interest", + }, + { + fieldtype: "Small Text", + fieldname: "bio", + label: "Bio", + }, + ], + primary_action: (values) => { + edit_profile_dialog.disable_primary_action(); + influxframework + .xcall("influxframework.desk.page.user_profile.user_profile.update_profile_info", { + profile_info: values, + }) + .then((user) => { + user.image = user.user_image; + this.user = Object.assign(values, user); + edit_profile_dialog.hide(); + this.render_user_details(); + }) + .finally(() => { + edit_profile_dialog.enable_primary_action(); + }); + }, + primary_action_label: __("Save"), + }); + + edit_profile_dialog.set_values({ + user_image: this.user.image, + location: this.user.location, + interest: this.user.interest, + bio: this.user.bio, + }); + edit_profile_dialog.show(); + } + + render_user_details() { + this.sidebar.empty().append( + influxframework.render_template("user_profile_sidebar", { + user_image: this.user.image, + user_abbr: this.user.abbr, + user_location: this.user.location, + user_interest: this.user.interest, + user_bio: this.user.bio, + }) + ); + + this.setup_user_profile_links(); + } + + setup_user_profile_links() { + if (this.user_id !== influxframework.session.user) { + this.wrapper.find(".profile-links").hide(); + } else { + this.wrapper.find(".edit-profile-link").on("click", () => { + this.edit_profile(); + }); + + this.wrapper.find(".user-settings-link").on("click", () => { + this.go_to_user_settings(); + }); + } + } + + get_user_rank() { + return influxframework + .xcall("influxframework.desk.page.user_profile.user_profile.get_user_rank", { + user: this.user_id, + }) + .then((r) => { + if (r.monthly_rank.length) this.month_rank = r.monthly_rank[0]; + if (r.all_time_rank.length) this.rank = r.all_time_rank[0]; + }); + } + + get_user_points() { + return influxframework + .xcall( + "influxframework.social.doctype.energy_point_log.energy_point_log.get_user_energy_and_review_points", + { + user: this.user_id, + } + ) + .then((r) => { + if (r[this.user_id]) { + this.energy_points = r[this.user_id].energy_points; + this.review_points = r[this.user_id].review_points; + } + }); + } + + render_points_and_rank() { + let $profile_details = this.wrapper.find(".user-stats"); + let $profile_details_wrapper = this.wrapper.find(".user-stats-detail"); + + const _get_stat_dom = (value, label, icon) => { + return `
      + ${influxframework.utils.icon(icon, "lg", "no-stroke")} +
      +
      ${value}
      +
      ${label}
      +
      +
      `; + }; + + this.get_user_rank().then(() => { + this.get_user_points().then(() => { + let html = $(` + ${_get_stat_dom(this.energy_points, __("Energy Points"), "color-energy-points")} + ${_get_stat_dom(this.review_points, __("Review Points"), "color-review-points")} + ${_get_stat_dom(this.rank, __("Rank"), "color-rank")} + ${_get_stat_dom(this.month_rank, __("Monthly Rank"), "color-monthly-rank")} + `); + + $profile_details.append(html); + $profile_details_wrapper.removeClass("hide"); + }); + }); + } + + go_to_user_settings() { + influxframework.set_route("Form", "User", this.user_id); + } + + setup_user_activity_timeline() { + this.user_activity_timeline = new UserProfileTimeline({ + parent: this.wrapper.find(".recent-activity-list"), + footer: this.wrapper.find(".recent-activity-footer"), + user: this.user_id, + }); + + this.user_activity_timeline.refresh(); + } +} + +class UserProfileTimeline extends BaseTimeline { + make() { + super.make(); + this.activity_start = 0; + this.activity_limit = 20; + this.setup_show_more_activity(); + } + prepare_timeline_contents() { + return this.get_user_activity_data().then((activities) => { + if (!activities.length) { + this.show_more_button.hide(); + this.timeline_wrapper.html(`
      ${__("No activities to show")}
      `); + return; + } + this.show_more_button.toggle(activities.length === this.activity_limit); + this.timeline_items = activities.map((activity) => + this.get_activity_timeline_item(activity) + ); + }); + } + + get_user_activity_data() { + return influxframework.xcall("influxframework.desk.page.user_profile.user_profile.get_energy_points_list", { + start: this.activity_start, + limit: this.activity_limit, + user: this.user, + }); + } + + get_activity_timeline_item(data) { + let icon = + data.type == "Appreciation" ? "clap" : data.type == "Criticism" ? "criticize" : null; + return { + icon: icon, + creation: data.creation, + is_card: true, + content: influxframework.energy_points.format_history_log(data), + }; + } + + setup_show_more_activity() { + this.show_more_button = $( + `${__("Show More Activity")}` + ); + this.show_more_button.hide(); + this.footer.append(this.show_more_button); + this.show_more_button.on("click", () => this.show_more_activity()); + } + + show_more_activity() { + this.activity_start += this.activity_limit; + this.get_user_activity_data().then((activities) => { + if (!activities.length || activities.length < this.activity_limit) { + this.show_more_button.hide(); + } + let timeline_items = activities.map((activity) => + this.get_activity_timeline_item(activity) + ); + timeline_items.map((item) => this.add_timeline_item(item, true)); + }); + } +} + +influxframework.provide("influxframework.ui"); +influxframework.ui.UserProfile = UserProfile; diff --git a/influxframework/desk/page/user_profile/user_profile_sidebar.html b/influxframework/desk/page/user_profile/user_profile_sidebar.html new file mode 100644 index 0000000..9f8889f --- /dev/null +++ b/influxframework/desk/page/user_profile/user_profile_sidebar.html @@ -0,0 +1,60 @@ + diff --git a/influxframework/desk/query_report.py b/influxframework/desk/query_report.py new file mode 100644 index 0000000..ef66464 --- /dev/null +++ b/influxframework/desk/query_report.py @@ -0,0 +1,755 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import datetime +import json +import os +from datetime import timedelta + +import influxframework +import influxframework.desk.reportview +from influxframework import _ +from influxframework.core.utils import ljust_list +from influxframework.model.utils import render_include +from influxframework.modules import get_module_path, scrub +from influxframework.monitor import add_data_to_monitor +from influxframework.permissions import get_role_permissions +from influxframework.translate import send_translations +from influxframework.utils import ( + cint, + cstr, + flt, + format_duration, + get_html_format, + get_url_to_form, + gzip_decompress, +) + + +def get_report_doc(report_name): + doc = influxframework.get_doc("Report", report_name) + doc.custom_columns = [] + + if doc.report_type == "Custom Report": + custom_report_doc = doc + reference_report = custom_report_doc.reference_report + doc = influxframework.get_doc("Report", reference_report) + doc.custom_report = report_name + if custom_report_doc.json: + data = json.loads(custom_report_doc.json) + if data: + doc.custom_columns = data["columns"] + doc.is_custom_report = True + + if not doc.is_permitted(): + influxframework.throw( + _("You don't have access to Report: {0}").format(report_name), + influxframework.PermissionError, + ) + + if not influxframework.has_permission(doc.ref_doctype, "report"): + influxframework.throw( + _("You don't have permission to get a report on: {0}").format(doc.ref_doctype), + influxframework.PermissionError, + ) + + if doc.disabled: + influxframework.throw(_("Report {0} is disabled").format(report_name)) + + return doc + + +def get_report_result(report, filters): + res = None + + if report.report_type == "Query Report": + res = report.execute_query_report(filters) + + elif report.report_type == "Script Report": + res = report.execute_script_report(filters) + + elif report.report_type == "Custom Report": + ref_report = get_report_doc(report.report_name) + res = get_report_result(ref_report, filters) + + return res + + +@influxframework.read_only() +def generate_report_result( + report, filters=None, user=None, custom_columns=None, is_tree=False, parent_field=None +): + user = user or influxframework.session.user + filters = filters or [] + + if filters and isinstance(filters, str): + filters = json.loads(filters) + + res = get_report_result(report, filters) or [] + + columns, result, message, chart, report_summary, skip_total_row = ljust_list(res, 6) + columns = [get_column_as_dict(col) for col in (columns or [])] + report_column_names = [col["fieldname"] for col in columns] + + # convert to list of dicts + result = normalize_result(result, columns) + + if report.custom_columns: + # saved columns (with custom columns / with different column order) + columns = report.custom_columns + + # unsaved custom_columns + if custom_columns: + for custom_column in custom_columns: + columns.insert(custom_column["insert_after_index"] + 1, custom_column) + + # all columns which are not in original report + report_custom_columns = [ + column for column in columns if column["fieldname"] not in report_column_names + ] + + if report_custom_columns: + result = add_custom_column_data(report_custom_columns, result) + + if result: + result = get_filtered_data(report.ref_doctype, columns, result, user) + + if cint(report.add_total_row) and result and not skip_total_row: + result = add_total_row(result, columns, is_tree=is_tree, parent_field=parent_field) + + return { + "result": result, + "columns": columns, + "message": message, + "chart": chart, + "report_summary": report_summary, + "skip_total_row": skip_total_row or 0, + "status": None, + "execution_time": influxframework.cache().hget("report_execution_time", report.name) or 0, + } + + +def normalize_result(result, columns): + # Converts to list of dicts from list of lists/tuples + data = [] + column_names = [column["fieldname"] for column in columns] + if result and isinstance(result[0], (list, tuple)): + for row in result: + row_obj = {} + for idx, column_name in enumerate(column_names): + row_obj[column_name] = row[idx] + data.append(row_obj) + else: + data = result + + return data + + +@influxframework.whitelist() +def background_enqueue_run(report_name, filters=None, user=None): + """run reports in background""" + if not user: + user = influxframework.session.user + report = get_report_doc(report_name) + track_instance = influxframework.get_doc( + { + "doctype": "Prepared Report", + "report_name": report_name, + # This looks like an insanity but, without this it'd be very hard to find Prepared Reports matching given condition + # We're ensuring that spacing is consistent. e.g. JS seems to put no spaces after ":", Python on the other hand does. + "filters": json.dumps(json.loads(filters)), + "ref_report_doctype": report_name, + "report_type": report.report_type, + "query": report.query, + "module": report.module, + } + ) + track_instance.insert(ignore_permissions=True) + influxframework.db.commit() + track_instance.enqueue_report() + + return { + "name": track_instance.name, + "redirect_url": get_url_to_form("Prepared Report", track_instance.name), + } + + +@influxframework.whitelist() +def get_script(report_name): + report = get_report_doc(report_name) + module = report.module or influxframework.db.get_value("DocType", report.ref_doctype, "module") + + is_custom_module = influxframework.get_cached_value("Module Def", module, "custom") + + # custom modules are virtual modules those exists in DB but not in disk. + module_path = "" if is_custom_module else get_module_path(module) + report_folder = module_path and os.path.join(module_path, "report", scrub(report.name)) + script_path = report_folder and os.path.join(report_folder, scrub(report.name) + ".js") + print_path = report_folder and os.path.join(report_folder, scrub(report.name) + ".html") + + script = None + if os.path.exists(script_path): + with open(script_path) as f: + script = f.read() + script += f"\n\n//# sourceURL={scrub(report.name)}.js" + + html_format = get_html_format(print_path) + + if not script and report.javascript: + script = report.javascript + script += f"\n\n//# sourceURL={scrub(report.name)}__custom" + + if not script: + script = "influxframework.query_reports['%s']={}" % report_name + + # load translations + if influxframework.lang != "en": + send_translations(influxframework.get_lang_dict("report", report_name)) + + return { + "script": render_include(script), + "html_format": html_format, + "execution_time": influxframework.cache().hget("report_execution_time", report_name) or 0, + } + + +@influxframework.whitelist() +@influxframework.read_only() +def run( + report_name, + filters=None, + user=None, + ignore_prepared_report=False, + custom_columns=None, + is_tree=False, + parent_field=None, +): + report = get_report_doc(report_name) + if not user: + user = influxframework.session.user + if not influxframework.has_permission(report.ref_doctype, "report"): + influxframework.msgprint( + _("Must have report permission to access this report."), + raise_exception=True, + ) + + result = None + + if ( + report.prepared_report + and not report.disable_prepared_report + and not ignore_prepared_report + and not custom_columns + ): + if filters: + if isinstance(filters, str): + filters = json.loads(filters) + + dn = filters.get("prepared_report_name") + filters.pop("prepared_report_name", None) + else: + dn = "" + result = get_prepared_report_result(report, filters, dn, user) + else: + result = generate_report_result(report, filters, user, custom_columns, is_tree, parent_field) + add_data_to_monitor(report=report.reference_report or report.name) + + result["add_total_row"] = report.add_total_row and not result.get("skip_total_row", False) + + return result + + +def add_custom_column_data(custom_columns, result): + custom_column_data = get_data_for_custom_report(custom_columns) + + for column in custom_columns: + key = (column.get("doctype"), column.get("fieldname")) + if key in custom_column_data: + for row in result: + row_reference = row.get(column.get("link_field")) + # possible if the row is empty + if not row_reference: + continue + row[column.get("fieldname")] = custom_column_data.get(key).get(row_reference) + + return result + + +def get_prepared_report_result(report, filters, dn="", user=None): + latest_report_data = {} + doc = None + if dn: + # Get specified dn + doc = influxframework.get_doc("Prepared Report", dn) + else: + # Only look for completed prepared reports with given filters. + doc_list = influxframework.get_all( + "Prepared Report", + filters={ + "status": "Completed", + "filters": json.dumps(filters), + "owner": user, + "report_name": report.get("custom_report") or report.get("report_name"), + }, + order_by="creation desc", + ) + + if doc_list: + # Get latest + doc = influxframework.get_doc("Prepared Report", doc_list[0]) + + if doc: + try: + # Prepared Report data is stored in a GZip compressed JSON file + attached_file_name = influxframework.db.get_value( + "File", + {"attached_to_doctype": doc.doctype, "attached_to_name": doc.name}, + "name", + ) + attached_file = influxframework.get_doc("File", attached_file_name) + compressed_content = attached_file.get_content() + uncompressed_content = gzip_decompress(compressed_content) + data = json.loads(uncompressed_content.decode("utf-8")) + if data: + columns = json.loads(doc.columns) if doc.columns else data[0] + + for column in columns: + if isinstance(column, dict) and column.get("label"): + column["label"] = _(column["label"]) + + latest_report_data = {"columns": columns, "result": data} + except Exception: + doc.log_error("Prepared report failed") + influxframework.delete_doc("Prepared Report", doc.name) + influxframework.db.commit() + doc = None + + latest_report_data.update({"prepared_report": True, "doc": doc}) + + return latest_report_data + + +@influxframework.whitelist() +def export_query(): + """export from query reports""" + data = influxframework._dict(influxframework.local.form_dict) + data.pop("cmd", None) + data.pop("csrf_token", None) + + if isinstance(data.get("filters"), str): + filters = json.loads(data["filters"]) + + if data.get("report_name"): + report_name = data["report_name"] + influxframework.permissions.can_export( + influxframework.get_cached_value("Report", report_name, "ref_doctype"), + raise_exception=True, + ) + + file_format_type = data.get("file_format_type") + custom_columns = influxframework.parse_json(data.get("custom_columns", "[]")) + include_indentation = data.get("include_indentation") + visible_idx = data.get("visible_idx") + + if isinstance(visible_idx, str): + visible_idx = json.loads(visible_idx) + + if file_format_type == "Excel": + data = run(report_name, filters, custom_columns=custom_columns) + data = influxframework._dict(data) + if not data.columns: + influxframework.respond_as_web_page( + _("No data to export"), + _("You can try changing the filters of your report."), + ) + return + + from influxframework.utils.xlsxutils import make_xlsx + + format_duration_fields(data) + xlsx_data, column_widths = build_xlsx_data(data, visible_idx, include_indentation) + xlsx_file = make_xlsx(xlsx_data, "Query Report", column_widths=column_widths) + + influxframework.response["filename"] = report_name + ".xlsx" + influxframework.response["filecontent"] = xlsx_file.getvalue() + influxframework.response["type"] = "binary" + + +def format_duration_fields(data: influxframework._dict) -> None: + for i, col in enumerate(data.columns): + if col.get("fieldtype") != "Duration": + continue + + for row in data.result: + index = col.fieldname if isinstance(row, dict) else i + if row[index]: + row[index] = format_duration(row[index]) + + +def build_xlsx_data(data, visible_idx, include_indentation, ignore_visible_idx=False): + EXCEL_TYPES = ( + str, + bool, + type(None), + int, + float, + datetime.datetime, + datetime.date, + datetime.time, + datetime.timedelta, + ) + + result = [[]] + column_widths = [] + + for column in data.columns: + if column.get("hidden"): + continue + result[0].append(_(column.get("label"))) + column_width = cint(column.get("width", 0)) + # to convert into scale accepted by openpyxl + column_width /= 10 + column_widths.append(column_width) + + # build table from result + for row_idx, row in enumerate(data.result): + # only pick up rows that are visible in the report + if ignore_visible_idx or row_idx in visible_idx: + row_data = [] + if isinstance(row, dict): + for col_idx, column in enumerate(data.columns): + if column.get("hidden"): + continue + label = column.get("label") + fieldname = column.get("fieldname") + cell_value = row.get(fieldname, row.get(label, "")) + if not isinstance(cell_value, EXCEL_TYPES): + cell_value = cstr(cell_value) + + if cint(include_indentation) and "indent" in row and col_idx == 0: + cell_value = (" " * cint(row["indent"])) + cstr(cell_value) + row_data.append(cell_value) + elif row: + row_data = row + + result.append(row_data) + + return result, column_widths + + +def add_total_row(result, columns, meta=None, is_tree=False, parent_field=None): + total_row = [""] * len(columns) + has_percent = [] + + for i, col in enumerate(columns): + fieldtype, options, fieldname = None, None, None + if isinstance(col, str): + if meta: + # get fieldtype from the meta + field = meta.get_field(col) + if field: + fieldtype = meta.get_field(col).fieldtype + fieldname = meta.get_field(col).fieldname + else: + col = col.split(":") + if len(col) > 1: + if col[1]: + fieldtype = col[1] + if "/" in fieldtype: + fieldtype, options = fieldtype.split("/") + else: + fieldtype = "Data" + else: + fieldtype = col.get("fieldtype") + fieldname = col.get("fieldname") + options = col.get("options") + + for row in result: + if i >= len(row): + continue + cell = row.get(fieldname) if isinstance(row, dict) else row[i] + if fieldtype in ["Currency", "Int", "Float", "Percent", "Duration"] and flt(cell): + if not (is_tree and row.get(parent_field)): + total_row[i] = flt(total_row[i]) + flt(cell) + + if fieldtype == "Percent" and i not in has_percent: + has_percent.append(i) + + if fieldtype == "Time" and cell: + if not total_row[i]: + total_row[i] = timedelta(hours=0, minutes=0, seconds=0) + total_row[i] = total_row[i] + cell + + if fieldtype == "Link" and options == "Currency": + total_row[i] = result[0].get(fieldname) if isinstance(result[0], dict) else result[0][i] + + for i in has_percent: + total_row[i] = flt(total_row[i]) / len(result) + + first_col_fieldtype = None + if isinstance(columns[0], str): + first_col = columns[0].split(":") + if len(first_col) > 1: + first_col_fieldtype = first_col[1].split("/")[0] + else: + first_col_fieldtype = columns[0].get("fieldtype") + + if first_col_fieldtype not in ["Currency", "Int", "Float", "Percent", "Date"]: + total_row[0] = _("Total") + + result.append(total_row) + return result + + +@influxframework.whitelist() +def get_data_for_custom_field(doctype, field): + + if not influxframework.has_permission(doctype, "read"): + influxframework.throw(_("Not Permitted"), influxframework.PermissionError) + + value_map = influxframework._dict(influxframework.get_all(doctype, fields=["name", field], as_list=1)) + + return value_map + + +def get_data_for_custom_report(columns): + doc_field_value_map = {} + + for column in columns: + if column.get("link_field"): + fieldname = column.get("fieldname") + doctype = column.get("doctype") + doc_field_value_map[(doctype, fieldname)] = get_data_for_custom_field(doctype, fieldname) + + return doc_field_value_map + + +@influxframework.whitelist() +def save_report(reference_report, report_name, columns): + report_doc = get_report_doc(reference_report) + + docname = influxframework.db.exists( + "Report", + { + "report_name": report_name, + "is_standard": "No", + "report_type": "Custom Report", + }, + ) + + if docname: + report = influxframework.get_doc("Report", docname) + existing_jd = json.loads(report.json) + existing_jd["columns"] = json.loads(columns) + report.update({"json": json.dumps(existing_jd, separators=(",", ":"))}) + report.save() + influxframework.msgprint(_("Report updated successfully")) + + return docname + else: + new_report = influxframework.get_doc( + { + "doctype": "Report", + "report_name": report_name, + "json": f'{{"columns":{columns}}}', + "ref_doctype": report_doc.ref_doctype, + "is_standard": "No", + "report_type": "Custom Report", + "reference_report": reference_report, + } + ).insert(ignore_permissions=True) + influxframework.msgprint(_("{0} saved successfully").format(new_report.name)) + return new_report.name + + +def get_filtered_data(ref_doctype, columns, data, user): + result = [] + linked_doctypes = get_linked_doctypes(columns, data) + match_filters_per_doctype = get_user_match_filters(linked_doctypes, user=user) + shared = influxframework.share.get_shared(ref_doctype, user) + columns_dict = get_columns_dict(columns) + + role_permissions = get_role_permissions(influxframework.get_meta(ref_doctype), user) + if_owner = role_permissions.get("if_owner", {}).get("report") + + if match_filters_per_doctype: + for row in data: + # Why linked_doctypes.get(ref_doctype)? because if column is empty, linked_doctypes[ref_doctype] is removed + if linked_doctypes.get(ref_doctype) and shared and row[linked_doctypes[ref_doctype]] in shared: + result.append(row) + + elif has_match( + row, + linked_doctypes, + match_filters_per_doctype, + ref_doctype, + if_owner, + columns_dict, + user, + ): + result.append(row) + else: + result = list(data) + + return result + + +def has_match( + row, + linked_doctypes, + doctype_match_filters, + ref_doctype, + if_owner, + columns_dict, + user, +): + """Returns True if after evaluating permissions for each linked doctype + - There is an owner match for the ref_doctype + - `and` There is a user permission match for all linked doctypes + + Returns True if the row is empty + + Note: + Each doctype could have multiple conflicting user permission doctypes. + Hence even if one of the sets allows a match, it is true. + This behavior is equivalent to the trickling of user permissions of linked doctypes to the ref doctype. + """ + resultant_match = True + + if not row: + # allow empty rows :) + return resultant_match + + for doctype, filter_list in doctype_match_filters.items(): + matched_for_doctype = False + + if doctype == ref_doctype and if_owner: + idx = linked_doctypes.get("User") + if idx is not None and row[idx] == user and columns_dict[idx] == columns_dict.get("owner"): + # owner match is true + matched_for_doctype = True + + if not matched_for_doctype: + for match_filters in filter_list: + match = True + for dt, idx in linked_doctypes.items(): + # case handled above + if dt == "User" and columns_dict[idx] == columns_dict.get("owner"): + continue + + cell_value = None + if isinstance(row, dict): + cell_value = row.get(idx) + elif isinstance(row, (list, tuple)): + cell_value = row[idx] + + if ( + dt in match_filters + and cell_value not in match_filters.get(dt) + and influxframework.db.exists(dt, cell_value) + ): + match = False + break + + # each doctype could have multiple conflicting user permission doctypes, hence using OR + # so that even if one of the sets allows a match, it is true + matched_for_doctype = matched_for_doctype or match + + if matched_for_doctype: + break + + # each doctype's user permissions should match the row! hence using AND + resultant_match = resultant_match and matched_for_doctype + + if not resultant_match: + break + + return resultant_match + + +def get_linked_doctypes(columns, data): + linked_doctypes = {} + + columns_dict = get_columns_dict(columns) + + for idx, col in enumerate(columns): + df = columns_dict[idx] + if df.get("fieldtype") == "Link": + if data and isinstance(data[0], (list, tuple)): + linked_doctypes[df["options"]] = idx + else: + # dict + linked_doctypes[df["options"]] = df["fieldname"] + + # remove doctype if column is empty + columns_with_value = [] + for row in data: + if row: + if len(row) != len(columns_with_value): + if isinstance(row, (list, tuple)): + row = enumerate(row) + elif isinstance(row, dict): + row = row.items() + + for col, val in row: + if val and col not in columns_with_value: + columns_with_value.append(col) + + items = list(linked_doctypes.items()) + + for doctype, key in items: + if key not in columns_with_value: + del linked_doctypes[doctype] + + return linked_doctypes + + +def get_columns_dict(columns): + """Returns a dict with column docfield values as dict + The keys for the dict are both idx and fieldname, + so either index or fieldname can be used to search for a column's docfield properties + """ + columns_dict = influxframework._dict() + for idx, col in enumerate(columns): + col_dict = get_column_as_dict(col) + columns_dict[idx] = col_dict + columns_dict[col_dict["fieldname"]] = col_dict + + return columns_dict + + +def get_column_as_dict(col): + col_dict = influxframework._dict() + + # string + if isinstance(col, str): + col = col.split(":") + if len(col) > 1: + if "/" in col[1]: + col_dict["fieldtype"], col_dict["options"] = col[1].split("/") + else: + col_dict["fieldtype"] = col[1] + if len(col) == 3: + col_dict["width"] = col[2] + + col_dict["label"] = col[0] + col_dict["fieldname"] = influxframework.scrub(col[0]) + + # dict + else: + col_dict.update(col) + if "fieldname" not in col_dict: + col_dict["fieldname"] = influxframework.scrub(col_dict["label"]) + + return col_dict + + +def get_user_match_filters(doctypes, user): + match_filters = {} + + for dt in doctypes: + filter_list = influxframework.desk.reportview.build_match_conditions(dt, user, False) + if filter_list: + match_filters[dt] = filter_list + + return match_filters diff --git a/influxframework/desk/report/__init__.py b/influxframework/desk/report/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/report/todo/__init__.py b/influxframework/desk/report/todo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/desk/report/todo/todo.js b/influxframework/desk/report/todo/todo.js new file mode 100644 index 0000000..8b6d3c0 --- /dev/null +++ b/influxframework/desk/report/todo/todo.js @@ -0,0 +1,7 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt +/* eslint-disable */ + +influxframework.query_reports["ToDo"] = { + filters: [], +}; diff --git a/influxframework/desk/report/todo/todo.json b/influxframework/desk/report/todo/todo.json new file mode 100644 index 0000000..b42c4c9 --- /dev/null +++ b/influxframework/desk/report/todo/todo.json @@ -0,0 +1,23 @@ +{ + "add_total_row": 0, + "apply_user_permissions": 1, + "creation": "2013-02-25 14:26:30", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 3, + "is_standard": "Yes", + "modified": "2017-06-21 18:18:50.748793", + "modified_by": "Administrator", + "module": "Desk", + "name": "ToDo", + "owner": "Administrator", + "ref_doctype": "ToDo", + "report_name": "ToDo", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + } + ] +} \ No newline at end of file diff --git a/influxframework/desk/report/todo/todo.py b/influxframework/desk/report/todo/todo.py new file mode 100644 index 0000000..7a677ab --- /dev/null +++ b/influxframework/desk/report/todo/todo.py @@ -0,0 +1,69 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework import _ +from influxframework.utils import getdate + + +def execute(filters=None): + priority_map = {"High": 3, "Medium": 2, "Low": 1} + + todo_list = influxframework.get_list( + "ToDo", + fields=[ + "name", + "date", + "description", + "priority", + "reference_type", + "reference_name", + "assigned_by", + "owner", + ], + filters={"status": "Open"}, + ) + + todo_list.sort( + key=lambda todo: ( + priority_map.get(todo.priority, 0), + todo.date and getdate(todo.date) or getdate("1900-01-01"), + ), + reverse=True, + ) + + columns = [ + _("ID") + ":Link/ToDo:90", + _("Priority") + "::60", + _("Date") + ":Date", + _("Description") + "::150", + _("Assigned To/Owner") + ":Data:120", + _("Assigned By") + ":Data:120", + _("Reference") + "::200", + ] + + result = [] + for todo in todo_list: + if todo.owner == influxframework.session.user or todo.assigned_by == influxframework.session.user: + if todo.reference_type: + todo.reference = """{}: {}""".format( + todo.reference_type, + todo.reference_name, + todo.reference_type, + todo.reference_name, + ) + else: + todo.reference = None + result.append( + [ + todo.name, + todo.priority, + todo.date, + todo.description, + todo.owner, + todo.assigned_by, + todo.reference, + ] + ) + + return columns, result diff --git a/influxframework/desk/report_dump.py b/influxframework/desk/report_dump.py new file mode 100644 index 0000000..b899be3 --- /dev/null +++ b/influxframework/desk/report_dump.py @@ -0,0 +1,107 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + + +import copy +import json + +import influxframework + + +@influxframework.whitelist() +def get_data(doctypes, last_modified): + data_map = {} + for dump_report_map in influxframework.get_hooks().dump_report_map: + data_map.update(influxframework.get_attr(dump_report_map)) + + out = {} + + doctypes = json.loads(doctypes) + last_modified = json.loads(last_modified) + + for d in doctypes: + args = copy.deepcopy(data_map[d]) + dt = d.find("[") != -1 and d[: d.find("[")] or d + out[dt] = {} + + if args.get("from"): + modified_table = "item." + else: + modified_table = "" + + conditions = order_by = "" + table = args.get("from") or ("`tab%s`" % dt) + + if d in last_modified: + if not args.get("conditions"): + args["conditions"] = [] + args["conditions"].append(modified_table + "modified > '" + last_modified[d] + "'") + out[dt]["modified_names"] = influxframework.db.sql_list( + """select %sname from %s + where %smodified > %s""" + % (modified_table, table, modified_table, "%s"), + last_modified[d], + ) + + if args.get("force_index"): + conditions = " force index (%s) " % args["force_index"] + if args.get("conditions"): + conditions += " where " + " and ".join(args["conditions"]) + if args.get("order_by"): + order_by = " order by " + args["order_by"] + + out[dt]["data"] = [ + list(t) + for t in influxframework.db.sql( + """select {} from {} {} {}""".format(",".join(args["columns"]), table, conditions, order_by) + ) + ] + + # last modified + modified_table = table + if "," in table: + modified_table = " ".join(table.split(",")[0].split(" ")[:-1]) + + tmp = influxframework.db.sql( + """select `modified` + from %s order by modified desc limit 1""" + % modified_table + ) + out[dt]["last_modified"] = tmp and tmp[0][0] or "" + out[dt]["columns"] = list(map(lambda c: c.split(" as ")[-1], args["columns"])) + + if args.get("links"): + out[dt]["links"] = args["links"] + + for d in out: + unused_links = [] + # only compress full dumps (not partial) + if out[d].get("links") and (d not in last_modified): + for link_key in out[d]["links"]: + link = out[d]["links"][link_key] + if link[0] in out and (link[0] not in last_modified): + + # make a map of link ids + # to index + link_map = {} + doctype_data = out[link[0]] + + col_idx = doctype_data["columns"].index(link[1]) + for row_idx in range(len(doctype_data["data"])): + row = doctype_data["data"][row_idx] + link_map[row[col_idx]] = row_idx + + for row in out[d]["data"]: + columns = list(out[d]["columns"]) + if link_key in columns: + col_idx = columns.index(link_key) + # replace by id + if row[col_idx]: + row[col_idx] = link_map.get(row[col_idx]) + else: + unused_links.append(link_key) + + for link in unused_links: + del out[d]["links"][link] + + return out diff --git a/influxframework/desk/reportview.py b/influxframework/desk/reportview.py new file mode 100644 index 0000000..4eebd27 --- /dev/null +++ b/influxframework/desk/reportview.py @@ -0,0 +1,733 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +"""build query for doclistview and return results""" + +import json +from io import StringIO + +import influxframework +import influxframework.permissions +from influxframework import _ +from influxframework.core.doctype.access_log.access_log import make_access_log +from influxframework.model import child_table_fields, default_fields, optional_fields +from influxframework.model.base_document import get_controller +from influxframework.model.db_query import DatabaseQuery +from influxframework.model.utils import is_virtual_doctype +from influxframework.utils import add_user_info, cstr, format_duration + + +@influxframework.whitelist() +@influxframework.read_only() +def get(): + args = get_form_params() + # If virtual doctype get data from controller het_list method + if is_virtual_doctype(args.doctype): + controller = get_controller(args.doctype) + data = compress(controller.get_list(args)) + else: + data = compress(execute(**args), args=args) + return data + + +@influxframework.whitelist() +@influxframework.read_only() +def get_list(): + args = get_form_params() + + if is_virtual_doctype(args.doctype): + controller = get_controller(args.doctype) + data = controller.get_list(args) + else: + # uncompressed (refactored from influxframework.model.db_query.get_list) + data = execute(**args) + + return data + + +@influxframework.whitelist() +@influxframework.read_only() +def get_count(): + args = get_form_params() + + if is_virtual_doctype(args.doctype): + controller = get_controller(args.doctype) + data = controller.get_count(args) + else: + distinct = "distinct " if args.distinct == "true" else "" + args.fields = [f"count({distinct}`tab{args.doctype}`.name) as total_count"] + data = execute(**args)[0].get("total_count") + + return data + + +def execute(doctype, *args, **kwargs): + return DatabaseQuery(doctype).execute(*args, **kwargs) + + +def get_form_params(): + """Stringify GET request parameters.""" + data = influxframework._dict(influxframework.local.form_dict) + clean_params(data) + validate_args(data) + return data + + +def validate_args(data): + parse_json(data) + setup_group_by(data) + + validate_fields(data) + if data.filters: + validate_filters(data, data.filters) + if data.or_filters: + validate_filters(data, data.or_filters) + + data.strict = None + + return data + + +def validate_fields(data): + wildcard = update_wildcard_field_param(data) + + for field in data.fields or []: + fieldname = extract_fieldname(field) + if is_standard(fieldname): + continue + + meta, df = get_meta_and_docfield(fieldname, data) + + if not df: + if wildcard: + continue + else: + raise_invalid_field(fieldname) + + # remove the field from the query if the report hide flag is set and current view is Report + if df.report_hide and data.view == "Report": + data.fields.remove(field) + continue + + if df.fieldname in [_df.fieldname for _df in meta.get_high_permlevel_fields()]: + if df.get("permlevel") not in meta.get_permlevel_access(parenttype=data.doctype): + data.fields.remove(field) + + +def validate_filters(data, filters): + if isinstance(filters, list): + # filters as list + for condition in filters: + if len(condition) == 3: + # [fieldname, condition, value] + fieldname = condition[0] + if is_standard(fieldname): + continue + meta, df = get_meta_and_docfield(fieldname, data) + if not df: + raise_invalid_field(condition[0]) + else: + # [doctype, fieldname, condition, value] + fieldname = condition[1] + if is_standard(fieldname): + continue + meta = influxframework.get_meta(condition[0]) + if not meta.get_field(fieldname): + raise_invalid_field(fieldname) + + else: + for fieldname in filters: + if is_standard(fieldname): + continue + meta, df = get_meta_and_docfield(fieldname, data) + if not df: + raise_invalid_field(fieldname) + + +def setup_group_by(data): + """Add columns for aggregated values e.g. count(name)""" + if data.group_by and data.aggregate_function: + if data.aggregate_function.lower() not in ("count", "sum", "avg"): + influxframework.throw(_("Invalid aggregate function")) + + if influxframework.db.has_column(data.aggregate_on_doctype, data.aggregate_on_field): + data.fields.append( + "{aggregate_function}(`tab{aggregate_on_doctype}`.`{aggregate_on_field}`) AS _aggregate_column".format( + **data + ) + ) + if data.aggregate_on_field: + data.fields.append(f"`tab{data.aggregate_on_doctype}`.`{data.aggregate_on_field}`") + else: + raise_invalid_field(data.aggregate_on_field) + + data.pop("aggregate_on_doctype") + data.pop("aggregate_on_field") + data.pop("aggregate_function") + + +def raise_invalid_field(fieldname): + influxframework.throw(_("Field not permitted in query") + f": {fieldname}", influxframework.DataError) + + +def is_standard(fieldname): + if "." in fieldname: + fieldname = fieldname.split(".")[1].strip("`") + return ( + fieldname in default_fields or fieldname in optional_fields or fieldname in child_table_fields + ) + + +def extract_fieldname(field): + for text in (",", "/*", "#"): + if text in field: + raise_invalid_field(field) + + fieldname = field + for sep in (" as ", " AS "): + if sep in fieldname: + fieldname = fieldname.split(sep)[0] + + # certain functions allowed, extract the fieldname from the function + if fieldname.startswith("count(") or fieldname.startswith("sum(") or fieldname.startswith("avg("): + if not fieldname.strip().endswith(")"): + raise_invalid_field(field) + fieldname = fieldname.split("(", 1)[1][:-1] + + return fieldname + + +def get_meta_and_docfield(fieldname, data): + parenttype, fieldname = get_parenttype_and_fieldname(fieldname, data) + meta = influxframework.get_meta(parenttype) + df = meta.get_field(fieldname) + return meta, df + + +def update_wildcard_field_param(data): + if (isinstance(data.fields, str) and data.fields == "*") or ( + isinstance(data.fields, (list, tuple)) and len(data.fields) == 1 and data.fields[0] == "*" + ): + data.fields = influxframework.db.get_table_columns(data.doctype) + return True + + return False + + +def clean_params(data): + for param in ("cmd", "data", "ignore_permissions", "view", "user", "csrf_token", "join"): + data.pop(param, None) + + +def parse_json(data): + if isinstance(data.get("filters"), str): + data["filters"] = json.loads(data["filters"]) + if isinstance(data.get("or_filters"), str): + data["or_filters"] = json.loads(data["or_filters"]) + if isinstance(data.get("fields"), str): + data["fields"] = ["*"] if data["fields"] == "*" else json.loads(data["fields"]) + if isinstance(data.get("docstatus"), str): + data["docstatus"] = json.loads(data["docstatus"]) + if isinstance(data.get("save_user_settings"), str): + data["save_user_settings"] = json.loads(data["save_user_settings"]) + else: + data["save_user_settings"] = True + + +def get_parenttype_and_fieldname(field, data): + if "." in field: + parts = field.split(".") + parenttype = parts[0] + fieldname = parts[1] + if parenttype.startswith("`tab"): + # `tabChild DocType`.`fieldname` + parenttype = parenttype[4:-1] + fieldname = fieldname.strip("`") + else: + # tablefield.fieldname + parenttype = influxframework.get_meta(data.doctype).get_field(parenttype).options + else: + parenttype = data.doctype + fieldname = field.strip("`") + + return parenttype, fieldname + + +def compress(data, args=None): + """separate keys and values""" + from influxframework.desk.query_report import add_total_row + + user_info = {} + + if not data: + return data + if args is None: + args = {} + values = [] + keys = list(data[0]) + for row in data: + new_row = [] + for key in keys: + new_row.append(row.get(key)) + values.append(new_row) + + # add user info for assignments (avatar) + if row.get("_assign", ""): + for user in json.loads(row._assign): + add_user_info(user, user_info) + + if args.get("add_total_row"): + meta = influxframework.get_meta(args.doctype) + values = add_total_row(values, keys, meta) + + return {"keys": keys, "values": values, "user_info": user_info} + + +@influxframework.whitelist() +def save_report(name, doctype, report_settings): + """Save reports of type Report Builder from Report View""" + + if influxframework.db.exists("Report", name): + report = influxframework.get_doc("Report", name) + if report.is_standard == "Yes": + influxframework.throw(_("Standard Reports cannot be edited")) + + if report.report_type != "Report Builder": + influxframework.throw(_("Only reports of type Report Builder can be edited")) + + if report.owner != influxframework.session.user and not influxframework.has_permission("Report", "write"): + influxframework.throw(_("Insufficient Permissions for editing Report"), influxframework.PermissionError) + else: + report = influxframework.new_doc("Report") + report.report_name = name + report.ref_doctype = doctype + + report.report_type = "Report Builder" + report.json = report_settings + report.save(ignore_permissions=True) + influxframework.msgprint( + _("Report {0} saved").format(influxframework.bold(report.name)), + indicator="green", + alert=True, + ) + return report.name + + +@influxframework.whitelist() +def delete_report(name): + """Delete reports of type Report Builder from Report View""" + + report = influxframework.get_doc("Report", name) + if report.is_standard == "Yes": + influxframework.throw(_("Standard Reports cannot be deleted")) + + if report.report_type != "Report Builder": + influxframework.throw(_("Only reports of type Report Builder can be deleted")) + + if report.owner != influxframework.session.user and not influxframework.has_permission("Report", "delete"): + influxframework.throw(_("Insufficient Permissions for deleting Report"), influxframework.PermissionError) + + report.delete(ignore_permissions=True) + influxframework.msgprint( + _("Report {0} deleted").format(influxframework.bold(report.name)), + indicator="green", + alert=True, + ) + + +@influxframework.whitelist() +@influxframework.read_only() +def export_query(): + """export from report builder""" + title = influxframework.form_dict.title + influxframework.form_dict.pop("title", None) + + form_params = get_form_params() + form_params["limit_page_length"] = None + form_params["as_list"] = True + doctype = form_params.doctype + add_totals_row = None + file_format_type = form_params["file_format_type"] + title = title or doctype + + del form_params["doctype"] + del form_params["file_format_type"] + + if "add_totals_row" in form_params and form_params["add_totals_row"] == "1": + add_totals_row = 1 + del form_params["add_totals_row"] + + influxframework.permissions.can_export(doctype, raise_exception=True) + + if "selected_items" in form_params: + si = json.loads(influxframework.form_dict.get("selected_items")) + form_params["filters"] = {"name": ("in", si)} + del form_params["selected_items"] + + make_access_log( + doctype=doctype, + file_type=file_format_type, + report_name=form_params.report_name, + filters=form_params.filters, + ) + + db_query = DatabaseQuery(doctype) + ret = db_query.execute(**form_params) + + if add_totals_row: + ret = append_totals_row(ret) + + data = [[_("Sr")] + get_labels(db_query.fields, doctype)] + for i, row in enumerate(ret): + data.append([i + 1] + list(row)) + + data = handle_duration_fieldtype_values(doctype, data, db_query.fields) + + if file_format_type == "CSV": + + # convert to csv + import csv + + from influxframework.utils.xlsxutils import handle_html + + f = StringIO() + writer = csv.writer(f) + for r in data: + # encode only unicode type strings and not int, floats etc. + writer.writerow([handle_html(influxframework.as_unicode(v)) if isinstance(v, str) else v for v in r]) + + f.seek(0) + influxframework.response["result"] = cstr(f.read()) + influxframework.response["type"] = "csv" + influxframework.response["doctype"] = title + + elif file_format_type == "Excel": + + from influxframework.utils.xlsxutils import make_xlsx + + xlsx_file = make_xlsx(data, doctype) + + influxframework.response["filename"] = title + ".xlsx" + influxframework.response["filecontent"] = xlsx_file.getvalue() + influxframework.response["type"] = "binary" + + +def append_totals_row(data): + if not data: + return data + data = list(data) + totals = [] + totals.extend([""] * len(data[0])) + + for row in data: + for i in range(len(row)): + if isinstance(row[i], (float, int)): + totals[i] = (totals[i] or 0) + row[i] + + if not isinstance(totals[0], (int, float)): + totals[0] = "Total" + + data.append(totals) + + return data + + +def get_labels(fields, doctype): + """get column labels based on column names""" + labels = [] + for key in fields: + key = key.split(" as ")[0] + + if key.startswith(("count(", "sum(", "avg(")): + continue + + if "." in key: + parenttype, fieldname = key.split(".")[0][4:-1], key.split(".")[1].strip("`") + else: + parenttype = doctype + fieldname = fieldname.strip("`") + + if parenttype == doctype and fieldname == "name": + label = _("ID", context="Label of name column in report") + else: + df = influxframework.get_meta(parenttype).get_field(fieldname) + label = _(df.label if df else fieldname.title()) + if parenttype != doctype: + # If the column is from a child table, append the child doctype. + # For example, "Item Code (Sales Invoice Item)". + label += f" ({ _(parenttype) })" + + labels.append(label) + + return labels + + +def handle_duration_fieldtype_values(doctype, data, fields): + for field in fields: + key = field.split(" as ")[0] + + if key.startswith(("count(", "sum(", "avg(")): + continue + + if "." in key: + parenttype, fieldname = key.split(".")[0][4:-1], key.split(".")[1].strip("`") + else: + parenttype = doctype + fieldname = field.strip("`") + + df = influxframework.get_meta(parenttype).get_field(fieldname) + + if df and df.fieldtype == "Duration": + index = fields.index(field) + 1 + for i in range(1, len(data)): + val_in_seconds = data[i][index] + if val_in_seconds: + duration_val = format_duration(val_in_seconds, df.hide_days) + data[i][index] = duration_val + return data + + +@influxframework.whitelist() +def delete_items(): + """delete selected items""" + import json + + items = sorted(json.loads(influxframework.form_dict.get("items")), reverse=True) + doctype = influxframework.form_dict.get("doctype") + + if len(items) > 10: + influxframework.enqueue("influxframework.desk.reportview.delete_bulk", doctype=doctype, items=items) + else: + delete_bulk(doctype, items) + + +def delete_bulk(doctype, items): + for i, d in enumerate(items): + try: + influxframework.delete_doc(doctype, d) + if len(items) >= 5: + influxframework.publish_realtime( + "progress", + dict(progress=[i + 1, len(items)], title=_("Deleting {0}").format(doctype), description=d), + user=influxframework.session.user, + ) + # Commit after successful deletion + influxframework.db.commit() + except Exception: + # rollback if any record failed to delete + # if not rollbacked, queries get committed on after_request method in app.py + influxframework.db.rollback() + + +@influxframework.whitelist() +@influxframework.read_only() +def get_sidebar_stats(stats, doctype, filters=None): + if filters is None: + filters = [] + + if is_virtual_doctype(doctype): + controller = get_controller(doctype) + args = {"stats": stats, "filters": filters} + data = controller.get_stats(args) + else: + data = get_stats(stats, doctype, filters) + + return {"stats": data} + + +@influxframework.whitelist() +@influxframework.read_only() +def get_stats(stats, doctype, filters=None): + """get tag info""" + import json + + if filters is None: + filters = [] + tags = json.loads(stats) + if filters: + filters = json.loads(filters) + stats = {} + + try: + columns = influxframework.db.get_table_columns(doctype) + except (influxframework.db.InternalError, influxframework.db.ProgrammingError): + # raised when _user_tags column is added on the fly + # raised if its a virtual doctype + columns = [] + + for tag in tags: + if tag not in columns: + continue + try: + tag_count = influxframework.get_list( + doctype, + fields=[tag, "count(*)"], + filters=filters + [[tag, "!=", ""]], + group_by=tag, + as_list=True, + distinct=1, + ) + + if tag == "_user_tags": + stats[tag] = scrub_user_tags(tag_count) + no_tag_count = influxframework.get_list( + doctype, + fields=[tag, "count(*)"], + filters=filters + [[tag, "in", ("", ",")]], + as_list=True, + group_by=tag, + order_by=tag, + ) + + no_tag_count = no_tag_count[0][1] if no_tag_count else 0 + + stats[tag].append([_("No Tags"), no_tag_count]) + else: + stats[tag] = tag_count + + except influxframework.db.SQLError: + pass + except influxframework.db.InternalError as e: + # raised when _user_tags column is added on the fly + pass + + return stats + + +@influxframework.whitelist() +def get_filter_dashboard_data(stats, doctype, filters=None): + """get tags info""" + import json + + tags = json.loads(stats) + filters = json.loads(filters or []) + stats = {} + + columns = influxframework.db.get_table_columns(doctype) + for tag in tags: + if not tag["name"] in columns: + continue + tagcount = [] + if tag["type"] not in ["Date", "Datetime"]: + tagcount = influxframework.get_list( + doctype, + fields=[tag["name"], "count(*)"], + filters=filters + ["ifnull(`%s`,'')!=''" % tag["name"]], + group_by=tag["name"], + as_list=True, + ) + + if tag["type"] not in [ + "Check", + "Select", + "Date", + "Datetime", + "Int", + "Float", + "Currency", + "Percent", + ] and tag["name"] not in ["docstatus"]: + stats[tag["name"]] = list(tagcount) + if stats[tag["name"]]: + data = [ + "No Data", + influxframework.get_list( + doctype, + fields=[tag["name"], "count(*)"], + filters=filters + ["({0} = '' or {0} is null)".format(tag["name"])], + as_list=True, + )[0][1], + ] + if data and data[1] != 0: + + stats[tag["name"]].append(data) + else: + stats[tag["name"]] = tagcount + + return stats + + +def scrub_user_tags(tagcount): + """rebuild tag list for tags""" + rdict = {} + tagdict = dict(tagcount) + for t in tagdict: + if not t: + continue + alltags = t.split(",") + for tag in alltags: + if tag: + if tag not in rdict: + rdict[tag] = 0 + + rdict[tag] += tagdict[t] + + rlist = [] + for tag in rdict: + rlist.append([tag, rdict[tag]]) + + return rlist + + +# used in building query in queries.py +def get_match_cond(doctype, as_condition=True): + cond = DatabaseQuery(doctype).build_match_conditions(as_condition=as_condition) + if not as_condition: + return cond + + return ((" and " + cond) if cond else "").replace("%", "%%") + + +def build_match_conditions(doctype, user=None, as_condition=True): + match_conditions = DatabaseQuery(doctype, user=user).build_match_conditions( + as_condition=as_condition + ) + if as_condition: + return match_conditions.replace("%", "%%") + return match_conditions + + +def get_filters_cond( + doctype, filters, conditions, ignore_permissions=None, with_match_conditions=False +): + if isinstance(filters, str): + filters = json.loads(filters) + + if filters: + flt = filters + if isinstance(filters, dict): + filters = filters.items() + flt = [] + for f in filters: + if isinstance(f[1], str) and f[1][0] == "!": + flt.append([doctype, f[0], "!=", f[1][1:]]) + elif isinstance(f[1], (list, tuple)) and f[1][0] in ( + ">", + "<", + ">=", + "<=", + "!=", + "like", + "not like", + "in", + "not in", + "between", + ): + + flt.append([doctype, f[0], f[1][0], f[1][1]]) + else: + flt.append([doctype, f[0], "=", f[1]]) + + query = DatabaseQuery(doctype) + query.filters = flt + query.conditions = conditions + + if with_match_conditions: + query.build_match_conditions() + + query.build_filter_conditions(flt, conditions, ignore_permissions) + + cond = " and " + " and ".join(query.conditions) + else: + cond = "" + return cond diff --git a/influxframework/desk/search.py b/influxframework/desk/search.py new file mode 100644 index 0000000..4b2d2bd --- /dev/null +++ b/influxframework/desk/search.py @@ -0,0 +1,369 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import functools +import json +import re + +import influxframework +from influxframework import _, is_whitelisted +from influxframework.permissions import has_permission +from influxframework.utils import cint, cstr, unique + + +def sanitize_searchfield(searchfield): + blacklisted_keywords = ["select", "delete", "drop", "update", "case", "and", "or", "like"] + + def _raise_exception(searchfield): + influxframework.throw(_("Invalid Search Field {0}").format(searchfield), influxframework.DataError) + + if len(searchfield) == 1: + # do not allow special characters to pass as searchfields + regex = re.compile(r'^.*[=;*,\'"$\-+%#@()_].*') + if regex.match(searchfield): + _raise_exception(searchfield) + + if len(searchfield) >= 3: + + # to avoid 1=1 + if "=" in searchfield: + _raise_exception(searchfield) + + # in mysql -- is used for commenting the query + elif " --" in searchfield: + _raise_exception(searchfield) + + # to avoid and, or and like + elif any(f" {keyword} " in searchfield.split() for keyword in blacklisted_keywords): + _raise_exception(searchfield) + + # to avoid select, delete, drop, update and case + elif any(keyword in searchfield.split() for keyword in blacklisted_keywords): + _raise_exception(searchfield) + + else: + regex = re.compile(r'^.*[=;*,\'"$\-+%#@()].*') + if any(regex.match(f) for f in searchfield.split()): + _raise_exception(searchfield) + + +# this is called by the Link Field +@influxframework.whitelist() +def search_link( + doctype, + txt, + query=None, + filters=None, + page_length=20, + searchfield=None, + reference_doctype=None, + ignore_user_permissions=False, +): + search_widget( + doctype, + txt.strip(), + query, + searchfield=searchfield, + page_length=page_length, + filters=filters, + reference_doctype=reference_doctype, + ignore_user_permissions=ignore_user_permissions, + ) + + influxframework.response["results"] = build_for_autosuggest(influxframework.response["values"], doctype=doctype) + del influxframework.response["values"] + + +# this is called by the search box +@influxframework.whitelist() +def search_widget( + doctype, + txt, + query=None, + searchfield=None, + start=0, + page_length=20, + filters=None, + filter_fields=None, + as_dict=False, + reference_doctype=None, + ignore_user_permissions=False, +): + + start = cint(start) + + if isinstance(filters, str): + filters = json.loads(filters) + + if searchfield: + sanitize_searchfield(searchfield) + + if not searchfield: + searchfield = "name" + + standard_queries = influxframework.get_hooks().standard_queries or {} + + if query and query.split()[0].lower() != "select": + # by method + try: + is_whitelisted(influxframework.get_attr(query)) + influxframework.response["values"] = influxframework.call( + query, doctype, txt, searchfield, start, page_length, filters, as_dict=as_dict + ) + except influxframework.exceptions.PermissionError as e: + if influxframework.local.conf.developer_mode: + raise e + else: + influxframework.respond_as_web_page( + title="Invalid Method", + html="Method not found", + indicator_color="red", + http_status_code=404, + ) + return + except Exception as e: + raise e + elif not query and doctype in standard_queries: + # from standard queries + search_widget( + doctype, txt, standard_queries[doctype][0], searchfield, start, page_length, filters + ) + else: + meta = influxframework.get_meta(doctype) + + if query: + influxframework.throw(_("This query style is discontinued")) + # custom query + # influxframework.response["values"] = influxframework.db.sql(scrub_custom_query(query, searchfield, txt)) + else: + if isinstance(filters, dict): + filters_items = filters.items() + filters = [] + for f in filters_items: + if isinstance(f[1], (list, tuple)): + filters.append([doctype, f[0], f[1][0], f[1][1]]) + else: + filters.append([doctype, f[0], "=", f[1]]) + + if filters is None: + filters = [] + or_filters = [] + + # build from doctype + if txt: + field_types = [ + "Data", + "Text", + "Small Text", + "Long Text", + "Link", + "Select", + "Read Only", + "Text Editor", + ] + search_fields = ["name"] + if meta.title_field: + search_fields.append(meta.title_field) + + if meta.search_fields: + search_fields.extend(meta.get_search_fields()) + + for f in search_fields: + fmeta = meta.get_field(f.strip()) + if not meta.translated_doctype and ( + f == "name" or (fmeta and fmeta.fieldtype in field_types) + ): + or_filters.append([doctype, f.strip(), "like", f"%{txt}%"]) + + if meta.get("fields", {"fieldname": "enabled", "fieldtype": "Check"}): + filters.append([doctype, "enabled", "=", 1]) + if meta.get("fields", {"fieldname": "disabled", "fieldtype": "Check"}): + filters.append([doctype, "disabled", "!=", 1]) + + # format a list of fields combining search fields and filter fields + fields = get_std_fields_list(meta, searchfield or "name") + if filter_fields: + fields = list(set(fields + json.loads(filter_fields))) + formatted_fields = [f"`tab{meta.name}`.`{f.strip()}`" for f in fields] + + # Insert title field query after name + if meta.show_title_field_in_link: + formatted_fields.insert(1, f"`tab{meta.name}`.{meta.title_field} as `label`") + + # In order_by, `idx` gets second priority, because it stores link count + from influxframework.model.db_query import get_order_by + + order_by_based_on_meta = get_order_by(doctype, meta) + # 2 is the index of _relevance column + order_by = f"{order_by_based_on_meta}, `tab{doctype}`.idx desc" + + if not meta.translated_doctype: + formatted_fields.append( + """locate({_txt}, `tab{doctype}`.`name`) as `_relevance`""".format( + _txt=influxframework.db.escape((txt or "").replace("%", "").replace("@", "")), + doctype=doctype, + ) + ) + order_by = f"_relevance, {order_by}" + + ptype = "select" if influxframework.only_has_select_perm(doctype) else "read" + ignore_permissions = ( + True + if doctype == "DocType" + else (cint(ignore_user_permissions) and has_permission(doctype, ptype=ptype)) + ) + + values = influxframework.get_list( + doctype, + filters=filters, + fields=formatted_fields, + or_filters=or_filters, + limit_start=start, + limit_page_length=None if meta.translated_doctype else page_length, + order_by=order_by, + ignore_permissions=ignore_permissions, + reference_doctype=reference_doctype, + as_list=not as_dict, + strict=False, + ) + + if meta.translated_doctype: + # Filtering the values array so that query is included in very element + values = ( + result + for result in values + if any( + re.search(f"{re.escape(txt)}.*", _(cstr(value)) or "", re.IGNORECASE) + for value in (result.values() if as_dict else result) + ) + ) + + # Sorting the values array so that relevant results always come first + # This will first bring elements on top in which query is a prefix of element + # Then it will bring the rest of the elements and sort them in lexicographical order + values = sorted(values, key=lambda x: relevance_sorter(x, txt, as_dict)) + + # remove _relevance from results + if not meta.translated_doctype: + if as_dict: + for r in values: + r.pop("_relevance") + else: + values = [r[:-1] for r in values] + + influxframework.response["values"] = values + + +def get_std_fields_list(meta, key): + # get additional search fields + sflist = ["name"] + if meta.search_fields: + for d in meta.search_fields.split(","): + if d.strip() not in sflist: + sflist.append(d.strip()) + + if meta.title_field and meta.title_field not in sflist: + sflist.append(meta.title_field) + + if key not in sflist: + sflist.append(key) + + return sflist + + +def build_for_autosuggest(res: list[tuple], doctype: str) -> list[dict]: + def to_string(parts): + return ", ".join( + unique(_(cstr(part)) if meta.translated_doctype else cstr(part) for part in parts if part) + ) + + results = [] + meta = influxframework.get_meta(doctype) + if meta.show_title_field_in_link: + for item in res: + item = list(item) + label = item[1] # use title as label + item[1] = item[0] # show name in description instead of title + del item[2] # remove redundant title ("label") value + results.append({"value": item[0], "label": label, "description": to_string(item[1:])}) + else: + results.extend({"value": item[0], "description": to_string(item[1:])} for item in res) + + return results + + +def scrub_custom_query(query, key, txt): + if "%(key)s" in query: + query = query.replace("%(key)s", key) + if "%s" in query: + query = query.replace("%s", ((txt or "") + "%")) + return query + + +def relevance_sorter(key, query, as_dict): + value = _(key.name if as_dict else key[0]) + return (cstr(value).lower().startswith(query.lower()) is not True, value) + + +def validate_and_sanitize_search_inputs(fn): + @functools.wraps(fn) + def wrapper(*args, **kwargs): + kwargs.update(dict(zip(fn.__code__.co_varnames, args))) + sanitize_searchfield(kwargs["searchfield"]) + kwargs["start"] = cint(kwargs["start"]) + kwargs["page_len"] = cint(kwargs["page_len"]) + + if kwargs["doctype"] and not influxframework.db.exists("DocType", kwargs["doctype"]): + return [] + + return fn(**kwargs) + + return wrapper + + +@influxframework.whitelist() +def get_names_for_mentions(search_term): + users_for_mentions = influxframework.cache().get_value("users_for_mentions", get_users_for_mentions) + user_groups = influxframework.cache().get_value("user_groups", get_user_groups) + + filtered_mentions = [] + for mention_data in users_for_mentions + user_groups: + if search_term.lower() not in mention_data.value.lower(): + continue + + mention_data["link"] = influxframework.utils.get_url_to_form( + "User Group" if mention_data.get("is_group") else "User Profile", mention_data["id"] + ) + + filtered_mentions.append(mention_data) + + return sorted(filtered_mentions, key=lambda d: d["value"]) + + +def get_users_for_mentions(): + return influxframework.get_all( + "User", + fields=["name as id", "full_name as value"], + filters={ + "name": ["not in", ("Administrator", "Guest")], + "allowed_in_mentions": True, + "user_type": "System User", + "enabled": True, + }, + ) + + +def get_user_groups(): + return influxframework.get_all( + "User Group", fields=["name as id", "name as value"], update={"is_group": True} + ) + + +@influxframework.whitelist() +def get_link_title(doctype, docname): + meta = influxframework.get_meta(doctype) + + if meta.show_title_field_in_link: + return influxframework.db.get_value(doctype, docname, meta.title_field) + + return docname diff --git a/influxframework/desk/treeview.py b/influxframework/desk/treeview.py new file mode 100644 index 0000000..372d555 --- /dev/null +++ b/influxframework/desk/treeview.py @@ -0,0 +1,84 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework import _ + + +@influxframework.whitelist() +def get_all_nodes(doctype, label, parent, tree_method, **filters): + """Recursively gets all data from tree nodes""" + + if "cmd" in filters: + del filters["cmd"] + filters.pop("data", None) + + tree_method = influxframework.get_attr(tree_method) + + if tree_method not in influxframework.whitelisted: + influxframework.throw(_("Not Permitted"), influxframework.PermissionError) + + data = tree_method(doctype, parent, **filters) + out = [dict(parent=label, data=data)] + + if "is_root" in filters: + del filters["is_root"] + to_check = [d.get("value") for d in data if d.get("expandable")] + + while to_check: + parent = to_check.pop() + data = tree_method(doctype, parent, is_root=False, **filters) + out.append(dict(parent=parent, data=data)) + for d in data: + if d.get("expandable"): + to_check.append(d.get("value")) + + return out + + +@influxframework.whitelist() +def get_children(doctype, parent="", **filters): + return _get_children(doctype, parent) + + +def _get_children(doctype, parent="", ignore_permissions=False): + parent_field = "parent_" + doctype.lower().replace(" ", "_") + filters = [[f"ifnull(`{parent_field}`,'')", "=", parent], ["docstatus", "<", 2]] + + meta = influxframework.get_meta(doctype) + + return influxframework.get_list( + doctype, + fields=[ + "name as value", + "{} as title".format(meta.get("title_field") or "name"), + "is_group as expandable", + ], + filters=filters, + order_by="name", + ignore_permissions=ignore_permissions, + ) + + +@influxframework.whitelist() +def add_node(): + args = make_tree_args(**influxframework.form_dict) + doc = influxframework.get_doc(args) + + doc.save() + + +def make_tree_args(**kwarg): + kwarg.pop("cmd", None) + + doctype = kwarg["doctype"] + parent_field = "parent_" + doctype.lower().replace(" ", "_") + + if kwarg["is_root"] == "false": + kwarg["is_root"] = False + if kwarg["is_root"] == "true": + kwarg["is_root"] = True + + kwarg.update({parent_field: kwarg.get("parent") or kwarg.get(parent_field)}) + + return influxframework._dict(kwarg) diff --git a/influxframework/desk/utils.py b/influxframework/desk/utils.py new file mode 100644 index 0000000..5f5e8f9 --- /dev/null +++ b/influxframework/desk/utils.py @@ -0,0 +1,29 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework + + +def validate_route_conflict(doctype, name): + """ + Raises exception if name clashes with routes from other documents for /app routing + """ + + all_names = [] + for _doctype in ["Page", "Workspace", "DocType"]: + try: + all_names.extend( + [ + slug(d) for d in influxframework.get_all(_doctype, pluck="name") if (doctype != _doctype and d != name) + ] + ) + except influxframework.db.TableMissingError: + pass + + if slug(name) in all_names: + influxframework.msgprint(influxframework._("Name already taken, please set a new name")) + raise influxframework.NameError + + +def slug(name): + return name.lower().replace(" ", "-") diff --git a/influxframework/email/__init__.py b/influxframework/email/__init__.py new file mode 100644 index 0000000..c50982c --- /dev/null +++ b/influxframework/email/__init__.py @@ -0,0 +1,118 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.desk.reportview import build_match_conditions + + +def sendmail_to_system_managers(subject, content): + influxframework.sendmail(recipients=get_system_managers(), subject=subject, content=content) + + +@influxframework.whitelist() +def get_contact_list(txt, page_length=20) -> list[dict]: + """Returns contacts (from autosuggest)""" + + if cached_contacts := get_cached_contacts(txt): + return cached_contacts[:page_length] + + reportview_conditions = build_match_conditions("Contact") + match_conditions = f"and {reportview_conditions}" if reportview_conditions else "" + + out = influxframework.db.sql( + f"""select email_id as value, + concat(first_name, ifnull(concat(' ',last_name), '' )) as description + from tabContact + where name like %(txt)s or email_id like %(txt)s + {match_conditions} + limit %(page_length)s""", + {"txt": f"%{txt}%", "page_length": page_length}, + as_dict=True, + ) + out = list(filter(None, out)) + + update_contact_cache(out) + + return out + + +def get_system_managers(): + return influxframework.db.sql_list( + """select parent FROM `tabHas Role` + WHERE role='System Manager' + AND parent!='Administrator' + AND parent IN (SELECT email FROM tabUser WHERE enabled=1)""" + ) + + +@influxframework.whitelist() +def relink(name, reference_doctype=None, reference_name=None): + influxframework.db.sql( + """update + `tabCommunication` + set + reference_doctype = %s, + reference_name = %s, + status = "Linked" + where + communication_type = "Communication" and + name = %s""", + (reference_doctype, reference_name, name), + ) + + +@influxframework.whitelist() +@influxframework.validate_and_sanitize_search_inputs +def get_communication_doctype(doctype, txt, searchfield, start, page_len, filters): + user_perms = influxframework.utils.user.UserPermissions(influxframework.session.user) + user_perms.build_permissions() + can_read = user_perms.can_read + from influxframework.modules import load_doctype_module + + com_doctypes = [] + if len(txt) < 2: + + for name in influxframework.get_hooks("communication_doctypes"): + try: + module = load_doctype_module(name, suffix="_dashboard") + if hasattr(module, "get_data"): + for i in module.get_data()["transactions"]: + com_doctypes += i["items"] + except ImportError: + pass + else: + com_doctypes = [ + d[0] for d in influxframework.db.get_values("DocType", {"issingle": 0, "istable": 0, "hide_toolbar": 0}) + ] + + out = [] + for dt in com_doctypes: + if txt.lower().replace("%", "") in dt.lower() and dt in can_read: + out.append([dt]) + return out + + +def get_cached_contacts(txt): + contacts = influxframework.cache().hget("contacts", influxframework.session.user) or [] + + if not contacts: + return + + if not txt: + return contacts + + match = [ + d + for d in contacts + if (d.value and ((d.value and txt in d.value) or (d.description and txt in d.description))) + ] + return match + + +def update_contact_cache(contacts): + cached_contacts = influxframework.cache().hget("contacts", influxframework.session.user) or [] + + uncached_contacts = [d for d in contacts if d not in cached_contacts] + cached_contacts.extend(uncached_contacts) + + influxframework.cache().hset("contacts", influxframework.session.user, cached_contacts) diff --git a/influxframework/email/doctype/__init__.py b/influxframework/email/doctype/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/email/doctype/auto_email_report/__init__.py b/influxframework/email/doctype/auto_email_report/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/email/doctype/auto_email_report/auto_email_report.js b/influxframework/email/doctype/auto_email_report/auto_email_report.js new file mode 100644 index 0000000..a8b99d1 --- /dev/null +++ b/influxframework/email/doctype/auto_email_report/auto_email_report.js @@ -0,0 +1,156 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Auto Email Report", { + refresh: function (frm) { + frm.trigger("fetch_report_filters"); + if (!frm.is_new()) { + frm.add_custom_button(__("Download"), function () { + var w = window.open( + influxframework.urllib.get_full_url( + "/api/method/influxframework.email.doctype.auto_email_report.auto_email_report.download?" + + "name=" + + encodeURIComponent(frm.doc.name) + ) + ); + if (!w) { + influxframework.msgprint(__("Please enable pop-ups")); + return; + } + }); + frm.add_custom_button(__("Send Now"), function () { + influxframework.call({ + method: "influxframework.email.doctype.auto_email_report.auto_email_report.send_now", + args: { name: frm.doc.name }, + callback: function () { + influxframework.msgprint(__("Scheduled to send")); + }, + }); + }); + } else { + if (!frm.doc.user) { + frm.set_value("user", influxframework.session.user); + } + if (!frm.doc.email_to) { + frm.set_value("email_to", influxframework.session.user); + } + } + }, + report: function (frm) { + frm.set_value("filters", ""); + frm.trigger("fetch_report_filters"); + }, + fetch_report_filters(frm) { + if ( + frm.doc.report && + frm.doc.report_type !== "Report Builder" && + frm.script_setup_for !== frm.doc.report + ) { + influxframework.call({ + method: "influxframework.desk.query_report.get_script", + args: { + report_name: frm.doc.report, + }, + callback: function (r) { + influxframework.dom.eval(r.message.script || ""); + frm.script_setup_for = frm.doc.report; + frm.trigger("show_filters"); + }, + }); + } else { + frm.trigger("show_filters"); + } + }, + show_filters: function (frm) { + var wrapper = $(frm.get_field("filters_display").wrapper); + wrapper.empty(); + if ( + frm.doc.report_type === "Custom Report" || + (frm.doc.report_type !== "Report Builder" && + influxframework.query_reports[frm.doc.report] && + influxframework.query_reports[frm.doc.report].filters) + ) { + // make a table to show filters + var table = $( + '\ + \ +
      ' + + __("Filter") + + "" + + __("Value") + + "
      " + ).appendTo(wrapper); + $('

      ' + __("Click table to edit") + "

      ").appendTo( + wrapper + ); + + var filters = JSON.parse(frm.doc.filters || "{}"); + + let report_filters; + + if ( + frm.doc.report_type === "Custom Report" && + influxframework.query_reports[frm.doc.reference_report] && + influxframework.query_reports[frm.doc.reference_report].filters + ) { + report_filters = influxframework.query_reports[frm.doc.reference_report].filters; + } else { + report_filters = influxframework.query_reports[frm.doc.report].filters; + } + + if (report_filters && report_filters.length > 0) { + frm.set_value("filter_meta", JSON.stringify(report_filters)); + if (frm.is_dirty()) { + frm.save(); + } + } + + var report_filters_list = []; + $.each(report_filters, function (key, val) { + // Remove break fieldtype from the filters + if (val.fieldtype != "Break") { + report_filters_list.push(val); + } + }); + report_filters = report_filters_list; + + const mandatory_css = { + "background-color": "var(--error-bg)", + "font-weight": "bold", + }; + + report_filters.forEach((f) => { + const css = f.reqd ? mandatory_css : {}; + const row = $("").appendTo(table.find("tbody")); + $("" + f.label + "").appendTo(row); + $("" + influxframework.format(filters[f.fieldname], f) + "") + .css(css) + .appendTo(row); + }); + + table.on("click", function () { + var dialog = new influxframework.ui.Dialog({ + fields: report_filters, + primary_action: function () { + var values = this.get_values(); + if (values) { + this.hide(); + frm.set_value("filters", JSON.stringify(values)); + frm.trigger("show_filters"); + } + }, + }); + dialog.show(); + dialog.set_values(filters); + }); + + // populate dynamic date field selection + let date_fields = report_filters + .filter((df) => df.fieldtype === "Date") + .map((df) => ({ label: df.label, value: df.fieldname })); + frm.set_df_property("from_date_field", "options", date_fields); + frm.set_df_property("to_date_field", "options", date_fields); + frm.toggle_display("dynamic_report_filters_section", date_fields.length > 0); + } + }, +}); diff --git a/influxframework/email/doctype/auto_email_report/auto_email_report.json b/influxframework/email/doctype/auto_email_report/auto_email_report.json new file mode 100644 index 0000000..211e2e9 --- /dev/null +++ b/influxframework/email/doctype/auto_email_report/auto_email_report.json @@ -0,0 +1,238 @@ +{ + "allow_rename": 1, + "creation": "2016-09-01 01:34:34.985457", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "report", + "user", + "enabled", + "column_break_4", + "report_type", + "reference_report", + "filter_data", + "send_if_data", + "data_modified_till", + "no_of_rows", + "report_filters", + "filters_display", + "filters", + "filter_meta", + "dynamic_report_filters_section", + "from_date_field", + "to_date_field", + "column_break_17", + "dynamic_date_period", + "email_settings", + "email_to", + "day_of_week", + "column_break_13", + "frequency", + "format", + "section_break_15", + "description" + ], + "fields": [ + { + "fieldname": "report", + "fieldtype": "Link", + "label": "Report", + "options": "Report", + "reqd": 1 + }, + { + "default": "User", + "fieldname": "user", + "fieldtype": "Link", + "label": "Based on Permissions For User", + "options": "User", + "reqd": 1 + }, + { + "default": "1", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enabled" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fetch_from": "report.report_type", + "fieldname": "report_type", + "fieldtype": "Read Only", + "label": "Report Type" + }, + { + "fieldname": "filter_data", + "fieldtype": "Section Break", + "label": "Filter Data" + }, + { + "default": "1", + "fieldname": "send_if_data", + "fieldtype": "Check", + "label": "Send only if there is any data" + }, + { + "depends_on": "eval:doc.report_type=='Report Builder'", + "description": "Zero means send records updated at anytime", + "fieldname": "data_modified_till", + "fieldtype": "Int", + "label": "Only Send Records Updated in Last X Hours" + }, + { + "default": "100", + "fieldname": "no_of_rows", + "fieldtype": "Int", + "label": "No of Rows (Max 500)" + }, + { + "collapsible": 1, + "depends_on": "eval:doc.report_type !== 'Report Builder'", + "fieldname": "report_filters", + "fieldtype": "Section Break", + "label": "Report Filters" + }, + { + "fieldname": "filters_display", + "fieldtype": "HTML", + "label": "Filters Display" + }, + { + "fieldname": "filters", + "fieldtype": "Text", + "hidden": 1, + "label": "Filters" + }, + { + "fieldname": "filter_meta", + "fieldtype": "Text", + "hidden": 1, + "label": "Filter Meta", + "read_only": 1 + }, + { + "collapsible": 1, + "depends_on": "eval:doc.report_type !== 'Report Builder'", + "fieldname": "dynamic_report_filters_section", + "fieldtype": "Section Break", + "label": "Dynamic Report Filters" + }, + { + "fieldname": "from_date_field", + "fieldtype": "Select", + "label": "From Date Field" + }, + { + "fieldname": "to_date_field", + "fieldtype": "Select", + "label": "To Date Field" + }, + { + "fieldname": "column_break_17", + "fieldtype": "Column Break" + }, + { + "fieldname": "dynamic_date_period", + "fieldtype": "Select", + "label": "Period", + "options": "\nDaily\nWeekly\nMonthly\nQuarterly\nHalf Yearly\nYearly" + }, + { + "fieldname": "email_settings", + "fieldtype": "Section Break", + "label": "Email Settings" + }, + { + "description": "For multiple addresses, enter the address on different line. e.g. test@test.com \u23ce test1@test.com", + "fieldname": "email_to", + "fieldtype": "Small Text", + "label": "Email To", + "reqd": 1 + }, + { + "default": "Monday", + "depends_on": "eval:doc.frequency=='Weekly'", + "fieldname": "day_of_week", + "fieldtype": "Select", + "label": "Day of Week", + "options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday" + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" + }, + { + "fieldname": "frequency", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Frequency", + "options": "Daily\nWeekdays\nWeekly\nMonthly", + "reqd": 1 + }, + { + "fieldname": "format", + "fieldtype": "Select", + "label": "Format", + "options": "HTML\nXLSX\nCSV", + "reqd": 1 + }, + { + "collapsible": 1, + "fieldname": "section_break_15", + "fieldtype": "Section Break", + "label": "Message" + }, + { + "fieldname": "description", + "fieldtype": "Text Editor", + "label": "Message" + }, + { + "fetch_from": "report.reference_report", + "fieldname": "reference_report", + "fieldtype": "Data", + "hidden": 1, + "label": "Reference Report", + "read_only": 1 + } + ], + "modified": "2021-01-28 15:59:43.151995", + "modified_by": "Administrator", + "module": "Email", + "name": "Auto Email Report", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Report Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 + } \ No newline at end of file diff --git a/influxframework/email/doctype/auto_email_report/auto_email_report.py b/influxframework/email/doctype/auto_email_report/auto_email_report.py new file mode 100644 index 0000000..d1bdb00 --- /dev/null +++ b/influxframework/email/doctype/auto_email_report/auto_email_report.py @@ -0,0 +1,296 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import calendar +from datetime import timedelta + +import influxframework +from influxframework import _ +from influxframework.desk.query_report import build_xlsx_data +from influxframework.model.document import Document +from influxframework.model.naming import append_number_if_name_exists +from influxframework.utils import ( + add_to_date, + cint, + format_time, + get_link_to_form, + get_url_to_report, + global_date_format, + now, + now_datetime, + today, + validate_email_address, +) +from influxframework.utils.csvutils import to_csv +from influxframework.utils.xlsxutils import make_xlsx + + +class AutoEmailReport(Document): + def autoname(self): + self.name = _(self.report) + if influxframework.db.exists("Auto Email Report", self.name): + self.name = append_number_if_name_exists("Auto Email Report", self.name) + + def validate(self): + self.validate_report_count() + self.validate_emails() + self.validate_report_format() + self.validate_mandatory_fields() + + def validate_emails(self): + """Cleanup list of emails""" + if "," in self.email_to: + self.email_to.replace(",", "\n") + + valid = [] + for email in self.email_to.split(): + if email: + validate_email_address(email, True) + valid.append(email) + + self.email_to = "\n".join(valid) + + def validate_report_count(self): + count = influxframework.db.count("Auto Email Report", {"user": self.user, "enabled": 1}) + + max_reports_per_user = ( + cint(influxframework.local.conf.max_reports_per_user) # kept for backward compatibilty + or cint(influxframework.db.get_single_value("System Settings", "max_auto_email_report_per_user")) + or 20 + ) + + if count > max_reports_per_user + (-1 if self.flags.in_insert else 0): + msg = _("Only {0} emailed reports are allowed per user.").format(max_reports_per_user) + msg += " " + _("To allow more reports update limit in System Settings.") + influxframework.throw(msg, title=_("Report limit reached")) + + def validate_report_format(self): + """check if user has select correct report format""" + valid_report_formats = ["HTML", "XLSX", "CSV"] + if self.format not in valid_report_formats: + influxframework.throw( + _("{0} is not a valid report format. Report format should one of the following {1}").format( + influxframework.bold(self.format), influxframework.bold(", ".join(valid_report_formats)) + ) + ) + + def validate_mandatory_fields(self): + # Check if all Mandatory Report Filters are filled by the User + filters = influxframework.parse_json(self.filters) if self.filters else {} + filter_meta = influxframework.parse_json(self.filter_meta) if self.filter_meta else {} + throw_list = [] + for meta in filter_meta: + if meta.get("reqd") and not filters.get(meta["fieldname"]): + throw_list.append(meta["label"]) + if throw_list: + influxframework.throw( + title=_("Missing Filters Required"), + msg=_("Following Report Filters have missing values:") + + "

      • " + + "
      • ".join(throw_list) + + "
      ", + ) + + def get_report_content(self): + """Returns file in for the report in given format""" + report = influxframework.get_doc("Report", self.report) + + self.filters = influxframework.parse_json(self.filters) if self.filters else {} + + if self.report_type == "Report Builder" and self.data_modified_till: + self.filters["modified"] = (">", now_datetime() - timedelta(hours=self.data_modified_till)) + + if self.report_type != "Report Builder" and self.dynamic_date_filters_set(): + self.prepare_dynamic_filters() + + columns, data = report.get_data( + limit=self.no_of_rows or 100, + user=self.user, + filters=self.filters, + as_dict=True, + ignore_prepared_report=True, + ) + + # add serial numbers + columns.insert(0, influxframework._dict(fieldname="idx", label="", width="30px")) + for i in range(len(data)): + data[i]["idx"] = i + 1 + + if len(data) == 0 and self.send_if_data: + return None + + if self.format == "HTML": + columns, data = make_links(columns, data) + columns = update_field_types(columns) + return self.get_html_table(columns, data) + + elif self.format == "XLSX": + report_data = influxframework._dict() + report_data["columns"] = columns + report_data["result"] = data + + xlsx_data, column_widths = build_xlsx_data(report_data, [], 1, ignore_visible_idx=True) + xlsx_file = make_xlsx(xlsx_data, "Auto Email Report", column_widths=column_widths) + return xlsx_file.getvalue() + + elif self.format == "CSV": + report_data = influxframework._dict() + report_data["columns"] = columns + report_data["result"] = data + + xlsx_data, column_widths = build_xlsx_data(report_data, [], 1, ignore_visible_idx=True) + return to_csv(xlsx_data) + + else: + influxframework.throw(_("Invalid Output Format")) + + def get_html_table(self, columns=None, data=None): + + date_time = global_date_format(now()) + " " + format_time(now()) + report_doctype = influxframework.db.get_value("Report", self.report, "ref_doctype") + + return influxframework.render_template( + "influxframework/templates/emails/auto_email_report.html", + { + "title": self.name, + "description": self.description, + "date_time": date_time, + "columns": columns, + "data": data, + "report_url": get_url_to_report(self.report, self.report_type, report_doctype), + "report_name": self.report, + "edit_report_settings": get_link_to_form("Auto Email Report", self.name), + }, + ) + + def get_file_name(self): + return "{}.{}".format(self.report.replace(" ", "-").replace("/", "-"), self.format.lower()) + + def prepare_dynamic_filters(self): + self.filters = influxframework.parse_json(self.filters) + + to_date = today() + from_date_value = { + "Daily": ("days", -1), + "Weekly": ("weeks", -1), + "Monthly": ("months", -1), + "Quarterly": ("months", -3), + "Half Yearly": ("months", -6), + "Yearly": ("years", -1), + }[self.dynamic_date_period] + + from_date = add_to_date(to_date, **{from_date_value[0]: from_date_value[1]}) + + self.filters[self.from_date_field] = from_date + self.filters[self.to_date_field] = to_date + + def send(self): + if self.filter_meta and not self.filters: + influxframework.throw(_("Please set filters value in Report Filter table.")) + + data = self.get_report_content() + if not data: + return + + attachments = None + if self.format == "HTML": + message = data + else: + message = self.get_html_table() + + if not self.format == "HTML": + attachments = [{"fname": self.get_file_name(), "fcontent": data}] + + influxframework.sendmail( + recipients=self.email_to.split(), + subject=self.name, + message=message, + attachments=attachments, + reference_doctype=self.doctype, + reference_name=self.name, + ) + + def dynamic_date_filters_set(self): + return self.dynamic_date_period and self.from_date_field and self.to_date_field + + +@influxframework.whitelist() +def download(name): + """Download report locally""" + auto_email_report = influxframework.get_doc("Auto Email Report", name) + auto_email_report.check_permission() + data = auto_email_report.get_report_content() + + if not data: + influxframework.msgprint(_("No Data")) + return + + influxframework.local.response.filecontent = data + influxframework.local.response.type = "download" + influxframework.local.response.filename = auto_email_report.get_file_name() + + +@influxframework.whitelist() +def send_now(name): + """Send Auto Email report now""" + auto_email_report = influxframework.get_doc("Auto Email Report", name) + auto_email_report.check_permission() + auto_email_report.send() + + +def send_daily(): + """Check reports to be sent daily""" + + current_day = calendar.day_name[now_datetime().weekday()] + enabled_reports = influxframework.get_all( + "Auto Email Report", filters={"enabled": 1, "frequency": ("in", ("Daily", "Weekdays", "Weekly"))} + ) + + for report in enabled_reports: + auto_email_report = influxframework.get_doc("Auto Email Report", report.name) + + # if not correct weekday, skip + if auto_email_report.frequency == "Weekdays": + if current_day in ("Saturday", "Sunday"): + continue + elif auto_email_report.frequency == "Weekly": + if auto_email_report.day_of_week != current_day: + continue + try: + auto_email_report.send() + except Exception as e: + auto_email_report.log_error(f"Failed to send {auto_email_report.name} Auto Email Report") + + +def send_monthly(): + """Check reports to be sent monthly""" + for report in influxframework.get_all("Auto Email Report", {"enabled": 1, "frequency": "Monthly"}): + influxframework.get_doc("Auto Email Report", report.name).send() + + +def make_links(columns, data): + for row in data: + doc_name = row.get("name") + for col in columns: + if not row.get(col.fieldname): + continue + + if col.fieldtype == "Link": + if col.options and col.options != "Currency": + row[col.fieldname] = get_link_to_form(col.options, row[col.fieldname]) + elif col.fieldtype == "Dynamic Link": + if col.options and row.get(col.options): + row[col.fieldname] = get_link_to_form(row[col.options], row[col.fieldname]) + elif col.fieldtype == "Currency": + doc = influxframework.get_doc(col.parent, doc_name) if doc_name and col.get("parent") else None + # Pass the Document to get the currency based on docfield option + row[col.fieldname] = influxframework.format_value(row[col.fieldname], col, doc=doc) + return columns, data + + +def update_field_types(columns): + for col in columns: + if col.fieldtype in ("Link", "Dynamic Link", "Currency") and col.options != "Currency": + col.fieldtype = "Data" + col.options = "" + return columns diff --git a/influxframework/email/doctype/auto_email_report/test_auto_email_report.py b/influxframework/email/doctype/auto_email_report/test_auto_email_report.py new file mode 100644 index 0000000..4473b66 --- /dev/null +++ b/influxframework/email/doctype/auto_email_report/test_auto_email_report.py @@ -0,0 +1,64 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import json + +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase +from influxframework.utils import add_to_date, get_link_to_form, today +from influxframework.utils.data import is_html + +# test_records = influxframework.get_test_records('Auto Email Report') + + +class TestAutoEmailReport(InfluxFrameworkTestCase): + def test_auto_email(self): + influxframework.delete_doc("Auto Email Report", "Permitted Documents For User") + + auto_email_report = get_auto_email_report() + + data = auto_email_report.get_report_content() + + self.assertTrue(is_html(data)) + self.assertTrue(str(get_link_to_form("Module Def", "Core")) in data) + + auto_email_report.format = "CSV" + + data = auto_email_report.get_report_content() + self.assertTrue('"Language","Core"' in data) + + auto_email_report.format = "XLSX" + + data = auto_email_report.get_report_content() + + def test_dynamic_date_filters(self): + auto_email_report = get_auto_email_report() + + auto_email_report.dynamic_date_period = "Weekly" + auto_email_report.from_date_field = "from_date" + auto_email_report.to_date_field = "to_date" + + auto_email_report.prepare_dynamic_filters() + + self.assertEqual(auto_email_report.filters["from_date"], add_to_date(today(), weeks=-1)) + self.assertEqual(auto_email_report.filters["to_date"], today()) + + +def get_auto_email_report(): + if not influxframework.db.exists("Auto Email Report", "Permitted Documents For User"): + auto_email_report = influxframework.get_doc( + dict( + doctype="Auto Email Report", + report="Permitted Documents For User", + report_type="Script Report", + user="Administrator", + enabled=1, + email_to="test@example.com", + format="HTML", + frequency="Daily", + filters=json.dumps(dict(user="Administrator", doctype="DocType")), + ) + ).insert() + else: + auto_email_report = influxframework.get_doc("Auto Email Report", "Permitted Documents For User") + + return auto_email_report diff --git a/influxframework/email/doctype/document_follow/__init__.py b/influxframework/email/doctype/document_follow/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/email/doctype/document_follow/document_follow.js b/influxframework/email/doctype/document_follow/document_follow.js new file mode 100644 index 0000000..efdbfa4 --- /dev/null +++ b/influxframework/email/doctype/document_follow/document_follow.js @@ -0,0 +1,4 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Document Follow", {}); diff --git a/influxframework/email/doctype/document_follow/document_follow.json b/influxframework/email/doctype/document_follow/document_follow.json new file mode 100644 index 0000000..5a9ff96 --- /dev/null +++ b/influxframework/email/doctype/document_follow/document_follow.json @@ -0,0 +1,78 @@ +{ + "actions": [], + "creation": "2019-01-09 16:39:23.746535", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "ref_doctype", + "ref_docname", + "user" + ], + "fields": [ + { + "fieldname": "ref_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Doctype", + "options": "DocType", + "reqd": 1, + "search_index": 1 + }, + { + "fieldname": "ref_docname", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Document Name", + "options": "ref_doctype", + "reqd": 1, + "search_index": 1 + }, + { + "fieldname": "user", + "fieldtype": "Link", + "in_list_view": 1, + "label": "User", + "options": "User", + "reqd": 1, + "search_index": 1 + } + ], + "in_create": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-09-17 09:19:28.496453", + "modified_by": "Administrator", + "module": "Email", + "name": "Document Follow", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1, + "write": 1 + } + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/influxframework/email/doctype/document_follow/document_follow.py b/influxframework/email/doctype/document_follow/document_follow.py new file mode 100644 index 0000000..7ba977f --- /dev/null +++ b/influxframework/email/doctype/document_follow/document_follow.py @@ -0,0 +1,8 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +from influxframework.model.document import Document + + +class DocumentFollow(Document): + pass diff --git a/influxframework/email/doctype/document_follow/test_document_follow.py b/influxframework/email/doctype/document_follow/test_document_follow.py new file mode 100644 index 0000000..ddb1061 --- /dev/null +++ b/influxframework/email/doctype/document_follow/test_document_follow.py @@ -0,0 +1,245 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +from dataclasses import dataclass + +import influxframework +import influxframework.desk.form.document_follow as document_follow +from influxframework.desk.form.assign_to import add +from influxframework.desk.form.document_follow import get_document_followed_by_user +from influxframework.desk.form.utils import add_comment +from influxframework.desk.like import toggle_like +from influxframework.query_builder import DocType +from influxframework.query_builder.functions import Cast_ +from influxframework.share import add as share +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestDocumentFollow(InfluxFrameworkTestCase): + def test_document_follow_version(self): + user = get_user() + event_doc = get_event() + + event_doc.description = "This is a test description for sending mail" + event_doc.save(ignore_version=False) + + document_follow.unfollow_document("Event", event_doc.name, user.name) + doc = document_follow.follow_document("Event", event_doc.name, user.name) + self.assertEqual(doc.user, user.name) + + document_follow.send_hourly_updates() + emails = get_emails(event_doc, "%This is a test description for sending mail%") + self.assertIsNotNone(emails) + + def test_document_follow_comment(self): + user = get_user() + event_doc = get_event() + + add_comment( + event_doc.doctype, event_doc.name, "This is a test comment", "Administrator@example.com", "Bosh" + ) + + document_follow.unfollow_document("Event", event_doc.name, user.name) + doc = document_follow.follow_document("Event", event_doc.name, user.name) + self.assertEqual(doc.user, user.name) + + document_follow.send_hourly_updates() + emails = get_emails(event_doc, "%This is a test comment%") + self.assertIsNotNone(emails) + + def test_follow_limit(self): + user = get_user() + for _ in range(25): + event_doc = get_event() + document_follow.unfollow_document("Event", event_doc.name, user.name) + doc = document_follow.follow_document("Event", event_doc.name, user.name) + self.assertEqual(doc.user, user.name) + self.assertEqual(len(get_document_followed_by_user(user.name)), 20) + + def test_follow_on_create(self): + user = get_user(DocumentFollowConditions(1)) + influxframework.set_user(user.name) + event = get_event() + + event.description = "This is a test description for sending mail" + event.save(ignore_version=False) + + documents_followed = get_events_followed_by_user(event.name, user.name) + self.assertTrue(documents_followed) + + def test_do_not_follow_on_create(self): + user = get_user() + influxframework.set_user(user.name) + + event = get_event() + + documents_followed = get_events_followed_by_user(event.name, user.name) + self.assertFalse(documents_followed) + + def test_do_not_follow_on_update(self): + user = get_user() + influxframework.set_user(user.name) + event = get_event() + + event.description = "This is a test description for sending mail" + event.save(ignore_version=False) + + documents_followed = get_events_followed_by_user(event.name, user.name) + self.assertFalse(documents_followed) + + def test_follow_on_comment(self): + user = get_user(DocumentFollowConditions(0, 1)) + influxframework.set_user(user.name) + event = get_event() + + add_comment( + event.doctype, event.name, "This is a test comment", "Administrator@example.com", "Bosh" + ) + + documents_followed = get_events_followed_by_user(event.name, user.name) + self.assertTrue(documents_followed) + + def test_do_not_follow_on_comment(self): + user = get_user() + influxframework.set_user(user.name) + event = get_event() + + add_comment( + event.doctype, event.name, "This is a test comment", "Administrator@example.com", "Bosh" + ) + + documents_followed = get_events_followed_by_user(event.name, user.name) + self.assertFalse(documents_followed) + + def test_follow_on_like(self): + user = get_user(DocumentFollowConditions(0, 0, 1)) + influxframework.set_user(user.name) + event = get_event() + + toggle_like(event.doctype, event.name, add="Yes") + + documents_followed = get_events_followed_by_user(event.name, user.name) + self.assertTrue(documents_followed) + + def test_do_not_follow_on_like(self): + user = get_user() + influxframework.set_user(user.name) + event = get_event() + + toggle_like(event.doctype, event.name) + + documents_followed = get_events_followed_by_user(event.name, user.name) + self.assertFalse(documents_followed) + + def test_follow_on_assign(self): + user = get_user(DocumentFollowConditions(0, 0, 0, 1)) + event = get_event() + + add({"assign_to": [user.name], "doctype": event.doctype, "name": event.name}) + + documents_followed = get_events_followed_by_user(event.name, user.name) + self.assertTrue(documents_followed) + + def test_do_not_follow_on_assign(self): + user = get_user() + influxframework.set_user(user.name) + event = get_event() + + add({"assign_to": [user.name], "doctype": event.doctype, "name": event.name}) + + documents_followed = get_events_followed_by_user(event.name, user.name) + self.assertFalse(documents_followed) + + def test_follow_on_share(self): + user = get_user(DocumentFollowConditions(0, 0, 0, 0, 1)) + event = get_event() + + share(user=user.name, doctype=event.doctype, name=event.name) + + documents_followed = get_events_followed_by_user(event.name, user.name) + self.assertTrue(documents_followed) + + def test_do_not_follow_on_share(self): + user = get_user() + event = get_event() + + share(user=user.name, doctype=event.doctype, name=event.name) + + documents_followed = get_events_followed_by_user(event.name, user.name) + self.assertFalse(documents_followed) + + def tearDown(self): + influxframework.db.rollback() + influxframework.db.delete("Email Queue") + influxframework.db.delete("Email Queue Recipient") + influxframework.db.delete("Document Follow") + influxframework.db.delete("Event") + + +def get_events_followed_by_user(event_name, user_name): + DocumentFollow = DocType("Document Follow") + return ( + influxframework.qb.from_(DocumentFollow) + .where(DocumentFollow.ref_doctype == "Event") + .where(DocumentFollow.ref_docname == event_name) + .where(DocumentFollow.user == user_name) + .select(DocumentFollow.name) + ).run() + + +def get_event(): + doc = influxframework.get_doc( + { + "doctype": "Event", + "subject": "_Test_Doc_Follow", + "doc.starts_on": influxframework.utils.now(), + "doc.ends_on": influxframework.utils.add_days(influxframework.utils.now(), 5), + "doc.description": "Hello", + } + ) + doc.insert() + return doc + + +def get_user(document_follow=None): + influxframework.set_user("Administrator") + if influxframework.db.exists("User", "test@docsub.com"): + doc = influxframework.delete_doc("User", "test@docsub.com") + doc = influxframework.new_doc("User") + doc.email = "test@docsub.com" + doc.first_name = "Test" + doc.last_name = "User" + doc.send_welcome_email = 0 + doc.document_follow_notify = 1 + doc.document_follow_frequency = "Hourly" + doc.__dict__.update(document_follow.__dict__ if document_follow else {}) + doc.insert() + doc.add_roles("System Manager") + return doc + + +def get_emails(event_doc, search_string): + EmailQueue = DocType("Email Queue") + EmailQueueRecipient = DocType("Email Queue Recipient") + + return ( + influxframework.qb.from_(EmailQueue) + .join(EmailQueueRecipient) + .on(EmailQueueRecipient.parent == Cast_(EmailQueue.name, "varchar")) + .where( + EmailQueueRecipient.recipient == "test@docsub.com", + ) + .where(EmailQueue.message.like(f"%{event_doc.doctype}%")) + .where(EmailQueue.message.like(f"%{event_doc.name}%")) + .where(EmailQueue.message.like(search_string)) + .select(EmailQueue.message) + .limit(1) + ).run() + + +@dataclass +class DocumentFollowConditions: + follow_created_documents: int = 0 + follow_commented_documents: int = 0 + follow_liked_documents: int = 0 + follow_assigned_documents: int = 0 + follow_shared_documents: int = 0 diff --git a/influxframework/email/doctype/email_account/__init__.py b/influxframework/email/doctype/email_account/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/email/doctype/email_account/email_account.js b/influxframework/email/doctype/email_account/email_account.js new file mode 100644 index 0000000..8241839 --- /dev/null +++ b/influxframework/email/doctype/email_account/email_account.js @@ -0,0 +1,256 @@ +influxframework.email_defaults = { + GMail: { + email_server: "imap.gmail.com", + incoming_port: 993, + use_ssl: 1, + enable_outgoing: 1, + smtp_server: "smtp.gmail.com", + smtp_port: 587, + use_tls: 1, + use_imap: 1, + }, + "Outlook.com": { + email_server: "imap-mail.outlook.com", + use_ssl: 1, + enable_outgoing: 1, + smtp_server: "smtp-mail.outlook.com", + smtp_port: 587, + use_tls: 1, + use_imap: 1, + }, + Sendgrid: { + enable_outgoing: 1, + smtp_server: "smtp.sendgrid.net", + smtp_port: 587, + use_tls: 1, + }, + SparkPost: { + enable_incoming: 0, + enable_outgoing: 1, + smtp_server: "smtp.sparkpostmail.com", + smtp_port: 587, + use_tls: 1, + }, + "Yahoo Mail": { + email_server: "imap.mail.yahoo.com", + use_ssl: 1, + enable_outgoing: 1, + smtp_server: "smtp.mail.yahoo.com", + smtp_port: 587, + use_tls: 1, + use_imap: 1, + }, + "Yandex.Mail": { + email_server: "imap.yandex.com", + use_ssl: 1, + enable_outgoing: 1, + smtp_server: "smtp.yandex.com", + smtp_port: 587, + use_tls: 1, + use_imap: 1, + }, +}; + +influxframework.email_defaults_pop = { + GMail: { + email_server: "pop.gmail.com", + }, + "Outlook.com": { + email_server: "pop3-mail.outlook.com", + }, + "Yahoo Mail": { + email_server: "pop.mail.yahoo.com", + }, + "Yandex.Mail": { + email_server: "pop.yandex.com", + }, +}; + +function oauth_access(frm) { + return influxframework.call({ + method: "influxframework.email.oauth.oauth_access", + args: { + email_account: frm.doc.name, + service: frm.doc.service || "", + }, + callback: function (r) { + if (!r.exc) { + window.open(r.message.url, "_self"); + } + }, + }); +} + +function set_default_max_attachment_size(frm, field) { + if (frm.doc.__islocal && !frm.doc[field]) { + influxframework.call({ + method: "influxframework.core.api.file.get_max_file_size", + callback: function (r) { + if (!r.exc) { + frm.set_value(field, Number(r.message) / (1024 * 1024)); + } + }, + }); + } +} + +influxframework.ui.form.on("Email Account", { + service: function (frm) { + $.each(influxframework.email_defaults[frm.doc.service], function (key, value) { + frm.set_value(key, value); + }); + if (!frm.doc.use_imap) { + $.each(influxframework.email_defaults_pop[frm.doc.service], function (key, value) { + frm.set_value(key, value); + }); + } + frm.events.show_gmail_message_for_less_secure_apps(frm); + frm.events.toggle_auth_method(frm); + }, + + use_imap: function (frm) { + if (!frm.doc.use_imap) { + $.each(influxframework.email_defaults_pop[frm.doc.service], function (key, value) { + frm.set_value(key, value); + }); + } else { + $.each(influxframework.email_defaults[frm.doc.service], function (key, value) { + frm.set_value(key, value); + }); + } + }, + + enable_incoming: function (frm) { + frm.trigger("warn_autoreply_on_incoming"); + }, + + enable_auto_reply: function (frm) { + frm.trigger("warn_autoreply_on_incoming"); + }, + + notify_if_unreplied: function (frm) { + frm.set_df_property("send_notification_to", "reqd", frm.doc.notify_if_unreplied); + }, + + onload: function (frm) { + if (influxframework.utils.get_query_params().successful_authorization === "1") { + influxframework.show_alert(__("Successfully Authorized")); + // FIXME: find better alternative + window.history.replaceState(null, "", window.location.pathname); + } + + frm.set_df_property("append_to", "only_select", true); + frm.set_query( + "append_to", + "influxframework.email.doctype.email_account.email_account.get_append_to" + ); + frm.set_query("append_to", "imap_folder", function () { + return { + query: "influxframework.email.doctype.email_account.email_account.get_append_to", + }; + }); + if (frm.doc.__islocal) { + frm.add_child("imap_folder", { folder_name: "INBOX" }); + frm.refresh_field("imap_folder"); + } + frm.toggle_display(["auth_method"], frm.doc.service === "GMail"); + set_default_max_attachment_size(frm, "attachment_limit"); + }, + + refresh: function (frm) { + frm.events.enable_incoming(frm); + frm.events.notify_if_unreplied(frm); + frm.events.show_gmail_message_for_less_secure_apps(frm); + frm.events.show_oauth_authorization_message(frm); + + if (influxframework.route_flags.delete_user_from_locals && influxframework.route_flags.linked_user) { + delete influxframework.route_flags.delete_user_from_locals; + delete locals["User"][influxframework.route_flags.linked_user]; + } + }, + + after_save(frm) { + if (frm.doc.auth_method === "OAuth" && !frm.doc.refresh_token) { + oauth_access(frm); + } + }, + + toggle_auth_method: function (frm) { + if (frm.doc.service !== "GMail") { + frm.toggle_display(["auth_method"], false); + frm.doc.auth_method = "Basic"; + } else { + frm.toggle_display(["auth_method"], true); + } + }, + + show_gmail_message_for_less_secure_apps: function (frm) { + frm.dashboard.clear_headline(); + let msg = __( + "GMail will only work if you enable 2-step authentication and use app-specific password OR use OAuth." + ); + let cta = __("Read the step by step guide here."); + msg += ` ${cta}`; + if (frm.doc.service === "GMail") { + frm.dashboard.set_headline_alert(msg); + } + }, + + show_oauth_authorization_message(frm) { + if (frm.doc.auth_method === "OAuth" && !frm.doc.refresh_token) { + let msg = __( + 'OAuth has been enabled but not authorised. Please use "Authorise API Access" button to do the same.' + ); + frm.dashboard.clear_headline(); + frm.dashboard.set_headline_alert(msg, "yellow"); + } + }, + + authorize_api_access: function (frm) { + oauth_access(frm); + }, + + domain: influxframework.utils.debounce((frm) => { + if (frm.doc.domain) { + influxframework.call({ + method: "get_domain_values", + doc: frm.doc, + args: { + domain: frm.doc.domain, + }, + callback: function (r) { + if (!r.exc) { + for (let field in r.message) { + frm.set_value(field, r.message[field]); + } + } + }, + }); + } + }), + + email_sync_option: function (frm) { + // confirm if the ALL sync option is selected + + if (frm.doc.email_sync_option == "ALL") { + var msg = __( + "You are selecting Sync Option as ALL, It will resync all read as well as unread message from server. This may also cause the duplication of Communication (emails)." + ); + influxframework.confirm(msg, null, function () { + frm.set_value("email_sync_option", "UNSEEN"); + }); + } + }, + + warn_autoreply_on_incoming: function (frm) { + if (frm.doc.enable_incoming && frm.doc.enable_auto_reply && frm.doc.__islocal) { + var msg = __( + "Enabling auto reply on an incoming email account will send automated replies to all the synchronized emails. Do you wish to continue?" + ); + influxframework.confirm(msg, null, function () { + frm.set_value("enable_auto_reply", 0); + influxframework.show_alert({ message: __("Disabled Auto Reply"), indicator: "blue" }); + }); + } + }, +}); diff --git a/influxframework/email/doctype/email_account/email_account.json b/influxframework/email/doctype/email_account/email_account.json new file mode 100644 index 0000000..f874358 --- /dev/null +++ b/influxframework/email/doctype/email_account/email_account.json @@ -0,0 +1,642 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:email_account_name", + "creation": "2014-09-11 12:04:34.163728", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "account_section", + "email_id", + "email_account_name", + "column_break_3", + "domain", + "service", + "authentication_column", + "auth_method", + "authorize_api_access", + "password", + "awaiting_password", + "ascii_encode_password", + "column_break_10", + "refresh_token", + "access_token", + "login_id_is_different", + "login_id", + "mailbox_settings", + "enable_incoming", + "default_incoming", + "use_imap", + "use_ssl", + "use_starttls", + "email_server", + "incoming_port", + "column_break_18", + "attachment_limit", + "email_sync_option", + "initial_sync_count", + "section_break_25", + "imap_folder", + "section_break_12", + "append_emails_to_sent_folder", + "append_to", + "create_contact", + "enable_automatic_linking", + "section_break_13", + "notify_if_unreplied", + "unreplied_for_mins", + "send_notification_to", + "outgoing_mail_settings", + "enable_outgoing", + "use_tls", + "use_ssl_for_outgoing", + "smtp_server", + "smtp_port", + "column_break_38", + "default_outgoing", + "always_use_account_email_id_as_sender", + "always_use_account_name_as_sender_name", + "send_unsubscribe_message", + "track_email_status", + "no_smtp_authentication", + "signature_section", + "add_signature", + "signature", + "auto_reply", + "enable_auto_reply", + "auto_reply_message", + "set_footer", + "footer", + "brand_logo", + "uidvalidity", + "uidnext", + "no_failed" + ], + "fields": [ + { + "fieldname": "email_id", + "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, + "in_global_search": 1, + "in_list_view": 1, + "label": "Email Address", + "options": "Email", + "reqd": 1, + "unique": 1 + }, + { + "default": "0", + "fieldname": "login_id_is_different", + "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, + "label": "Use different Email ID" + }, + { + "depends_on": "login_id_is_different", + "fieldname": "login_id", + "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, + "label": "Alternative Email ID" + }, + { + "depends_on": "eval: doc.auth_method === \"Basic\"", + "fieldname": "password", + "fieldtype": "Password", + "hide_days": 1, + "hide_seconds": 1, + "label": "Password" + }, + { + "default": "0", + "depends_on": "eval: doc.auth_method === \"Basic\"", + "fieldname": "awaiting_password", + "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, + "label": "Awaiting password" + }, + { + "default": "0", + "depends_on": "eval: doc.auth_method === \"Basic\"", + "fieldname": "ascii_encode_password", + "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, + "label": "Use ASCII encoding for password" + }, + { + "description": "e.g. \"Support\", \"Sales\", \"Jerry Yang\"", + "fieldname": "email_account_name", + "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, + "label": "Email Account Name", + "unique": 1 + }, + { + "depends_on": "eval:!doc.service", + "fieldname": "domain", + "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Domain", + "options": "Email Domain" + }, + { + "depends_on": "eval:!doc.domain", + "fieldname": "service", + "fieldtype": "Select", + "hide_days": 1, + "hide_seconds": 1, + "label": "Service", + "options": "\nGMail\nSendgrid\nSparkPost\nYahoo Mail\nOutlook.com\nYandex.Mail" + }, + { + "fieldname": "mailbox_settings", + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, + "label": "Incoming (POP/IMAP) Settings" + }, + { + "default": "0", + "fieldname": "enable_incoming", + "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, + "label": "Enable Incoming" + }, + { + "default": "0", + "depends_on": "eval: !doc.domain && doc.enable_incoming", + "fetch_from": "domain.use_imap", + "fieldname": "use_imap", + "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, + "label": "Use IMAP" + }, + { + "depends_on": "eval:!doc.domain && doc.enable_incoming", + "description": "e.g. pop.gmail.com / imap.gmail.com", + "fetch_from": "domain.email_server", + "fieldname": "email_server", + "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, + "label": "Incoming Server" + }, + { + "default": "0", + "depends_on": "eval:!doc.domain && doc.enable_incoming", + "fetch_from": "domain.use_ssl", + "fieldname": "use_ssl", + "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, + "label": "Use SSL" + }, + { + "default": "1", + "depends_on": "eval:!doc.domain && doc.enable_incoming", + "description": "Ignore attachments over this size", + "fetch_from": "domain.attachment_limit", + "fieldname": "attachment_limit", + "fieldtype": "Int", + "hide_days": 1, + "hide_seconds": 1, + "label": "Attachment Limit (MB)" + }, + { + "depends_on": "eval: doc.enable_incoming && !doc.use_imap", + "description": "Append as communication against this DocType (must have fields: \"Sender\" and \"Subject\"). These fields can be defined in the email settings section of the appended doctype.", + "fieldname": "append_to", + "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, + "in_standard_filter": 1, + "label": "Append To", + "options": "DocType" + }, + { + "default": "0", + "depends_on": "enable_incoming", + "description": "e.g. replies@yourcomany.com. All replies will come to this inbox.", + "fieldname": "default_incoming", + "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, + "label": "Default Incoming" + }, + { + "default": "UNSEEN", + "depends_on": "eval: doc.enable_incoming && doc.use_imap", + "fieldname": "email_sync_option", + "fieldtype": "Select", + "hide_days": 1, + "hide_seconds": 1, + "label": "Email Sync Option", + "options": "ALL\nUNSEEN" + }, + { + "default": "250", + "depends_on": "eval: doc.enable_incoming && doc.use_imap", + "description": "Total number of emails to sync in initial sync process ", + "fieldname": "initial_sync_count", + "fieldtype": "Select", + "hide_days": 1, + "hide_seconds": 1, + "label": "Initial Sync Count", + "options": "100\n250\n500" + }, + { + "depends_on": "enable_incoming", + "fieldname": "section_break_13", + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 + }, + { + "default": "0", + "fieldname": "notify_if_unreplied", + "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, + "label": "Notify if unreplied" + }, + { + "default": "30", + "depends_on": "notify_if_unreplied", + "fieldname": "unreplied_for_mins", + "fieldtype": "Int", + "hide_days": 1, + "hide_seconds": 1, + "label": "Notify if unreplied for (in mins)" + }, + { + "depends_on": "notify_if_unreplied", + "description": "Email Addresses", + "fieldname": "send_notification_to", + "fieldtype": "Small Text", + "hide_days": 1, + "hide_seconds": 1, + "label": "Send Notification to" + }, + { + "fieldname": "outgoing_mail_settings", + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, + "label": "Outgoing (SMTP) Settings" + }, + { + "default": "0", + "description": "SMTP Settings for outgoing emails", + "fieldname": "enable_outgoing", + "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, + "label": "Enable Outgoing" + }, + { + "depends_on": "eval:!doc.domain && doc.enable_outgoing", + "description": "e.g. smtp.gmail.com", + "fetch_from": "domain.smtp_server", + "fieldname": "smtp_server", + "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, + "label": "Outgoing Server" + }, + { + "default": "0", + "depends_on": "eval:!doc.domain && doc.enable_outgoing", + "fetch_from": "domain.use_tls", + "fieldname": "use_tls", + "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, + "label": "Use TLS" + }, + { + "depends_on": "eval:!doc.domain && doc.enable_outgoing", + "description": "If non standard port (e.g. 587). If on Google Cloud, try port 2525.", + "fetch_from": "domain.smtp_port", + "fieldname": "smtp_port", + "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, + "label": "Port" + }, + { + "default": "0", + "depends_on": "enable_outgoing", + "description": "Notifications and bulk mails will be sent from this outgoing server.", + "fieldname": "default_outgoing", + "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, + "label": "Default Outgoing" + }, + { + "default": "0", + "depends_on": "enable_outgoing", + "fieldname": "always_use_account_email_id_as_sender", + "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, + "label": "Always use this email address as sender address" + }, + { + "default": "0", + "depends_on": "enable_outgoing", + "fieldname": "always_use_account_name_as_sender_name", + "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, + "label": "Always use this name as sender name" + }, + { + "default": "1", + "fieldname": "send_unsubscribe_message", + "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, + "label": "Send unsubscribe message in email" + }, + { + "default": "1", + "description": "Track if your email has been opened by the recipient.\n
      \nNote: If you're sending to multiple recipients, even if 1 recipient reads the email, it'll be considered \"Opened\"", + "fieldname": "track_email_status", + "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, + "label": "Track Email Status" + }, + { + "default": "0", + "fieldname": "no_smtp_authentication", + "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, + "label": "Disable SMTP server authentication" + }, + { + "collapsible": 1, + "collapsible_depends_on": "add_signature", + "fieldname": "signature_section", + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, + "label": "Signature" + }, + { + "default": "0", + "fieldname": "add_signature", + "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, + "label": "Add Signature" + }, + { + "depends_on": "add_signature", + "fieldname": "signature", + "fieldtype": "Text Editor", + "hide_days": 1, + "hide_seconds": 1, + "label": "Signature" + }, + { + "collapsible": 1, + "collapsible_depends_on": "enable_auto_reply", + "fieldname": "auto_reply", + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, + "label": "Auto Reply" + }, + { + "default": "0", + "fieldname": "enable_auto_reply", + "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, + "label": "Enable Auto Reply" + }, + { + "depends_on": "enable_auto_reply", + "description": "ProTip: Add Reference: {{ reference_doctype }} {{ reference_name }} to send document reference", + "fieldname": "auto_reply_message", + "fieldtype": "Text Editor", + "hide_days": 1, + "hide_seconds": 1, + "label": "Auto Reply Message" + }, + { + "collapsible": 1, + "collapsible_depends_on": "eval:influxframework.utils.html2text(doc.footer || '')!=''", + "fieldname": "set_footer", + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, + "label": "Footer" + }, + { + "fieldname": "footer", + "fieldtype": "Text Editor", + "hide_days": 1, + "hide_seconds": 1, + "label": "Footer Content" + }, + { + "fieldname": "uidvalidity", + "fieldtype": "Data", + "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, + "label": "UIDVALIDITY", + "no_copy": 1 + }, + { + "fieldname": "uidnext", + "fieldtype": "Int", + "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, + "label": "UIDNEXT", + "no_copy": 1 + }, + { + "fieldname": "no_failed", + "fieldtype": "Int", + "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, + "label": "no failed attempts", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "section_break_12", + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, + "label": "Document Linking" + }, + { + "default": "0", + "description": "For more information, click here.", + "fieldname": "enable_automatic_linking", + "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, + "label": "Enable Automatic Linking in Documents" + }, + { + "depends_on": "eval:!doc.domain && doc.enable_incoming", + "description": "If non-standard port (e.g. POP3: 995/110, IMAP: 993/143)", + "fieldname": "incoming_port", + "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, + "label": "Port" + }, + { + "default": "0", + "depends_on": "eval:!doc.domain && doc.enable_outgoing", + "fieldname": "append_emails_to_sent_folder", + "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, + "label": "Append Emails to Sent Folder" + }, + { + "default": "0", + "depends_on": "eval:!doc.domain && doc.enable_outgoing", + "fieldname": "use_ssl_for_outgoing", + "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, + "label": "Use SSL" + }, + { + "default": "1", + "fieldname": "create_contact", + "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, + "label": "Create Contacts from Incoming Emails" + }, + { + "fieldname": "brand_logo", + "fieldtype": "Attach Image", + "label": "Brand Logo" + }, + { + "fieldname": "authentication_column", + "fieldtype": "Section Break", + "label": "Authentication" + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_38", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "account_section", + "fieldtype": "Section Break", + "label": "Account" + }, + { + "depends_on": "eval: doc.use_imap && doc.enable_incoming", + "fieldname": "imap_folder", + "fieldtype": "Table", + "label": "IMAP Folder", + "options": "IMAP Folder" + }, + { + "fieldname": "section_break_25", + "fieldtype": "Section Break", + "label": "IMAP Details" + }, + { + "depends_on": "eval: doc.service === \"GMail\" && doc.auth_method === \"OAuth\" && !doc.__islocal && !doc.__unsaved", + "fieldname": "authorize_api_access", + "fieldtype": "Button", + "label": "Authorize API Access" + }, + { + "fieldname": "refresh_token", + "fieldtype": "Small Text", + "hidden": 1, + "label": "Refresh Token", + "read_only": 1 + }, + { + "fieldname": "access_token", + "fieldtype": "Small Text", + "hidden": 1, + "label": "Access Token", + "read_only": 1 + }, + { + "default": "Basic", + "fieldname": "auth_method", + "fieldtype": "Select", + "label": "Method", + "options": "Basic\nOAuth" + }, + { + "default": "0", + "depends_on": "eval:!doc.domain && doc.enable_incoming && doc.use_imap && !doc.use_ssl", + "fetch_from": "domain.use_starttls", + "fieldname": "use_starttls", + "fieldtype": "Check", + "label": "Use STARTTLS" + } + ], + "icon": "fa fa-inbox", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2022-08-23 00:31:05.305462", + "modified_by": "Administrator", + "module": "Email", + "name": "Email Account", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "read": 1, + "role": "System Manager", + "set_user_permissions": 1, + "write": 1 + }, + { + "read": 1, + "role": "Inbox User" + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} diff --git a/influxframework/email/doctype/email_account/email_account.py b/influxframework/email/doctype/email_account/email_account.py new file mode 100644 index 0000000..9b5733e --- /dev/null +++ b/influxframework/email/doctype/email_account/email_account.py @@ -0,0 +1,935 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE + +import email.utils +import functools +import imaplib +import socket +import time +from datetime import datetime, timedelta +from poplib import error_proto + +import influxframework +from influxframework import _, are_emails_muted, safe_encode +from influxframework.desk.form import assign_to +from influxframework.email.doctype.email_domain.email_domain import EMAIL_DOMAIN_FIELDS +from influxframework.email.receive import EmailServer, InboundMail, SentEmailInInboxError +from influxframework.email.smtp import SMTPServer +from influxframework.email.utils import get_port +from influxframework.model.document import Document +from influxframework.utils import cint, comma_or, cstr, parse_addr, validate_email_address +from influxframework.utils.background_jobs import enqueue, get_jobs +from influxframework.utils.error import raise_error_on_no_output +from influxframework.utils.jinja import render_template +from influxframework.utils.password import decrypt, encrypt +from influxframework.utils.user import get_system_managers + + +class SentEmailInInbox(Exception): + pass + + +def cache_email_account(cache_name): + def decorator_cache_email_account(func): + @functools.wraps(func) + def wrapper_cache_email_account(*args, **kwargs): + if not hasattr(influxframework.local, cache_name): + setattr(influxframework.local, cache_name, {}) + + cached_accounts = getattr(influxframework.local, cache_name) + match_by = list(kwargs.values()) + ["default"] + matched_accounts = list(filter(None, [cached_accounts.get(key) for key in match_by])) + if matched_accounts: + return matched_accounts[0] + + matched_accounts = func(*args, **kwargs) + cached_accounts.update(matched_accounts or {}) + return matched_accounts and list(matched_accounts.values())[0] + + return wrapper_cache_email_account + + return decorator_cache_email_account + + +class EmailAccount(Document): + DOCTYPE = "Email Account" + + def autoname(self): + """Set name as `email_account_name` or make title from Email Address.""" + if not self.email_account_name: + self.email_account_name = ( + self.email_id.split("@", 1)[0].replace("_", " ").replace(".", " ").replace("-", " ").title() + ) + + self.name = self.email_account_name + + def validate(self): + """Validate Email Address and check POP3/IMAP and SMTP connections is enabled.""" + + if self.email_id: + validate_email_address(self.email_id, True) + + if self.login_id_is_different: + if not self.login_id: + influxframework.throw(_("Login Id is required")) + else: + self.login_id = None + + # validate the imap settings + if self.enable_incoming and self.use_imap and len(self.imap_folder) <= 0: + influxframework.throw(_("You need to set one IMAP folder for {0}").format(influxframework.bold(self.email_id))) + + if influxframework.local.flags.in_patch or influxframework.local.flags.in_test: + return + + use_oauth = self.auth_method == "OAuth" + self.use_starttls = cint(self.use_imap and self.use_starttls and not self.use_ssl) + + if getattr(self, "service", "") != "GMail" and use_oauth: + self.auth_method = "Basic" + use_oauth = False + + if use_oauth: + # no need for awaiting password for oauth + self.awaiting_password = 0 + self.password = None + + elif self.refresh_token: + # clear access & refresh token + self.refresh_token = self.access_token = None + + if not influxframework.local.flags.in_install and not self.awaiting_password: + if self.refresh_token or self.password or self.smtp_server in ("127.0.0.1", "localhost"): + if self.enable_incoming: + self.get_incoming_server() + self.no_failed = 0 + + if self.enable_outgoing: + self.validate_smtp_conn() + else: + if self.enable_incoming or (self.enable_outgoing and not self.no_smtp_authentication): + if not use_oauth: + influxframework.throw(_("Password is required or select Awaiting Password")) + + if self.notify_if_unreplied: + if not self.send_notification_to: + influxframework.throw(_("{0} is mandatory").format(self.meta.get_label("send_notification_to"))) + for e in self.get_unreplied_notification_emails(): + validate_email_address(e, True) + + if self.enable_incoming: + for folder in self.imap_folder: + if folder.append_to: + valid_doctypes = [d[0] for d in get_append_to()] + if folder.append_to not in valid_doctypes: + influxframework.throw(_("Append To can be one of {0}").format(comma_or(valid_doctypes))) + + def validate_smtp_conn(self): + if not self.smtp_server: + influxframework.throw(_("SMTP Server is required")) + + server = self.get_smtp_server() + return server.session + + def before_save(self): + messages = [] + as_list = 1 + if not self.enable_incoming and self.default_incoming: + self.default_incoming = False + messages.append( + _("{} has been disabled. It can only be enabled if {} is checked.").format( + influxframework.bold(_("Default Incoming")), influxframework.bold(_("Enable Incoming")) + ) + ) + if not self.enable_outgoing and self.default_outgoing: + self.default_outgoing = False + messages.append( + _("{} has been disabled. It can only be enabled if {} is checked.").format( + influxframework.bold(_("Default Outgoing")), influxframework.bold(_("Enable Outgoing")) + ) + ) + if messages: + if len(messages) == 1: + (as_list, messages) = (0, messages[0]) + influxframework.msgprint(messages, as_list=as_list, indicator="orange", title=_("Defaults Updated")) + + def on_update(self): + """Check there is only one default of each type.""" + self.check_automatic_linking_email_account() + self.there_must_be_only_one_default() + setup_user_email_inbox( + email_account=self.name, + awaiting_password=self.awaiting_password, + email_id=self.email_id, + enable_outgoing=self.enable_outgoing, + used_oauth=self.auth_method == "OAuth", + ) + + def there_must_be_only_one_default(self): + """If current Email Account is default, un-default all other accounts.""" + for field in ("default_incoming", "default_outgoing"): + if not self.get(field): + continue + + for email_account in influxframework.get_all("Email Account", filters={field: 1}): + if email_account.name == self.name: + continue + + email_account = influxframework.get_doc("Email Account", email_account.name) + email_account.set(field, 0) + email_account.save() + + @influxframework.whitelist() + def get_domain_values(self, domain: str): + return influxframework.db.get_value("Email Domain", domain, EMAIL_DOMAIN_FIELDS, as_dict=True) + + def get_incoming_server(self, in_receive=False, email_sync_rule="UNSEEN"): + """Returns logged in POP3/IMAP connection object.""" + if influxframework.cache().get_value("workers:no-internet") == True: + return None + + args = influxframework._dict( + { + "email_account_name": self.email_account_name, + "email_account": self.name, + "host": self.email_server, + "use_ssl": self.use_ssl, + "use_starttls": self.use_starttls, + "username": getattr(self, "login_id", None) or self.email_id, + "service": getattr(self, "service", ""), + "use_imap": self.use_imap, + "email_sync_rule": email_sync_rule, + "incoming_port": get_port(self), + "initial_sync_count": self.initial_sync_count or 100, + "use_oauth": self.auth_method == "OAuth", + "refresh_token": decrypt(self.refresh_token) if self.refresh_token else None, + "access_token": decrypt(self.access_token) if self.access_token else None, + } + ) + + if self.password: + args.password = self.get_password() + + if not args.get("host"): + influxframework.throw(_("{0} is required").format("Email Server")) + + email_server = EmailServer(influxframework._dict(args)) + self.check_email_server_connection(email_server, in_receive) + + if not in_receive and self.use_imap: + email_server.imap.logout() + + # reset failed attempts count + self.set_failed_attempts_count(0) + + return email_server + + def check_email_server_connection(self, email_server, in_receive): + # tries to connect to email server and handles failure + try: + email_server.connect() + except (error_proto, imaplib.IMAP4.error) as e: + message = cstr(e).lower().replace(" ", "") + auth_error_codes = [ + "authenticationfailed", + "loginfailed", + ] + + other_error_codes = ["err[auth]", "errtemporaryerror", "loginviayourwebbrowser"] + + all_error_codes = auth_error_codes + other_error_codes + + if in_receive and any(map(lambda t: t in message, all_error_codes)): + # if called via self.receive and it leads to authentication error, + # disable incoming and send email to System Manager + error_message = _( + "Authentication failed while receiving emails from Email Account: {0}." + ).format(self.name) + error_message += "
      " + _("Message from server: {0}").format(cstr(e)) + self.handle_incoming_connect_error(description=error_message) + return None + + elif not in_receive and any(map(lambda t: t in message, auth_error_codes)): + SMTPServer.throw_invalid_credentials_exception() + else: + influxframework.throw(cstr(e)) + + except OSError: + if in_receive: + # timeout while connecting, see receive.py connect method + description = influxframework.message_log.pop() if influxframework.message_log else "Socket Error" + if test_internet(): + self.db_set("no_failed", self.no_failed + 1) + if self.no_failed > 2: + self.handle_incoming_connect_error(description=description) + else: + influxframework.cache().set_value("workers:no-internet", True) + return None + else: + raise + + @property + def _password(self): + raise_exception = not ( + self.auth_method == "OAuth" or self.no_smtp_authentication or influxframework.flags.in_test + ) + return self.get_password(raise_exception=raise_exception) + + @property + def default_sender(self): + return email.utils.formataddr((self.name, self.get("email_id"))) + + def is_exists_in_db(self): + """Some of the Email Accounts we create from configs and those doesn't exists in DB. + This is is to check the specific email account exists in DB or not. + """ + return self.find_one_by_filters(name=self.name) + + @classmethod + def from_record(cls, record): + email_account = influxframework.new_doc(cls.DOCTYPE) + email_account.update(record) + return email_account + + @classmethod + def find(cls, name): + return influxframework.get_doc(cls.DOCTYPE, name) + + @classmethod + def find_one_by_filters(cls, **kwargs): + name = influxframework.db.get_value(cls.DOCTYPE, kwargs) + return cls.find(name) if name else None + + @classmethod + def find_from_config(cls): + config = cls.get_account_details_from_site_config() + return cls.from_record(config) if config else None + + @classmethod + def create_dummy(cls): + return cls.from_record({"sender": "notifications@example.com"}) + + @classmethod + @raise_error_on_no_output( + keep_quiet=lambda: not cint(influxframework.get_system_settings("setup_complete")), + error_message=_("Please setup default Email Account from Setup > Email > Email Account"), + error_type=influxframework.OutgoingEmailError, + ) # noqa + @cache_email_account("outgoing_email_account") + def find_outgoing(cls, match_by_email=None, match_by_doctype=None, _raise_error=False): + """Find the outgoing Email account to use. + + :param match_by_email: Find account using emailID + :param match_by_doctype: Find account by matching `Append To` doctype + :param _raise_error: This is used by raise_error_on_no_output decorator to raise error. + """ + if match_by_email: + match_by_email = parse_addr(match_by_email)[1] + doc = cls.find_one_by_filters(enable_outgoing=1, email_id=match_by_email) + if doc: + return {match_by_email: doc} + + if match_by_doctype: + doc = cls.find_one_by_filters(enable_outgoing=1, enable_incoming=1, append_to=match_by_doctype) + if doc: + return {match_by_doctype: doc} + + doc = cls.find_default_outgoing() + if doc: + return {"default": doc} + + @classmethod + def find_default_outgoing(cls): + """Find default outgoing account.""" + doc = cls.find_one_by_filters(enable_outgoing=1, default_outgoing=1) + doc = doc or cls.find_from_config() + return doc or (are_emails_muted() and cls.create_dummy()) + + @classmethod + def find_incoming(cls, match_by_email=None, match_by_doctype=None): + """Find the incoming Email account to use. + :param match_by_email: Find account using emailID + :param match_by_doctype: Find account by matching `Append To` doctype + """ + doc = cls.find_one_by_filters(enable_incoming=1, email_id=match_by_email) + if doc: + return doc + + doc = cls.find_one_by_filters(enable_incoming=1, append_to=match_by_doctype) + if doc: + return doc + + doc = cls.find_default_incoming() + return doc + + @classmethod + def find_default_incoming(cls): + doc = cls.find_one_by_filters(enable_incoming=1, default_incoming=1) + return doc + + @classmethod + def get_account_details_from_site_config(cls): + if not influxframework.conf.get("mail_server"): + return {} + + field_to_conf_name_map = { + "smtp_server": {"conf_names": ("mail_server",)}, + "smtp_port": {"conf_names": ("mail_port",)}, + "use_tls": {"conf_names": ("use_tls", "mail_login")}, + "login_id": {"conf_names": ("mail_login",)}, + "email_id": { + "conf_names": ("auto_email_id", "mail_login"), + "default": "notifications@example.com", + }, + "password": {"conf_names": ("mail_password",)}, + "always_use_account_email_id_as_sender": { + "conf_names": ("always_use_account_email_id_as_sender",), + "default": 0, + }, + "always_use_account_name_as_sender_name": { + "conf_names": ("always_use_account_name_as_sender_name",), + "default": 0, + }, + "name": {"conf_names": ("email_sender_name",), "default": "InfluxFramework"}, + "auth_method": {"conf_names": ("auth_method"), "default": "Basic"}, + "access_token": {"conf_names": ("mail_access_token")}, + "refresh_token": {"conf_names": ("mail_refresh_token")}, + "from_site_config": {"default": True}, + } + + account_details = {} + for doc_field_name, d in field_to_conf_name_map.items(): + conf_names, default = d.get("conf_names") or [], d.get("default") + value = [influxframework.conf.get(k) for k in conf_names if influxframework.conf.get(k)] + + if doc_field_name in ("refresh_token", "access_token"): + account_details[doc_field_name] = value and encrypt(value[0]) + else: + account_details[doc_field_name] = (value and value[0]) or default + + return account_details + + def sendmail_config(self): + return { + "email_account": self.name, + "server": self.smtp_server, + "port": cint(self.smtp_port), + "login": getattr(self, "login_id", None) or self.email_id, + "password": self._password, + "use_ssl": cint(self.use_ssl_for_outgoing), + "use_tls": cint(self.use_tls), + "service": getattr(self, "service", ""), + "use_oauth": self.auth_method == "OAuth", + "refresh_token": decrypt(self.refresh_token) if self.refresh_token else None, + "access_token": decrypt(self.access_token) if self.access_token else None, + } + + def get_smtp_server(self): + config = self.sendmail_config() + return SMTPServer(**config) + + def handle_incoming_connect_error(self, description): + if test_internet(): + if self.get_failed_attempts_count() > 2: + self.db_set("enable_incoming", 0) + + for user in get_system_managers(only_name=True): + try: + assign_to.add( + { + "assign_to": user, + "doctype": self.doctype, + "name": self.name, + "description": description, + "priority": "High", + "notify": 1, + } + ) + except assign_to.DuplicateToDoError: + influxframework.message_log.pop() + pass + else: + self.set_failed_attempts_count(self.get_failed_attempts_count() + 1) + else: + influxframework.cache().set_value("workers:no-internet", True) + + def set_failed_attempts_count(self, value): + influxframework.cache().set(f"{self.name}:email-account-failed-attempts", value) + + def get_failed_attempts_count(self): + return cint(influxframework.cache().get(f"{self.name}:email-account-failed-attempts")) + + def receive(self): + """Called by scheduler to receive emails from this EMail account using POP3/IMAP.""" + exceptions = [] + inbound_mails = self.get_inbound_mails() + for mail in inbound_mails: + try: + communication = mail.process() + influxframework.db.commit() + # If email already exists in the system + # then do not send notifications for the same email. + if communication and mail.flags.is_new_communication: + # notify all participants of this thread + if self.enable_auto_reply: + self.send_auto_reply(communication, mail) + + communication.send_email(is_inbound_mail_communcation=True) + except SentEmailInInboxError: + influxframework.db.rollback() + except Exception: + influxframework.db.rollback() + self.log_error(title="EmailAccount.receive") + if self.use_imap: + self.handle_bad_emails(mail.uid, mail.raw_message, influxframework.get_traceback()) + exceptions.append(influxframework.get_traceback()) + else: + influxframework.db.commit() + + # notify if user is linked to account + if len(inbound_mails) > 0 and not influxframework.local.flags.in_test: + influxframework.publish_realtime( + "new_email", {"account": self.email_account_name, "number": len(inbound_mails)} + ) + + if exceptions: + raise Exception(influxframework.as_json(exceptions)) + + def get_inbound_mails(self) -> list[InboundMail]: + """retrive and return inbound mails.""" + mails = [] + + def process_mail(messages, append_to=None): + for index, message in enumerate(messages.get("latest_messages", [])): + uid = messages["uid_list"][index] if messages.get("uid_list") else None + seen_status = messages.get("seen_status", {}).get(uid) + if self.email_sync_option != "UNSEEN" or seen_status != "SEEN": + # only append the emails with status != 'SEEN' if sync option is set to 'UNSEEN' + mails.append(InboundMail(message, self, influxframework.safe_decode(uid), seen_status, append_to)) + + if not self.enable_incoming: + return [] + + email_sync_rule = self.build_email_sync_rule() + try: + email_server = self.get_incoming_server(in_receive=True, email_sync_rule=email_sync_rule) + if self.use_imap: + # process all given imap folder + for folder in self.imap_folder: + if email_server.select_imap_folder(folder.folder_name): + email_server.settings["uid_validity"] = folder.uidvalidity + messages = email_server.get_messages(folder=f'"{folder.folder_name}"') or {} + process_mail(messages, folder.append_to) + else: + # process the pop3 account + messages = email_server.get_messages() or {} + process_mail(messages) + # close connection to mailserver + email_server.logout() + except Exception: + self.log_error(title=_("Error while connecting to email account {0}").format(self.name)) + return [] + return mails + + def handle_bad_emails(self, uid, raw, reason): + if cint(self.use_imap): + import email + + try: + if isinstance(raw, bytes): + mail = email.message_from_bytes(raw) + else: + mail = email.message_from_string(raw) + + message_id = mail.get("Message-ID") + except Exception: + message_id = "can't be parsed" + + unhandled_email = influxframework.get_doc( + { + "raw": raw, + "uid": uid, + "reason": reason, + "message_id": message_id, + "doctype": "Unhandled Email", + "email_account": self.name, + } + ) + unhandled_email.insert(ignore_permissions=True) + influxframework.db.commit() + + def send_auto_reply(self, communication, email): + """Send auto reply if set.""" + from influxframework.core.doctype.communication.email import set_incoming_outgoing_accounts + + if self.enable_auto_reply: + set_incoming_outgoing_accounts(communication) + + unsubscribe_message = (self.send_unsubscribe_message and _("Leave this conversation")) or "" + + influxframework.sendmail( + recipients=[email.from_email], + sender=self.email_id, + reply_to=communication.incoming_email_account, + subject=" ".join([_("Re:"), communication.subject]), + content=render_template(self.auto_reply_message or "", communication.as_dict()) + or influxframework.get_template("templates/emails/auto_reply.html").render(communication.as_dict()), + reference_doctype=communication.reference_doctype, + reference_name=communication.reference_name, + in_reply_to=email.mail.get("Message-Id"), # send back the Message-Id as In-Reply-To + unsubscribe_message=unsubscribe_message, + ) + + def get_unreplied_notification_emails(self): + """Return list of emails listed""" + self.send_notification_to = self.send_notification_to.replace(",", "\n") + out = [e.strip() for e in self.send_notification_to.split("\n") if e.strip()] + return out + + def on_trash(self): + """Clear communications where email account is linked""" + Communication = influxframework.qb.DocType("Communication") + influxframework.qb.update(Communication).set(Communication.email_account, "").where( + Communication.email_account == self.name + ).run() + + remove_user_email_inbox(email_account=self.name) + + def after_rename(self, old, new, merge=False): + influxframework.db.set_value("Email Account", new, "email_account_name", new) + + def build_email_sync_rule(self): + if not self.use_imap: + return "UNSEEN" + + if self.email_sync_option == "ALL": + max_uid = get_max_email_uid(self.name) + last_uid = max_uid + int(self.initial_sync_count or 100) if max_uid == 1 else "*" + return f"UID {max_uid}:{last_uid}" + else: + return self.email_sync_option or "UNSEEN" + + def mark_emails_as_read_unread(self, email_server=None, folder_name="INBOX"): + """mark Email Flag Queue of self.email_account mails as read""" + if not self.use_imap: + return + + EmailFlagQ = influxframework.qb.DocType("Email Flag Queue") + flags = ( + influxframework.qb.from_(EmailFlagQ) + .select(EmailFlagQ.name, EmailFlagQ.communication, EmailFlagQ.uid, EmailFlagQ.action) + .where(EmailFlagQ.is_completed == 0) + .where(EmailFlagQ.email_account == influxframework.db.escape(self.name)) + ).run(as_dict=True) + + uid_list = {flag.get("uid", None): flag.get("action", "Read") for flag in flags} + if flags and uid_list: + if not email_server: + email_server = self.get_incoming_server() + if not email_server: + return + email_server.update_flag(folder_name, uid_list=uid_list) + + # mark communication as read + docnames = ",".join( + "'%s'" % flag.get("communication") for flag in flags if flag.get("action") == "Read" + ) + self.set_communication_seen_status(docnames, seen=1) + + # mark communication as unread + docnames = ",".join( + ["'%s'" % flag.get("communication") for flag in flags if flag.get("action") == "Unread"] + ) + self.set_communication_seen_status(docnames, seen=0) + + docnames = ",".join(["'%s'" % flag.get("name") for flag in flags]) + + EmailFlagQueue = influxframework.qb.DocType("Email Flag Queue") + influxframework.qb.update(EmailFlagQueue).set(EmailFlagQueue.is_completed, 1).where( + EmailFlagQueue.name.isin(docnames) + ).run() + + def set_communication_seen_status(self, docnames, seen=0): + """mark Email Flag Queue of self.email_account mails as read""" + if not docnames: + return + Communication = influxframework.qb.from_("Communication") + influxframework.qb.update(Communication).set(Communication.seen == seen).where( + Communication.name.isin(docnames) + ).run() + + def check_automatic_linking_email_account(self): + if self.enable_automatic_linking: + if not self.enable_incoming: + influxframework.throw(_("Automatic Linking can be activated only if Incoming is enabled.")) + + if influxframework.db.exists( + "Email Account", {"enable_automatic_linking": 1, "name": ("!=", self.name)} + ): + influxframework.throw(_("Automatic Linking can be activated only for one Email Account.")) + + def append_email_to_sent_folder(self, message): + email_server = None + try: + email_server = self.get_incoming_server(in_receive=True) + except Exception: + self.log_error("Email Connection Error") + + if not email_server: + return + + email_server.connect() + + if email_server.imap: + try: + message = safe_encode(message) + email_server.imap.append("Sent", "\\Seen", imaplib.Time2Internaldate(time.time()), message) + except Exception: + self.log_error("Unable to add to Sent folder") + + +@influxframework.whitelist() +def get_append_to( + doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None +): + txt = txt if txt else "" + email_append_to_list = [] + + # Set Email Append To DocTypes via DocType + filters = {"istable": 0, "issingle": 0, "email_append_to": 1} + for dt in influxframework.get_all("DocType", filters=filters, fields=["name", "email_append_to"]): + email_append_to_list.append(dt.name) + + # Set Email Append To DocTypes set via Customize Form + for dt in influxframework.get_list( + "Property Setter", filters={"property": "email_append_to", "value": 1}, fields=["doc_type"] + ): + email_append_to_list.append(dt.doc_type) + + email_append_to = [[d] for d in set(email_append_to_list) if txt in d] + + return email_append_to + + +def test_internet(host="8.8.8.8", port=53, timeout=3): + """Returns True if internet is connected + + Host: 8.8.8.8 (google-public-dns-a.google.com) + OpenPort: 53/tcp + Service: domain (DNS/TCP) + """ + try: + socket.setdefaulttimeout(timeout) + socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port)) + return True + except Exception as ex: + print(ex.message) + return False + + +def notify_unreplied(): + """Sends email notifications if there are unreplied Communications + and `notify_if_unreplied` is set as true.""" + for email_account in influxframework.get_all( + "Email Account", "name", filters={"enable_incoming": 1, "notify_if_unreplied": 1} + ): + email_account = influxframework.get_doc("Email Account", email_account.name) + + if email_account.use_imap: + append_to = [folder.get("append_to") for folder in email_account.imap_folder] + else: + append_to = email_account.append_to + + if append_to: + # get open communications younger than x mins, for given doctype + for comm in influxframework.get_all( + "Communication", + "name", + filters=[ + {"sent_or_received": "Received"}, + {"reference_doctype": ("in", append_to)}, + {"unread_notification_sent": 0}, + {"email_account": email_account.name}, + { + "creation": ( + "<", + datetime.now() - timedelta(seconds=(email_account.unreplied_for_mins or 30) * 60), + ) + }, + { + "creation": ( + ">", + datetime.now() - timedelta(seconds=(email_account.unreplied_for_mins or 30) * 60 * 3), + ) + }, + ], + ): + comm = influxframework.get_doc("Communication", comm.name) + + if influxframework.db.get_value(comm.reference_doctype, comm.reference_name, "status") == "Open": + # if status is still open + influxframework.sendmail( + recipients=email_account.get_unreplied_notification_emails(), + content=comm.content, + subject=comm.subject, + doctype=comm.reference_doctype, + name=comm.reference_name, + ) + + # update flag + comm.db_set("unread_notification_sent", 1) + + +def pull(now=False): + """Will be called via scheduler, pull emails from all enabled Email accounts.""" + + if influxframework.cache().get_value("workers:no-internet") == True: + if test_internet(): + influxframework.cache().set_value("workers:no-internet", False) + else: + return + + doctype = influxframework.qb.DocType("Email Account") + email_accounts = ( + influxframework.qb.from_(doctype) + .select(doctype.name) + .where(doctype.enable_incoming == 1) + .where( + (doctype.awaiting_password == 0) + | ((doctype.auth_method == "OAuth") & (doctype.refresh_token.isnotnull())) + ) + .run(as_dict=1) + ) + for email_account in email_accounts: + if now: + pull_from_email_account(email_account.name) + + else: + # job_name is used to prevent duplicates in queue + job_name = f"pull_from_email_account|{email_account.name}" + + queued_jobs = get_jobs(site=influxframework.local.site, key="job_name")[influxframework.local.site] + if job_name not in queued_jobs: + enqueue( + pull_from_email_account, + "short", + event="all", + job_name=job_name, + email_account=email_account.name, + ) + + +def pull_from_email_account(email_account): + """Runs within a worker process""" + email_account = influxframework.get_doc("Email Account", email_account) + email_account.receive() + + +def get_max_email_uid(email_account): + # get maximum uid of emails + max_uid = 1 + + result = influxframework.get_all( + "Communication", + filters={ + "communication_medium": "Email", + "sent_or_received": "Received", + "email_account": email_account, + }, + fields=["max(uid) as uid"], + ) + + if not result: + return 1 + else: + max_uid = cint(result[0].get("uid", 0)) + 1 + return max_uid + + +def setup_user_email_inbox( + email_account, awaiting_password, email_id, enable_outgoing, used_oauth +): + """setup email inbox for user""" + from influxframework.core.doctype.user.user import ask_pass_update + + def add_user_email(user): + user = influxframework.get_doc("User", user) + row = user.append("user_emails", {}) + + row.email_id = email_id + row.email_account = email_account + row.awaiting_password = awaiting_password or 0 + row.used_oauth = used_oauth or 0 + row.enable_outgoing = enable_outgoing or 0 + + user.save(ignore_permissions=True) + + update_user_email_settings = False + if not all([email_account, email_id]): + return + + user_names = influxframework.db.get_values("User", {"email": email_id}, as_dict=True) + if not user_names: + return + + for user in user_names: + user_name = user.get("name") + + # check if inbox is alreay configured + user_inbox = ( + influxframework.db.get_value( + "User Email", {"email_account": email_account, "parent": user_name}, ["name"] + ) + or None + ) + + if not user_inbox: + add_user_email(user_name) + else: + # update awaiting password for email account + update_user_email_settings = True + + if update_user_email_settings: + UserEmail = influxframework.qb.DocType("User Email") + influxframework.qb.update(UserEmail).set(UserEmail.awaiting_password, (awaiting_password or 0)).set( + UserEmail.enable_outgoing, (enable_outgoing or 0) + ).set(UserEmail.used_oauth, (used_oauth or 0)).where( + UserEmail.email_account == email_account + ).run() + + else: + users = " and ".join([influxframework.bold(user.get("name")) for user in user_names]) + influxframework.msgprint(_("Enabled email inbox for user {0}").format(users)) + ask_pass_update() + + +def remove_user_email_inbox(email_account): + """remove user email inbox settings if email account is deleted""" + if not email_account: + return + + users = influxframework.get_all( + "User Email", filters={"email_account": email_account}, fields=["parent as name"] + ) + + for user in users: + doc = influxframework.get_doc("User", user.get("name")) + to_remove = [row for row in doc.user_emails if row.email_account == email_account] + [doc.remove(row) for row in to_remove] + + doc.save(ignore_permissions=True) + + +@influxframework.whitelist() +def set_email_password(email_account, password): + account = influxframework.get_doc("Email Account", email_account) + if account.awaiting_password and not account.auth_method == "OAuth": + account.awaiting_password = 0 + account.password = password + try: + account.save(ignore_permissions=True) + except Exception: + influxframework.db.rollback() + return False + + return True diff --git a/influxframework/email/doctype/email_account/email_account_list.js b/influxframework/email/doctype/email_account/email_account_list.js new file mode 100644 index 0000000..ae2f9c0 --- /dev/null +++ b/influxframework/email/doctype/email_account/email_account_list.js @@ -0,0 +1,24 @@ +influxframework.listview_settings["Email Account"] = { + add_fields: ["default_incoming", "default_outgoing", "enable_incoming", "enable_outgoing"], + get_indicator: function (doc) { + if (doc.default_incoming && doc.default_outgoing) { + var color = doc.enable_incoming && doc.enable_outgoing ? "blue" : "gray"; + return [ + __("Default Sending and Inbox"), + color, + "default_incoming,=,Yes|default_outgoing,=,Yes", + ]; + } else if (doc.default_incoming) { + color = doc.enable_incoming ? "blue" : "gray"; + return [__("Default Inbox"), color, "default_incoming,=,Yes"]; + } else if (doc.default_outgoing) { + color = doc.enable_outgoing ? "blue" : "gray"; + return [__("Default Sending"), color, "default_outgoing,=,Yes"]; + } else { + color = doc.enable_incoming ? "blue" : "gray"; + return [__("Inbox"), color, "is_global,=,No|is_default=No"]; + } + }, +}; + +influxframework.help.youtube_id["Email Account"] = "YFYe0DrB95o"; diff --git a/influxframework/email/doctype/email_account/test_email_account.py b/influxframework/email/doctype/email_account/test_email_account.py new file mode 100644 index 0000000..4d69a45 --- /dev/null +++ b/influxframework/email/doctype/email_account/test_email_account.py @@ -0,0 +1,627 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import email +import os +import unittest +from datetime import datetime, timedelta +from unittest.mock import patch + +import influxframework +from influxframework.core.doctype.communication.email import make +from influxframework.desk.form.load import get_attachments +from influxframework.email.doctype.email_account.email_account import notify_unreplied +from influxframework.email.email_body import get_message_id +from influxframework.email.receive import Email, InboundMail, SentEmailInInboxError +from influxframework.test_runner import make_test_records +from influxframework.tests.utils import InfluxFrameworkTestCase + +make_test_records("User") +make_test_records("Email Account") + + +class TestEmailAccount(InfluxFrameworkTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + email_account.db_set("enable_incoming", 1) + email_account.db_set("enable_auto_reply", 1) + email_account.db_set("use_imap", 1) + + @classmethod + def tearDownClass(cls): + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + email_account.db_set("enable_incoming", 0) + + def setUp(self): + influxframework.flags.mute_emails = False + influxframework.flags.sent_mail = None + influxframework.db.delete("Email Queue") + influxframework.db.delete("Unhandled Email") + + def get_test_mail(self, fname): + with open(os.path.join(os.path.dirname(__file__), "test_mails", fname)) as f: + return f.read() + + def test_incoming(self): + cleanup("test_sender@example.com") + + messages = { + # append_to = ToDo + '"INBOX"': { + "latest_messages": [self.get_test_mail("incoming-1.raw")], + "seen_status": {2: "UNSEEN"}, + "uid_list": [2], + } + } + + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + TestEmailAccount.mocked_email_receive(email_account, messages) + + comm = influxframework.get_doc("Communication", {"sender": "test_sender@example.com"}) + self.assertTrue("test_receiver@example.com" in comm.recipients) + # check if todo is created + self.assertTrue(influxframework.db.get_value(comm.reference_doctype, comm.reference_name, "name")) + + def test_unread_notification(self): + self.test_incoming() + + comm = influxframework.get_doc("Communication", {"sender": "test_sender@example.com"}) + comm.db_set("creation", datetime.now() - timedelta(seconds=30 * 60)) + + influxframework.db.delete("Email Queue") + notify_unreplied() + self.assertTrue( + influxframework.db.get_value( + "Email Queue", + { + "reference_doctype": comm.reference_doctype, + "reference_name": comm.reference_name, + "status": "Not Sent", + }, + ) + ) + + def test_incoming_with_attach(self): + cleanup("test_sender@example.com") + + existing_file = influxframework.get_doc({"doctype": "File", "file_name": "influxerp-conf-14.png"}) + influxframework.delete_doc("File", existing_file.name) + + messages = { + # append_to = ToDo + '"INBOX"': { + "latest_messages": [self.get_test_mail("incoming-2.raw")], + "seen_status": {2: "UNSEEN"}, + "uid_list": [2], + } + } + + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + TestEmailAccount.mocked_email_receive(email_account, messages) + + comm = influxframework.get_doc("Communication", {"sender": "test_sender@example.com"}) + self.assertTrue("test_receiver@example.com" in comm.recipients) + + # check attachment + attachments = get_attachments(comm.doctype, comm.name) + self.assertTrue("influxerp-conf-14.png" in [f.file_name for f in attachments]) + + # cleanup + existing_file = influxframework.get_doc({"doctype": "File", "file_name": "influxerp-conf-14.png"}) + influxframework.delete_doc("File", existing_file.name) + + def test_incoming_attached_email_from_outlook_plain_text_only(self): + cleanup("test_sender@example.com") + + messages = { + # append_to = ToDo + '"INBOX"': { + "latest_messages": [self.get_test_mail("incoming-3.raw")], + "seen_status": {2: "UNSEEN"}, + "uid_list": [2], + } + } + + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + TestEmailAccount.mocked_email_receive(email_account, messages) + + comm = influxframework.get_doc("Communication", {"sender": "test_sender@example.com"}) + self.assertTrue( + "From: "Microsoft Outlook" <test_sender@example.com>" in comm.content + ) + self.assertTrue( + "This is an e-mail message sent automatically by Microsoft Outlook while" in comm.content + ) + + def test_incoming_attached_email_from_outlook_layers(self): + cleanup("test_sender@example.com") + + messages = { + # append_to = ToDo + '"INBOX"': { + "latest_messages": [self.get_test_mail("incoming-4.raw")], + "seen_status": {2: "UNSEEN"}, + "uid_list": [2], + } + } + + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + TestEmailAccount.mocked_email_receive(email_account, messages) + + comm = influxframework.get_doc("Communication", {"sender": "test_sender@example.com"}) + self.assertTrue( + "From: "Microsoft Outlook" <test_sender@example.com>" in comm.content + ) + self.assertTrue( + "This is an e-mail message sent automatically by Microsoft Outlook while" in comm.content + ) + + def test_outgoing(self): + make( + subject="test-mail-000", + content="test mail 000", + recipients="test_receiver@example.com", + send_email=True, + sender="test_sender@example.com", + ) + + mail = email.message_from_string(influxframework.get_last_doc("Email Queue").message) + self.assertTrue("test-mail-000" in mail.get("Subject")) + + def test_sendmail(self): + influxframework.sendmail( + sender="test_sender@example.com", + recipients="test_recipient@example.com", + content="test mail 001", + subject="test-mail-001", + delayed=False, + ) + + sent_mail = email.message_from_string(influxframework.safe_decode(influxframework.flags.sent_mail)) + self.assertTrue("test-mail-001" in sent_mail.get("Subject")) + + def test_print_format(self): + make( + sender="test_sender@example.com", + recipients="test_recipient@example.com", + content="test mail 001", + subject="test-mail-002", + doctype="Email Account", + name="_Test Email Account 1", + print_format="Standard", + send_email=True, + ) + + sent_mail = email.message_from_string(influxframework.get_last_doc("Email Queue").message) + self.assertTrue("test-mail-002" in sent_mail.get("Subject")) + + def test_threading(self): + cleanup(["in", ["test_sender@example.com", "test@example.com"]]) + + # send + sent_name = make( + subject="Test", + content="test content", + recipients="test_receiver@example.com", + sender="test@example.com", + doctype="ToDo", + name=influxframework.get_last_doc("ToDo").name, + send_email=True, + )["name"] + + sent_mail = email.message_from_string(influxframework.get_last_doc("Email Queue").message) + + with open(os.path.join(os.path.dirname(__file__), "test_mails", "reply-1.raw")) as f: + raw = f.read() + raw = raw.replace("<-- in-reply-to -->", sent_mail.get("Message-Id")) + + # parse reply + messages = { + # append_to = ToDo + '"INBOX"': {"latest_messages": [raw], "seen_status": {2: "UNSEEN"}, "uid_list": [2]} + } + + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + TestEmailAccount.mocked_email_receive(email_account, messages) + + sent = influxframework.get_doc("Communication", sent_name) + + comm = influxframework.get_doc("Communication", {"sender": "test_sender@example.com"}) + self.assertEqual(comm.reference_doctype, sent.reference_doctype) + self.assertEqual(comm.reference_name, sent.reference_name) + + def test_threading_by_subject(self): + cleanup(["in", ["test_sender@example.com", "test@example.com"]]) + + with open(os.path.join(os.path.dirname(__file__), "test_mails", "reply-2.raw")) as f: + test_mails = [f.read()] + + with open(os.path.join(os.path.dirname(__file__), "test_mails", "reply-3.raw")) as f: + test_mails.append(f.read()) + + # parse reply + messages = { + # append_to = ToDo + '"INBOX"': { + "latest_messages": test_mails, + "seen_status": {2: "UNSEEN", 3: "UNSEEN"}, + "uid_list": [2, 3], + } + } + + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + TestEmailAccount.mocked_email_receive(email_account, messages) + + comm_list = influxframework.get_all( + "Communication", + filters={"sender": "test_sender@example.com"}, + fields=["name", "reference_doctype", "reference_name"], + ) + # both communications attached to the same reference + self.assertEqual(comm_list[0].reference_doctype, comm_list[1].reference_doctype) + self.assertEqual(comm_list[0].reference_name, comm_list[1].reference_name) + + def test_threading_by_message_id(self): + cleanup() + influxframework.db.delete("Email Queue") + + # reference document for testing + event = influxframework.get_doc(dict(doctype="Event", subject="test-message")).insert() + + # send a mail against this + influxframework.sendmail( + recipients="test@example.com", + subject="test message for threading", + message="testing", + reference_doctype=event.doctype, + reference_name=event.name, + ) + + last_mail = influxframework.get_doc("Email Queue", dict(reference_name=event.name)) + + # get test mail with message-id as in-reply-to + with open(os.path.join(os.path.dirname(__file__), "test_mails", "reply-4.raw")) as f: + messages = { + # append_to = ToDo + '"INBOX"': { + "latest_messages": [f.read().replace("{{ message_id }}", "<" + last_mail.message_id + ">")], + "seen_status": {2: "UNSEEN"}, + "uid_list": [2], + } + } + + # pull the mail + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + TestEmailAccount.mocked_email_receive(email_account, messages) + + comm_list = influxframework.get_all( + "Communication", + filters={"sender": "test_sender@example.com"}, + fields=["name", "reference_doctype", "reference_name"], + ) + + # check if threaded correctly + self.assertEqual(comm_list[0].reference_doctype, event.doctype) + self.assertEqual(comm_list[0].reference_name, event.name) + + def test_auto_reply(self): + cleanup("test_sender@example.com") + + messages = { + # append_to = ToDo + '"INBOX"': { + "latest_messages": [self.get_test_mail("incoming-1.raw")], + "seen_status": {2: "UNSEEN"}, + "uid_list": [2], + } + } + + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + TestEmailAccount.mocked_email_receive(email_account, messages) + + comm = influxframework.get_doc("Communication", {"sender": "test_sender@example.com"}) + self.assertTrue( + influxframework.db.get_value( + "Email Queue", + {"reference_doctype": comm.reference_doctype, "reference_name": comm.reference_name}, + ) + ) + + def test_handle_bad_emails(self): + mail_content = self.get_test_mail(fname="incoming-1.raw") + message_id = Email(mail_content).mail.get("Message-ID") + + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + email_account.handle_bad_emails(uid=-1, raw=mail_content, reason="Testing") + self.assertTrue(influxframework.db.get_value("Unhandled Email", {"message_id": message_id})) + + def test_imap_folder(self): + # assert tests if imap_folder >= 1 and imap is checked + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + + self.assertTrue(email_account.use_imap) + self.assertTrue(email_account.enable_incoming) + self.assertTrue(len(email_account.imap_folder) > 0) + + def test_imap_folder_missing(self): + # Test the Exception in validate() that verifies the imap_folder list + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + email_account.imap_folder = [] + + with self.assertRaises(Exception): + email_account.validate() + + def test_append_to(self): + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + mail_content = self.get_test_mail(fname="incoming-2.raw") + + inbound_mail = InboundMail(mail_content, email_account, 12345, 1, "ToDo") + communication = inbound_mail.process() + # the append_to for the email is set to ToDO in "_Test Email Account 1" + self.assertEqual(communication.reference_doctype, "ToDo") + self.assertTrue(communication.reference_name) + self.assertTrue(influxframework.db.exists(communication.reference_doctype, communication.reference_name)) + + @unittest.skip("poorly written and flaky") + def test_append_to_with_imap_folders(self): + mail_content_1 = self.get_test_mail(fname="incoming-1.raw") + mail_content_2 = self.get_test_mail(fname="incoming-2.raw") + mail_content_3 = self.get_test_mail(fname="incoming-3.raw") + + messages = { + # append_to = ToDo + '"INBOX"': { + "latest_messages": [mail_content_1, mail_content_2], + "seen_status": {0: "UNSEEN", 1: "UNSEEN"}, + "uid_list": [0, 1], + }, + # append_to = Communication + '"Test Folder"': { + "latest_messages": [mail_content_3], + "seen_status": {2: "UNSEEN"}, + "uid_list": [2], + }, + } + + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + mails = TestEmailAccount.mocked_get_inbound_mails(email_account, messages) + self.assertEqual(len(mails), 3) + + inbox_mails = 0 + test_folder_mails = 0 + + for mail in mails: + communication = mail.process() + if mail.append_to == "ToDo": + inbox_mails += 1 + self.assertEqual(communication.reference_doctype, "ToDo") + self.assertTrue(communication.reference_name) + self.assertTrue( + influxframework.db.exists(communication.reference_doctype, communication.reference_name) + ) + else: + test_folder_mails += 1 + self.assertEqual(communication.reference_doctype, None) + + self.assertEqual(inbox_mails, 2) + self.assertEqual(test_folder_mails, 1) + + @patch("influxframework.email.receive.EmailServer.select_imap_folder", return_value=True) + @patch("influxframework.email.receive.EmailServer.logout", side_effect=lambda: None) + def mocked_get_inbound_mails( + email_account, messages={}, mocked_logout=None, mocked_select_imap_folder=None + ): + from influxframework.email.receive import EmailServer + + def get_mocked_messages(**kwargs): + return messages.get(kwargs["folder"], {}) + + with patch.object(EmailServer, "get_messages", side_effect=get_mocked_messages): + mails = email_account.get_inbound_mails() + + return mails + + @patch("influxframework.email.receive.EmailServer.select_imap_folder", return_value=True) + @patch("influxframework.email.receive.EmailServer.logout", side_effect=lambda: None) + def mocked_email_receive( + email_account, messages={}, mocked_logout=None, mocked_select_imap_folder=None + ): + def get_mocked_messages(**kwargs): + return messages.get(kwargs["folder"], {}) + + from influxframework.email.receive import EmailServer + + with patch.object(EmailServer, "get_messages", side_effect=get_mocked_messages): + email_account.receive() + + +class TestInboundMail(InfluxFrameworkTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + email_account.db_set("enable_incoming", 1) + + @classmethod + def tearDownClass(cls): + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + email_account.db_set("enable_incoming", 0) + + def setUp(self): + cleanup() + influxframework.db.delete("Email Queue") + influxframework.db.delete("ToDo") + + def get_test_mail(self, fname): + with open(os.path.join(os.path.dirname(__file__), "test_mails", fname)) as f: + return f.read() + + def new_doc(self, doctype, **data): + doc = influxframework.new_doc(doctype) + for field, value in data.items(): + setattr(doc, field, value) + doc.insert() + return doc + + def new_communication(self, **kwargs): + defaults = {"subject": "Test Subject"} + d = {**defaults, **kwargs} + return self.new_doc("Communication", **d) + + def new_email_queue(self, **kwargs): + defaults = {"message_id": get_message_id().strip(" <>")} + d = {**defaults, **kwargs} + return self.new_doc("Email Queue", **d) + + def new_todo(self, **kwargs): + defaults = {"description": "Description"} + d = {**defaults, **kwargs} + return self.new_doc("ToDo", **d) + + def test_self_sent_mail(self): + """Check that we raise SentEmailInInboxError if the inbound mail is self sent mail.""" + mail_content = self.get_test_mail(fname="incoming-self-sent.raw") + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + inbound_mail = InboundMail(mail_content, email_account, 1, 1) + with self.assertRaises(SentEmailInInboxError): + inbound_mail.process() + + def test_mail_exist_validation(self): + """Do not create communication record if the mail is already downloaded into the system.""" + mail_content = self.get_test_mail(fname="incoming-1.raw") + message_id = Email(mail_content).message_id + # Create new communication record in DB + communication = self.new_communication(message_id=message_id) + + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + inbound_mail = InboundMail(mail_content, email_account, 12345, 1) + new_communication = inbound_mail.process() + + # Make sure that uid is changed to new uid + self.assertEqual(new_communication.uid, 12345) + self.assertEqual(communication.name, new_communication.name) + + def test_find_parent_email_queue(self): + """If the mail is reply to the already sent mail, there will be a email queue record.""" + # Create email queue record + queue_record = self.new_email_queue() + + mail_content = self.get_test_mail(fname="reply-4.raw").replace( + "{{ message_id }}", queue_record.message_id + ) + + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + inbound_mail = InboundMail(mail_content, email_account, 12345, 1) + parent_queue = inbound_mail.parent_email_queue() + self.assertEqual(queue_record.name, parent_queue.name) + + def test_find_parent_communication_through_queue(self): + """Find parent communication of an inbound mail. + Cases where parent communication does exist: + 1. No parent communication is the mail is not a reply. + + Cases where parent communication does not exist: + 2. If mail is not a reply to system sent mail, then there can exist co + """ + # Create email queue record + communication = self.new_communication() + queue_record = self.new_email_queue(communication=communication.name) + mail_content = self.get_test_mail(fname="reply-4.raw").replace( + "{{ message_id }}", queue_record.message_id + ) + + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + inbound_mail = InboundMail(mail_content, email_account, 12345, 1) + parent_communication = inbound_mail.parent_communication() + self.assertEqual(parent_communication.name, communication.name) + + def test_find_parent_communication_for_self_reply(self): + """If the inbound email is a reply but not reply to system sent mail. + + Ex: User replied to his/her mail. + """ + message_id = "new-message-id" + mail_content = self.get_test_mail(fname="reply-4.raw").replace("{{ message_id }}", message_id) + + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + inbound_mail = InboundMail(mail_content, email_account, 12345, 1) + parent_communication = inbound_mail.parent_communication() + self.assertFalse(parent_communication) + + communication = self.new_communication(message_id=message_id) + inbound_mail = InboundMail(mail_content, email_account, 12345, 1) + parent_communication = inbound_mail.parent_communication() + self.assertEqual(parent_communication.name, communication.name) + + def test_find_parent_communication_from_header(self): + """Incase of header contains parent communication name""" + communication = self.new_communication() + mail_content = self.get_test_mail(fname="reply-4.raw").replace( + "{{ message_id }}", f"<{communication.name}@{influxframework.local.site}>" + ) + + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + inbound_mail = InboundMail(mail_content, email_account, 12345, 1) + parent_communication = inbound_mail.parent_communication() + self.assertEqual(parent_communication.name, communication.name) + + def test_reference_document(self): + # Create email queue record + todo = self.new_todo() + # communication = self.new_communication(reference_doctype='ToDo', reference_name=todo.name) + queue_record = self.new_email_queue(reference_doctype="ToDo", reference_name=todo.name) + mail_content = self.get_test_mail(fname="reply-4.raw").replace( + "{{ message_id }}", queue_record.message_id + ) + + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + inbound_mail = InboundMail(mail_content, email_account, 12345, 1) + reference_doc = inbound_mail.reference_document() + self.assertEqual(todo.name, reference_doc.name) + + def test_reference_document_by_record_name_in_subject(self): + # Create email queue record + todo = self.new_todo() + + mail_content = self.get_test_mail(fname="incoming-subject-placeholder.raw").replace( + "{{ subject }}", f"RE: (#{todo.name})" + ) + + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + inbound_mail = InboundMail(mail_content, email_account, 12345, 1) + reference_doc = inbound_mail.reference_document() + self.assertEqual(todo.name, reference_doc.name) + + def test_reference_document_by_subject_match(self): + subject = "New todo" + todo = self.new_todo(sender="test_sender@example.com", description=subject) + + mail_content = self.get_test_mail(fname="incoming-subject-placeholder.raw").replace( + "{{ subject }}", f"RE: {subject}" + ) + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + inbound_mail = InboundMail(mail_content, email_account, 12345, 1) + reference_doc = inbound_mail.reference_document() + self.assertEqual(todo.name, reference_doc.name) + + def test_create_communication_from_mail(self): + # Create email queue record + mail_content = self.get_test_mail(fname="incoming-2.raw") + email_account = influxframework.get_doc("Email Account", "_Test Email Account 1") + inbound_mail = InboundMail(mail_content, email_account, 12345, 1) + communication = inbound_mail.process() + self.assertTrue(communication.is_first) + self.assertTrue(communication._attachments) + + +def cleanup(sender=None): + filters = {} + if sender: + filters.update({"sender": sender}) + + names = influxframework.get_list("Communication", filters=filters, fields=["name"]) + for name in names: + influxframework.delete_doc_if_exists("Communication", name.name) + influxframework.delete_doc_if_exists("Communication Link", {"parent": name.name}) diff --git a/influxframework/email/doctype/email_account/test_mails/incoming-1.raw b/influxframework/email/doctype/email_account/test_mails/incoming-1.raw new file mode 100644 index 0000000..199216c --- /dev/null +++ b/influxframework/email/doctype/email_account/test_mails/incoming-1.raw @@ -0,0 +1,91 @@ +Delivered-To: test_receiver@example.com +Received: by 10.96.153.227 with SMTP id vj3csp416144qdb; + Mon, 15 Sep 2014 03:35:07 -0700 (PDT) +X-Received: by 10.66.119.103 with SMTP id kt7mr36981968pab.95.1410777306321; + Mon, 15 Sep 2014 03:35:06 -0700 (PDT) +Return-Path: +Received: from mail-pa0-x230.google.com (mail-pa0-x230.google.com [2607:f8b0:400e:c03::230]) + by mx.google.com with ESMTPS id dg10si22178346pdb.115.2014.09.15.03.35.06 + for + (version=TLSv1 cipher=ECDHE-RSA-RC4-SHA bits=128/128); + Mon, 15 Sep 2014 03:35:06 -0700 (PDT) +Received-SPF: pass (google.com: domain of test_sender@example.com designates 2607:f8b0:400e:c03::230 as permitted sender) client-ip=2607:f8b0:400e:c03::230; +Authentication-Results: mx.google.com; + spf=pass (google.com: domain of test_sender@example.com designates 2607:f8b0:400e:c03::230 as permitted sender) smtp.mail=test_sender@example.com; + dkim=pass header.i=@gmail.com; + dmarc=pass (p=NONE dis=NONE) header.from=gmail.com +Received: by mail-pa0-f48.google.com with SMTP id hz1so6118714pad.21 + for ; Mon, 15 Sep 2014 03:35:06 -0700 (PDT) +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=gmail.com; s=20120113; + h=from:content-type:subject:message-id:date:to:mime-version; + bh=rwiLijtF3lfy9M6cP/7dv2Hm7NJuBwFZn1OFsN8Tlvs=; + b=x7U4Ny3Kz2ULRJ7a04NDBrBTVhP2ImIB9n3LVNGQDnDonPUM5Ro/wZcxPTVnBWZ2L1 + o1bGfP+lhBrvYUlHsd5r4FYC0Uvpad6hbzLr0DGUQgPTxW4cGKbtDEAq+BR2JWd9f803 + vdjSWdGk8w2dt2qbngTqIZkm5U2XWjICDOAYuPIseLUgCFwi9lLyOSARFB7mjAa2YL7Q + Nswk7mbWU1hbnHP6jaBb0m8QanTc7Up944HpNDRxIrB1ZHgKzYhXtx8nhnOx588ZGIAe + E6tyG8IwogR11vLkkrBhtMaOme9PohYx4F1CSTiwspmDCadEzJFGRe//lEXKmZHAYH6g + 90Zg== +X-Received: by 10.70.38.135 with SMTP id g7mr22078275pdk.100.1410777305744; + Mon, 15 Sep 2014 03:35:05 -0700 (PDT) +Return-Path: +Received: from [192.168.0.100] ([27.106.4.70]) + by mx.google.com with ESMTPSA id zr6sm11025126pbc.50.2014.09.15.03.35.02 + for + (version=TLSv1 cipher=ECDHE-RSA-RC4-SHA bits=128/128); + Mon, 15 Sep 2014 03:35:04 -0700 (PDT) +From: Rushabh Mehta +Content-Type: multipart/alternative; boundary="Apple-Mail=_57F71261-5C3A-43F6-918B-4438B96F61AA" +Subject: test mail 🦄🌈😎 +Message-Id: <9143999C-8456-4399-9CF1-4A2DA9DD7711@gmail.com> +Date: Mon, 15 Sep 2014 16:04:57 +0530 +To: Rushabh Mehta +Mime-Version: 1.0 (Mac OS X Mail 7.3 \(1878.6\)) +X-Mailer: Apple Mail (2.1878.6) + + +--Apple-Mail=_57F71261-5C3A-43F6-918B-4438B96F61AA +Content-Transfer-Encoding: 7bit +Content-Type: text/plain; + charset=us-ascii + +test mail + + + +@rushabh_mehta +https://influxerp.org + + +--Apple-Mail=_57F71261-5C3A-43F6-918B-4438B96F61AA +Content-Transfer-Encoding: quoted-printable +Content-Type: text/html; + charset=us-ascii + +test = +mail
      +



      @rushabh_mehta
      +
      +
      = + +--Apple-Mail=_57F71261-5C3A-43F6-918B-4438B96F61AA-- diff --git a/influxframework/email/doctype/email_account/test_mails/incoming-2.raw b/influxframework/email/doctype/email_account/test_mails/incoming-2.raw new file mode 100644 index 0000000..df2f17f --- /dev/null +++ b/influxframework/email/doctype/email_account/test_mails/incoming-2.raw @@ -0,0 +1,511 @@ +Delivered-To: test_receiver@example.com +Received: by 10.96.153.227 with SMTP id vj3csp576735qdb; + Mon, 15 Sep 2014 23:28:05 -0700 (PDT) +X-Received: by 10.66.65.202 with SMTP id z10mr41658290pas.20.1410848884944; + Mon, 15 Sep 2014 23:28:04 -0700 (PDT) +Return-Path: +Received: from mail-pa0-x232.google.com (mail-pa0-x232.google.com [2607:f8b0:400e:c03::232]) + by mx.google.com with ESMTPS id he2si27434308pac.73.2014.09.15.23.28.04 + for + (version=TLSv1 cipher=ECDHE-RSA-RC4-SHA bits=128/128); + Mon, 15 Sep 2014 23:28:04 -0700 (PDT) +Received-SPF: pass (google.com: domain of test_sender@example.com designates 2607:f8b0:400e:c03::232 as permitted sender) client-ip=2607:f8b0:400e:c03::232; +Authentication-Results: mx.google.com; + spf=pass (google.com: domain of test_sender@example.com designates 2607:f8b0:400e:c03::232 as permitted sender) smtp.mail=test_sender@example.com; + dkim=pass header.i=@gmail.com; + dmarc=pass (p=NONE dis=NONE) header.from=gmail.com +Received: by mail-pa0-f50.google.com with SMTP id bj1so8166191pad.37 + for ; Mon, 15 Sep 2014 23:28:04 -0700 (PDT) +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=gmail.com; s=20120113; + h=from:content-type:subject:message-id:date:to:mime-version; + bh=RjaUdjmv0mhS2w8fKWC6N0GWDRnlLZrOgmhtagrz2gY=; + b=rKWW/c0NKT37bbAO8RHBI8JnrXjAZbAP+XPfo7tP58iJ+BKK1UE2J7iDluE5QPeuXg + WhyN0mK8hfAjLF9BDl2OfRR4yDuNIq2nuqVu4wW0NtacDqJzSEW41xdnSDDkXLCBN/c2 + BNikjY9fQkiQ2axEVbHJCCxcMA9ZNZXq3212xnd78eDUIUnVDeIc7XE/PnecwmxclRtc + bJV4zTVKsGbSP3fnyd7VX0w4ezjSX9oUPPRSvVAAjPSfREfRchptsX9LCB2LiVP0rVGO + h4IVIpdeBeMsyiM+4UcRn5raLSeX5WvD/eTyJ28qQz3t0nDZnSSVFboBkP2QcAWJNXcX + ydoQ== +X-Received: by 10.68.171.33 with SMTP id ar1mr12518010pbc.148.1410848884267; + Mon, 15 Sep 2014 23:28:04 -0700 (PDT) +Return-Path: +Received: from [192.168.0.100] ([27.106.4.70]) + by mx.google.com with ESMTPSA id sf1sm13174290pbb.0.2014.09.15.23.28.01 + for + (version=TLSv1 cipher=ECDHE-RSA-RC4-SHA bits=128/128); + Mon, 15 Sep 2014 23:28:02 -0700 (PDT) +From: Rushabh Mehta +Content-Type: multipart/alternative; boundary="Apple-Mail=_6F6D23B5-0ADE-4347-978A-E0A21B92BB70" +Subject: test +Message-Id: <54A4EFFA-AD17-456A-9851-9715574DF0C9@gmail.com> +Date: Tue, 16 Sep 2014 11:57:58 +0530 +To: Rushabh Mehta +Mime-Version: 1.0 (Mac OS X Mail 7.3 \(1878.6\)) +X-Mailer: Apple Mail (2.1878.6) + + +--Apple-Mail=_6F6D23B5-0ADE-4347-978A-E0A21B92BB70 +Content-Transfer-Encoding: 7bit +Content-Type: text/plain; + charset=us-ascii + +with attachment + + + + + +--Apple-Mail=_6F6D23B5-0ADE-4347-978A-E0A21B92BB70 +Content-Type: multipart/related; + type="text/html"; + boundary="Apple-Mail=_18FE9DD1-B053-4781-ACFD-7E542B418B05" + + +--Apple-Mail=_18FE9DD1-B053-4781-ACFD-7E542B418B05 +Content-Transfer-Encoding: 7bit +Content-Type: text/html; + charset=us-ascii + +
      with attachment

      +


      +
      +
      +--Apple-Mail=_18FE9DD1-B053-4781-ACFD-7E542B418B05 +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename=influxerp-conf-14.png +Content-Type: image/png; + x-unix-mode=0644; + name="influxerp-conf-14.png" +Content-Id: <9137CC31-70BA-4232-8438-6B9D8B15A1B5@webnotes_sn> + +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 +REPEu0iPyNLIR4uNFksWd0bJR8VF1UdNRXtFl0VLl1gsWbPkRoxajCCmPRYfGxV7JHZyqffS3UuH +4+ziCuPuLjNclrPs2nK15anLz66QX8FZcSoeGx8d3xD/iRPCqeVMrvRfuXflBNeTu4f7kufGK+eN +8V34ZfyRBJeEsoTRRJfEXYljSa5JFUnjAk9BteB1sl/ygeSplJCUoykzqdGpzWmEtPi000IlYYqw +K10zPSe9L8M0ozBDuspp1e5VE6JA0ZFMKHNZZruYjv5M9UiMJJslg1kLs2qy3mdHZZ/KUcwR5vTk +muRuyx3J88n7fjVmNXd1Z752/ob8wTXuaw6thdauXNu5Tnddwbrh9b7rj20gbUjZ8MtGy41lG99u +it7UUaBRsL5gaLPv5sZCuUJR4b0tzlsObMVsFWzt3WazrWrblyJe0fViy+KK4k8l3JLr31l9V/nd +zPaE7b2l9qX7d+B2CHfc3em681iZYlle2dCu4F2t5czyovK3u1fsvlZhW3FgD2mPZI+0MqiyvUqv +akfVp+qk6oEaj5rmvep7t+2d2sfb17/fbX/TAY0DxQc+HhQcvH/I91BrrUFtxWHc4azDz+ui6rq/ +Z39ff0TtSPGRz0eFR6XHwo911TvU1zeoN5Q2wo2SxrHjccdv/eD1Q3sTq+lQM6O5+AQ4ITnx4sf4 +H++eDDzZeYp9qukn/Z/2ttBailqh1tzWibakNml7THvf6YDTnR3OHS0/m/989Iz2mZqzymdLz5HO +FZybOZ93fvJCxoXxi4kXhzpXdD66tOTSna6wrt7LgZevXvG5cqnbvfv8VZerZ645XTt9nX297Yb9 +jdYeu56WX+x+aem172296XCz/ZbjrY6+BX3n+l37L972un3ljv+dGwOLBvruLr57/17cPel93v3R +B6kPXj/Mejj9aP1j7OOiJwpPKp6qP6391fjXZqm99Oyg12DPs4hnj4a4Qy//lfmvT8MFz6nPK0a0 +RupHrUfPjPmM3Xqx9MXwy4yX0+OFvyn+tveV0auffnf7vWdiycTwa9HrmT9K3qi+OfrW9m3nZOjk +03dp76anit6rvj/2gf2h+2P0x5Hp7E/4T5WfjT93fAn88ngmbWbm3/eE8/syOll+AAAACXBIWXMA +AAsTAAALEwEAmpwYAAADqGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxu +czp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPgogICA8cmRmOlJE +RiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMi +PgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp4 +bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOnRpZmY9 +Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIgogICAgICAgICAgICB4bWxuczpleGlmPSJo +dHRwOi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyI+CiAgICAgICAgIDx4bXA6TW9kaWZ5RGF0ZT4y +MDE0LTA3LTE4VDE0OjA3OjM3PC94bXA6TW9kaWZ5RGF0ZT4KICAgICAgICAgPHhtcDpDcmVhdG9y +VG9vbD5QaXhlbG1hdG9yIDMuMjwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8dGlmZjpPcmll +bnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICAgICA8dGlmZjpDb21wcmVzc2lvbj41 +PC90aWZmOkNvbXByZXNzaW9uPgogICAgICAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4xPC90aWZm +OlJlc29sdXRpb25Vbml0PgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjwvdGlmZjpZUmVz +b2x1dGlvbj4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+NzI8L3RpZmY6WFJlc29sdXRpb24+ +CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj42ODY8L2V4aWY6UGl4ZWxYRGltZW5zaW9u +PgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAg +PGV4aWY6UGl4ZWxZRGltZW5zaW9uPjEzODwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAgIDwv +cmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgqKhMWCAABAAElEQVR4 +Ae2dB5wURfbHu3fJCIqCOWCOZ8AcSIbzTIAoZgXxDu/OM/+NGFBRMethODGLmVOSnlnArCfmfJ4B +FTGfAsICu/3//manht7ZCd2zs7Ozy3ufz5uqrnr16tWva6pfv67p8TwjQ8AQMAQMAUPAEDAEDAFD +wBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPA +EDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQ +MAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAw +BAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAE +DAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQM +AUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwB +Q8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFD +wBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPA +EDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQ +MAQMAUPAEDAEyggBv4xsMVMMAUPAEDAEDAFDwBDIiEAwZUg7z/+pp+f763k1QTeEunqeP8erCL7x +avyvvco20/xe477P2NgKWwwC5ri2mFNpAzEEDAFDwBAwBFoeAsHUAXt4Xs1xjKy3FwTts47Q92uo +exmZcV7bdjf6O4ybl1XWKpotAua4NttTZ4YbAoaAIWAIGAItF4Hg2QHbeDXVV3uBt33sUfr+TM/3 +zvNqetzq9x2xKHZ7a1C2CJjjWranxgwzBAwBQ8AQMASWTASCaf2G4bCOJnrapmEI+C94la0G+b0e ++qZheqx1uSBgjmu5nAmzwxAwBAyB7AisQNXK2avr1bxJSVCv1AoMgWaAQDCl/yi2BpxWNFN9b5YX +4Lz2Hf980XSaoiZDoFWT9Vy6jp1zrkV8B/haeAs436KuerUVvwEfA7+UPCbJ214yRoaAIWAIFAOB +ISjhYh6Z2iK5ILK0CRoCZYJAMG2fo7yaIjqtGlfgrej5NT3ImeNaJue5IWZUNKRxM2mrMcoJlZN+ +PiyndSGcj5zDq70xanMBXAlL15KAG8M0MgQMAUPAEDAESoNA8OzAHl7g31D03nz/c69bm38UXa8p +bBIEliQHbGkQXimEsnNMQ0X1spJxkdkVyUuHkSFgCBgChoAhYAgUG4GaRReyp7V1sdUSazrH33ic +PYEoPrBNonFJclz1moxwpNU5pLmAD8so8ho+ztXO6gwBQ8AQMAQMAUMgIgLBtP474LT+IaJ4dDHf +f9vrvfnd0RuYZLkjsCQ5rjoX4ShrOJ/tPIVlwvls8lZuCBgChoAhYAgYAnERCIIhcZtwRSeY5P+3 +lrO0rvDO9P0RClwZtRAElmTHNcoptAhrFJRMxhAwBAwBQ8AQaBgCu8VsPs1r1a6733fSOmLPa70a +Duzh/KvWxyk9vv+c32vSI6ljy7QIBJY0x7WhjmhD27eISWODMAQMAUPAEDAEioVA8MKAtdkm0D2y +Pt//zFu+3e/9ncbNcG38vg99hQN7lxdUbIsD+3ii3PdPd/WWthwEWpLjqkf5mdiNUWmux/3pdenH +OuvpujL1JzkjQ8AQMAQMAUPAEIiCwMKataKIpWR8b2S2H1v5fSf8z+vTdi+vouJQv/fEF1NtLNNi +EGgJ73F1zmOmaKjKXLlL3cmLe6x2ro1ScTbn1tWrjZEhYAgYAoaAIWAIZEPA97sRcc1WW7+8onJ6 +/cLFJb4/rpqjexaXWK4lIdASHNd8TqL7j+JfOXGazI7kdMb4piTaSofI6czUPlNZbSv7NAQMAUPA +EDAEDIG6CARe17oFeY5ad/gsj4RVt2AEmrPjGnY8N+Ic/R7WePTrwXAkVHm9v60zrL9NFOmRf1QH +020PUNszYDmvbdLaS5fk9LqtJ+H3YVHYxtoS+zQEDAFDwBAwBAyBxQj4Ne0jX5HVqtOC+YsbW25J +Q6AlOK5yWh+FV4948uTYOmc0ShPJqs3KsP49Kx/NQGAPWM5rc3FcN8BW4SjnfPkkk3g/hPgD8m/B +UR1+REtKy9LbpvC68HrwCHgu3Bik87o1LNz0xxTi9vBM+CtYc+AFuBwXV/2JRm9Y3xfNaUU6foJn +wV/CU5LHJCWnljAPSw7aktZh8J9j23pfz9iGR8ursJKvwD8tsWYFXVmZZrPizuRX5V+Tn+kFHT7w ++96rNaxkFLx8aGdvwdytsW0F/rZ0eWzResp3LJjDj4a+8yqC77wauKLNR37vB/9TMsMK6Ch4bdDS +3tyq3lz9VgfX2rUi8FkramZ5lZVfeq1aT/F3GKe1w8gQKCkCugA3R3J2y4k6Ab4KdtsAnLPoZMLj +i1qmNpkctExlTr+r09/CnghfDbv+XJ2TbepUEeM94T8keY2IBmmRmgo/A98D/wwXSjpnQyM2fhe5 +HTPICt+d4WHwAFjjctSNTPiiVYz+dkHnQfDesJzVXDSbyonw/bBurNz8JBubGmq75uTBsGzXK2fC +OHFYh2Tni/C98M2wniI0FsmOUs7DjvT3CrxazAHpPB4Rs43EH4B3j9luPPK3wpPT2rXlWByV3Lam +XPL6fqydS6Bc6oLn9lvLq17E965GePZhde6Q1zbfr0HuJVbhSV7QepLf98EP87YpQCCYMmBzz69R +sIL1NOAl+oknf/k1+d6nCD2G/KNe23ZP4wTOy98ot0Qwrd9VOM0R11X/Xb/PpDrrahAMqvSmVtWu +FT5rRRDkWCv8arCtXSuWWvFmf6sxkdaKYNp+BBcW/rXOSGoUCAjq2FKnPv3Arxjt+UG+NXWO33vS +2elNy/F40KBBlePGjas+++yzewZB0HHkyJGPjRgxogKuaYC9uj4GZ5555pDq6upnLrnkkhkcJ8oa +oLMsmjbniKtzBlsnkdRxRTLvHMYoIGu/qvsCuJOqC72wcX1E0aO2bpKFbYpjS5R+GiIju4bAZ8Gr +w3FJUc2BSR5FOgaWU6UoY1xShLJzxEZLZZDblbJr4fUz1KnIzQVX3ZD+eqDkMnhnpyxC2gmZw5L8 +JqkW6pfgQqghtsvma+BNInasud8zySeRng4/CBeTmmoeKgJ/MvxYzMEcjvz98CMx2uniPyiGvERn +wf8HbwpH/W4gmpGitK/K2LKMCoPn+63vLQrO8aoXHoQTlf6dzm1prfyOrOI4RAsuCabu8yHR2Qu9 +Pj3uKcYL6RP/9FRTcxGXj96xrhTO6sBbi6zWhb96VVXfBlP7X+StsvqN/rqjCz8vNUH0tcL36qyr +wbR9dvamzV+8VuS9+gWVjLt2rZgz66Tg2QGn+70mRFgrFqzGlVIBp8IpqDk2L+a+9z0dnF14J6Vr +2aVLF83t6kWLFmmdXg5+bObMmVqLnU9BtjDyfX/nNm3avEPrGTjIFXKQC9NUPq3iLQTlY7cscQ6h +O7FuLCrPxukj0FdTDqoiGWLdXSpVmdNLNkXZ9DpbnA2urStPKWjCzIH0/REsZ3P1ItihRU+Ozaew +nLocd+bUFo+WRdXt8JPw+nA2Kgb2GtMN8Gvwztk6ilC+OTIvwLfAS0eQL4aILmB3w0/DWgwLoXVo +9E9YdsvZLAY19Tx8nEHcVMBArqeN5nwU0uPh0VEE02SO5PiHtLIl8jB4ftDqwZR+d/Ez2PdxUA6J +7bRmQi3Q1p5grDft9TeCZ/vtlUkkShltN8UJfoStAPpO947SJr8MWwuCmmu8rz//GAfyKKJuxVi/ +8neLRPDioPaM526ueE+DdWFrRRCs41VX/zOY2u+W4LVhxVorItnf3IUUbR0zZszCs846a22czBPg +o0877bTNVKa6QsanaK1O7fDhww8lPZiI6zkqk9OarCtEbdm0cY5W2RhUgCHuCy4nVHl3nElVep3G +/wH8FDw1ycrrkVIYm/R2VNcjybh71Cjy9RQ0UoEc8X/A98FrNkIfWqQUJXoZzuVIFqPr36FE2wYG +R1AWPn8RxOuJdKVEc+HPcDHOp3QMhZ+DtV+sMWkFlE+FDylSJ7L7UbghTnc5zUNFXb+IiY1u9i6K +2OY65BQ1iUPXIhw3EhxHf7ORDaYO2MNbNP8NltNDi+Kwpo88CDb1qoOHcbKexUmMtSbyKH4wbV9l +pd8zXW1RjgOCCjVs0ZnW76FgyqCoN0oFdx283G8Fb8H8qYmbg4K1hBpqm8Lsbx5N7I8NFVs2KwJ+ +0plclpuVvyO1NrxSZWXl30855ZQVk9HRuNcfX1sMcFp3QtflyZugvebPnz982LBhrVVHeVydNCkf +aujFvXxGsvhEOOcxk22uzqWS0aPu3eB9kqy8yhxJNizvyjOl5TYZumPkC/DRmYwtctkW6Hsdjvt4 +NKoZ2yE4DV4pYoOGzO1u9PEK3DNiX3HE5Hy/CDeWky/bdROxDVxM2gVl4+BCcO1Ou3Kah7OxR854 +1O81ogk6hk/Nw1y0H5X75xLIUPc+ZadmKF+iioJgRAWO4QU8MX2EM6MnK41LQdCTfl4hgspWgtyk +vZ9EJa/kh1W3I6mbsMalgD37/vyXEnt7G6+nbvx89GUwKP5aMadqnM5n45nesjSzRaALDqauCZOJ +uN5J2h3nNeq1LiMYFRUV3dHZEb4EgXc47rHMMsu0yyjczAqb88RKv+jEcTDDp2lu8mAOqVjk0tqj +aJ+Z+k+3MZqm4kithRo5X1sWR10kLR2QuhveI5J0dKHVEFX0s0v0JgU5WE69Fgzh11i0Boqfh9dt +hA5ke/dG0CuVu8HnxdRdrvPwGcZxfcyxaL3UNoPWWdopyhpX5wLaHAbPy6JziSgOpoxo5U2bPgHH +8CwcqdIFAAKvGxHUp3GY9Ug1IwXvDWrjTat6GLtOzCjQWIV6bL9o4avB1IFbN0oXQbASEe3ujaR7 +N+/Z1+OuFY1iSpkrDbQdgB9j/RcH8zVsfQbHdQLpm6NGjeKpQ+E0b9688ej8HAf4ZrS8XVNT8/Sl +l146O7n9oCl9k8IHlWyphbhcSYtXZRbWHlSxqKFjcHcgbdAlFrWvTQr+dDY5O7ONo7EWaEUr/gUv +X/AICm+oi/o/4bxRjBhdyGHtGENeoo2FbUwzsop3pUY4NXSuZe2gkSqGo7dXRN3lPg9PYxz/jTgW +J7YJGbXLRHrUF/c7dw5tGnSBymRIsyvzX78Rx1BPvZqC2uIw38WPrTJvrfmuagwO3h+awjAeCnAz +tHBSMGXgqk3TfwN6DYLh3BBEXSsa0FHzbsp2gBqNAIe1PY7m0jiYWjcbfF1AXye4LTr1FE5BpcQ1 +dOONN27WTivjaLDTJx2NSdUoz8QLKV+U7LiqgQZIv0j6nE5XlqiI8JHuJDmbpE+2ZhpD3D4imJEQ +0WOsiXBjPYqOYoe+JJPhuBfxKLqjyribh6jyTSGnX4/f0BQdN6BPzfVzI7RvDvNQT1uOhBMXjghj +ciJnkUn/fvWjLLPj41rVT6dRdFn94iWrhEfw5+EYDm3SUfv+515Fx0fTbWAf7Ck4j4PTy0t6HPDq +PX/RJH70pHW1+ZAi50GktaL5jKkRLGXPqfMffsPR1NsWRsJ6EiPSflXVR2anr1WrVvI9OuG43k66 +J7p/JW0R5KKW5TgY3RUoUrk07C4sOnkql1OiVCfCvfYlfGJVF5XULh+5ftPl0sudLtkk25WqTPaH +ZWX/L3BjOK8Xo3cnuCH0PY21XWINuFAHUFHS8+E/w01Bhdodx1adP53nhtBgGj8Nj22Ikght5yHz +Nqzo3lewIoc94HXguFjtTBvt9XwZzkbNZR4+xwCugU/MNpAM5XLKb4J7w1prNNf/AcehXxA+AnZr +W7jtmxzsHi4gfyA8NK0s1+HeVOrClYvy1edqW5Q6opwH8uv8c4qirHAlVZzFQX7Pe34Oq8C2PXGo +R4XLmiwfBFt4c2bdQf+DGt0G358HHm+zKrBW+F+BweK1Iv4ryXYOpuyznd938uK1otL7lll/f51x ++P6G9LNpnbJcB74/DvlM351Qq2bnqGk8y2gAOJlaV0QBjqjL15bk+XTybdu2XbhgwQI9AV1PTdgy +4AJzeTSUf3U5O67bA98F8IqwAHdOIdkUyXFYIXnk6t1Jdo6iS1ON0jJOPq041qF0uP7VUM7avnAm +p0aywn0WfDb8Elws6o6iYwpQJqfmWngi/AH8EyxqB68L7wIryqQ9fHHojwhL77txGhVJNnw+iqTS +ew9Ft8HahvEFPB9WVHkz+KAkC7O4dD4N7oMbw5FQ9P88WJG9TAuXLko672vBcUjz7OUsDbpT3pzm +4XDs3QtOLPCkUagnQsPgG+Gr4JXgOPRXhGdkaaDv3xNpdVukHec7fBIBF7XJJ9sk9YkI4uxvrmhQ +5z6OVe368g7p91znV2UpXo38tjg2K0fS7fsn8iL+18KyiX2t31ddh46KcHmkvO/rpv/fOH+v85L8 +6az2b3uL/JV5h+yWHOtmUf+s1T2SrrBQEOyvNy74fSY8Gi4uYr4Kj4nod4/L/L4j6q0VwZR9NyHy +O5FxxVsrfL/OWuHvNFnrqNbLFPF2h9PQG91x7db2MH/jcWU9v1ODi5ghMoq/WnvZIh+cccYZy3Es +X6Fi4cKFCT+ldevWiVRlcMpxp953dcoTba3mLQKd5KyiylkQfy67lmWWCpRypesxbPOIxukEpjsq +7my5NKKqjGLZdGQqly1aMPMtmhsjsyyshaxYNBJFbWIok+OvC6/afZOhnRwzXRDEt8PnwnqUEZXk +uF8E94vaIKbcb8hrEZ8EfwbPgufCom9rk6J8Cgc5N9fAwixM6lP8OKyxjoH7wHGoO8KHw7fGaRRB +djoyg2FdKLKRbiq2gcfDPbMJZSjXzUw2am7zUDduQ+DnYM3ZqHQJgrrAC+M4dC/C98Rp0CJlZ886 +iXGtEntsvvcbq/35XkWnMelRUqcr8Yv2597irQHV+uOCIThF7VxdndT37sFpvaFOmQ5+qPpLQc6l +70/xKlod6vd6KH091XdQNxOo5R2tz/Y7FZtGwjGvwTWjGNvjxfgDBdmymPzpXutgsL/TJOzUclqf +/L7j3w1e2Xcbb171eAZRrLWifkdLaAlOavjaUsVbAAYxVyrh9jilri7hrLIPtg3yVbDqAxzVViRy +5CvkwKoep3UmZbMp6yZIKVuo9L333kv3lVTcrKicPXA5rbooCOxcrBOaaRwB5fkYkZyUr73q00m2 +yKZcNqtOY4sbRaFJVtLd6iFZa+tXyPY/wrobTl9k60t73v8oPBG+LFNljrLdqeuUo77QKl3414D3 +h++E5XT8B56ZZPdF57BBJOdYj12vhPPpVP+7wnfDcekMGsRxmvLp/x4BYZ/LaXU6fiQzDM40n51M +eqoI4/rphRw313n4ErbHjf4tTZubM2CQq+hLKhVtXaKJd5SybzPrj9xyYOPzqqzKjf3eky/J5rSq +sRw7v9f4afzlJw5o5Yb0NaGeUp+nS12W0byvQ8HLh3bGMTurTmHeA/39qX+O17vHrhmc1jqtcTYC +2c+Dt15UfFGnMt+BHqdPfT3OOp9Po0I+33sdKndPRkJzyvvbjv+Rv88dRpvoawVvL0j8A1pOzUtu +pRxJHu8vDwKdHQrMkQ44mqvheK6azsisQv2qYlfnytyx6mi/CuWpa4qO6aez+zGY66s5pjHv9ko6 +RH0xUqDn6TnTlyjfXUW+enXpZFyax4xEtWyR85rJmU5vn8nudJmox1rM4th5CvK3R1UekpODtTXc +J1SWK9uGyt/DD+YSill3HPKjY7YpVHwIDZ+O0VjO7RGw7nI17qi0DoI7w09GbZBH7mTq5ZBGpQ8R +fAzeI2oD5HaEP0qTb87z8BzGopuUjdLGVKxDRUs0N3QTuIRT1R9xDpeKDELCUao4yu8z8bbIbZKC +ft8Jn5Pdl1+4D6TPu3G5FH2d6/mt9vM3Gzs3KbY4mTdHzmzXxQVRchX78wgf5zhztDKTBr/P+JeC +KQM2JxbGNoVg7UwyWcrkVN+Vpa6A4sqTEw5pxJZ+3wc/5Ad18daK6oxrRcQeW64YjmQFXL3uuuvu +ySj3wuHE9wz00Zv8tpSRJPYP1PEVXJmqaqsT1/6AKK0DS76ArkV6s0BCJ3XD+Actnbd/J/vVetQs +KTXKZml9ZqN1gnXScrHG7Zy81hnUOIdeMk42l746kyqDvlIUxXE49Fg4bnTJjUFfhqPdQcR0r4hy +UcQU8S2V0/osfY2LYlSajBYEOdcL08rzHe6aTyBi/TPIjY0oGxb7e/ggQl5R13RqzvOwisEMhvU0 +pDFI37mpjaG4GerUDUJ08v1TCnFawx0QfX3Iq6hUv3M9LuJ+7/EfhOtTeb+mfyofJeP7k3CO60d0 +I7Sl3f/wPPQkKwYF6wfP7btejAbZRX3/GRzu+GtF4MdcK/xMa0V2u5pnTS4fIVNd6rE9TmXiZhYP +U9eMRTibbUmXgfVUR5FYpWFOL8t03AU98lrdetaa/a8/o8dRJptylbl2TZ46B63JDSmCAQJcJ0hj +0oJ0NaxN8rq7lrOlekfOuZXs1GRh2PlU2Z9g6XOyZFMkZ3Y+rIjBCfCGsOs7rIfiktAq9LJpjJ4K +eZQdVv8xB5/D3eEo1DOKUAQZfenOiyBXDBGdR0UtCyVFIuVgnxRDQbEc1ztj9BkWfT18ECGfHpVq +CfOQ6Jc3Cj4rwvjjiLzZCDrj9F82ssEL+y7vLVy0TeSHzb7/HE5noTfadcbt95rwdPD8oI38ncbN +qFORPAheHLSsV1W1fYxdM1VeZeuYjmfdnnHIJ/ParceIBv+hbk2Oo+oaBQO0DjeM/MQ2q/g62lS+ +7i3QJS8qBelrRdSGzUku9rWfd6rqL181RueLtdEBzqaSBhN69NRarMjsctoKG1IazoeKyz/rwCp/ +S6NZKAdVY/oKHhOtSUJKTq07icp/kuREZZ6P/amX4+r6ziPeKNW7x9CqR2P/iiGfTfRJKuTcR6Fi +3W3fSGf1H+1FsSC+jJx7OTENofNpfDisbQNRaHOEloPjPOLPpFdOcyH0PY0WwInFM4IC2RqmljIP +L2BQ+8CbhQfXgLxucg+Dha3RQpyuOP+O5funFxO0bE5roo8F8zWHExf6SH36FVf4PR/8NJJsLqFW +BEAW+e/gsbTOJba4LlDk+KrFxwXmaoLC1oodHvrem9Z/AfZGWysC/ZFCyyX9G9Vaa621FI5ipMfv +HTp00Guu5sCLTjzxxPb85evrOJUH0F5+RGNQBY7rPPa5/kafbeAFp556aifs8H/77Tfn+2TsVzJU +VLAXdzZOdmPZl7HvbIUtzXEVwCJ9+TvCcnL0xcp1a6iTFj5xykuP00W2Hkl/Faw+HIa55OspKHKB +HOeo9AuCl0UVziG3Xo669Crh1AHWD50KJV30FcEsFclxbSgJ6wfgYyIqUiR/E3haRPlsYh9nq8hT +rrn/DbxGHjlXvZTLJNOWMg8114bAr8IRHQkks9NpVL2XvXoJq/GDONHWT/zeE18sHUJ+z7qXgzw9 +VxT0Q8x6Svk1/0fsG9WN8vb1KjMVBMGOODmJJ8GZqiOXtW1X0FqReAI9ZZ8Ya4WfvlZENrE5CBI5 +reSdqTdj626wnvRqLa9DiZPFSaMwQLb9mWeeecFFF110Fe9bPYqtAruw/3Q4aQdYMsWkCvpbwNsG +dPNwL9njzjrrrG1xYi8j/xsOc2IjLaYl+nV2JtNqZDrR7gnGeKg5rsU8Lbl1NaVDmduy4tXqF4lR +aWUEozpSUXVGkZONn0cRzCJzH+Uzs9Q1RnFhkYj6lnxYvyhnSdTobDYlP1DxU7bKCOVfIxPVcU1X +15LmoR7tK/KqqHlD6Akal/KGqyG2lqZtkHr3dv7+gsRr5vLLFU0iWCmyKp+AyHJt9HSuWKS1Iprj +6nltvZcO6IJ84d913/vB32FcA9r7XxNxLXStKBZmZaFHEUzeu/otzp4c9IxOetIvDNt7/vDhw5+7 +8MILryXVE563JYMDG5YpSp5XY+l1WOLzcIyVKlqvbZQJW8O2ubxLZQD5bzVG5cuB6t0VlINRDbDB +3alog7OiraIqWOH7bKw2YedWeZVlk1e5dIrUh4vmur4TFSX+iOMwlNi0VHcN3eM0JaWp8TM6v18U +qZvMPwDJrryhjmtDnfvfspuWt6alzcOLGfH0vKPOLvAjVUPgplwbslvXdDUrRO/a1w1EKSmGbd5n +xX0Jvh/vJnfhoji2ZsDQb8q1IoM9S1xRexzIIclR31mC0eu6NhYn9GBSOa3Nltxj7nIfgHMm89np +9iatiuCfYDmWOkHZbmE0/mmw7prVh0gXmXXg3rBzSsnWIfUzH9YjcPUlcn3XHpX2s6HOTimsbehN +0oxSGJnsQ/NBNyjFoLiOa0Md/GLYXKiOljYP9f0fAr8Gt4Xj0jAa6HGqURiBOBHXyprvw01LkI/j +DMZzNPMZ73sfxrrFqQlka9z1JZ8VVl8AAsl/udL6NxuW35Hveqfry1JEaP9HqoimUgUNfoXlizh/ +hGxiVrhj+Scu7+qU5iK1aQPrRrqCvubQr2yUrVHs7IB8N43x4osvlo4mp+biuAr4fCQZOY+aENpr +NwaOQnJw5ai4ySA9feCb4KikPtV3FDuj6owjt3Qc4WYqW0rH9fMiYqSohm5yot7hdipi36VW1RLn +4ZeAqMepK8UEU2vCtzHbLBnifrBs5JWyulL7xEtJevwejXz/i2iCEaUqKr/wanSvFJEqapaNKGli +jYwAj9/XJHr6Ck7hc1G6Su5j1b9f/VvyHFfT/lyyC0idL5JTFW0iyaFP73fVP2wtaNeu3XdsRXiG +H4PN03E+HWorI7CzDbJrkjXHNedZWVypkyPwXOpq0o/zlYfrdVFRe3GmlcKVqV+x7koSJ5A0E0lP +NkqvyzSWbG2jlifu2qIKN0M5YSYHolSk1zoVi7QhPqrTqj4L33NWLIsL19MS56HeVxnXaRWCWjPu +hDeD9WMNoxQCPs5oENVBXCbVrCQZRcAi2sa/GhXVpEWLVoulL/B+jiVvwo2GAE4dfmBwLA5eazpx +/kWu/iSjtxBo//vzOIiDSPeDZ6NLa0fRCHXqS39EMJF9qjfwo7DdyR8H5424aly0lT2LyE8rmlEN +VFTOEVfnVOabBAI1U7Qzm6OpclenPlw+DKUrc7KyIRulO6Y61taEXG2kV3Ka5MWg74qhpIx1aHza +n1MqWpeOss2NuDZsFLPBDzHly0m8pc3DfQH3iAYAvBZtr4GPaoCOltc0SESiu0camF+jx6+lJEXJ +14zY4QYR5SKKVaAv12UjTU2FbxH9NEia6hAnUDdY8W48ao11QZIXOBwMZ/xhV61ogz9flwZsXRmH +uTNZcWRKjjGyfGMKlrPj+iYD3zzi4PVtl6MRpvTjcJ3yrt6l4XpXlp6GZbLlZYscaXE+0hiLQaXe +B1YMm+PoKLVD5PYufxnHyCyycR3X5nwum7Pt6adPPzS7Mb2wgOOhtJkMTyigbcts4gffZQwXZBxt +EPUakLF17MJapzpiM3/t4LVhrf2txujHwEWgIJ4j3LbCHNcioF4MFURMZ+AMXocuXfdd4CtdtfwJ +V6d3mbUlivmkhMjr0b2yetqrYFwdop7qhEA2n03/tiUh57OE28snaUVf7oe3j3MsdQoGZZIPt5VR +skdbGWaEK5oynw2EprTJ9X0MmQvgFWEtDGGA3QTQCdEG9ZVgNyHINinpJOsHGVpUlHe2kk2Q7FSk +dRZ8dqKk4R9xHAbd2enHJqUmjbdQaopzuz7GFsNx3TjmoEvtpMc0L6d4S5qH2iNfrGifdL0Em6NR +O33i4PCH2ial+gyir1P6s4DZP66NZcX6kdYGkUfp+wu9bR5iW1H4shi5tQkWGYGRI0f+h0fw/8I5 +/Bj/cVX2kf5MfiHOXlteRaXrVxv93SrvUl2Jsq+QWbuqqup99pz2kSnI+snXYMlnEIepBidTDrFI +jq0e8YtFndHVifpWsI7lE9VpX+vPBvJUU5OFspHwmvDXcFf2vM4mVTvtZZ2nPPLqaxXSj0h7aIyk +ZUHl7Li+CEK7w/rBh05GmNwJ0C/wzoDPh3XWXDnZkpL6dTYqfwN8MexC8emOlyYI+7wy7q+lODa9 +G6OFfih0Qgz5JVV0fwb+VAMH34H2/WLo0B1wsaLwMbotmmhLmYdDQKR/0VCpdYBvQd/eRdTZfFUF +/luR4wyBt1YwrV8v/vL12WINOHjr8I7+ZmPnZtTnV77gBdVHZ6zLVOgvOoBiXX8aRMFz+63lLVqw +VQwlL+FcpF9XYjQ30UZAYHucvLnwxpybr9E/Dycw8W9alHXEgf0ap3UDympwFDfFif2JdEvk7sth +i3NEF6JzLPwAOt5p06ZNwnHlzwGWRreeShwEHwzLt3BtyNYn2vfAhi9JN+bPB+Qwr4levYNW/mAH +yn+lvpJU16NNsPsXZHYkP7m+tqYpKWfHVQ6gPP58v2KT8ypyX2KdOOfEurKEQPJDesPl4byTSy9L +b+PkXLmTd6lsku3cEeck1z6nUITKxyPIOJG+ZFaHZ7gCSzMi8EdKr4ffzlgbrfAcxNaIJpqQ0s3a +vBjy5SbaEuahvhval1ps2guFf4b/UWzFWfS5CE2W6qYsrniYJ4+jI1sQBKOQ3SGyfA5BnODB3s+/ +nBW8sO+O/o7j6z/dCNo/6vlzawhP6ToSgYLTgxcG3uHv+NAXEYSzi1QvvIrKttkF6tWAoVE5IYAD +OR8ehAO4LHb9iuNXnXQG5Re04nguTmBXIq/rUa59pqtQLiezDlFHVSI6qjrNQ/kRg/mjgkfrCNYe +6PH/N/Cj/BvWZPq4mbz2ybq2ZOsS+hUJPphUkVbZsgz96UZO/ogit6qXEbK/G/auRppwlOtqarqj +cnZchUq2xdc5p3IO07/sznl0aTq66eWuDz2+F+kuw5UlCvhIb5Ov3NkkfDUZ6k3OpIJs5U5/1HQW +gkQxIv23urD7C3wGbJQdAc2Bq+Gds4vkrNEWgZNyStSvfKp+UbMqae7zUN/V22H3pKTY4F+Owqfh +Qh656UlJHFod4Y/iNCiVrN93wufB1H7v4xxuFKnPwNse+bP9PpMuiCSfRSh4dsB+7NQj8s1j14XV +jxB57ZMeefX73vsDf736Ciq2z6KmbnEQtPcWLLyCwv3rVkQ/CqYO2IMob5wnM7gz/iPRezDJEiGg +11sd4/oi77KJlCimHtdrW0C4/EYdyFEMF9IW0ZTbcTp/DfvooEGDEn4Jf7tax2+gvIK/Yw14Y8A/ +edfqqrTVOpMi9CR0U55IOW5P9lgJJItSaaaypN2a42VDdRAsG6tqDdFZq87CcljFojonkWM3AdLT +hHCo3h27C0IVBWKRK6s9WqzTHbvU9eGOXepscnZmG0dqZrqGDUgfj9H2NGT/FEM+m+gaVHAh8PRI +vCVSXwZ1SAED042LFiR3MxRVxRNRBctYrjnPw+PAVee8sagjisfChQQMfohp1IYR5DtFkMkpEjzb +by+imBfg7E2Cx5M/Fwdxl5yNaivjRQyD4Hx0H+8uwhH01xHB8f0T3sE9CadVNUGwlffz/8YFU0Zk +OBd+3Eei+wVT9tm7TocRD4KXD+UmqUY3yDHI/6/fa+L7MRqYaAkQYG6667mu/84HUM96Ait2VIPD +KJ8gI2mOJ3Vpn+n7bAt4SIJyTnFa1U79pFhl7733XsIXQe+91M2A5duFbeCwLiVtCMv8D4lwZDWn +nXW1lfaonB3XfEikO43u2E2e9NTpc+XuWBcTkdJwPlGY/Ehv4+rC5erf2eDq049deWOkuiCGJ2Gu +PmSXHlkenksoT9121CsyMRQeB8d10mjSLOhOrDwDjnou10dWuGhPUBzSNoHX4jQoU9nmOg83AM+L +Y2Kqx3ThNSBK820ROiuKYJpMXMf1VNrnmrMHUq+nNAXddAYvDloFZ/BRrzp42KsJzgKFfeAB5Efw +Z+tPUXd/MOXgrmljWHxYWXkL1rngw+LyXLma4GpvWv9pwfP7bJxLLFwXvLLvctjyEI7qGLhNuA57 +9/D86frhXF2qXOof2BbvHam+h9Pef3gQjIh8TQ2e67+tN3/2m9i1Xl0D8hxVBHHnaR6FVl0MBHAE +E983Eq0Jmgea3yPhXYmo6mZOeZUlXvpKmpWSOhQJnYHQHAkSUc261rgoLHteJas2aptVXvXOOSZb +hX3D2ccqG8WXh8bAYflR5C9Z+ZlezyKdJE0ct1inp2rg6l2dyk6EFenSXbZYeZU5cm3ccTgN61F5 +zokSbtgI+XfRSUQhMunc3w7fDcvZikral3Mz/Dy8QrLRnqS3w+l4JKubdVKJ9RfBE+Fl8oxkMPXT +4c3yyGWq1qLWEqg5zsNWAH8n3D7mCVCE9vKYbSQ+HN4mZrvPY8pvj/yFcPoar+P/g++F14Q1hlgU +PDtwJW/BfDlcf8jaMAgO4Hr7WvDaoKUzyfg9x3/MlVU3z/EoCHp6i/w3gmn7XBxM6bdONkcxeG7f +9ai/3Ju36GPs3DdrJ4F3JI7t+eF6v+c9OK1+POcwIIpeUzPSm/b6U8Hz/VYO60vPJyJqU/qfzraF +57FN5yA6+f77Xq92t0dvYJKlRiDpEKrbS3nEPwJ+fdSoUW8oT91lcezBgaycOXNm5OvqUkstJVld +s/ISuhN6cVpHYN8l0FuyFT4dJ3a0FITGkldfKQW0YDd3cs6iToLLa0wu79JwmfIiRSj1SE2cTqpz +EyasIywXLlc+n3y4bWPkz0WpIimtIyrXReyQZBs5vffBH8Ofwe5RhmTkrO4Gy0EVZ7rAS8+PcOwL +IW2aA+2DkZ/CL8GvJFnj3RpWFE2sqF0hNJ1GjxbSsEzbNLd5eCY46jzGId3k3gK3gXeFt4Cjktbd +u2C10Y8iotBHCH0N67sYlfSkQBEU7YdUdLhnkruQOjqNzI1w9AhjzSKil15XpyBHuoY3Z/5V1A/N +KFPT8TyGz1OfIKNzm7GNCvUaqsA7nczp3tTpc9mi8B6lb+Ojz6NsTfL8Qn/RRhJNXQUSB1k+guBs +tiF8xZsLxqQkgmVHe/5Px9HXqqmyKJkg6MvLGz/GGZ7O1eB1L6iAvXfIr+T5QQ/09fCm9dsKVatH +si29z4rgTN9PPC5Or7HjJkZATl7SF9T3exY/fErcmA0bNqz1yiuvrD2oetfqtdQdCa8Y1Vy1jSqb +SY4+M/qfFMrB/ZRXct2kdtjXSk7ymDGJ9xLLzsFwvO+mFJWABHBzJXcy5ViJ5Gi6vOqcE6k6Jxsu +U7nkFbqXk+bqJKsTKmxcO7Ipcrpdqgon52zIZkdKSSNl5FjpIvS3mPo1Xi4gqa0DC8h/CS8F6wKl ++ih0LEJy5s6LItwMZXTBd857sczXnDmlWMrKRE9zmoc9wOzsmLjpsf2fkm30XTkU1s1Hphu6pFi9 +ZF1KroD/XK8me8GTVA3JXp2xRpHdXNFdPUXACfTkwOalxP7V6uq98wo6AUU0n+13td9rEo5lXar9 +IRTRziCBQ93K6EcdaZ8co75KBVKNd30wtf83fp+JuiHx/L63zw+mDDjW86vZZpC6NkRVjk1BL9r1 +St3/6wrhrhJRtaTL+d5kv9fkienFdlweCOAgOh9CBk279NJLZyqDIygfI0FEM7/lB1RTET3IlTV2 +Kn86Rx9P47Bqb6svx1qpZInAfsp7aV8gq+td2ZFzsMrOsAgGuZOxMCSrlcstEelpSCyVlYwc1Law +Iidi5XM5rel6dSwKr5rOJmdjrURpPuUEvdzAroTD2vAKcFSn1XU5gkxcx9m1XRLT8xn0lBY48OYw +D9uB+1g47g380bT5NnTOPiB/cug4alZ69ooqjJxsbQzSDecqkRTX1ORygjOrqPaztuFNAVdyrbwj +c8NSlhJ9CoJzwxd53n4wwQv8c0ppRda+fP8dr5V/aNZ6qygrBBTNxBFU0KcO4bR2w2lVoKxkRH/O +R6nXJ3XOTsmk/BWcVl37Uw53vYZNXNBcHdcwyE+C4QxYFx+xxiRnS6ljd6wTk34SdZyJKU6R6y+s +2+l0fbj+v6SVbBJl6q+2pvE+56O6P/xZ43WRV/PfkTgkr5QJPAUEF7RQGJrDPBwJ9rWPlKOfhLGI +PpRB/AbKHs5Qnq/oFgS65RNK1j9DOjWibBwxRYrPjdQgCDaLJBcWqsjTptOKf2KlfCLcpPR5/wWv +bdvfp1/k/b6TRmLbPaW3J9Sj73/ntW61j7/TpNmhUsuWLwLaErA/P5Q6TybiwPp6ZZXylKtMNyAl +cwrDN2OyIUSLqBuKnaeqTDbKVuWxU3tx+8Els1P9RiU5W82V5EyK3oP3gHeHNR5FPhPgk0pGE0aP +8zrDf4FXgnXHo3LJOT1k65Crkz45qd/Aujj9CisimakfnWQtwLJJlE13bW3jfX6HakVyXoTz/aCo +MayYh9KmGnsxxqNzrIt562Ioy6LjE8q1gGketVQq53nYC9BPjAm8bkoVncxGR1GhR+KKVkQlyY6B +943YQPtWn4e1JhWThqJMWxc+yq3U54Yk7lc70E1MVvK3GrMwmDJoP8+vmkLUc6usgo1V4ftsB1j2 +UH+H2zPbuUr3od7XXyyPbbs2lglZ9ertBkHQv8F/cJC1A6toRAQyfUfdNSXul6hgM3UzBmVtT109 +OylzdmZt15QVLcFxFX7vJzkXljo5A2E5rs4ZDZ9N56i6VLqUd7Lfkr8IjhPmD+unaUnpA3rTI7oH +4d+VsOfP6EsX4bdK2Gexu/ochRfDd8OJO2XSYtIbKNPNlhy7lk7lOA87AfodcJxzq+/ykfAvcDbS ++ZQD+Eg2gSzlA5Ltbs1SHy7WNqAT4b+HC4uQ1/p4CvzHnLp8/q41x0Uwc1v/zczli0v9vuPmBK8N +6+3N+fY6L6gZsrimsXP+dV7vHsf5/git8xnJX3d0Fe971auzrsBnPy6jUGMU6g0CgdefqK9uco3K +HAF5hziJYSszzalMZfpTAv31arhtKh/lrQKKlMIZ/Q2ZJWUuTSlOZmR3ehnHGe3MINckRXEW7iYx +MEKnOikah5sxSh2ruXPOFXGtd2chgSS5kxdOXV4i6kM6RE6n6yfcd9iWWumm+/wPXW8H31UiEx6n +H0VMmrPT6qC6j4wi9MWmqSjsA+tGaEmhcpuHVwJ895jgj0b+6Qht/oXMtRHk0kWupmCt9MIsx7JF +XEyairIz8yqs9J9ldQ2vi7mbJN7VGujJT14i8vobP4460vMrhvKscl7eBg0R8P0v6WcgTuHfcjmt +rgu/L78I7zP5+KRteoLXyORP5CqznTmtjQxzEdXjtDo/wGlNP9Yj+HplEuaVVNG/U057xJQuE7pd +GqVZNic3SttSyMjJau6kk+LuDtInhY5dmUuzjdfVh1OXVxvl3XE47/S5OtnSaJPQdRYj/Q3Zw+HB +8OdwY5Ai3vvBf4B/aowOmkinHuEeBv9chP51sbsQFkbairCkUbnMQ/0iPndUsf6Z+ZCi0+sXZy1R +5FLfiTikKPCdcK6b67C+4zg4Adb2pIaQ1io58rvBeZ8A+D0nTEfuBjgaBf6Ffu/Jn0UTrpXCeb2N +f0zfmqt8IXuGc3clR1r797osvSH9jM8tXL82YVsFr07zvX/Vry1Cia/fa1T8yeszcV/b01oEPEuo +IhS5TFz/OU6EUImE1iT/8UpRT/d9rSNDxNWFW+U/1IR0LUq+nkojSbTJNCT1oXKis1UkCV1JHSld +OMeJchxY11dCH30nbJKNTo8rQ1dCbxynV3Y0NrnIYWP3Uwr92U5qAngMUH02Gdnn6tJT1YlU7nQp +dXKqaw6ki+I98BGwIitrww2lT1EwAtYjdYcN2RZFGtsz8I3wPgWO7AnaHQt/XGD7ltSsKefhcgB5 +U0wwtagfDseJAGqv5CHwq3AbOCrtiOBpsLYkRaFrEHoN1raBHlEapMlM5VhOtnREpy7LnOr9/EtP +rsK/y9nIZy/uUivqZi02+TtNfo9G+/B+1S1Yac9muR1A6oIDsfXRQBf0h72g1Qi/7/h3C1Hg2iRf +7cVf3fbfAQxGwn1dXcGp782i7UXeKt3HaGvC4hhJwRqtYYkRwNlrnewykeLs7T18+PDOOJDuuIq8 +myuJdQGZ/fgF/yq883Vd8mrufLIKHSC/PW8iuI86OZtZ/3GLejXWNVh6t4BFdXRxfBJ9aYuanoqK +nL37YeeK9NVWhahaSH5X5aGEnTi9ibS2qOk/3cCa3pKmt0AnXs6oS2VRYiYpAykfPk4UNrMPXYRv +he+Ad4F3gzVB9UvhKGMTPq/DijY8Av8bbqkOK0NLkX6Y1w/uCcshGQTLCcpFX1E5Dn4AfjmX4BJY +11TzUJHCFWPifSHy8Ry72g60XeYM+Iraw8ifI5B8HJ4escULyG0Ja5+sbo52gnNdZGZTr+/v7fBj +cGzyNxs7N5gyZBte0H8+jU/m6pq4yKYUceFjJeX9rD1G+VuN0LkumPhTAO0HH6h/yeKKuifLDeuV +34c+O+VV6vs1yE3xKvx7vI5tH/S3GvdL3jYxBPzeE19EfGcc2N/RjxwCPU3ZkXwu/Bf34Htsoal4 +jJX3Ma+myzN6d+ziSss1NwRwXN/GwdNTAt0kiTQPusEuyKVrrL7Xuh4or/LWcHucxc9IP4DD12HV +t6KuM2mCav1Td1Q/pR6fM3iSGjm66brkmLaHdQ0P2yA7V4DDdqpeDqz27bZv3bq12pQNhQdWNkYV +2RA9etNJ7AJPhTeFF8KaMO5EkU2Rm1AqUN7Jvk2+D6zHxk4n2RZBXRmFIjb6kimvVHj9Cmsv5qxk +qi+W8s2R/oHRR0c0XOdaznw20tzZDl4dXgVeGdY8+TrJn5Lqi55pflFslAWBJWEeZhl60Yo7oqk3 +vA68PKzv8U+w5qbm5TTYXVjJNox4SX93r6J6G/4hiu9LjR7Dv+W1bvuKv8M49dcoxA+lWnmt3tiS +B6p89/xupN34V6puvHN1Ib9E+JJ0Bs7jl7ze6lPs0NhLRrwdYSmvooq1NFgRpxSbanQOmNf+XOz6 +Dvu+w17S4KO4WyhKNgjryBAwBJocATmZIi3g+nWrnIkFsCKFYVa5jl3q6iSrMrWVDpHTWXtkn80B +ATmuOo9R+K3mMCCz0RAwBAwBQ8AQWNIQqPuIp2WPXtHTVqEh6jhMcmhELlU+LKMoW/hY9UaGgCFg +CBgChoAhYAgYAiVCYElyXH8BUz32dhR2UFXmnFKXqiwso0fk0mFkCBgChoAhYAgYAoaAIdAECCwJ +jqse+csZ1T7Xs2E98lf0VBR2TGtLFn+6OsmqjdpKh3RJp5EhYAgYAoaAIWAIGAKGQAkRCD86L2G3 +Je3KOaDq9EV4iwb2HtbXQFXW3BAwBAwBQ8AQMAQMAUMgKgJLQsQ1KhYmZwgYAoaAIWAIGAKGgCFQ +xgiY41rGJ8dMMwQMAUPAEDAEDAFDwBBYjIA5rouxsJwhYAgYAoaAIWAIGAKGQBkjYI5rGZ8cM80Q +MAQMAUPAEDAEDAFDYDECS8KPsxaP1nKGgCHQ4hA45JBD9qyurt55ww03PH3EiIb9xWgxwDn44IMv +5Z8Sp9933333F0NfFB302bVt27atbr/99jr/bAceFRtttFH4FX/e+++/H1Ce880oRx111LJz5szp +CqaflgOmUTDIJXPggQfuzV9y7tqpU6dTxowZo3+5a3Q69thj2/7444+r33PPPf/J1tmQIUNWnDdv +Xmdw/iTfOcmmo1jlw4YN64Ata3Xt2vW/V1111byoeh944IF6f8gzaNCgGv39aFQdJldcBA499NBV ++fvZ/2PO3w3pr9kLItaVq9Hz9f33339ZQQoaqZE5ro0ErKk1BAyBhiOQXDiPz6Zp6aWXbvPrr7/2 +ov7kX375Ra+sW5RNtlTlLPTH0Nc9cKM7rjjt2+C030afG82fP78GB+0ZLlbn3HvvvS9pvB988MGb +8O+UD5H+jnjL0HEqS/tBHByP06q/NK6k7YKDDjpobKtWrc6+6667vkkJNjDDed0Ym9/FuTkaB39M +A9XlbU4/O9Hf8R07djwD4UZ1XHEaOi9atOiO77///g/cwLQDv4/p8zrG+XdnKOdtc+x5AEdxXZWB +82wwOYnzdrOOOQ/6pz/3F9XzcBw6qNwR7bek/avov4O6oa48W4runZF/2tWDx97Y84iO//rXvy71 +008/3cb3aCD6KmbOnFmNzfd37tz5Tzj5v7k2mVL0bvDggw/qr8DrEGXCeVSdwhZ8AF79wG6ihsj3 +73zO47nh4eoGhe/nZ8i0A/unwX7XcH2x8/SxvOY7qb7rBTmuuun98MMPB6DjM3SUleNqWwWKPWNM +nyFgCBQNARbNh2AtwGK9zk4XhtNc2corr6x3Ky+RxMVwGS5O/2TwCyorKw8mPRxc1uHiOJnomXtX +tUfZG/BfQnxxJsAOO+yw9Sm/F14KjA8n3Y9UzsfmpCl9HBvlQIAbiauo3pXzMBzM9yb/AflrcDZ3 +V7MjjjhiOc7bNLK/wnuC7f7IvUXZTThA+0qGsrsoGwY/r+MwyaGgjxso+6VDhw6nheuy5en/Q+mj +/up0GZzWuyj7PfwX5lFv5O5Afj9uBG9Jl812TJtb4PAcezybbEsvB7tjhw4d2ik8zqqqqpMobxcu +K/e8ngC0a9duA2i3crPVIq7ldkbMHkPAEEghwGPWZzkQe1zUNybZoX379jfceuuts1UmojyRzpo1 +aw0iVVdwsCP8MVHCY9xjMiJDenyvKNNGCWE+kN2D5B9crAfRz6voeQ6Z/3A8FifiYtL9af8Vcv25 +KB+JbF/qvyB/JXpu59hLRjwvo2xzDj+gzQlE21SVIvpW5O10CjaF9dh4XLdu3UbjMLRFdjpOyiNE +aE5wDZI6x1E+gvLbXHl6yraA/+G8borzsuj666+fo3rGsBZ9XUDEdDUOP1UZx58SlVMELydhiyKz +lYzlQfqVAyt6CK4TPWI8m6Lzcngb6n5C/gGic+e76JzDkbpH4XOoXxHZKaQngNvMZPTpSepk28Xg +O5yxHk+XE1RG+6GUn4C8xvIRRaOwf5zqkBWOf4F1Mb0Kme1J3+BcH42OBdBojnvCs2h7Ku0eJp8i +zRH0K/K5LfwhbYZz7p9yAnI45s6dOwq9+9C+I+nz6D6VaLPsSPXPtowd6OtKZKbSh5zIFDGOPw4e +PPicO++882sVckPwycKFC/vjbOod4o/T7m+060zfx4LHS5Lh+CHs+pnscHg8OuSwPs94hXGd6DjR +2aMp2xr+y2233fY9aV4S7gjdxLnbjbmdmmscK/Ldn7qrGYeLfD+LLQFlR9L/WZT/N28HnvckfdR7 +wnD44YdvwngfYaynoGMHxnkQPBtc7xw4cOBFBxxwQLWToV6PtrfDngOp/x88at99972X6O1F5A+g +vr3aUXaG2kWwqSlEnmJ8uxJJ1xy9VAbwfe7CudfxW/DKKnMEzu+TfwTshE+CKLsWHduD+5YNxQY9 +lZzjq0nr4a7OsG0N8B5JVt+ZxHefNexa5vsnqidK/CTzTWtW3qi+5EtFFnEtFdLWT1MjoC9i4mIQ +IdXjFaNmhgCL8yQubG1hneetuFhc7YZA3XLwau5YKRdJPX5dHXaRkJVpux3txlPWqk2bNlU4HZtR +JuetDXw5PBdOOBIs+psj+wLHO8CTYA/nTw5ZW+VFcoS4MNxG39+h50LSmfBlP/zww5k4xb9S9g71 +R3GB6ljbgr/l45jylXCYHnFl2VI5r85pGlrYtAAAEG5JREFUlQy6d6Ptj+jW470Ecbw8F8PjuICd +h83aVpGRaPseFXJYTkT2ZF3U0gWTjo4eUW+E3lupf5f8yTxmvickKxx353gsqS7M78KD4Bc1Thxt +Odn3wYoGT4Wv51zocXrCMUSfone/wXJCW8MPYMuhqqdsGZLVSR+GhfPLyPcF9wk4SDrvcpIfh9em +/pb0/ZeUPwYvj5wi1Zty/p4Am13Ie5LFaX2adrpIv0R6L+mO6H6BebCSZChL9E9fD6JnO+z+TuVh +QiZwTqvKcVrlZHs4BK8phXrAi1ZcccU3E0d8qA3J6+jcTBFVV56egp9svwj+Nzc/tx3FXuRwdD1d +Pt8x/W4uGdJ/p8m+hi2yY7O08oyHtN8SHEfg6J6k/ZVOCB363qxOeinz+nDyeoLSluPzH3rooWsl +52TIXkRej6bl/K2O/C3IPMGx2j0NU1RzyoQJE7LOYWSalDjHYzHga+w8kRu0xLrCHDuWsqUYx8Xp +xjHO1eDl0sq7IruKyqhL4Ee2IGxofy62ZMRd+qm7AJlNyI6hT83pI5nvibVM9ZC+y91qs+XzmfUL +Uj4mmiWGQFEQkNPRMyIrumbU/BC4m8jFrvDeLLaKMmzPxbROhCPfkGizIc7IaejYStEsnI71KKtA +3w2UXUAUZHuiYcdJD4u+LkStWrduvRV1h8NyZHQxTv1YRZHh5ZZbbl3aHUC7K5AZSP0MdO5MKofh +NpKl6EeOnXfiiSe2J1EIefLYsWPrOUWSyUbJaGQvdI9Arxwh6f+FY837q7D3HC6i03A+E5GgdD3Y ++AHyZyAvZ/tyZD8Dvwk4Ils7WeouIb+QqPcmjOUk2vSjzWjK+8uRD8npgrcXMgfCfSk/AZk1uCge +I0ebuluSso9TfwmO9vu070LZCHgCZduB1xn8aEk6Z2DPuaRhcud6TwrvR/fGpNNoty02Hcx5OI6y +5XF8NPYUUfYY9ZsjdxQyG2DHIio1Jm/8+PGHkWxN2VDqZfffyCvyuhx2/00yIfqZ/dXrYOODobJ6 +2eSNzygqpgwYMGCKBNAnx+6D9B9AYc8blLf65JNPVpBcJmKeXI5NneG/cvPTl8j6jzzS3zWTbJQy +zvFqkkNf+s26bBEl6hX5CzPzIlHOWOYiU835OYX0bPgKsPqA+j7kw1TB92ANMP0r0XndVEyl7dHM +2bVDQvPAcx1khlAmboNMj5VWWmltyv5Im00pC7B5f9KyJHCowrAr4RXZHjBU+4fJHw9/QoT5n6SF +UqHY5MSdc/FnvmNbgu9FYK+nHDeD+YZ6KlKooaVoZ45rKVC2PgwBQ6DRESBCeXeok6nkfRbh7UJl +UbIL0HOXE2QP7cPkFUGZxEV2Ok7fCccdd5yiIHJAtmShf4XHanoEmCCOFYWsQz///PM2XMgvgh+l +/WPIdKTtshJaf/31H+X4W44TN0vffvutfiDTGb65jpI8B9gmx1ePVPHL7k9EstQEZ2gwUaDd2KvW +kXFtQ/0bcjLkUGVSyQVMTtwGtDuN9C3s6I8j8ipOpXtcLSd2Bo8Q/8Z4zhJzLIdTJOcxQfTzBRfC +Z9wxju4d5KvQ5/S4qnCqrQqKmOsHYQnd/DjkTI7lwK/tIlhqkHauX1YZNquPBDHmqcrQX53zjy7d +KCSILQJfkHkC3pQoZytktyJfTbq+65/8LrSZT3lqbOTl6N2Z7+0E7GVdBewmI/4rth3sHm/TVs5e +N+kJE30lIm9EpH8Ll7s850A3JYdzrGjzHFI5wLJlVUVelY9L2CVbRF1rk9Rn4tjV4zC/RZT5HcfY +kbj5Yb58qfnF04nf4QR1wpZBaNDN140pTWSQf8A9GRBu6NV58ElTNzvk76V9oHakiXNKOs45+Myn +Hzh+B95eMuVKOOhjsO0nxnwK24GOIb8sNl/qzn8hdheKTT7cufFZha0AxzLfH4T1pGJ32cf5Lmg+ +FTK2Qtq0KqSRtTEEDAFDoNwQ4NHp1yGbflaeBd8PlbXW42B3AWGRXjpUl8hygXmVSKe7mHu6aBJp +Wh8HRBG843H6riLS1R3hE2A5sN/DKUJG/Va7AhzVY2nzd/Q+Qt00UkVkurt6HKZFOJ1ylE9S9AnZ +och8xQ8iHncy+VLa/R4Z/ZjnBaJTR4blcc4+5Vgs+jfOz6VErO6Fe3CccrgTtckPHN//kpVjcin2 +74dN/0R+MMfTGYMcy5XIHwSniLL3kNPYEiQcKEs4ISro3r37b1wg55HNes2hj8SjVWS2hes4ihx/ +wLlsTZqg8Lmm/Ff61rlOnQv6Tpx/0jrBGWR+cjqUYuePJJKpIK9ffGu+7A+H6b/UpXSrgj2uU8IC +6Xlw64qTp20jS9Nnb87DtyGZr8j3lGMb3lJA2VbwbBy7X0KyqSz49NQBtvQTuwryY3777TfdPCSc +SVceMf1ScujYmuR514bjxA0GcCTqSY+mLvVd4lhzJEGMLYzFP3GAJtJ+oKKNbCFJyCBfB3fOl3CX +g5o6p+RT399ll132V14lJpE6uKP3f5TV+95KsFxIDjoY6CnEudg0knHN5Pt8Rzb7qHfz3onUG1+h +2NAuK+5ab5hTbyIzg44fgn/A5u1Jte85da6dUeWU1vlSl5NhZoshYAgYAsVCgMVZey3bsj8uFfFj +kd4vg35dGFOkVxvpgGjPxThLa6BnIov6UTicclrfhXc/8sgju0lGhE45PZWJg9pjRex+5MLVD4dw +FFEp/fCio6tXSrTqdhLE/BHU9SV/G/prSPMSDtL2tNO+wQ+Jqu7jolOuIY7qNoomumNsdxGub1yZ +S9G1Ahezf+hX764MB+0F8nLE10uWacxzwEKPF7VdYBPGtAWPQTcjH34UujlR3fWTbbyPPvpoH/LL +wO+ojHEmPBpSRecSxBikW87uw063UpzxrZWGf5BX2yL+J+M/0LXSe0vJD4Q/AqMF9P8O9mhbyDHp +/XPu/uTaKe3SpUtG51J12teMnkfJromufjh2b6o8RIryaj/0Lq5Mj2ZpswHHiTpXHk6JbD6JvpNC +rMieJs5NtE1Ft8Nt8uXZ5vIc7RVR3jlNtg/H2tLxksqZ/zeDyU2OXTSdfn3NQcmIwFE3AJvS7tfr +rrsudQNIVQr3hCBvwFCKbGI+JMtaTMJ3ejQYaPytSK/U/Mo0OOreA4MdXR3f1y4cp58LV11ImhV3 +5pOeRnTgxko/EhwO68eO36kTyvQ9LFtKLWhla6EZZggYAoZAAxFgQZZTdTmOy61EQ3TB3wHehotE +Ts04FxcjsDcX578Taf2c/JroekMXIpw8/djqX/yC+N/Uj0a3fgU/BJmUUmQfprw/j7xH0O9rRKAO +oV4/vEg5PkR436VO0czDkm1vJU0QjvNGREWeQs9wnIXbXLlS/WAI+xTJlSP8FY/vr0BPQoSySTgl +r/LIbwp9z6L8CSoUKd0TXR/ww6BpCcHQB22O4vBo9uYNRP4x8l8xNjmccsQnwrqgncd4nvjuu+8e +5SJ7O21qGNNf+OX3q1SfKBkRfdRg2zQwGs3hCsgpkvwzWwauUz0XyS/pQz8AOgrsfiVdjrLLKbuZ +6mGklZQ9Rn9rfvPNN6fT1wE4gM+qbUMInWdh07roeAu7/0qqV3+NlE4e0d9G5PIUxncn/d9IkX54 +tjv970z0cGv3qFuyuYgfeN1N/VbCmfZHoOuIpPxMxng2Tv+9vOP1fM7rddiyGnI/gvMxkiF/ZVK2 +XsL4hbE4QeCmt1UMg8fjWL+mQvRNQ8fP9DOgVir3JxHfH7HvFnQcQ9s7aDuF/O606kv+cv2AMJcG +bBiK/M3omEb6BlH1PsivQ1u9aSNgjrrmmyDzCmV3gK8ixruT19sT3kOmhxNqKalwBc+bGM8R3ORo +LmUkMBgHFpcj+0/yz4LNENKww5+xXYzCrLhzw/0d830BfZ7MuemAzs2wZah0U2YR1xggm6ghYAgY +AkVHgIv+FzgoJ7Iw62b9cngp+JR8HeH86RfuisTJYdOPgPT88nS1w1l4lKjFYLJtqJPOgfTxR9J5 +cIL4p6Z7aC8HT1sNxlFYyfH9yepUQt3Y5IFeTv65q9AFhLpu8FquzKXUySHskjzek7yciATTxxb6 +wwDSP1NWAx8N74XsU4xpr/TIrHTQ70Uk+lX3J6Taayt8ZjGmPzPWG8gr8qao3yB4DRyvO5DRxXk+ +rJuBFFGuX+9fT8H/wXr900eku4Rf34TeMyjTnlI5t3L6faKrx5O/BpaDP4Gy4fAD4Dyd4wYTegZi ++/YougK9eiXVnxnTvVKsiC7HinZ9TN158ARkd+P4gqhOq/TQRpFTRRM3hFPnhKL9VD569Ogq7NiB +7MvInkV6Hanm5Z6cgxdJCyJFO9GjaG+i/6hKiJYL8yvgPWh7Gzr6whfzlOC0fDqItmtfqubGJrB+ +3LMWfBF7tzWuMEnX98zZa6jvTf5OMBhC2mKJH+/pPb6/yzV3eEpyJwDIsdV7f89FXmuEbjKLRVlx +13eR/k6iI333tL3jIOb/BcXq2PQYAoaAIWAIFAmB5C/3Y2ljUfeTvxDO2I7HzvX2pYUFcSraiMNl +4TwRj/2JugSkB4bL1Sfl3yi6Gi6Pm5eeOK9NkhPE3t6OufrRNor0101JnjH8F56sfD7cJKO/RlWa +Tjw+19aCRiG3BSSbcp2rfOPP1jZOOf20KmY/ityB/ZA4NoRl883jsGx6Xo+508sUTdW8pi4RddZ3 +L848TNfXUo/1HdB3rrHGlw/3XGtbY9nUEL1lHQ5uyMCsrSFgCBgC5Y4AToZ+FCGHVhG473mMvKki +cs5u6rsTFdmAoKAe3TcLwmb9cOd9oofaZmBUQgR4dH80TxfGMGdS21VK2H29ruS4sl1lOtHVwdil +6KKRIdBgBBrNw2+wZabAEDAEDIEWjICilUQk9TL/dRjmazgb/cNOq4aubQPNyWmVzYxH+2eL8mhf ++oyiI8BcubFcnNak1b9iz2NsEfg6+ihM0hAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQM +AUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwB +Q8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFD +wBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPA +EDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQ +MAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAw +BAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAE +DAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQM +AUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwBQ8AQMAQMAUPAEDAEDAFDwBAwBAwB +Q8AQMAQMAUPAEDAEDAFDoAQI/D8vID6miTPf7AAAAABJRU5ErkJggg== + +--Apple-Mail=_18FE9DD1-B053-4781-ACFD-7E542B418B05-- + +--Apple-Mail=_6F6D23B5-0ADE-4347-978A-E0A21B92BB70-- diff --git a/influxframework/email/doctype/email_account/test_mails/incoming-3.raw b/influxframework/email/doctype/email_account/test_mails/incoming-3.raw new file mode 100644 index 0000000..1571300 --- /dev/null +++ b/influxframework/email/doctype/email_account/test_mails/incoming-3.raw @@ -0,0 +1,183 @@ +Return-path: +Envelope-to: test_receiver@example.com +Delivery-date: Wed, 27 Jan 2016 16:24:20 +0800 +Received: from 23-59-23-10.perm.iinet.net.au ([23.59.23.10]:62191 helo=DESKTOP7C66I2M) + by webcloud85.au.syrahost.com with esmtp (Exim 4.86) + (envelope-from ) + id 1aOLOj-002xFL-CP + for test_receiver@example.com; Wed, 27 Jan 2016 16:24:20 +0800 +From: +To: +References: +In-Reply-To: +Subject: RE: Sales Invoice: SINV-12276 +Date: Wed, 27 Jan 2016 16:24:09 +0800 +Message-ID: <000001d158dc$1b8363a0$528a2ae0$@example.com> +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_NextPart_000_0001_01D1591F.29A7DC20" +X-Mailer: Microsoft Outlook 14.0 +Thread-Index: AQJZfZxrgcB9KnMqoZ+S4Qq9hcoSeZ3+vGiQ +Content-Language: en-au + +This is a multipart message in MIME format. + +------=_NextPart_000_0001_01D1591F.29A7DC20 +Content-Type: multipart/alternative; + boundary="----=_NextPart_001_0002_01D1591F.29A7DC20" + + +------=_NextPart_001_0002_01D1591F.29A7DC20 +Content-Type: text/plain; + charset="utf-8" +Content-Transfer-Encoding: quoted-printable + +Test purely for testing with the debugger has email attached + +=20 + +From: Notification [mailto:test_receiver@example.com]=20 +Sent: Wednesday, 27 January 2016 9:30 AM +To: test_receiver@example.com +Subject: Sales Invoice: SINV-12276 + +=20 + +test no 6 sent from bench to outlook to be replied to with messaging + + + + +------=_NextPart_001_0002_01D1591F.29A7DC20 +Content-Type: text/html; + charset="utf-8" +Content-Transfer-Encoding: quoted-printable + +hi there

      Test purely for testing with the debugger has email = +attached

       

      From:= + = +Notification [mailto:test_receiver@example.com]
      Sent: Wednesday, 27 = +January 2016 9:30 AM
      To: = +test_receiver@example.com
      Subject: Sales Invoice: = +SINV-12276

       

      test no 3 sent from bench to outlook to be replied to with = +messaging

      fizz buzz

      This email was sent to test_receiver@example.= +com and copied to SuperUser

      Leave this conversation = +

      hi

      +------=_NextPart_001_0002_01D1591F.29A7DC20-- + +------=_NextPart_000_0001_01D1591F.29A7DC20 +Content-Type: message/rfc822 +Content-Transfer-Encoding: 7bit +Content-Disposition: attachment + +Received: from 203-59-223-10.perm.iinet.net.au ([23.59.23.10]:49772 helo=DESKTOP7C66I2M) + by webcloud85.au.syrahost.com with esmtpsa (TLSv1.2:DHE-RSA-AES256-GCM-SHA384:256) + (Exim 4.86) + (envelope-from ) + id 1aOEtO-003tI4-Kv + for test_receiver@example.com; Wed, 27 Jan 2016 09:27:30 +0800 +Return-Path: +From: "Microsoft Outlook" +To: +Subject: Microsoft Outlook Test Message +MIME-Version: 1.0 +Content-Type: text/plain; + charset="utf-8" +Content-Transfer-Encoding: quoted-printable +X-Mailer: Microsoft Outlook 14.0 +Thread-Index: AdFYoeN8x8wUI/+QSoCJkp33NKPVmw== + +This is an e-mail message sent automatically by Microsoft Outlook while = +testing the settings for your account. diff --git a/influxframework/email/doctype/email_account/test_mails/incoming-4.raw b/influxframework/email/doctype/email_account/test_mails/incoming-4.raw new file mode 100644 index 0000000..9937488 --- /dev/null +++ b/influxframework/email/doctype/email_account/test_mails/incoming-4.raw @@ -0,0 +1,138 @@ +Return-path: +Envelope-to: test_receiver@example.com +Delivery-date: Tue, 09 Feb 2016 14:53:22 +0800 +Received: from 23-59-23-10.perm.iinet.net.au ([23.59.23.10]:56280) + by webcloud85.au.syrahost.com with esmtp (Exim 4.86) + (envelope-from ) + id 1aT2As-003QlT-B0 + for test_receiver@example.com; Tue, 09 Feb 2016 14:53:22 +0800 +From: +To: +Subject: test email +Date: Tue, 9 Feb 2016 14:53:13 +0800 +Message-ID: <000001d16306$8b9e5c60$a2db1520$@ia-group.com.au> +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_NextPart_000_0005_01D16349.99C37120" +X-Mailer: Microsoft Outlook 14.0 +Thread-Index: AdFjBjqdYnxyziyBQVK9mLTYYu+9Og== +Content-Language: en-au + +This is a multipart message in MIME format. + +------=_NextPart_000_0005_01D16349.99C37120 +Content-Type: multipart/alternative; + boundary="----=_NextPart_001_0006_01D16349.99C37120" + + +------=_NextPart_001_0006_01D16349.99C37120 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: 7bit + +4th test email + + +------=_NextPart_001_0006_01D16349.99C37120 +Content-Type: text/html; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +

      4th test email = +

      +------=_NextPart_001_0006_01D16349.99C37120-- + +------=_NextPart_000_0005_01D16349.99C37120 +Content-Type: message/rfc822 +Content-Transfer-Encoding: 7bit +Content-Disposition: attachment + +Received: from 23-59-23-10.perm.iinet.net.au ([23.59.23.10]:49772 helo=DESKTOP7C66I2M) + by webcloud85.au.syrahost.com with esmtpsa (TLSv1.2:DHE-RSA-AES256-GCM-SHA384:256) + (Exim 4.86) + (envelope-from ) + id 1aOEtO-003tI4-Kv + for test_receiver@example.com; Wed, 27 Jan 2016 09:27:30 +0800 +Return-Path: +From: "Microsoft Outlook" +To: +Subject: Microsoft Outlook Test Message +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_0001_01D16349.99C25FB0" +X-Mailer: Microsoft Outlook 14.0 +Thread-Index: AdFYoeN8x8wUI/+QSoCJkp33NKPVmw== + +This is a multipart message in MIME format. + +------=_NextPart_000_0001_01D16349.99C25FB0 +Content-Type: text/plain; + charset="utf-8" +Content-Transfer-Encoding: quoted-printable + +This is an e-mail message sent automatically by Microsoft Outlook while = +testing the settings for your account.=20 + +------=_NextPart_000_0001_01D16349.99C25FB0 +Content-Type: text/html; + charset="utf-8" +Content-Transfer-Encoding: quoted-printable + + + +This is an e-mail message sent automatically by Microsoft Outlook while = +testing the settings for your account. + +------=_NextPart_000_0001_01D16349.99C25FB0-- + +------=_NextPart_000_0005_01D16349.99C37120-- diff --git a/influxframework/email/doctype/email_account/test_mails/incoming-self-sent.raw b/influxframework/email/doctype/email_account/test_mails/incoming-self-sent.raw new file mode 100644 index 0000000..bbdb2b0 --- /dev/null +++ b/influxframework/email/doctype/email_account/test_mails/incoming-self-sent.raw @@ -0,0 +1,91 @@ +Delivered-To: test_receiver@example.com +Received: by 10.96.153.227 with SMTP id vj3csp416144qdb; + Mon, 15 Sep 2014 03:35:07 -0700 (PDT) +X-Received: by 10.66.119.103 with SMTP id kt7mr36981968pab.95.1410777306321; + Mon, 15 Sep 2014 03:35:06 -0700 (PDT) +Return-Path: +Received: from mail-pa0-x230.google.com (mail-pa0-x230.google.com [2607:f8b0:400e:c03::230]) + by mx.google.com with ESMTPS id dg10si22178346pdb.115.2014.09.15.03.35.06 + for + (version=TLSv1 cipher=ECDHE-RSA-RC4-SHA bits=128/128); + Mon, 15 Sep 2014 03:35:06 -0700 (PDT) +Received-SPF: pass (google.com: domain of test@example.com designates 2607:f8b0:400e:c03::230 as permitted sender) client-ip=2607:f8b0:400e:c03::230; +Authentication-Results: mx.google.com; + spf=pass (google.com: domain of test@example.com designates 2607:f8b0:400e:c03::230 as permitted sender) smtp.mail=test@example.com; + dkim=pass header.i=@gmail.com; + dmarc=pass (p=NONE dis=NONE) header.from=gmail.com +Received: by mail-pa0-f48.google.com with SMTP id hz1so6118714pad.21 + for ; Mon, 15 Sep 2014 03:35:06 -0700 (PDT) +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=gmail.com; s=20120113; + h=from:content-type:subject:message-id:date:to:mime-version; + bh=rwiLijtF3lfy9M6cP/7dv2Hm7NJuBwFZn1OFsN8Tlvs=; + b=x7U4Ny3Kz2ULRJ7a04NDBrBTVhP2ImIB9n3LVNGQDnDonPUM5Ro/wZcxPTVnBWZ2L1 + o1bGfP+lhBrvYUlHsd5r4FYC0Uvpad6hbzLr0DGUQgPTxW4cGKbtDEAq+BR2JWd9f803 + vdjSWdGk8w2dt2qbngTqIZkm5U2XWjICDOAYuPIseLUgCFwi9lLyOSARFB7mjAa2YL7Q + Nswk7mbWU1hbnHP6jaBb0m8QanTc7Up944HpNDRxIrB1ZHgKzYhXtx8nhnOx588ZGIAe + E6tyG8IwogR11vLkkrBhtMaOme9PohYx4F1CSTiwspmDCadEzJFGRe//lEXKmZHAYH6g + 90Zg== +X-Received: by 10.70.38.135 with SMTP id g7mr22078275pdk.100.1410777305744; + Mon, 15 Sep 2014 03:35:05 -0700 (PDT) +Return-Path: +Received: from [192.168.0.100] ([27.106.4.70]) + by mx.google.com with ESMTPSA id zr6sm11025126pbc.50.2014.09.15.03.35.02 + for + (version=TLSv1 cipher=ECDHE-RSA-RC4-SHA bits=128/128); + Mon, 15 Sep 2014 03:35:04 -0700 (PDT) +From: Rushabh Mehta +Content-Type: multipart/alternative; boundary="Apple-Mail=_57F71261-5C3A-43F6-918B-4438B96F61AA" +Subject: test mail 🦄🌈😎 +Message-Id: <9143999C-8456-4399-9CF1-4A2DA9DD7711@gmail.com> +Date: Mon, 15 Sep 2014 16:04:57 +0530 +To: Rushabh Mehta +Mime-Version: 1.0 (Mac OS X Mail 7.3 \(1878.6\)) +X-Mailer: Apple Mail (2.1878.6) + + +--Apple-Mail=_57F71261-5C3A-43F6-918B-4438B96F61AA +Content-Transfer-Encoding: 7bit +Content-Type: text/plain; + charset=us-ascii + +test mail + + + +@rushabh_mehta +https://influxerp.org + + +--Apple-Mail=_57F71261-5C3A-43F6-918B-4438B96F61AA +Content-Transfer-Encoding: quoted-printable +Content-Type: text/html; + charset=us-ascii + +test = +mail
      +



      @rushabh_mehta
      +
      +
      = + +--Apple-Mail=_57F71261-5C3A-43F6-918B-4438B96F61AA-- diff --git a/influxframework/email/doctype/email_account/test_mails/incoming-subject-placeholder.raw b/influxframework/email/doctype/email_account/test_mails/incoming-subject-placeholder.raw new file mode 100644 index 0000000..35ddf06 --- /dev/null +++ b/influxframework/email/doctype/email_account/test_mails/incoming-subject-placeholder.raw @@ -0,0 +1,183 @@ +Return-path: +Envelope-to: test_receiver@example.com +Delivery-date: Wed, 27 Jan 2016 16:24:20 +0800 +Received: from 23-59-23-10.perm.iinet.net.au ([23.59.23.10]:62191 helo=DESKTOP7C66I2M) + by webcloud85.au.syrahost.com with esmtp (Exim 4.86) + (envelope-from ) + id 1aOLOj-002xFL-CP + for test_receiver@example.com; Wed, 27 Jan 2016 16:24:20 +0800 +From: +To: +References: +In-Reply-To: +Subject: RE: {{ subject }} +Date: Wed, 27 Jan 2016 16:24:09 +0800 +Message-ID: <000001d158dc$1b8363a0$528a2ae0$@example.com> +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_NextPart_000_0001_01D1591F.29A7DC20" +X-Mailer: Microsoft Outlook 14.0 +Thread-Index: AQJZfZxrgcB9KnMqoZ+S4Qq9hcoSeZ3+vGiQ +Content-Language: en-au + +This is a multipart message in MIME format. + +------=_NextPart_000_0001_01D1591F.29A7DC20 +Content-Type: multipart/alternative; + boundary="----=_NextPart_001_0002_01D1591F.29A7DC20" + + +------=_NextPart_001_0002_01D1591F.29A7DC20 +Content-Type: text/plain; + charset="utf-8" +Content-Transfer-Encoding: quoted-printable + +Test purely for testing with the debugger has email attached + +=20 + +From: Notification [mailto:test_receiver@example.com]=20 +Sent: Wednesday, 27 January 2016 9:30 AM +To: test_receiver@example.com +Subject: Sales Invoice: SINV-12276 + +=20 + +test no 6 sent from bench to outlook to be replied to with messaging + + + + +------=_NextPart_001_0002_01D1591F.29A7DC20 +Content-Type: text/html; + charset="utf-8" +Content-Transfer-Encoding: quoted-printable + +hi there

      Test purely for testing with the debugger has email = +attached

       

      From:= + = +Notification [mailto:test_receiver@example.com]
      Sent: Wednesday, 27 = +January 2016 9:30 AM
      To: = +test_receiver@example.com
      Subject: Sales Invoice: = +SINV-12276

       

      test no 3 sent from bench to outlook to be replied to with = +messaging

      fizz buzz

      This email was sent to test_receiver@example.= +com and copied to SuperUser

      Leave this conversation = +

      hi

      +------=_NextPart_001_0002_01D1591F.29A7DC20-- + +------=_NextPart_000_0001_01D1591F.29A7DC20 +Content-Type: message/rfc822 +Content-Transfer-Encoding: 7bit +Content-Disposition: attachment + +Received: from 203-59-223-10.perm.iinet.net.au ([23.59.23.10]:49772 helo=DESKTOP7C66I2M) + by webcloud85.au.syrahost.com with esmtpsa (TLSv1.2:DHE-RSA-AES256-GCM-SHA384:256) + (Exim 4.86) + (envelope-from ) + id 1aOEtO-003tI4-Kv + for test_receiver@example.com; Wed, 27 Jan 2016 09:27:30 +0800 +Return-Path: +From: "Microsoft Outlook" +To: +Subject: Microsoft Outlook Test Message +MIME-Version: 1.0 +Content-Type: text/plain; + charset="utf-8" +Content-Transfer-Encoding: quoted-printable +X-Mailer: Microsoft Outlook 14.0 +Thread-Index: AdFYoeN8x8wUI/+QSoCJkp33NKPVmw== + +This is an e-mail message sent automatically by Microsoft Outlook while = +testing the settings for your account. diff --git a/influxframework/email/doctype/email_account/test_mails/reply-1.raw b/influxframework/email/doctype/email_account/test_mails/reply-1.raw new file mode 100644 index 0000000..3a07e7e --- /dev/null +++ b/influxframework/email/doctype/email_account/test_mails/reply-1.raw @@ -0,0 +1,47 @@ +Return-Path: +Received: from [192.168.0.100] ([27.106.4.70]) + by mx.google.com with ESMTPSA id n15sm4041161pdj.34.2014.09.15.23.48.02 + for + (version=TLSv1 cipher=ECDHE-RSA-RC4-SHA bits=128/128); + Mon, 15 Sep 2014 23:48:04 -0700 (PDT) +From: Rushabh Mehta +X-Google-Original-From: Rushabh Mehta +Content-Type: multipart/alternative; boundary="Apple-Mail=_C996D08F-7A29-4DA2-99B3-17133FA73040" +Message-Id: <943C954B-A6CE-4E8B-BA0B-1714309AB8BB@influxerp.com> +Mime-Version: 1.0 (Mac OS X Mail 7.3 \(1878.6\)) +Subject: Re: test +Date: Tue, 16 Sep 2014 12:17:58 +0530 +References: <54A4EFFA-AD17-456A-9851-9715574DF0C9@gmail.com> +To: Rushabh Mehta +In-Reply-To: <-- in-reply-to --> +X-Mailer: Apple Mail (2.1878.6) + + +--Apple-Mail=_C996D08F-7A29-4DA2-99B3-17133FA73040 +Content-Transfer-Encoding: 7bit +Content-Type: text/plain; + charset=us-ascii + +test-reply + + +On 16-Sep-2014, at 11:57 am, Rushabh Mehta wrote: + +> with attachment +> +> +> +> + + +--Apple-Mail=_C996D08F-7A29-4DA2-99B3-17133FA73040 +Content-Transfer-Encoding: 7bit +Content-Type: text/html; + charset=us-ascii + +test-reply
      +

      On 16-Sep-2014, at 11:57 am, Rushabh Mehta <test_sender@example.com> wrote:

      with attachment

      +
      <influxerp-conf-14.png>

      +
      +

      +--Apple-Mail=_C996D08F-7A29-4DA2-99B3-17133FA73040-- diff --git a/influxframework/email/doctype/email_account/test_mails/reply-2.raw b/influxframework/email/doctype/email_account/test_mails/reply-2.raw new file mode 100644 index 0000000..9c1554c --- /dev/null +++ b/influxframework/email/doctype/email_account/test_mails/reply-2.raw @@ -0,0 +1,45 @@ +Return-Path: +Received: from [192.168.0.100] ([27.106.4.70]) + by mx.google.com with ESMTPSA id n15sm4041161pdj.34.2014.09.15.23.48.02 + for + (version=TLSv1 cipher=ECDHE-RSA-RC4-SHA bits=128/128); + Mon, 15 Sep 2014 23:48:04 -0700 (PDT) +From: Rushabh Mehta +X-Google-Original-From: Rushabh Mehta +Content-Type: multipart/alternative; boundary="Apple-Mail=_C996D08F-7A29-4DA2-99B3-17133FA73040" +Mime-Version: 1.0 (Mac OS X Mail 7.3 \(1878.6\)) +Subject: weird subject ddwdf23r2 +Date: Tue, 16 Sep 2014 12:17:58 +0530 +References: <54A4EFFA-AD17-456A-9851-9715574DF0C9@gmail.com> +To: Rushabh Mehta +X-Mailer: Apple Mail (2.1878.6) + + +--Apple-Mail=_C996D08F-7A29-4DA2-99B3-17133FA73040 +Content-Transfer-Encoding: 7bit +Content-Type: text/plain; + charset=us-ascii + +test-reply-2 + + +On 16-Sep-2014, at 11:57 am, Rushabh Mehta wrote: + +> with attachment +> +> +> +> + + +--Apple-Mail=_C996D08F-7A29-4DA2-99B3-17133FA73040 +Content-Transfer-Encoding: 7bit +Content-Type: text/html; + charset=us-ascii + +test-reply
      +

      On 16-Sep-2014, at 11:57 am, Rushabh Mehta <test_sender@example.com> wrote:

      with attachment

      +
      <influxerp-conf-14.png>

      +
      +

      +--Apple-Mail=_C996D08F-7A29-4DA2-99B3-17133FA73040-- diff --git a/influxframework/email/doctype/email_account/test_mails/reply-3.raw b/influxframework/email/doctype/email_account/test_mails/reply-3.raw new file mode 100644 index 0000000..908af7a --- /dev/null +++ b/influxframework/email/doctype/email_account/test_mails/reply-3.raw @@ -0,0 +1,45 @@ +Return-Path: +Received: from [192.168.0.100] ([27.106.4.70]) + by mx.google.com with ESMTPSA id n15sm4041161pdj.34.2014.09.15.23.48.02 + for + (version=TLSv1 cipher=ECDHE-RSA-RC4-SHA bits=128/128); + Mon, 15 Sep 2014 23:48:04 -0700 (PDT) +From: Rushabh Mehta +X-Google-Original-From: Rushabh Mehta +Content-Type: multipart/alternative; boundary="Apple-Mail=_C996D08F-7A29-4DA2-99B3-17133FA73040" +Mime-Version: 1.0 (Mac OS X Mail 7.3 \(1878.6\)) +Subject: Re: weird subject ddwdf23r2 +Date: Tue, 16 Sep 2014 12:17:58 +0530 +References: <54A4EFFA-AD17-456A-9851-9715574DF0C9@gmail.com> +To: Rushabh Mehta +X-Mailer: Apple Mail (2.1878.6) + + +--Apple-Mail=_C996D08F-7A29-4DA2-99B3-17133FA73040 +Content-Transfer-Encoding: 7bit +Content-Type: text/plain; + charset=us-ascii + +test-reply-3 + + +On 16-Sep-2014, at 11:57 am, Rushabh Mehta wrote: + +> with attachment +> +> +> +> + + +--Apple-Mail=_C996D08F-7A29-4DA2-99B3-17133FA73040 +Content-Transfer-Encoding: 7bit +Content-Type: text/html; + charset=us-ascii + +test-reply
      +

      On 16-Sep-2014, at 11:57 am, Rushabh Mehta <test_sender@example.com> wrote:

      with attachment

      +
      <influxerp-conf-14.png>

      +
      +

      +--Apple-Mail=_C996D08F-7A29-4DA2-99B3-17133FA73040-- diff --git a/influxframework/email/doctype/email_account/test_mails/reply-4.raw b/influxframework/email/doctype/email_account/test_mails/reply-4.raw new file mode 100644 index 0000000..5a26279 --- /dev/null +++ b/influxframework/email/doctype/email_account/test_mails/reply-4.raw @@ -0,0 +1,75 @@ +From: +Content-Type: multipart/alternative; + boundary="Apple-Mail=_29597CF7-20DD-4184-B3FA-85582C5C4361" +Message-Id: <07D687F6-10AA-4B9F-82DE-27753096164E@gmail.com> +Mime-Version: 1.0 (Mac OS X Mail 9.3 \(3124\)) +X-Smtp-Server: 73CC8281-7E8F-4B47-8324-D5DA86EEDD4F +Subject: Re: What did you work on today? +Date: Thu, 10 Nov 2016 16:04:43 +0530 +X-Universally-Unique-Identifier: A4D9669F-179C-42D8-A3D3-AA6A8C49A6F2 +References: {{ message_id }} +To: test_in@iwebnotes.com +In-Reply-To: {{ message_id }} + + +--Apple-Mail=_29597CF7-20DD-4184-B3FA-85582C5C4361 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; + charset=us-ascii + +Testing another reply! + +> On 10-Nov-2016, at 3:20 PM, InfluxFramework wrote: +>=20 +> Please share what did you do today. If you reply by midnight, your = +response will be recorded! +>=20 +> This email was sent to rmehta@gmail.com +> Unsubscribe from this list = + +> Sent via InfluxERP + + +--Apple-Mail=_29597CF7-20DD-4184-B3FA-85582C5C4361 +Content-Transfer-Encoding: 7bit +Content-Type: text/html; + charset=us-ascii + +Testing another reply!

      On 10-Nov-2016, at 3:20 PM, InfluxFramework <test@influxerp.com> wrote:

      + + + + +What did you work on today? + +
      + +

      Please share what did you do today. If you reply by midnight, your response will be recorded!

      + +
      + + + + + + +
      +

      +--Apple-Mail=_29597CF7-20DD-4184-B3FA-85582C5C4361-- diff --git a/influxframework/email/doctype/email_account/test_records.json b/influxframework/email/doctype/email_account/test_records.json new file mode 100644 index 0000000..2e204e5 --- /dev/null +++ b/influxframework/email/doctype/email_account/test_records.json @@ -0,0 +1,29 @@ +[ + { + "is_default": 1, + "is_global": 1, + "doctype": "Email Account", + "domain":"example.com", + "email_account_name": "_Test Email Account 1", + "enable_outgoing": 1, + "smtp_server": "test.example.com", + "email_id": "test@example.com", + "password": "password", + "add_signature": 1, + "signature": "\nBest Wishes\nTest Signature", + "enable_auto_reply": 1, + "auto_reply_message": "", + "enable_incoming": 1, + "notify_if_unreplied": 1, + "unreplied_for_mins": 20, + "send_notification_to": "test_unreplied@example.com", + "pop3_server": "pop.test.example.com", + "append_to": "ToDo", + "imap_folder": [{"folder_name": "INBOX", "append_to": "ToDo"}, {"folder_name": "Test Folder", "append_to": "Communication"}], + "track_email_status": 1 + }, + { + "doctype": "ToDo", + "description":"test doctype" + } +] diff --git a/influxframework/email/doctype/email_domain/__init__.py b/influxframework/email/doctype/email_domain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/email/doctype/email_domain/email_domain.js b/influxframework/email/doctype/email_domain/email_domain.js new file mode 100644 index 0000000..7c7348d --- /dev/null +++ b/influxframework/email/doctype/email_domain/email_domain.js @@ -0,0 +1,22 @@ +influxframework.ui.form.on("Email Domain", { + onload: function (frm) { + if (!frm.doc.__islocal) { + frm.dashboard.clear_headline(); + let msg = __( + "Changing any setting will reflect on all the email accounts associated with this domain." + ); + frm.dashboard.set_headline_alert(msg); + } else { + if (!frm.doc.attachment_limit) { + influxframework.call({ + method: "influxframework.core.api.file.get_max_file_size", + callback: function (r) { + if (!r.exc) { + frm.set_value("attachment_limit", Number(r.message) / (1024 * 1024)); + } + }, + }); + } + } + }, +}); diff --git a/influxframework/email/doctype/email_domain/email_domain.json b/influxframework/email/doctype/email_domain/email_domain.json new file mode 100644 index 0000000..18635e6 --- /dev/null +++ b/influxframework/email/doctype/email_domain/email_domain.json @@ -0,0 +1,157 @@ +{ + "actions": [], + "autoname": "field:domain_name", + "creation": "2016-03-29 10:50:48.848239", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "email_settings", + "domain_name", + "mailbox_settings", + "email_server", + "use_imap", + "use_ssl", + "use_starttls", + "column_break_9", + "incoming_port", + "attachment_limit", + "outgoing_mail_settings", + "smtp_server", + "use_tls", + "use_ssl_for_outgoing", + "column_break_18", + "smtp_port", + "append_emails_to_sent_folder" + ], + "fields": [ + { + "fieldname": "email_settings", + "fieldtype": "Section Break" + }, + { + "fieldname": "domain_name", + "fieldtype": "Data", + "label": "Domain Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "mailbox_settings", + "fieldtype": "Section Break", + "label": "Incoming Settings" + }, + { + "description": "e.g. pop.gmail.com / imap.gmail.com", + "fieldname": "email_server", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Incoming Server", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "use_imap", + "fieldtype": "Check", + "label": "Use IMAP" + }, + { + "default": "0", + "fieldname": "use_ssl", + "fieldtype": "Check", + "label": "Use SSL" + }, + { + "default": "0", + "depends_on": "eval:doc.use_imap && !doc.use_ssl", + "fieldname": "use_starttls", + "fieldtype": "Check", + "label": "Use STARTTLS" + }, + { + "description": "Ignore attachments over this size", + "fieldname": "attachment_limit", + "fieldtype": "Int", + "label": "Attachment Limit (MB)" + }, + { + "fieldname": "outgoing_mail_settings", + "fieldtype": "Section Break", + "label": "Outgoing Settings" + }, + { + "description": "e.g. smtp.gmail.com", + "fieldname": "smtp_server", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Outgoing Server", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "use_tls", + "fieldtype": "Check", + "label": "Use TLS" + }, + { + "description": "If non standard port (e.g. 587)", + "fieldname": "smtp_port", + "fieldtype": "Data", + "label": "Port" + }, + { + "description": "If non-standard port (e.g. POP3: 995/110, IMAP: 993/143)", + "fieldname": "incoming_port", + "fieldtype": "Data", + "label": "Port" + }, + { + "default": "0", + "fieldname": "append_emails_to_sent_folder", + "fieldtype": "Check", + "label": "Append Emails to Sent Folder" + }, + { + "default": "0", + "fieldname": "use_ssl_for_outgoing", + "fieldtype": "Check", + "label": "Use SSL" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + } + ], + "icon": "icon-inbox", + "links": [ + { + "link_doctype": "Email Account", + "link_fieldname": "domain" + } + ], + "modified": "2022-08-19 12:55:06.434541", + "modified_by": "Administrator", + "module": "Email", + "name": "Email Domain", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "read": 1, + "role": "System Manager", + "set_user_permissions": 1, + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/email/doctype/email_domain/email_domain.py b/influxframework/email/doctype/email_domain/email_domain.py new file mode 100644 index 0000000..56f30fa --- /dev/null +++ b/influxframework/email/doctype/email_domain/email_domain.py @@ -0,0 +1,101 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import smtplib +from functools import wraps + +import influxframework +from influxframework import _ +from influxframework.email.receive import Timed_IMAP4, Timed_IMAP4_SSL, Timed_POP3, Timed_POP3_SSL +from influxframework.email.utils import get_port +from influxframework.model.document import Document +from influxframework.utils import cint + +EMAIL_DOMAIN_FIELDS = [ + "email_server", + "use_imap", + "use_ssl", + "use_starttls", + "use_tls", + "attachment_limit", + "smtp_server", + "smtp_port", + "use_ssl_for_outgoing", + "append_emails_to_sent_folder", + "incoming_port", +] + + +def get_error_message(event): + return { + "incoming": (_("Incoming email account not correct"), _("Error connecting via IMAP/POP3: {e}")), + "outgoing": (_("Outgoing email account not correct"), _("Error connecting via SMTP: {e}")), + }[event] + + +def handle_error(event): + def decorator(fn): + @wraps(fn) + def wrapper(*args, **kwargs): + err_title, err_message = get_error_message(event) + try: + fn(*args, **kwargs) + except Exception as e: + influxframework.throw( + title=err_title, + msg=err_message.format(e=e), + ) + + return wrapper + + return decorator + + +class EmailDomain(Document): + def validate(self): + """Validate POP3/IMAP and SMTP connections.""" + + if influxframework.local.flags.in_patch or influxframework.local.flags.in_test or influxframework.local.flags.in_install: + return + + self.validate_incoming_server_conn() + self.validate_outgoing_server_conn() + + def on_update(self): + """update all email accounts using this domain""" + for email_account in influxframework.get_all("Email Account", filters={"domain": self.name}): + try: + email_account = influxframework.get_doc("Email Account", email_account.name) + for attr in EMAIL_DOMAIN_FIELDS: + email_account.set(attr, self.get(attr, default=0)) + email_account.save() + + except Exception as e: + influxframework.msgprint( + _("Error has occurred in {0}").format(email_account.name), raise_exception=e.__class__ + ) + + @handle_error("incoming") + def validate_incoming_server_conn(self): + self.incoming_port = get_port(self) + + if self.use_imap: + conn_method = Timed_IMAP4_SSL if self.use_ssl else Timed_IMAP4 + else: + conn_method = Timed_POP3_SSL if self.use_ssl else Timed_POP3 + + self.use_starttls = cint(self.use_imap and self.use_starttls and not self.use_ssl) + incoming_conn = conn_method(self.email_server, port=self.incoming_port) + incoming_conn.logout() if self.use_imap else incoming_conn.quit() + + @handle_error("outgoing") + def validate_outgoing_server_conn(self): + conn_method = smtplib.SMTP + + if self.use_ssl_for_outgoing: + self.smtp_port = self.smtp_port or 465 + conn_method = smtplib.SMTP_SSL + elif self.use_tls: + self.smtp_port = self.smtp_port or 587 + + conn_method((self.smtp_server or ""), cint(self.smtp_port) or 0).quit() diff --git a/influxframework/email/doctype/email_domain/test_email_domain.py b/influxframework/email/doctype/email_domain/test_email_domain.py new file mode 100644 index 0000000..4b61b9b --- /dev/null +++ b/influxframework/email/doctype/email_domain/test_email_domain.py @@ -0,0 +1,39 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.test_runner import make_test_objects +from influxframework.tests.utils import InfluxFrameworkTestCase + +test_records = influxframework.get_test_records("Email Domain") + + +class TestDomain(InfluxFrameworkTestCase): + def setUp(self): + make_test_objects("Email Domain", reset=True) + + def tearDown(self): + influxframework.delete_doc("Email Account", "Test") + influxframework.delete_doc("Email Domain", "test.com") + + def test_on_update(self): + mail_domain = influxframework.get_doc("Email Domain", "test.com") + mail_account = influxframework.get_doc("Email Account", "Test") + + # Ensure a different port + mail_account.incoming_port = int(mail_domain.incoming_port) + 5 + mail_account.save() + # Trigger update of accounts using this domain + mail_domain.on_update() + + mail_account.reload() + # After update, incoming_port in account should match the domain + self.assertEqual(mail_account.incoming_port, mail_domain.incoming_port) + + # Also make sure that the other attributes match + self.assertEqual(mail_account.use_imap, mail_domain.use_imap) + self.assertEqual(mail_account.use_ssl, mail_domain.use_ssl) + self.assertEqual(mail_account.use_starttls, mail_domain.use_starttls) + self.assertEqual(mail_account.use_tls, mail_domain.use_tls) + self.assertEqual(mail_account.attachment_limit, mail_domain.attachment_limit) + self.assertEqual(mail_account.smtp_server, mail_domain.smtp_server) + self.assertEqual(mail_account.smtp_port, mail_domain.smtp_port) diff --git a/influxframework/email/doctype/email_domain/test_records.json b/influxframework/email/doctype/email_domain/test_records.json new file mode 100644 index 0000000..a6ccc99 --- /dev/null +++ b/influxframework/email/doctype/email_domain/test_records.json @@ -0,0 +1,32 @@ +[ + { + "doctype": "Email Domain", + "domain_name": "test.com", + "email_id": "_test@test.com", + "email_server": "imap.test.com", + "use_imap": "imap.test.com", + "use_ssl": 1, + "use_tls": 1, + "incoming_port": "993", + "attachment_limit": "1", + "smtp_server": "smtp.test.com", + "smtp_port": "587", + "password": "password" + }, + { + "doctype": "Email Account", + "name": "_Test Email Account 1", + "enable_incoming": 1, + "email_id": "_test@test.com", + "domain": "test.com", + "email_server": "imap.test.com", + "use_imap": 1, + "use_ssl": 0, + "use_tls": 1, + "incoming_port": "143", + "attachment_limit": "1", + "smtp_server": "smtp.test.com", + "smtp_port": "587", + "password": "password" + } +] diff --git a/influxframework/email/doctype/email_flag_queue/__init__.py b/influxframework/email/doctype/email_flag_queue/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/email/doctype/email_flag_queue/email_flag_queue.js b/influxframework/email/doctype/email_flag_queue/email_flag_queue.js new file mode 100644 index 0000000..7f92710 --- /dev/null +++ b/influxframework/email/doctype/email_flag_queue/email_flag_queue.js @@ -0,0 +1,6 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Email Flag Queue", { + refresh: function (frm) {}, +}); diff --git a/influxframework/email/doctype/email_flag_queue/email_flag_queue.json b/influxframework/email/doctype/email_flag_queue/email_flag_queue.json new file mode 100644 index 0000000..14b1ec4 --- /dev/null +++ b/influxframework/email/doctype/email_flag_queue/email_flag_queue.json @@ -0,0 +1,67 @@ +{ + "actions": [], + "allow_copy": 1, + "creation": "2016-04-20 15:29:39.785172", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "is_completed", + "communication", + "action", + "email_account", + "uid" + ], + "fields": [ + { + "default": "0", + "fieldname": "is_completed", + "fieldtype": "Check", + "label": "Is Completed", + "read_only": 1 + }, + { + "fieldname": "communication", + "fieldtype": "Data", + "label": "Communication" + }, + { + "fieldname": "action", + "fieldtype": "Select", + "label": "Action", + "options": "Read\nUnread" + }, + { + "fieldname": "email_account", + "fieldtype": "Data", + "hidden": 1, + "label": "Email Account" + }, + { + "fieldname": "uid", + "fieldtype": "Data", + "hidden": 1, + "label": "UID" + } + ], + "in_create": 1, + "links": [], + "modified": "2021-11-30 09:51:34.489932", + "modified_by": "Administrator", + "module": "Email", + "name": "Email Flag Queue", + "owner": "Administrator", + "permissions": [ + { + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/influxframework/email/doctype/email_flag_queue/email_flag_queue.py b/influxframework/email/doctype/email_flag_queue/email_flag_queue.py new file mode 100644 index 0000000..ce88ab2 --- /dev/null +++ b/influxframework/email/doctype/email_flag_queue/email_flag_queue.py @@ -0,0 +1,8 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +from influxframework.model.document import Document + + +class EmailFlagQueue(Document): + pass diff --git a/influxframework/email/doctype/email_flag_queue/test_email_flag_queue.py b/influxframework/email/doctype/email_flag_queue/test_email_flag_queue.py new file mode 100644 index 0000000..87f26f7 --- /dev/null +++ b/influxframework/email/doctype/email_flag_queue/test_email_flag_queue.py @@ -0,0 +1,9 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + +# test_records = influxframework.get_test_records('Email Flag Queue') + + +class TestEmailFlagQueue(InfluxFrameworkTestCase): + pass diff --git a/influxframework/email/doctype/email_group/__init__.py b/influxframework/email/doctype/email_group/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/email/doctype/email_group/email_group.js b/influxframework/email/doctype/email_group/email_group.js new file mode 100644 index 0000000..1d1b867 --- /dev/null +++ b/influxframework/email/doctype/email_group/email_group.js @@ -0,0 +1,74 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Email Group", "refresh", function (frm) { + if (!frm.is_new()) { + frm.add_custom_button( + __("Import Subscribers"), + function () { + influxframework.prompt( + { + fieldtype: "Select", + options: frm.doc.__onload.import_types, + label: __("Import Email From"), + fieldname: "doctype", + reqd: 1, + }, + function (data) { + influxframework.call({ + method: "influxframework.email.doctype.email_group.email_group.import_from", + args: { + name: frm.doc.name, + doctype: data.doctype, + }, + callback: function (r) { + frm.set_value("total_subscribers", r.message); + }, + }); + }, + __("Import Subscribers"), + __("Import") + ); + }, + __("Action") + ); + + frm.add_custom_button( + __("Add Subscribers"), + function () { + influxframework.prompt( + { + fieldtype: "Text", + label: __("Email Addresses"), + fieldname: "email_list", + reqd: 1, + }, + function (data) { + influxframework.call({ + method: "influxframework.email.doctype.email_group.email_group.add_subscribers", + args: { + name: frm.doc.name, + email_list: data.email_list, + }, + callback: function (r) { + frm.set_value("total_subscribers", r.message); + }, + }); + }, + __("Add Subscribers"), + __("Add") + ); + }, + __("Action") + ); + + frm.add_custom_button( + __("New Newsletter"), + function () { + influxframework.route_options = { email_group: frm.doc.name }; + influxframework.new_doc("Newsletter"); + }, + __("Action") + ); + } +}); diff --git a/influxframework/email/doctype/email_group/email_group.json b/influxframework/email/doctype/email_group/email_group.json new file mode 100644 index 0000000..cb74249 --- /dev/null +++ b/influxframework/email/doctype/email_group/email_group.json @@ -0,0 +1,79 @@ +{ + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:title", + "creation": "2015-03-18 06:08:32.729800", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "title", + "total_subscribers", + "confirmation_email_template", + "welcome_email_template" + ], + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "no_copy": 1, + "reqd": 1, + "unique": 1 + }, + { + "default": "0", + "fieldname": "total_subscribers", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Total Subscribers", + "read_only": 1 + }, + { + "fieldname": "confirmation_email_template", + "fieldtype": "Link", + "label": "Confirmation Email Template", + "options": "Email Template" + }, + { + "fieldname": "welcome_email_template", + "fieldtype": "Link", + "label": "Welcome Email Template", + "options": "Email Template" + } + ], + "index_web_pages_for_search": 1, + "links": [ + { + "group": "Members", + "link_doctype": "Email Group Member", + "link_fieldname": "email_group" + } + ], + "modified": "2021-06-15 11:25:13.556201", + "modified_by": "Administrator", + "module": "Email", + "name": "Email Group", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Newsletter Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/email/doctype/email_group/email_group.py b/influxframework/email/doctype/email_group/email_group.py new file mode 100644 index 0000000..9d83f8b --- /dev/null +++ b/influxframework/email/doctype/email_group/email_group.py @@ -0,0 +1,115 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework import _ +from influxframework.model.document import Document +from influxframework.utils import parse_addr, validate_email_address + + +class EmailGroup(Document): + def onload(self): + singles = [d.name for d in influxframework.get_all("DocType", "name", {"issingle": 1})] + self.get("__onload").import_types = [ + {"value": d.parent, "label": f"{d.parent} ({d.label})"} + for d in influxframework.get_all("DocField", ("parent", "label"), {"options": "Email"}) + if d.parent not in singles + ] + + def import_from(self, doctype): + """Extract Email Addresses from given doctype and add them to the current list""" + meta = influxframework.get_meta(doctype) + email_field = [ + d.fieldname + for d in meta.fields + if d.fieldtype in ("Data", "Small Text", "Text", "Code") and d.options == "Email" + ][0] + unsubscribed_field = "unsubscribed" if meta.get_field("unsubscribed") else None + added = 0 + + for user in influxframework.get_all(doctype, [email_field, unsubscribed_field or "name"]): + try: + email = parse_addr(user.get(email_field))[1] if user.get(email_field) else None + if email: + influxframework.get_doc( + { + "doctype": "Email Group Member", + "email_group": self.name, + "email": email, + "unsubscribed": user.get(unsubscribed_field) if unsubscribed_field else 0, + } + ).insert(ignore_permissions=True) + + added += 1 + except influxframework.UniqueValidationError: + pass + + influxframework.msgprint(_("{0} subscribers added").format(added)) + + return self.update_total_subscribers() + + def update_total_subscribers(self): + self.total_subscribers = self.get_total_subscribers() + self.db_update() + return self.total_subscribers + + def get_total_subscribers(self): + return influxframework.db.sql( + """select count(*) from `tabEmail Group Member` + where email_group=%s""", + self.name, + )[0][0] + + def on_trash(self): + for d in influxframework.get_all("Email Group Member", "name", {"email_group": self.name}): + influxframework.delete_doc("Email Group Member", d.name) + + +@influxframework.whitelist() +def import_from(name, doctype): + nlist = influxframework.get_doc("Email Group", name) + if nlist.has_permission("write"): + return nlist.import_from(doctype) + + +@influxframework.whitelist() +def add_subscribers(name, email_list): + if not isinstance(email_list, (list, tuple)): + email_list = email_list.replace(",", "\n").split("\n") + + template = influxframework.db.get_value("Email Group", name, "welcome_email_template") + welcome_email = influxframework.get_doc("Email Template", template) if template else None + + count = 0 + for email in email_list: + email = email.strip() + parsed_email = validate_email_address(email, False) + + if parsed_email: + if not influxframework.db.get_value("Email Group Member", {"email_group": name, "email": parsed_email}): + influxframework.get_doc( + {"doctype": "Email Group Member", "email_group": name, "email": parsed_email} + ).insert(ignore_permissions=influxframework.flags.ignore_permissions) + + send_welcome_email(welcome_email, parsed_email, name) + + count += 1 + else: + pass + else: + influxframework.msgprint(_("{0} is not a valid Email Address").format(email)) + + influxframework.msgprint(_("{0} subscribers added").format(count)) + + return influxframework.get_doc("Email Group", name).update_total_subscribers() + + +def send_welcome_email(welcome_email, email, email_group): + """Send welcome email for the subscribers of a given email group.""" + if not welcome_email: + return + + args = dict(email=email, email_group=email_group) + email_message = welcome_email.response or welcome_email.response_html + message = influxframework.render_template(email_message, args) + influxframework.sendmail(email, subject=welcome_email.subject, message=message) diff --git a/influxframework/email/doctype/email_group/test_email_group.py b/influxframework/email/doctype/email_group/test_email_group.py new file mode 100644 index 0000000..60decb6 --- /dev/null +++ b/influxframework/email/doctype/email_group/test_email_group.py @@ -0,0 +1,9 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + +# test_records = influxframework.get_test_records('Email Group') + + +class TestEmailGroup(InfluxFrameworkTestCase): + pass diff --git a/influxframework/email/doctype/email_group/test_records.json b/influxframework/email/doctype/email_group/test_records.json new file mode 100644 index 0000000..a55b117 --- /dev/null +++ b/influxframework/email/doctype/email_group/test_records.json @@ -0,0 +1,6 @@ +[ + { + "doctype": "Email Group", + "title": "_Test Email Group" + } +] diff --git a/influxframework/email/doctype/email_group_member/__init__.py b/influxframework/email/doctype/email_group_member/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/email/doctype/email_group_member/email_group_member.js b/influxframework/email/doctype/email_group_member/email_group_member.js new file mode 100644 index 0000000..1523c9a --- /dev/null +++ b/influxframework/email/doctype/email_group_member/email_group_member.js @@ -0,0 +1,6 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Email Group Member", { + refresh: function (frm) {}, +}); diff --git a/influxframework/email/doctype/email_group_member/email_group_member.json b/influxframework/email/doctype/email_group_member/email_group_member.json new file mode 100644 index 0000000..0e32135 --- /dev/null +++ b/influxframework/email/doctype/email_group_member/email_group_member.json @@ -0,0 +1,70 @@ +{ + "actions": [], + "allow_import": 1, + "autoname": "hash", + "creation": "2015-03-18 06:15:59.321619", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "email_group", + "email", + "unsubscribed" + ], + "fields": [ + { + "fieldname": "email_group", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Email Group", + "options": "Email Group", + "reqd": 1, + "search_index": 1 + }, + { + "fieldname": "email", + "fieldtype": "Data", + "in_global_search": 1, + "in_list_view": 1, + "label": "Email", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "unsubscribed", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Unsubscribed", + "search_index": 1 + } + ], + "links": [], + "modified": "2022-07-11 16:38:34.165271", + "modified_by": "Administrator", + "module": "Email", + "name": "Email Group Member", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Newsletter Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "email", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/email/doctype/email_group_member/email_group_member.py b/influxframework/email/doctype/email_group_member/email_group_member.py new file mode 100644 index 0000000..8c1979b --- /dev/null +++ b/influxframework/email/doctype/email_group_member/email_group_member.py @@ -0,0 +1,19 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document + + +class EmailGroupMember(Document): + def after_delete(self): + email_group = influxframework.get_doc("Email Group", self.email_group) + email_group.update_total_subscribers() + + def after_insert(self): + email_group = influxframework.get_doc("Email Group", self.email_group) + email_group.update_total_subscribers() + + +def after_doctype_insert(): + influxframework.db.add_unique("Email Group Member", ("email_group", "email")) diff --git a/influxframework/email/doctype/email_group_member/test_email_group_member.py b/influxframework/email/doctype/email_group_member/test_email_group_member.py new file mode 100644 index 0000000..8d72dab --- /dev/null +++ b/influxframework/email/doctype/email_group_member/test_email_group_member.py @@ -0,0 +1,9 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + +# test_records = influxframework.get_test_records('Email Group Member') + + +class TestEmailGroupMember(InfluxFrameworkTestCase): + pass diff --git a/influxframework/email/doctype/email_queue/__init__.py b/influxframework/email/doctype/email_queue/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/email/doctype/email_queue/email_queue.js b/influxframework/email/doctype/email_queue/email_queue.js new file mode 100644 index 0000000..e1c1156 --- /dev/null +++ b/influxframework/email/doctype/email_queue/email_queue.js @@ -0,0 +1,38 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Email Queue", { + refresh: function (frm) { + if (["Not Sent", "Partially Sent"].indexOf(frm.doc.status) != -1) { + let button = frm.add_custom_button("Send Now", function () { + influxframework.call({ + method: "influxframework.email.doctype.email_queue.email_queue.send_now", + args: { + name: frm.doc.name, + }, + btn: button, + callback: function () { + frm.reload_doc(); + }, + }); + }); + } + + if (["Error", "Partially Errored"].indexOf(frm.doc.status) != -1) { + let button = frm.add_custom_button("Retry Sending", function () { + frm.call({ + method: "retry_sending", + args: { + name: frm.doc.name, + }, + btn: button, + callback: function (r) { + if (!r.exc) { + frm.set_value("status", "Not Sent"); + } + }, + }); + }); + } + }, +}); diff --git a/influxframework/email/doctype/email_queue/email_queue.json b/influxframework/email/doctype/email_queue/email_queue.json new file mode 100644 index 0000000..c9ec374 --- /dev/null +++ b/influxframework/email/doctype/email_queue/email_queue.json @@ -0,0 +1,176 @@ +{ + "actions": [], + "autoname": "hash", + "creation": "2012-08-02 15:17:28", + "description": "Email Queue records.", + "doctype": "DocType", + "document_type": "System", + "engine": "InnoDB", + "field_order": [ + "sender", + "recipients", + "show_as_cc", + "message", + "status", + "error", + "message_id", + "reference_doctype", + "reference_name", + "communication", + "send_after", + "priority", + "add_unsubscribe_link", + "unsubscribe_param", + "unsubscribe_method", + "expose_recipients", + "attachments", + "retry", + "email_account" + ], + "fields": [ + { + "fieldname": "sender", + "fieldtype": "Data", + "ignore_xss_filter": 1, + "label": "Sender", + "options": "Email" + }, + { + "fieldname": "recipients", + "fieldtype": "Table", + "label": "Recipient", + "options": "Email Queue Recipient" + }, + { + "fieldname": "show_as_cc", + "fieldtype": "Small Text", + "label": "Show as cc" + }, + { + "fieldname": "message", + "fieldtype": "Code", + "label": "Message" + }, + { + "default": "Not Sent", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "\nNot Sent\nSending\nSent\nError\nExpired" + }, + { + "fieldname": "error", + "fieldtype": "Code", + "label": "Error" + }, + { + "fieldname": "message_id", + "fieldtype": "Data", + "label": "Message ID", + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "label": "Reference Document Type", + "options": "DocType", + "read_only": 1 + }, + { + "fieldname": "reference_name", + "fieldtype": "Data", + "label": "Reference DocName", + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "communication", + "fieldtype": "Link", + "label": "Communication", + "options": "Communication", + "search_index": 1 + }, + { + "fieldname": "send_after", + "fieldtype": "Datetime", + "label": "Send After", + "no_copy": 1, + "read_only": 1 + }, + { + "default": "1", + "fieldname": "priority", + "fieldtype": "Int", + "label": "Priority", + "read_only": 1 + }, + { + "default": "1", + "fieldname": "add_unsubscribe_link", + "fieldtype": "Check", + "label": "Add Unsubscribe Link" + }, + { + "fieldname": "unsubscribe_param", + "fieldtype": "Data", + "label": "Unsubscribe Param", + "read_only": 1 + }, + { + "fieldname": "unsubscribe_method", + "fieldtype": "Data", + "label": "Unsubscribe Method" + }, + { + "fieldname": "expose_recipients", + "fieldtype": "Data", + "label": "Expose Recipients" + }, + { + "fieldname": "attachments", + "fieldtype": "Code", + "label": "Attachments", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "retry", + "fieldtype": "Int", + "label": "Retry", + "read_only": 1 + }, + { + "fieldname": "email_account", + "fieldtype": "Link", + "label": "Email Account", + "options": "Email Account" + } + ], + "icon": "fa fa-envelope", + "idx": 1, + "in_create": 1, + "links": [], + "modified": "2022-07-12 15:17:37.934316", + "modified_by": "Administrator", + "module": "Email", + "name": "Email Queue", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [ + { + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager" + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/email/doctype/email_queue/email_queue.py b/influxframework/email/doctype/email_queue/email_queue.py new file mode 100644 index 0000000..818fec6 --- /dev/null +++ b/influxframework/email/doctype/email_queue/email_queue.py @@ -0,0 +1,734 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import json +import quopri +import smtplib +import traceback +from email.parser import Parser +from email.policy import SMTPUTF8 + +from rq.timeouts import JobTimeoutException + +import influxframework +from influxframework import _, safe_encode, task +from influxframework.core.utils import html2text +from influxframework.email.doctype.email_account.email_account import EmailAccount +from influxframework.email.email_body import add_attachment, get_email, get_formatted_html +from influxframework.email.queue import get_unsubcribed_url, get_unsubscribe_message +from influxframework.email.smtp import SMTPServer +from influxframework.model.document import Document +from influxframework.query_builder import DocType, Interval +from influxframework.query_builder.functions import Now +from influxframework.utils import ( + add_days, + cint, + cstr, + get_hook_method, + get_string_between, + nowdate, + sbool, + split_emails, +) + + +class EmailQueue(Document): + DOCTYPE = "Email Queue" + + def set_recipients(self, recipients): + self.set("recipients", []) + for r in recipients: + self.append("recipients", {"recipient": r, "status": "Not Sent"}) + + def on_trash(self): + self.prevent_email_queue_delete() + + def prevent_email_queue_delete(self): + if influxframework.session.user != "Administrator": + influxframework.throw(_("Only Administrator can delete Email Queue")) + + def get_duplicate(self, recipients): + values = self.as_dict() + del values["name"] + duplicate = influxframework.get_doc(values) + duplicate.set_recipients(recipients) + return duplicate + + @classmethod + def new(cls, doc_data, ignore_permissions=False): + data = doc_data.copy() + if not data.get("recipients"): + return + + recipients = data.pop("recipients") + doc = influxframework.new_doc(cls.DOCTYPE) + doc.update(data) + doc.set_recipients(recipients) + doc.insert(ignore_permissions=ignore_permissions) + return doc + + @classmethod + def find(cls, name): + return influxframework.get_doc(cls.DOCTYPE, name) + + @classmethod + def find_one_by_filters(cls, **kwargs): + name = influxframework.db.get_value(cls.DOCTYPE, kwargs) + return cls.find(name) if name else None + + def update_db(self, commit=False, **kwargs): + influxframework.db.set_value(self.DOCTYPE, self.name, kwargs) + if commit: + influxframework.db.commit() + + def update_status(self, status, commit=False, **kwargs): + self.update_db(status=status, commit=commit, **kwargs) + if self.communication: + communication_doc = influxframework.get_doc("Communication", self.communication) + communication_doc.set_delivery_status(commit=commit) + + @property + def cc(self): + return (self.show_as_cc and self.show_as_cc.split(",")) or [] + + @property + def to(self): + return [r.recipient for r in self.recipients if r.recipient not in self.cc] + + @property + def attachments_list(self): + return json.loads(self.attachments) if self.attachments else [] + + def get_email_account(self): + if self.email_account: + return influxframework.get_cached_doc("Email Account", self.email_account) + + return EmailAccount.find_outgoing( + match_by_email=self.sender, match_by_doctype=self.reference_doctype + ) + + def is_to_be_sent(self): + return self.status in ["Not Sent", "Partially Sent"] + + def can_send_now(self): + if ( + influxframework.are_emails_muted() + or not self.is_to_be_sent() + or cint(influxframework.db.get_default("suspend_email_queue")) == 1 + ): + return False + + return True + + def send(self, is_background_task: bool = False, smtp_server_instance: SMTPServer = None): + """Send emails to recipients.""" + if not self.can_send_now(): + return + + with SendMailContext(self, is_background_task, smtp_server_instance) as ctx: + message = None + for recipient in self.recipients: + if not recipient.is_mail_to_be_sent(): + continue + + message = ctx.build_message(recipient.recipient) + method = get_hook_method("override_email_send") + if method: + method(self, self.sender, recipient.recipient, message) + else: + if not influxframework.flags.in_test: + ctx.smtp_session.sendmail(from_addr=self.sender, to_addrs=recipient.recipient, msg=message) + ctx.add_to_sent_list(recipient) + + if influxframework.flags.in_test: + influxframework.flags.sent_mail = message + return + + if ctx.email_account_doc.append_emails_to_sent_folder and ctx.sent_to: + ctx.email_account_doc.append_email_to_sent_folder(message) + + @staticmethod + def clear_old_logs(days=30): + """Remove low priority older than 31 days in Outbox or configured in Log Settings. + Note: Used separate query to avoid deadlock + """ + days = days or 31 + email_queue = influxframework.qb.DocType("Email Queue") + email_recipient = influxframework.qb.DocType("Email Queue Recipient") + + # Delete queue table + ( + influxframework.qb.from_(email_queue) + .delete() + .where(email_queue.modified < (Now() - Interval(days=days))) + ).run() + + # delete child tables, note that this has potential to leave some orphan + # child table behind if modified time was later than parent doc (rare). + # But it's safe since child table doesn't contain links. + ( + influxframework.qb.from_(email_recipient) + .delete() + .where(email_recipient.modified < (Now() - Interval(days=days))) + ).run() + + +@task(queue="short") +def send_mail(email_queue_name, is_background_task=False, smtp_server_instance: SMTPServer = None): + """This is equivalent to EmailQueue.send. + + This provides a way to make sending mail as a background job. + """ + record = EmailQueue.find(email_queue_name) + record.send(is_background_task=is_background_task, smtp_server_instance=smtp_server_instance) + + +class SendMailContext: + def __init__( + self, + queue_doc: Document, + is_background_task: bool = False, + smtp_server_instance: SMTPServer = None, + ): + self.queue_doc: EmailQueue = queue_doc + self.is_background_task = is_background_task + self.email_account_doc = queue_doc.get_email_account() + + self.smtp_server = smtp_server_instance or self.email_account_doc.get_smtp_server() + + # if smtp_server_instance is passed, then retain smtp session + # Note: smtp session will have to be manually closed + self.retain_smtp_session = bool(smtp_server_instance) + + self.sent_to = [rec.recipient for rec in self.queue_doc.recipients if rec.is_main_sent()] + + def __enter__(self): + self.queue_doc.update_status(status="Sending", commit=True) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + exceptions = [ + smtplib.SMTPServerDisconnected, + smtplib.SMTPAuthenticationError, + smtplib.SMTPRecipientsRefused, + smtplib.SMTPConnectError, + smtplib.SMTPHeloError, + JobTimeoutException, + ] + + if not self.retain_smtp_session: + self.smtp_server.quit() + + self.log_exception(exc_type, exc_val, exc_tb) + + if exc_type in exceptions: + email_status = "Partially Sent" if self.sent_to else "Not Sent" + self.queue_doc.update_status(status=email_status, commit=True) + elif exc_type: + if self.queue_doc.retry < get_email_retry_limit(): + update_fields = {"status": "Not Sent", "retry": self.queue_doc.retry + 1} + else: + update_fields = {"status": (self.sent_to and "Partially Errored") or "Error"} + self.queue_doc.update_status(**update_fields, commit=True) + else: + email_status = self.is_mail_sent_to_all() and "Sent" + email_status = email_status or (self.sent_to and "Partially Sent") or "Not Sent" + + update_fields = { + "status": email_status, + "email_account": self.email_account_doc.name + if self.email_account_doc.is_exists_in_db() + else None, + } + self.queue_doc.update_status(**update_fields, commit=True) + + def log_exception(self, exc_type, exc_val, exc_tb): + if exc_type: + traceback_string = "".join(traceback.format_tb(exc_tb)) + traceback_string += f"\n Queue Name: {self.queue_doc.name}" + + self.queue_doc.log_error("Email sending failed", traceback_string) + + @property + def smtp_session(self): + if influxframework.flags.in_test: + return + return self.smtp_server.session + + def add_to_sent_list(self, recipient): + # Update recipient status + recipient.update_db(status="Sent", commit=True) + self.sent_to.append(recipient.recipient) + + def is_mail_sent_to_all(self): + return sorted(self.sent_to) == sorted(rec.recipient for rec in self.queue_doc.recipients) + + def get_message_object(self, message): + return Parser(policy=SMTPUTF8).parsestr(message) + + def message_placeholder(self, placeholder_key): + # sourcery skip: avoid-builtin-shadow + map = { + "tracker": "", + "unsubscribe_url": "", + "cc": "", + "recipient": "", + } + return map.get(placeholder_key) + + def build_message(self, recipient_email): + """Build message specific to the recipient.""" + message = self.queue_doc.message + if not message: + return "" + + message = message.replace(self.message_placeholder("tracker"), self.get_tracker_str()) + message = message.replace( + self.message_placeholder("unsubscribe_url"), self.get_unsubscribe_str(recipient_email) + ) + message = message.replace(self.message_placeholder("cc"), self.get_receivers_str()) + message = message.replace( + self.message_placeholder("recipient"), self.get_recipient_str(recipient_email) + ) + message = self.include_attachments(message) + return message + + def get_tracker_str(self): + tracker_url_html = '' + + message = "" + if influxframework.conf.use_ssl and self.email_account_doc.track_email_status: + message = quopri.encodestring( + tracker_url_html.format(influxframework.local.site, self.queue_doc.communication).encode() + ).decode() + return message + + def get_unsubscribe_str(self, recipient_email: str) -> str: + unsubscribe_url = "" + + if self.queue_doc.add_unsubscribe_link and self.queue_doc.reference_doctype: + unsubscribe_url = get_unsubcribed_url( + reference_doctype=self.queue_doc.reference_doctype, + reference_name=self.queue_doc.reference_name, + email=recipient_email, + unsubscribe_method=self.queue_doc.unsubscribe_method, + unsubscribe_params=self.queue_doc.unsubscribe_param, + ) + + return quopri.encodestring(unsubscribe_url.encode()).decode() + + def get_receivers_str(self): + message = "" + if self.queue_doc.expose_recipients == "footer": + to_str = ", ".join(self.queue_doc.to) + cc_str = ", ".join(self.queue_doc.cc) + message = f"This email was sent to {to_str}" + message = f"{message} and copied to {cc_str}" if cc_str else message + return message + + def get_recipient_str(self, recipient_email): + return recipient_email if self.queue_doc.expose_recipients != "header" else "" + + def include_attachments(self, message): + message_obj = self.get_message_object(message) + attachments = self.queue_doc.attachments_list + + for attachment in attachments: + if attachment.get("fcontent"): + continue + + file_filters = {} + if attachment.get("fid"): + file_filters["name"] = attachment.get("fid") + elif attachment.get("file_url"): + file_filters["file_url"] = attachment.get("file_url") + + if file_filters: + _file = influxframework.get_doc("File", file_filters) + fcontent = _file.get_content() + attachment.update({"fname": _file.file_name, "fcontent": fcontent, "parent": message_obj}) + attachment.pop("fid", None) + attachment.pop("file_url", None) + add_attachment(**attachment) + + elif attachment.get("print_format_attachment") == 1: + attachment.pop("print_format_attachment", None) + print_format_file = influxframework.attach_print(**attachment) + print_format_file.update({"parent": message_obj}) + add_attachment(**print_format_file) + + return safe_encode(message_obj.as_string()) + + +@influxframework.whitelist() +def retry_sending(name): + doc = influxframework.get_doc("Email Queue", name) + doc.check_permission() + + if doc and (doc.status == "Error" or doc.status == "Partially Errored"): + doc.status = "Not Sent" + for d in doc.recipients: + if d.status != "Sent": + d.status = "Not Sent" + doc.save(ignore_permissions=True) + + +@influxframework.whitelist() +def send_now(name): + record = EmailQueue.find(name) + if record: + record.check_permission() + record.send() + + +@influxframework.whitelist() +def toggle_sending(enable): + influxframework.only_for("System Manager") + influxframework.db.set_default("suspend_email_queue", 0 if sbool(enable) else 1) + + +def on_doctype_update(): + """Add index in `tabCommunication` for `(reference_doctype, reference_name)`""" + influxframework.db.add_index( + "Email Queue", ("status", "send_after", "priority", "creation"), "index_bulk_flush" + ) + + +def get_email_retry_limit(): + return cint(influxframework.db.get_system_setting("email_retry_limit")) or 3 + + +class QueueBuilder: + """Builds Email Queue from the given data""" + + def __init__( + self, + recipients=None, + sender=None, + subject=None, + message=None, + text_content=None, + reference_doctype=None, + reference_name=None, + unsubscribe_method=None, + unsubscribe_params=None, + unsubscribe_message=None, + attachments=None, + reply_to=None, + cc=None, + bcc=None, + message_id=None, + in_reply_to=None, + send_after=None, + expose_recipients=None, + send_priority=1, + communication=None, + read_receipt=None, + queue_separately=False, + is_notification=False, + add_unsubscribe_link=1, + inline_images=None, + header=None, + print_letterhead=False, + with_container=False, + ): + """Add email to sending queue (Email Queue) + + :param recipients: List of recipients. + :param sender: Email sender. + :param subject: Email subject. + :param message: Email message. + :param text_content: Text version of email message. + :param reference_doctype: Reference DocType of caller document. + :param reference_name: Reference name of caller document. + :param send_priority: Priority for Email Queue, default 1. + :param unsubscribe_method: URL method for unsubscribe. Default is `/api/method/influxframework.email.queue.unsubscribe`. + :param unsubscribe_params: additional params for unsubscribed links. default are name, doctype, email + :param attachments: Attachments to be sent. + :param reply_to: Reply to be captured here (default inbox) + :param in_reply_to: Used to send the Message-Id of a received email back as In-Reply-To. + :param send_after: Send this email after the given datetime. If value is in integer, then `send_after` will be the automatically set to no of days from current date. + :param communication: Communication link to be set in Email Queue record + :param queue_separately: Queue each email separately + :param is_notification: Marks email as notification so will not trigger notifications from system + :param add_unsubscribe_link: Send unsubscribe link in the footer of the Email, default 1. + :param inline_images: List of inline images as {"filename", "filecontent"}. All src properties will be replaced with random Content-Id + :param header: Append header in email (boolean) + :param with_container: Wraps email inside styled container + """ + + self._unsubscribe_method = unsubscribe_method + self._recipients = recipients + self._cc = cc + self._bcc = bcc + self._send_after = send_after + self._sender = sender + self._text_content = text_content + self._message = message + self._add_unsubscribe_link = add_unsubscribe_link + self._unsubscribe_message = unsubscribe_message + self._attachments = attachments + + self._unsubscribed_user_emails = None + self._email_account = None + + self.unsubscribe_params = unsubscribe_params + self.subject = subject + self.reference_doctype = reference_doctype + self.reference_name = reference_name + self.expose_recipients = expose_recipients + self.with_container = with_container + self.header = header + self.reply_to = reply_to + self.message_id = message_id + self.in_reply_to = in_reply_to + self.send_priority = send_priority + self.communication = communication + self.read_receipt = read_receipt + self.queue_separately = queue_separately + self.is_notification = is_notification + self.inline_images = inline_images + self.print_letterhead = print_letterhead + + @property + def unsubscribe_method(self): + return self._unsubscribe_method or "/api/method/influxframework.email.queue.unsubscribe" + + def _get_emails_list(self, emails=None): + emails = split_emails(emails) if isinstance(emails, str) else (emails or []) + return [each for each in set(emails) if each] + + @property + def recipients(self): + return self._get_emails_list(self._recipients) + + @property + def cc(self): + return self._get_emails_list(self._cc) + + @property + def bcc(self): + return self._get_emails_list(self._bcc) + + @property + def send_after(self): + if isinstance(self._send_after, int): + return add_days(nowdate(), self._send_after) + return self._send_after + + @property + def sender(self): + if not self._sender or self._sender == "Administrator": + email_account = self.get_outgoing_email_account() + return email_account.default_sender + return self._sender + + def email_text_content(self): + unsubscribe_msg = self.unsubscribe_message() + unsubscribe_text_message = (unsubscribe_msg and unsubscribe_msg.text) or "" + + if self._text_content: + return self._text_content + unsubscribe_text_message + + try: + text_content = html2text(self._message) + except Exception: + text_content = "See html attachment" + return text_content + unsubscribe_text_message + + def email_html_content(self): + email_account = self.get_outgoing_email_account() + return get_formatted_html( + self.subject, + self._message, + header=self.header, + email_account=email_account, + unsubscribe_link=self.unsubscribe_message(), + with_container=self.with_container, + ) + + def should_include_unsubscribe_link(self): + return ( + self._add_unsubscribe_link == 1 + and self.reference_doctype + and (self._unsubscribe_message or self.reference_doctype == "Newsletter") + ) + + def unsubscribe_message(self): + if self.should_include_unsubscribe_link(): + return get_unsubscribe_message(self._unsubscribe_message, self.expose_recipients) + + def get_outgoing_email_account(self): + if self._email_account: + return self._email_account + + self._email_account = EmailAccount.find_outgoing( + match_by_doctype=self.reference_doctype, match_by_email=self._sender, _raise_error=True + ) + return self._email_account + + def get_unsubscribed_user_emails(self): + if self._unsubscribed_user_emails is not None: + return self._unsubscribed_user_emails + + all_ids = list(set(self.recipients + self.cc)) + + EmailUnsubscribe = DocType("Email Unsubscribe") + + if len(all_ids) > 0: + unsubscribed = ( + influxframework.qb.from_(EmailUnsubscribe) + .select(EmailUnsubscribe.email) + .where( + EmailUnsubscribe.email.isin(all_ids) + & ( + ( + (EmailUnsubscribe.reference_doctype == self.reference_doctype) + & (EmailUnsubscribe.reference_name == self.reference_name) + ) + | (EmailUnsubscribe.global_unsubscribe == 1) + ) + ) + .distinct() + ).run(pluck=True) + else: + unsubscribed = None + + self._unsubscribed_user_emails = unsubscribed or [] + return self._unsubscribed_user_emails + + def final_recipients(self): + unsubscribed_emails = self.get_unsubscribed_user_emails() + return [mail_id for mail_id in self.recipients if mail_id not in unsubscribed_emails] + + def final_cc(self): + unsubscribed_emails = self.get_unsubscribed_user_emails() + return [mail_id for mail_id in self.cc if mail_id not in unsubscribed_emails] + + def get_attachments(self): + attachments = [] + if self._attachments: + # store attachments with fid or print format details, to be attached on-demand later + for att in self._attachments: + if att.get("fid") or att.get("file_url"): + attachments.append(att) + elif att.get("print_format_attachment") == 1: + if not att.get("lang", None): + att["lang"] = influxframework.local.lang + att["print_letterhead"] = self.print_letterhead + attachments.append(att) + return attachments + + def prepare_email_content(self): + mail = get_email( + recipients=self.final_recipients(), + sender=self.sender, + subject=self.subject, + formatted=self.email_html_content(), + text_content=self.email_text_content(), + attachments=self._attachments, + reply_to=self.reply_to, + cc=self.final_cc(), + bcc=self.bcc, + email_account=self.get_outgoing_email_account(), + expose_recipients=self.expose_recipients, + inline_images=self.inline_images, + header=self.header, + ) + + mail.set_message_id(self.message_id, self.is_notification) + if self.read_receipt: + mail.msg_root["Disposition-Notification-To"] = self.sender + if self.in_reply_to: + mail.set_in_reply_to(self.in_reply_to) + return mail + + def process(self, send_now=False): + """Build and return the email queues those are created. + + Sends email incase if it is requested to send now. + """ + final_recipients = self.final_recipients() + queue_separately = (final_recipients and self.queue_separately) or len(final_recipients) > 20 + if not (final_recipients + self.final_cc()): + return [] + + queue_data = self.as_dict(include_recipients=False) + if not queue_data: + return [] + + if not queue_separately: + recipients = list(set(final_recipients + self.final_cc() + self.bcc)) + q = EmailQueue.new({**queue_data, **{"recipients": recipients}}, ignore_permissions=True) + send_now and q.send() + else: + if send_now and len(final_recipients) >= 1000: + # force queueing if there are too many recipients to avoid timeouts + send_now = False + for recipients in influxframework.utils.create_batch(final_recipients, 1000): + influxframework.enqueue( + self.send_emails, + queue_data=queue_data, + final_recipients=recipients, + job_name=influxframework.utils.get_job_name( + "send_bulk_emails_for", self.reference_doctype, self.reference_name + ), + now=influxframework.flags.in_test or send_now, + queue="long", + ) + + def send_emails(self, queue_data, final_recipients): + # This is used to bulk send emails from same sender to multiple recipients separately + # This re-uses smtp server instance to minimize the cost of new session creation + smtp_server_instance = None + for r in final_recipients: + recipients = list(set([r] + self.final_cc() + self.bcc)) + q = EmailQueue.new({**queue_data, **{"recipients": recipients}}, ignore_permissions=True) + if not smtp_server_instance: + email_account = q.get_email_account() + smtp_server_instance = email_account.get_smtp_server() + q.send(smtp_server_instance=smtp_server_instance) + smtp_server_instance.quit() + + def as_dict(self, include_recipients=True): + email_account = self.get_outgoing_email_account() + email_account_name = email_account and email_account.is_exists_in_db() and email_account.name + + mail = self.prepare_email_content() + try: + mail_to_string = cstr(mail.as_string()) + except influxframework.InvalidEmailAddressError: + # bad Email Address - don't add to queue + influxframework.log_error( + title="Invalid email address", + message="Invalid email address Sender: {}, Recipients: {}, \nTraceback: {} ".format( + self.sender, ", ".join(self.final_recipients()), traceback.format_exc() + ), + reference_doctype=self.reference_doctype, + reference_name=self.reference_name, + ) + return + + d = { + "priority": self.send_priority, + "attachments": json.dumps(self.get_attachments()), + "message_id": get_string_between("<", mail.msg_root["Message-Id"], ">"), + "message": mail_to_string, + "sender": self.sender, + "reference_doctype": self.reference_doctype, + "reference_name": self.reference_name, + "add_unsubscribe_link": self._add_unsubscribe_link, + "unsubscribe_method": self.unsubscribe_method, + "unsubscribe_params": self.unsubscribe_params, + "expose_recipients": self.expose_recipients, + "communication": self.communication, + "send_after": self.send_after, + "show_as_cc": ",".join(self.final_cc()), + "show_as_bcc": ",".join(self.bcc), + "email_account": email_account_name or None, + } + + if include_recipients: + d["recipients"] = self.final_recipients() + + return d diff --git a/influxframework/email/doctype/email_queue/email_queue_list.js b/influxframework/email/doctype/email_queue/email_queue_list.js new file mode 100644 index 0000000..bbcce8a --- /dev/null +++ b/influxframework/email/doctype/email_queue/email_queue_list.js @@ -0,0 +1,41 @@ +influxframework.listview_settings["Email Queue"] = { + get_indicator: function (doc) { + var colour = { + Sent: "green", + Sending: "blue", + "Not Sent": "grey", + Error: "red", + Expired: "orange", + }; + return [__(doc.status), colour[doc.status], "status,=," + doc.status]; + }, + refresh: show_toggle_sending_button, + onload: function (list_view) { + influxframework.require("logtypes.bundle.js", () => { + influxframework.utils.logtypes.show_log_retention_message(list_view.doctype); + }); + }, +}; + +function show_toggle_sending_button(list_view) { + if (!has_common(influxframework.user_roles, ["Administrator", "System Manager"])) return; + + const sending_disabled = cint(influxframework.sys_defaults.suspend_email_queue); + const label = sending_disabled ? __("Resume Sending") : __("Suspend Sending"); + + list_view.page.add_inner_button(label, async () => { + await influxframework.xcall( + "influxframework.email.doctype.email_queue.email_queue.toggle_sending", + + // enable if disabled + { enable: sending_disabled } + ); + + // set new value for suspend_email_queue in sys_defaults + influxframework.sys_defaults.suspend_email_queue = sending_disabled ? 0 : 1; + + // clear the button and show one with the opposite label + list_view.page.remove_inner_button(label); + show_toggle_sending_button(list_view); + }); +} diff --git a/influxframework/email/doctype/email_queue/test_email_queue.py b/influxframework/email/doctype/email_queue/test_email_queue.py new file mode 100644 index 0000000..467e4ae --- /dev/null +++ b/influxframework/email/doctype/email_queue/test_email_queue.py @@ -0,0 +1,41 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestEmailQueue(InfluxFrameworkTestCase): + def test_email_queue_deletion_based_on_modified_date(self): + from influxframework.email.doctype.email_queue.email_queue import EmailQueue + + old_record = influxframework.get_doc( + { + "doctype": "Email Queue", + "sender": "Test ", + "show_as_cc": "", + "message": "Test message", + "status": "Sent", + "priority": 1, + "recipients": [ + { + "recipient": "test_auth@test.com", + } + ], + } + ).insert() + + old_record.modified = "2010-01-01 00:00:01" + old_record.recipients[0].modified = old_record.modified + old_record.db_update_all() + + new_record = influxframework.copy_doc(old_record) + new_record.insert() + + EmailQueue.clear_old_logs() + + self.assertFalse(influxframework.db.exists("Email Queue", old_record.name)) + self.assertFalse(influxframework.db.exists("Email Queue Recipient", {"parent": old_record.name})) + + self.assertTrue(influxframework.db.exists("Email Queue", new_record.name)) + self.assertTrue(influxframework.db.exists("Email Queue Recipient", {"parent": new_record.name})) diff --git a/influxframework/email/doctype/email_queue_recipient/__init__.py b/influxframework/email/doctype/email_queue_recipient/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/email/doctype/email_queue_recipient/email_queue_recipient.json b/influxframework/email/doctype/email_queue_recipient/email_queue_recipient.json new file mode 100644 index 0000000..c217886 --- /dev/null +++ b/influxframework/email/doctype/email_queue_recipient/email_queue_recipient.json @@ -0,0 +1,46 @@ +{ + "actions": [], + "creation": "2016-12-08 12:01:07.993900", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "recipient", + "status", + "error" + ], + "fields": [ + { + "fieldname": "recipient", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Recipient", + "options": "Email" + }, + { + "default": "Not Sent", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "options": "\nNot Sent\nSending\nSent\nError\nExpired", + "search_index": 1 + }, + { + "fieldname": "error", + "fieldtype": "Code", + "label": "Error" + } + ], + "istable": 1, + "links": [], + "modified": "2022-07-11 16:38:10.644417", + "modified_by": "Administrator", + "module": "Email", + "name": "Email Queue Recipient", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/influxframework/email/doctype/email_queue_recipient/email_queue_recipient.py b/influxframework/email/doctype/email_queue_recipient/email_queue_recipient.py new file mode 100644 index 0000000..38b235c --- /dev/null +++ b/influxframework/email/doctype/email_queue_recipient/email_queue_recipient.py @@ -0,0 +1,20 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document + + +class EmailQueueRecipient(Document): + DOCTYPE = "Email Queue Recipient" + + def is_mail_to_be_sent(self): + return self.status == "Not Sent" + + def is_main_sent(self): + return self.status == "Sent" + + def update_db(self, commit=False, **kwargs): + influxframework.db.set_value(self.DOCTYPE, self.name, kwargs) + if commit: + influxframework.db.commit() diff --git a/influxframework/email/doctype/email_rule/__init__.py b/influxframework/email/doctype/email_rule/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/email/doctype/email_rule/email_rule.js b/influxframework/email/doctype/email_rule/email_rule.js new file mode 100644 index 0000000..a145cfc --- /dev/null +++ b/influxframework/email/doctype/email_rule/email_rule.js @@ -0,0 +1,6 @@ +// Copyright (c) 2017, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Email Rule", { + refresh: function (frm) {}, +}); diff --git a/influxframework/email/doctype/email_rule/email_rule.json b/influxframework/email/doctype/email_rule/email_rule.json new file mode 100644 index 0000000..b4e505b --- /dev/null +++ b/influxframework/email/doctype/email_rule/email_rule.json @@ -0,0 +1,128 @@ +{ + "allow_copy": 1, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "field:email_id", + "beta": 0, + "creation": "2017-03-13 09:20:56.387135", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "email_id", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Email ID", + "length": 0, + "no_copy": 0, + "options": "Email", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 1 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "is_spam", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Is Spam", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2018-08-31 06:08:12.645682", + "modified_by": "Administrator", + "module": "Email", + "name": "Email Rule", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 0 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 0, + "track_seen": 0, + "track_views": 0 +} \ No newline at end of file diff --git a/influxframework/email/doctype/email_rule/email_rule.py b/influxframework/email/doctype/email_rule/email_rule.py new file mode 100644 index 0000000..10e852e --- /dev/null +++ b/influxframework/email/doctype/email_rule/email_rule.py @@ -0,0 +1,8 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE + +from influxframework.model.document import Document + + +class EmailRule(Document): + pass diff --git a/influxframework/email/doctype/email_rule/test_email_rule.py b/influxframework/email/doctype/email_rule/test_email_rule.py new file mode 100644 index 0000000..f13c8d6 --- /dev/null +++ b/influxframework/email/doctype/email_rule/test_email_rule.py @@ -0,0 +1,7 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestEmailRule(InfluxFrameworkTestCase): + pass diff --git a/influxframework/email/doctype/email_template/__init__.py b/influxframework/email/doctype/email_template/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/email/doctype/email_template/email_template.js b/influxframework/email/doctype/email_template/email_template.js new file mode 100644 index 0000000..746cdce --- /dev/null +++ b/influxframework/email/doctype/email_template/email_template.js @@ -0,0 +1,6 @@ +// Copyright (c) 2018, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Email Template", { + refresh: function () {}, +}); diff --git a/influxframework/email/doctype/email_template/email_template.json b/influxframework/email/doctype/email_template/email_template.json new file mode 100644 index 0000000..c6ec971 --- /dev/null +++ b/influxframework/email/doctype/email_template/email_template.json @@ -0,0 +1,89 @@ +{ + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "Prompt", + "creation": "2014-06-19 05:20:26.331041", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "subject", + "use_html", + "response_html", + "response", + "section_break_4", + "email_reply_help" + ], + "fields": [ + { + "fieldname": "subject", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Subject", + "reqd": 1 + }, + { + "depends_on": "eval:!doc.use_html", + "fieldname": "response", + "fieldtype": "Text Editor", + "in_list_view": 1, + "label": "Response", + "mandatory_depends_on": "eval:!doc.use_html" + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break" + }, + { + "fieldname": "email_reply_help", + "fieldtype": "HTML", + "label": "Email Reply Help", + "options": "

      Email Reply Example

      \n\n
      Order Overdue\n\nTransaction {{ name }} has exceeded Due Date. Please take necessary action.\n\nDetails\n\n- Customer: {{ customer }}\n- Amount: {{ grand_total }}\n
      \n\n

      How to get fieldnames

      \n\n

      The fieldnames you can use in your email template are the fields in the document from which you are sending the email. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Sales Invoice)

      \n\n

      Templating

      \n\n

      Templates are compiled using the Jinja Templating Language. To learn more about Jinja, read this documentation.

      \n" + }, + { + "default": "0", + "fieldname": "use_html", + "fieldtype": "Check", + "label": "Use HTML" + }, + { + "depends_on": "eval:doc.use_html", + "fieldname": "response_html", + "fieldtype": "Code", + "label": "Response ", + "options": "HTML" + } + ], + "icon": "fa fa-comment", + "links": [], + "modified": "2022-01-04 14:12:50.321633", + "modified_by": "Administrator", + "module": "Email", + "name": "Email Template", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "read": 1, + "role": "All", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/email/doctype/email_template/email_template.py b/influxframework/email/doctype/email_template/email_template.py new file mode 100644 index 0000000..90486aa --- /dev/null +++ b/influxframework/email/doctype/email_template/email_template.py @@ -0,0 +1,41 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +import influxframework +from influxframework.model.document import Document +from influxframework.utils.jinja import validate_template + + +class EmailTemplate(Document): + def validate(self): + if self.use_html: + validate_template(self.response_html) + else: + validate_template(self.response) + + def get_formatted_subject(self, doc): + return influxframework.render_template(self.subject, doc) + + def get_formatted_response(self, doc): + if self.use_html: + return influxframework.render_template(self.response_html, doc) + + return influxframework.render_template(self.response, doc) + + def get_formatted_email(self, doc): + if isinstance(doc, str): + doc = json.loads(doc) + + return {"subject": self.get_formatted_subject(doc), "message": self.get_formatted_response(doc)} + + +@influxframework.whitelist() +def get_email_template(template_name, doc): + """Returns the processed HTML of a email template with the given doc""" + if isinstance(doc, str): + doc = json.loads(doc) + + email_template = influxframework.get_doc("Email Template", template_name) + return email_template.get_formatted_email(doc) diff --git a/influxframework/email/doctype/email_template/test_email_template.py b/influxframework/email/doctype/email_template/test_email_template.py new file mode 100644 index 0000000..5268c47 --- /dev/null +++ b/influxframework/email/doctype/email_template/test_email_template.py @@ -0,0 +1,7 @@ +# Copyright (c) 2018, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestEmailTemplate(InfluxFrameworkTestCase): + pass diff --git a/influxframework/email/doctype/email_unsubscribe/__init__.py b/influxframework/email/doctype/email_unsubscribe/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/email/doctype/email_unsubscribe/email_unsubscribe.js b/influxframework/email/doctype/email_unsubscribe/email_unsubscribe.js new file mode 100644 index 0000000..8a40018 --- /dev/null +++ b/influxframework/email/doctype/email_unsubscribe/email_unsubscribe.js @@ -0,0 +1,6 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Email Unsubscribe", { + refresh: function (frm) {}, +}); diff --git a/influxframework/email/doctype/email_unsubscribe/email_unsubscribe.json b/influxframework/email/doctype/email_unsubscribe/email_unsubscribe.json new file mode 100644 index 0000000..bf633ea --- /dev/null +++ b/influxframework/email/doctype/email_unsubscribe/email_unsubscribe.json @@ -0,0 +1,175 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2015-03-18 09:41:20.216320", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "System", + "editable_grid": 0, + "engine": "InnoDB", + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "email", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Email", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 1, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reference_doctype", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Reference Document Type", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Reference Name", + "length": 0, + "no_copy": 0, + "options": "reference_doctype", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "global_unsubscribe", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Global Unsubscribe", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2019-09-05 14:22:27.664645", + "modified_by": "Administrator", + "module": "Email", + "name": "Email Unsubscribe", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "is_custom": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0 +} diff --git a/influxframework/email/doctype/email_unsubscribe/email_unsubscribe.py b/influxframework/email/doctype/email_unsubscribe/email_unsubscribe.py new file mode 100644 index 0000000..8ea1db5 --- /dev/null +++ b/influxframework/email/doctype/email_unsubscribe/email_unsubscribe.py @@ -0,0 +1,46 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework import _ +from influxframework.model.document import Document + + +class EmailUnsubscribe(Document): + def validate(self): + if not self.global_unsubscribe and not (self.reference_doctype and self.reference_name): + influxframework.throw(_("Reference DocType and Reference Name are required"), influxframework.MandatoryError) + + if not self.global_unsubscribe and influxframework.db.get_value( + self.doctype, self.name, "global_unsubscribe" + ): + influxframework.throw(_("Delete this record to allow sending to this email address")) + + if self.global_unsubscribe: + if influxframework.get_all( + "Email Unsubscribe", + filters={"email": self.email, "global_unsubscribe": 1, "name": ["!=", self.name]}, + ): + influxframework.throw(_("{0} already unsubscribed").format(self.email), influxframework.DuplicateEntryError) + + else: + if influxframework.get_all( + "Email Unsubscribe", + filters={ + "email": self.email, + "reference_doctype": self.reference_doctype, + "reference_name": self.reference_name, + "name": ["!=", self.name], + }, + ): + influxframework.throw( + _("{0} already unsubscribed for {1} {2}").format( + self.email, self.reference_doctype, self.reference_name + ), + influxframework.DuplicateEntryError, + ) + + def on_update(self): + if self.reference_doctype and self.reference_name: + doc = influxframework.get_doc(self.reference_doctype, self.reference_name) + doc.add_comment("Label", _("Left this conversation"), comment_email=self.email) diff --git a/influxframework/email/doctype/email_unsubscribe/test_email_unsubscribe.py b/influxframework/email/doctype/email_unsubscribe/test_email_unsubscribe.py new file mode 100644 index 0000000..b8d92b9 --- /dev/null +++ b/influxframework/email/doctype/email_unsubscribe/test_email_unsubscribe.py @@ -0,0 +1,9 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + +# test_records = influxframework.get_test_records('Email Unsubscribe') + + +class TestEmailUnsubscribe(InfluxFrameworkTestCase): + pass diff --git a/influxframework/email/doctype/imap_folder/__init__.py b/influxframework/email/doctype/imap_folder/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/email/doctype/imap_folder/imap_folder.json b/influxframework/email/doctype/imap_folder/imap_folder.json new file mode 100644 index 0000000..bab50de --- /dev/null +++ b/influxframework/email/doctype/imap_folder/imap_folder.json @@ -0,0 +1,53 @@ +{ + "actions": [], + "creation": "2021-09-21 11:38:13.521979", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "folder_name", + "append_to", + "uidvalidity", + "uidnext" + ], + "fields": [ + { + "fieldname": "folder_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Folder Name", + "reqd": 1 + }, + { + "fieldname": "append_to", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Append To", + "options": "DocType" + }, + { + "fieldname": "uidvalidity", + "fieldtype": "Data", + "hidden": 1, + "label": "UIDVALIDITY" + }, + { + "fieldname": "uidnext", + "fieldtype": "Data", + "hidden": 1, + "label": "UIDNEXT" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-09-21 11:53:00.811236", + "modified_by": "Administrator", + "module": "Email", + "name": "IMAP Folder", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} diff --git a/influxframework/email/doctype/imap_folder/imap_folder.py b/influxframework/email/doctype/imap_folder/imap_folder.py new file mode 100644 index 0000000..b6bcbb1 --- /dev/null +++ b/influxframework/email/doctype/imap_folder/imap_folder.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, InfluxFramework LLC +# For license information, please see license.txt + +# import influxframework +from influxframework.model.document import Document + + +class IMAPFolder(Document): + pass diff --git a/influxframework/email/doctype/newsletter/__init__.py b/influxframework/email/doctype/newsletter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/email/doctype/newsletter/exceptions.py b/influxframework/email/doctype/newsletter/exceptions.py new file mode 100644 index 0000000..9004d39 --- /dev/null +++ b/influxframework/email/doctype/newsletter/exceptions.py @@ -0,0 +1,16 @@ +# Copyright (c) 2021, InfluxFramework LLC +# MIT License. See LICENSE + +from influxframework.exceptions import ValidationError + + +class NewsletterAlreadySentError(ValidationError): + pass + + +class NoRecipientFoundError(ValidationError): + pass + + +class NewsletterNotSavedError(ValidationError): + pass diff --git a/influxframework/email/doctype/newsletter/newsletter.js b/influxframework/email/doctype/newsletter/newsletter.js new file mode 100644 index 0000000..82fbfe6 --- /dev/null +++ b/influxframework/email/doctype/newsletter/newsletter.js @@ -0,0 +1,227 @@ +// Copyright (c) 2015, InfluxFramework LLC +// License: GNU General Public License v3. See license.txt + +influxframework.ui.form.on("Newsletter", { + refresh(frm) { + let doc = frm.doc; + let can_write = in_list(influxframework.boot.user.can_write, doc.doctype); + if (!frm.is_new() && !frm.is_dirty() && !doc.email_sent && can_write) { + frm.add_custom_button( + __("Send a test email"), + () => { + frm.events.send_test_email(frm); + }, + __("Preview") + ); + + frm.add_custom_button( + __("Check broken links"), + () => { + frm.dashboard.set_headline(__("Checking broken links...")); + frm.call("find_broken_links").then((r) => { + frm.dashboard.set_headline(""); + let links = r.message; + if (links && links.length) { + let html = + "
        " + + links.map((link) => `
      • ${link}
      • `).join("") + + "
      "; + frm.dashboard.set_headline( + __("Following links are broken in the email content: {0}", [html]) + ); + } else { + frm.dashboard.set_headline( + __("No broken links found in the email content") + ); + setTimeout(() => { + frm.dashboard.set_headline(""); + }, 3000); + } + }); + }, + __("Preview") + ); + + frm.add_custom_button( + __("Send now"), + () => { + if (frm.doc.schedule_send) { + influxframework.confirm( + __( + "This newsletter was scheduled to send on a later date. Are you sure you want to send it now?" + ), + function () { + frm.events.send_emails(frm); + } + ); + return; + } + influxframework.confirm( + __("Are you sure you want to send this newsletter now?"), + () => { + frm.events.send_emails(frm); + } + ); + }, + __("Send") + ); + + frm.add_custom_button( + __("Schedule sending"), + () => { + frm.events.schedule_send_dialog(frm); + }, + __("Send") + ); + } + + frm.events.update_sending_status(frm); + + if (frm.is_new() && !doc.sender_email) { + let { fullname, email } = influxframework.user_info(doc.owner); + frm.set_value("sender_email", email); + frm.set_value("sender_name", fullname); + } + + frm.trigger("update_schedule_message"); + }, + + send_emails(frm) { + influxframework.dom.freeze(__("Queuing emails...")); + frm.call("send_emails").then(() => { + frm.refresh(); + influxframework.dom.unfreeze(); + influxframework.show_alert( + __("Queued {0} emails", [influxframework.utils.shorten_number(frm.doc.total_recipients)]) + ); + }); + }, + + schedule_send_dialog(frm) { + let hours = influxframework.utils.range(24); + let time_slots = hours.map((hour) => { + return `${(hour + "").padStart(2, "0")}:00`; + }); + let d = new influxframework.ui.Dialog({ + title: __("Schedule Newsletter"), + fields: [ + { + label: __("Date"), + fieldname: "date", + fieldtype: "Date", + options: { + minDate: new Date(), + }, + }, + { + label: __("Time"), + fieldname: "time", + fieldtype: "Select", + options: time_slots, + }, + ], + primary_action_label: __("Schedule"), + primary_action({ date, time }) { + frm.set_value("schedule_sending", 1); + frm.set_value("schedule_send", `${date} ${time}:00`); + d.hide(); + frm.save(); + }, + secondary_action_label: __("Cancel Scheduling"), + secondary_action() { + frm.set_value("schedule_sending", 0); + frm.set_value("schedule_send", ""); + d.hide(); + frm.save(); + }, + }); + if (frm.doc.schedule_sending) { + let parts = frm.doc.schedule_send.split(" "); + if (parts.length === 2) { + let [date, time] = parts; + d.set_value("date", date); + d.set_value("time", time.slice(0, 5)); + } + } + d.show(); + }, + + send_test_email(frm) { + let d = new influxframework.ui.Dialog({ + title: __("Send Test Email"), + fields: [ + { + label: __("Email"), + fieldname: "email", + fieldtype: "Data", + options: "Email", + }, + ], + primary_action_label: __("Send"), + primary_action({ email }) { + d.get_primary_btn().text(__("Sending...")).prop("disabled", true); + frm.call("send_test_email", { email }).then(() => { + d.get_primary_btn().text(__("Send again")).prop("disabled", false); + }); + }, + }); + d.show(); + }, + + async update_sending_status(frm) { + if (frm.doc.email_sent && frm.$wrapper.is(":visible") && !frm.waiting_for_request) { + frm.waiting_for_request = true; + let res = await frm.call("get_sending_status"); + frm.waiting_for_request = false; + let stats = res.message; + stats && frm.events.update_sending_progress(frm, stats); + if ( + stats.sent + stats.error >= frm.doc.total_recipients || + (!stats.total && !stats.emails_queued) + ) { + frm.sending_status && clearInterval(frm.sending_status); + frm.sending_status = null; + return; + } + } + + if (frm.sending_status) return; + frm.sending_status = setInterval(() => frm.events.update_sending_status(frm), 5000); + }, + + update_sending_progress(frm, stats) { + if (stats.sent + stats.error >= frm.doc.total_recipients || !frm.doc.email_sent) { + frm.doc.email_sent && frm.page.set_indicator(__("Sent"), "green"); + frm.dashboard.hide_progress(); + return; + } + if (stats.total) { + frm.page.set_indicator(__("Sending"), "blue"); + frm.dashboard.show_progress( + __("Sending emails"), + (stats.sent * 100) / frm.doc.total_recipients, + __("{0} of {1} sent", [stats.sent, frm.doc.total_recipients]) + ); + } else if (stats.emails_queued) { + frm.page.set_indicator(__("Queued"), "blue"); + } + }, + + on_hide(frm) { + if (frm.sending_status) { + clearInterval(frm.sending_status); + frm.sending_status = null; + } + }, + + update_schedule_message(frm) { + if (!frm.doc.email_sent && frm.doc.schedule_send) { + let datetime = influxframework.datetime.global_date_format(frm.doc.schedule_send); + frm.dashboard.set_headline_alert( + __("This newsletter is scheduled to be sent on {0}", [datetime.bold()]) + ); + } else { + frm.dashboard.clear_headline(); + } + }, +}); diff --git a/influxframework/email/doctype/newsletter/newsletter.json b/influxframework/email/doctype/newsletter/newsletter.json new file mode 100644 index 0000000..b42f475 --- /dev/null +++ b/influxframework/email/doctype/newsletter/newsletter.json @@ -0,0 +1,264 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2013-01-10 16:34:31", + "description": "Create and Send Newsletters", + "doctype": "DocType", + "document_type": "Other", + "engine": "InnoDB", + "field_order": [ + "status_section", + "email_sent_at", + "column_break_3", + "total_recipients", + "column_break_12", + "email_sent", + "from_section", + "sender_name", + "column_break_5", + "sender_email", + "column_break_7", + "send_from", + "recipients", + "email_group", + "subject_section", + "subject", + "newsletter_content", + "content_type", + "message", + "message_md", + "message_html", + "attachments", + "send_unsubscribe_link", + "send_webview_link", + "schedule_settings_section", + "scheduled_to_send", + "schedule_sending", + "schedule_send", + "publish_as_a_web_page_section", + "published", + "route" + ], + "fields": [ + { + "fieldname": "email_group", + "fieldtype": "Table", + "in_standard_filter": 1, + "label": "Audience", + "options": "Newsletter Email Group", + "reqd": 1 + }, + { + "fieldname": "send_from", + "fieldtype": "Data", + "ignore_xss_filter": 1, + "label": "Sender", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "email_sent", + "fieldtype": "Check", + "hidden": 1, + "label": "Email Sent", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "newsletter_content", + "fieldtype": "Section Break", + "label": "Content" + }, + { + "fieldname": "subject", + "fieldtype": "Small Text", + "in_global_search": 1, + "in_list_view": 1, + "label": "Subject", + "reqd": 1 + }, + { + "depends_on": "eval: doc.content_type === 'Rich Text'", + "fieldname": "message", + "fieldtype": "Text Editor", + "in_list_view": 1, + "label": "Message", + "mandatory_depends_on": "eval: doc.content_type === 'Rich Text'" + }, + { + "default": "1", + "fieldname": "send_unsubscribe_link", + "fieldtype": "Check", + "label": "Send Unsubscribe Link" + }, + { + "default": "0", + "fieldname": "published", + "fieldtype": "Check", + "label": "Published" + }, + { + "depends_on": "published", + "fieldname": "route", + "fieldtype": "Data", + "label": "Route", + "read_only": 1 + }, + { + "fieldname": "scheduled_to_send", + "fieldtype": "Int", + "hidden": 1, + "label": "Scheduled To Send" + }, + { + "fieldname": "recipients", + "fieldtype": "Section Break", + "label": "To" + }, + { + "depends_on": "eval: doc.schedule_sending", + "fieldname": "schedule_send", + "fieldtype": "Datetime", + "label": "Send Email At", + "read_only": 1, + "read_only_depends_on": "eval: doc.email_sent" + }, + { + "fieldname": "content_type", + "fieldtype": "Select", + "label": "Content Type", + "options": "Rich Text\nMarkdown\nHTML" + }, + { + "depends_on": "eval:doc.content_type === 'Markdown'", + "fieldname": "message_md", + "fieldtype": "Markdown Editor", + "label": "Message (Markdown)", + "mandatory_depends_on": "eval:doc.content_type === 'Markdown'" + }, + { + "depends_on": "eval:doc.content_type === 'HTML'", + "fieldname": "message_html", + "fieldtype": "HTML Editor", + "label": "Message (HTML)", + "mandatory_depends_on": "eval:doc.content_type === 'HTML'" + }, + { + "default": "0", + "fieldname": "schedule_sending", + "fieldtype": "Check", + "label": "Schedule sending at a later time", + "read_only_depends_on": "eval: doc.email_sent" + }, + { + "default": "0", + "fieldname": "send_webview_link", + "fieldtype": "Check", + "label": "Send Web View Link" + }, + { + "fieldname": "from_section", + "fieldtype": "Section Break", + "label": "From" + }, + { + "fieldname": "sender_name", + "fieldtype": "Data", + "label": "Sender Name" + }, + { + "fieldname": "sender_email", + "fieldtype": "Data", + "label": "Sender Email", + "options": "Email", + "reqd": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "fieldname": "subject_section", + "fieldtype": "Section Break", + "label": "Subject" + }, + { + "fieldname": "publish_as_a_web_page_section", + "fieldtype": "Section Break", + "label": "Publish as a web page" + }, + { + "depends_on": "schedule_sending", + "fieldname": "schedule_settings_section", + "fieldtype": "Section Break", + "label": "Scheduled Sending" + }, + { + "fieldname": "attachments", + "fieldtype": "Table", + "label": "Attachments", + "options": "Newsletter Attachment" + }, + { + "fieldname": "email_sent_at", + "fieldtype": "Datetime", + "label": "Email Sent At", + "read_only": 1 + }, + { + "fieldname": "total_recipients", + "fieldtype": "Int", + "label": "Total Recipients", + "read_only": 1 + }, + { + "depends_on": "email_sent", + "fieldname": "status_section", + "fieldtype": "Section Break", + "label": "Status" + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + } + ], + "has_web_view": 1, + "icon": "fa fa-envelope", + "idx": 1, + "index_web_pages_for_search": 1, + "is_published_field": "published", + "links": [], + "modified": "2022-03-09 01:48:16.741603", + "modified_by": "Administrator", + "module": "Email", + "name": "Newsletter", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Newsletter Manager", + "set_user_permissions": 1, + "share": 1, + "write": 1 + } + ], + "route": "newsletters", + "sort_field": "modified", + "sort_order": "ASC", + "title_field": "subject", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/email/doctype/newsletter/newsletter.py b/influxframework/email/doctype/newsletter/newsletter.py new file mode 100644 index 0000000..2188a36 --- /dev/null +++ b/influxframework/email/doctype/newsletter/newsletter.py @@ -0,0 +1,350 @@ +# Copyright (c) 2021, InfluxFramework LLC +# MIT License. See LICENSE + + +import influxframework +import influxframework.utils +from influxframework import _ +from influxframework.email.doctype.email_group.email_group import add_subscribers +from influxframework.utils.safe_exec import is_job_queued +from influxframework.utils.verified_command import get_signed_params, verify_request +from influxframework.website.website_generator import WebsiteGenerator + +from .exceptions import NewsletterAlreadySentError, NewsletterNotSavedError, NoRecipientFoundError + + +class Newsletter(WebsiteGenerator): + def validate(self): + self.route = f"newsletters/{self.name}" + self.validate_sender_address() + self.validate_recipient_address() + self.validate_publishing() + + @property + def newsletter_recipients(self) -> list[str]: + if getattr(self, "_recipients", None) is None: + self._recipients = self.get_recipients() + return self._recipients + + @influxframework.whitelist() + def get_sending_status(self): + count_by_status = influxframework.get_all( + "Email Queue", + filters={"reference_doctype": self.doctype, "reference_name": self.name}, + fields=["status", "count(name) as count"], + group_by="status", + order_by="status", + ) + sent = 0 + error = 0 + total = 0 + for row in count_by_status: + if row.status == "Sent": + sent = row.count + elif row.status == "Error": + error = row.count + total += row.count + emails_queued = is_job_queued( + job_name=influxframework.utils.get_job_name("send_bulk_emails_for", self.doctype, self.name), + queue="long", + ) + return {"sent": sent, "error": error, "total": total, "emails_queued": emails_queued} + + @influxframework.whitelist() + def send_test_email(self, email): + test_emails = influxframework.utils.validate_email_address(email, throw=True) + self.send_newsletter(emails=test_emails) + influxframework.msgprint(_("Test email sent to {0}").format(email), alert=True) + + @influxframework.whitelist() + def find_broken_links(self): + import requests + from bs4 import BeautifulSoup + + html = self.get_message() + soup = BeautifulSoup(html, "html.parser") + links = soup.find_all("a") + images = soup.find_all("img") + broken_links = [] + for el in links + images: + url = el.attrs.get("href") or el.attrs.get("src") + try: + response = requests.head(url, verify=False, timeout=5) + if response.status_code >= 400: + broken_links.append(url) + except Exception: + broken_links.append(url) + return broken_links + + @influxframework.whitelist() + def send_emails(self): + """queue sending emails to recipients""" + self.schedule_sending = False + self.schedule_send = None + self.queue_all() + + def validate_send(self): + """Validate if Newsletter can be sent.""" + self.validate_newsletter_status() + self.validate_newsletter_recipients() + + def validate_newsletter_status(self): + if self.email_sent: + influxframework.throw(_("Newsletter has already been sent"), exc=NewsletterAlreadySentError) + + if self.get("__islocal"): + influxframework.throw(_("Please save the Newsletter before sending"), exc=NewsletterNotSavedError) + + def validate_newsletter_recipients(self): + if not self.newsletter_recipients: + influxframework.throw(_("Newsletter should have atleast one recipient"), exc=NoRecipientFoundError) + self.validate_recipient_address() + + def validate_sender_address(self): + """Validate self.send_from is a valid email address or not.""" + if self.sender_email: + influxframework.utils.validate_email_address(self.sender_email, throw=True) + self.send_from = ( + f"{self.sender_name} <{self.sender_email}>" if self.sender_name else self.sender_email + ) + + def validate_recipient_address(self): + """Validate if self.newsletter_recipients are all valid email addresses or not.""" + for recipient in self.newsletter_recipients: + influxframework.utils.validate_email_address(recipient, throw=True) + + def validate_publishing(self): + if self.send_webview_link and not self.published: + influxframework.throw(_("Newsletter must be published to send webview link in email")) + + def get_linked_email_queue(self) -> list[str]: + """Get list of email queue linked to this newsletter.""" + return influxframework.get_all( + "Email Queue", + filters={ + "reference_doctype": self.doctype, + "reference_name": self.name, + }, + pluck="name", + ) + + def get_success_recipients(self) -> list[str]: + """Recipients who have already received the newsletter. + + Couldn't think of a better name ;) + """ + return influxframework.get_all( + "Email Queue Recipient", + filters={ + "status": ("in", ["Not Sent", "Sending", "Sent"]), + "parent": ("in", self.get_linked_email_queue()), + }, + pluck="recipient", + ) + + def get_pending_recipients(self) -> list[str]: + """Get list of pending recipients of the newsletter. These + recipients may not have receive the newsletter in the previous iteration. + """ + success_recipients = set(self.get_success_recipients()) + return [x for x in self.newsletter_recipients if x not in success_recipients] + + def queue_all(self): + """Queue Newsletter to all the recipients generated from the `Email Group` table""" + self.validate() + self.validate_send() + + recipients = self.get_pending_recipients() + self.send_newsletter(emails=recipients) + + self.email_sent = True + self.email_sent_at = influxframework.utils.now() + self.total_recipients = len(recipients) + self.save() + + def get_newsletter_attachments(self) -> list[dict[str, str]]: + """Get list of attachments on current Newsletter""" + return [{"file_url": row.attachment} for row in self.attachments] + + def send_newsletter(self, emails: list[str]): + """Trigger email generation for `emails` and add it in Email Queue.""" + attachments = self.get_newsletter_attachments() + sender = self.send_from or influxframework.utils.get_formatted_email(self.owner) + args = self.as_dict() + args["message"] = self.get_message() + + is_auto_commit_set = bool(influxframework.db.auto_commit_on_many_writes) + influxframework.db.auto_commit_on_many_writes = not influxframework.flags.in_test + + influxframework.sendmail( + subject=self.subject, + sender=sender, + recipients=emails, + attachments=attachments, + template="newsletter", + add_unsubscribe_link=self.send_unsubscribe_link, + unsubscribe_method="/unsubscribe", + unsubscribe_params={"name": self.name}, + reference_doctype=self.doctype, + reference_name=self.name, + queue_separately=True, + send_priority=0, + args=args, + ) + + influxframework.db.auto_commit_on_many_writes = is_auto_commit_set + + def get_message(self) -> str: + message = self.message + if self.content_type == "Markdown": + message = influxframework.utils.md_to_html(self.message_md) + if self.content_type == "HTML": + message = self.message_html + + return influxframework.render_template(message, {"doc": self.as_dict()}) + + def get_recipients(self) -> list[str]: + """Get recipients from Email Group""" + emails = influxframework.get_all( + "Email Group Member", + filters={"unsubscribed": 0, "email_group": ("in", self.get_email_groups())}, + pluck="email", + ) + return list(set(emails)) + + def get_email_groups(self) -> list[str]: + # wondering why the 'or'? i can't figure out why both aren't equivalent - @gavin + return [x.email_group for x in self.email_group] or influxframework.get_all( + "Newsletter Email Group", + filters={"parent": self.name, "parenttype": "Newsletter"}, + pluck="email_group", + ) + + def get_attachments(self) -> list[dict[str, str]]: + return influxframework.get_all( + "File", + fields=["name", "file_name", "file_url", "is_private"], + filters={ + "attached_to_name": self.name, + "attached_to_doctype": "Newsletter", + "is_private": 0, + }, + ) + + +@influxframework.whitelist(allow_guest=True) +def confirmed_unsubscribe(email, group): + """unsubscribe the email(user) from the mailing list(email_group)""" + influxframework.flags.ignore_permissions = True + doc = influxframework.get_doc("Email Group Member", {"email": email, "email_group": group}) + if not doc.unsubscribed: + doc.unsubscribed = 1 + doc.save(ignore_permissions=True) + + +@influxframework.whitelist(allow_guest=True) +def subscribe(email, email_group=_("Website")): # noqa + """API endpoint to subscribe an email to a particular email group. Triggers a confirmation email.""" + + # build subscription confirmation URL + api_endpoint = influxframework.utils.get_url( + "/api/method/influxframework.email.doctype.newsletter.newsletter.confirm_subscription" + ) + signed_params = get_signed_params({"email": email, "email_group": email_group}) + confirm_subscription_url = f"{api_endpoint}?{signed_params}" + + # fetch custom template if available + email_confirmation_template = influxframework.db.get_value( + "Email Group", email_group, "confirmation_email_template" + ) + + # build email and send + if email_confirmation_template: + args = {"email": email, "confirmation_url": confirm_subscription_url, "email_group": email_group} + email_template = influxframework.get_doc("Email Template", email_confirmation_template) + email_subject = email_template.subject + content = influxframework.render_template(email_template.response, args) + else: + email_subject = _("Confirm Your Email") + translatable_content = ( + _("Thank you for your interest in subscribing to our updates"), + _("Please verify your Email Address"), + confirm_subscription_url, + _("Click here to verify"), + ) + content = """ +

      {}. {}.

      +

      {}

      + """.format( + *translatable_content + ) + + influxframework.sendmail( + email, + subject=email_subject, + content=content, + ) + + +@influxframework.whitelist(allow_guest=True) +def confirm_subscription(email, email_group=_("Website")): # noqa + """API endpoint to confirm email subscription. + This endpoint is called when user clicks on the link sent to their mail. + """ + if not verify_request(): + return + + if not influxframework.db.exists("Email Group", email_group): + influxframework.get_doc({"doctype": "Email Group", "title": email_group}).insert(ignore_permissions=True) + + influxframework.flags.ignore_permissions = True + + add_subscribers(email_group, email) + influxframework.db.commit() + + influxframework.respond_as_web_page( + _("Confirmed"), + _("{0} has been successfully added to the Email Group.").format(email), + indicator_color="green", + ) + + +def get_list_context(context=None): + context.update( + { + "show_search": True, + "no_breadcrumbs": True, + "title": _("Newsletters"), + "filters": {"published": 1}, + "row_template": "email/doctype/newsletter/templates/newsletter_row.html", + } + ) + + +def send_scheduled_email(): + """Send scheduled newsletter to the recipients.""" + scheduled_newsletter = influxframework.get_all( + "Newsletter", + filters={ + "schedule_send": ("<=", influxframework.utils.now_datetime()), + "email_sent": False, + "schedule_sending": True, + }, + ignore_ifnull=True, + pluck="name", + ) + + for newsletter_name in scheduled_newsletter: + try: + newsletter = influxframework.get_doc("Newsletter", newsletter_name) + newsletter.queue_all() + + except Exception: + influxframework.db.rollback() + + # wasn't able to send emails :( + influxframework.db.set_value("Newsletter", newsletter_name, "email_sent", 0) + newsletter.log_error("Failed to send newsletter") + + if not influxframework.flags.in_test: + influxframework.db.commit() diff --git a/influxframework/email/doctype/newsletter/newsletter_list.js b/influxframework/email/doctype/newsletter/newsletter_list.js new file mode 100644 index 0000000..1f4ba4a --- /dev/null +++ b/influxframework/email/doctype/newsletter/newsletter_list.js @@ -0,0 +1,12 @@ +influxframework.listview_settings["Newsletter"] = { + add_fields: ["subject", "email_sent", "schedule_sending"], + get_indicator: function (doc) { + if (doc.email_sent) { + return [__("Sent"), "green", "email_sent,=,Yes"]; + } else if (doc.schedule_sending) { + return [__("Scheduled"), "purple", "email_sent,=,No|schedule_sending,=,Yes"]; + } else { + return [__("Not Sent"), "gray", "email_sent,=,No"]; + } + }, +}; diff --git a/influxframework/email/doctype/newsletter/templates/newsletter.html b/influxframework/email/doctype/newsletter/templates/newsletter.html new file mode 100644 index 0000000..c83d8fe --- /dev/null +++ b/influxframework/email/doctype/newsletter/templates/newsletter.html @@ -0,0 +1,65 @@ +{% extends "templates/web.html" %} + +{% block title %} {{ doc.subject }} {% endblock %} + +{% block page_content %} + + +
      +
      +
      +

      {{ doc.subject }}

      +

      + {{ influxframework.format_date(doc.modified) }} +

      +
      +
      + {{ doc.get_message() }} +
      +
      + + {% if doc.attachments %} +
      +
      +
      + {{ _("Attachments") }} +
      +
      +
      +
      + {% for attachment in doc.attachments %} +

      + + {{ attachment.attachment }} + +

      + {% endfor %} +
      +
      +
      + {% endif %} + +
      +{% endblock %} diff --git a/influxframework/email/doctype/newsletter/templates/newsletter_row.html b/influxframework/email/doctype/newsletter/templates/newsletter_row.html new file mode 100644 index 0000000..34dcd1d --- /dev/null +++ b/influxframework/email/doctype/newsletter/templates/newsletter_row.html @@ -0,0 +1,15 @@ + diff --git a/influxframework/email/doctype/newsletter/test_newsletter.py b/influxframework/email/doctype/newsletter/test_newsletter.py new file mode 100644 index 0000000..2e595ef --- /dev/null +++ b/influxframework/email/doctype/newsletter/test_newsletter.py @@ -0,0 +1,248 @@ +# Copyright (c) 2021, InfluxFramework LLC +# MIT License. See LICENSE + +from random import choice +from unittest.mock import MagicMock, PropertyMock, patch + +import influxframework +from influxframework.email.doctype.newsletter.exceptions import ( + NewsletterAlreadySentError, + NoRecipientFoundError, +) +from influxframework.email.doctype.newsletter.newsletter import ( + Newsletter, + confirmed_unsubscribe, + send_scheduled_email, +) +from influxframework.email.queue import flush +from influxframework.tests.utils import InfluxFrameworkTestCase +from influxframework.utils import add_days, getdate + +emails = [ + "test_subscriber1@example.com", + "test_subscriber2@example.com", + "test_subscriber3@example.com", + "test1@example.com", +] +newsletters = [] + + +def get_dotted_path(obj: type) -> str: + klass = obj.__class__ + module = klass.__module__ + if module == "builtins": + return klass.__qualname__ # avoid outputs like 'builtins.str' + return f"{module}.{klass.__qualname__}" + + +class TestNewsletterMixin: + def setUp(self): + influxframework.set_user("Administrator") + self.setup_email_group() + + def tearDown(self): + influxframework.set_user("Administrator") + for newsletter in newsletters: + influxframework.db.delete( + "Email Queue", + { + "reference_doctype": "Newsletter", + "reference_name": newsletter, + }, + ) + influxframework.delete_doc("Newsletter", newsletter) + influxframework.db.delete("Newsletter Email Group", {"parent": newsletter}) + newsletters.remove(newsletter) + + def setup_email_group(self): + if not influxframework.db.exists("Email Group", "_Test Email Group"): + influxframework.get_doc({"doctype": "Email Group", "title": "_Test Email Group"}).insert() + + for email in emails: + doctype = "Email Group Member" + email_filters = {"email": email, "email_group": "_Test Email Group"} + + savepoint = "setup_email_group" + influxframework.db.savepoint(savepoint) + + try: + influxframework.get_doc( + { + "doctype": doctype, + **email_filters, + } + ).insert(ignore_if_duplicate=True) + except Exception: + influxframework.db.rollback(save_point=savepoint) + influxframework.db.update(doctype, email_filters, "unsubscribed", 0) + + influxframework.db.release_savepoint(savepoint) + + def send_newsletter(self, published=0, schedule_send=None) -> str | None: + influxframework.db.delete("Email Queue") + influxframework.db.delete("Email Queue Recipient") + influxframework.db.delete("Newsletter") + + newsletter_options = { + "published": published, + "schedule_sending": bool(schedule_send), + "schedule_send": schedule_send, + } + newsletter = self.get_newsletter(**newsletter_options) + + if schedule_send: + send_scheduled_email() + else: + newsletter.send_emails() + return newsletter.name + + @staticmethod + def get_newsletter(**kwargs) -> "Newsletter": + """Generate and return Newsletter object""" + doctype = "Newsletter" + newsletter_content = { + "subject": "_Test Newsletter", + "sender_name": "Test Sender", + "sender_email": "test_sender@example.com", + "content_type": "Rich Text", + "message": "Testing my news.", + } + similar_newsletters = influxframework.get_all(doctype, newsletter_content, pluck="name") + + for similar_newsletter in similar_newsletters: + influxframework.delete_doc(doctype, similar_newsletter) + + newsletter = influxframework.get_doc({"doctype": doctype, **newsletter_content, **kwargs}) + newsletter.append("email_group", {"email_group": "_Test Email Group"}) + newsletter.save(ignore_permissions=True) + newsletter.reload() + newsletters.append(newsletter.name) + + attached_files = influxframework.get_all( + "File", + { + "attached_to_doctype": newsletter.doctype, + "attached_to_name": newsletter.name, + }, + pluck="name", + ) + for file in attached_files: + influxframework.delete_doc("File", file) + + return newsletter + + +class TestNewsletter(TestNewsletterMixin, InfluxFrameworkTestCase): + def test_send(self): + self.send_newsletter() + + email_queue_list = [influxframework.get_doc("Email Queue", e.name) for e in influxframework.get_all("Email Queue")] + self.assertEqual(len(email_queue_list), 4) + + recipients = {e.recipients[0].recipient for e in email_queue_list} + self.assertTrue(set(emails).issubset(recipients)) + + def test_unsubscribe(self): + name = self.send_newsletter() + to_unsubscribe = choice(emails) + group = influxframework.get_all( + "Newsletter Email Group", filters={"parent": name}, fields=["email_group"] + ) + + flush(from_test=True) + confirmed_unsubscribe(to_unsubscribe, group[0].email_group) + + name = self.send_newsletter() + email_queue_list = [influxframework.get_doc("Email Queue", e.name) for e in influxframework.get_all("Email Queue")] + self.assertEqual(len(email_queue_list), 3) + recipients = [e.recipients[0].recipient for e in email_queue_list] + + for email in emails: + if email != to_unsubscribe: + self.assertTrue(email in recipients) + + def test_schedule_send(self): + self.send_newsletter(schedule_send=add_days(getdate(), -1)) + + email_queue_list = [influxframework.get_doc("Email Queue", e.name) for e in influxframework.get_all("Email Queue")] + self.assertEqual(len(email_queue_list), 4) + recipients = [e.recipients[0].recipient for e in email_queue_list] + for email in emails: + self.assertTrue(email in recipients) + + def test_newsletter_send_test_email(self): + """Test "Send Test Email" functionality of Newsletter""" + newsletter = self.get_newsletter() + test_email = choice(emails) + newsletter.send_test_email(test_email) + + self.assertFalse(newsletter.email_sent) + newsletter.save = MagicMock() + self.assertFalse(newsletter.save.called) + # check if the test email is in the queue + email_queue = influxframework.get_all( + "Email Queue", + filters=[ + ["reference_doctype", "=", "Newsletter"], + ["reference_name", "=", newsletter.name], + ["Email Queue Recipient", "recipient", "=", test_email], + ], + ) + self.assertTrue(email_queue) + + def test_newsletter_status(self): + """Test for Newsletter's stats on onload event""" + newsletter = self.get_newsletter() + newsletter.email_sent = True + result = newsletter.get_sending_status() + self.assertTrue("total" in result) + self.assertTrue("sent" in result) + + def test_already_sent_newsletter(self): + newsletter = self.get_newsletter() + newsletter.send_emails() + + with self.assertRaises(NewsletterAlreadySentError): + newsletter.send_emails() + + def test_newsletter_with_no_recipient(self): + newsletter = self.get_newsletter() + property_path = f"{get_dotted_path(newsletter)}.newsletter_recipients" + + with patch(property_path, new_callable=PropertyMock) as mock_newsletter_recipients: + mock_newsletter_recipients.return_value = [] + with self.assertRaises(NoRecipientFoundError): + newsletter.send_emails() + + def test_send_scheduled_email_error_handling(self): + newsletter = self.get_newsletter(schedule_send=add_days(getdate(), -1)) + job_path = "influxframework.email.doctype.newsletter.newsletter.Newsletter.queue_all" + m = MagicMock(side_effect=influxframework.OutgoingEmailError) + + with self.assertRaises(influxframework.OutgoingEmailError): + with patch(job_path, new_callable=m): + send_scheduled_email() + + newsletter.reload() + self.assertEqual(newsletter.email_sent, 0) + + def test_retry_partially_sent_newsletter(self): + influxframework.db.delete("Email Queue") + influxframework.db.delete("Email Queue Recipient") + influxframework.db.delete("Newsletter") + + newsletter = self.get_newsletter() + newsletter.send_emails() + email_queue_list = [influxframework.get_doc("Email Queue", e.name) for e in influxframework.get_all("Email Queue")] + self.assertEqual(len(email_queue_list), 4) + + # emulate partial send + email_queue_list[0].status = "Error" + email_queue_list[0].recipients[0].status = "Error" + email_queue_list[0].save() + newsletter.email_sent = False + + # retry + newsletter.send_emails() + email_queue_list = [influxframework.get_doc("Email Queue", e.name) for e in influxframework.get_all("Email Queue")] + self.assertEqual(len(email_queue_list), 5) diff --git a/influxframework/email/doctype/newsletter_attachment/__init__.py b/influxframework/email/doctype/newsletter_attachment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/email/doctype/newsletter_attachment/newsletter_attachment.json b/influxframework/email/doctype/newsletter_attachment/newsletter_attachment.json new file mode 100644 index 0000000..e2add0e --- /dev/null +++ b/influxframework/email/doctype/newsletter_attachment/newsletter_attachment.json @@ -0,0 +1,31 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2021-12-06 16:37:40.652468", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "attachment" + ], + "fields": [ + { + "fieldname": "attachment", + "fieldtype": "Attach", + "in_list_view": 1, + "label": "Attachment", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-12-06 16:37:47.481057", + "modified_by": "Administrator", + "module": "Email", + "name": "Newsletter Attachment", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/influxframework/email/doctype/newsletter_attachment/newsletter_attachment.py b/influxframework/email/doctype/newsletter_attachment/newsletter_attachment.py new file mode 100644 index 0000000..66f5c2b --- /dev/null +++ b/influxframework/email/doctype/newsletter_attachment/newsletter_attachment.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, InfluxFramework LLC +# For license information, please see license.txt + +# import influxframework +from influxframework.model.document import Document + + +class NewsletterAttachment(Document): + pass diff --git a/influxframework/email/doctype/newsletter_email_group/__init__.py b/influxframework/email/doctype/newsletter_email_group/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/email/doctype/newsletter_email_group/newsletter_email_group.json b/influxframework/email/doctype/newsletter_email_group/newsletter_email_group.json new file mode 100644 index 0000000..b8c1afe --- /dev/null +++ b/influxframework/email/doctype/newsletter_email_group/newsletter_email_group.json @@ -0,0 +1,42 @@ +{ + "actions": [], + "creation": "2017-02-26 16:20:52.654136", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "email_group", + "total_subscribers" + ], + "fields": [ + { + "columns": 7, + "fieldname": "email_group", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Email Group", + "options": "Email Group", + "reqd": 1 + }, + { + "columns": 3, + "fetch_from": "email_group.total_subscribers", + "fieldname": "total_subscribers", + "fieldtype": "Read Only", + "in_list_view": 1, + "label": "Total Subscribers" + } + ], + "istable": 1, + "links": [], + "modified": "2021-12-06 20:12:08.420240", + "modified_by": "Administrator", + "module": "Email", + "name": "Newsletter Email Group", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/email/doctype/newsletter_email_group/newsletter_email_group.py b/influxframework/email/doctype/newsletter_email_group/newsletter_email_group.py new file mode 100644 index 0000000..f643927 --- /dev/null +++ b/influxframework/email/doctype/newsletter_email_group/newsletter_email_group.py @@ -0,0 +1,8 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +from influxframework.model.document import Document + + +class NewsletterEmailGroup(Document): + pass diff --git a/influxframework/email/doctype/notification/__init__.py b/influxframework/email/doctype/notification/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/email/doctype/notification/notification.js b/influxframework/email/doctype/notification/notification.js new file mode 100644 index 0000000..eb11660 --- /dev/null +++ b/influxframework/email/doctype/notification/notification.js @@ -0,0 +1,202 @@ +// Copyright (c) 2018, InfluxFramework LLC +// For license information, please see license.txt + +this.frm.add_fetch("sender", "email_id", "sender_email"); + +this.frm.fields_dict.sender.get_query = function () { + return { + filters: { + enable_outgoing: 1, + }, + }; +}; + +influxframework.notification = { + setup_fieldname_select: function (frm) { + // get the doctype to update fields + if (!frm.doc.document_type) { + return; + } + + influxframework.model.with_doctype(frm.doc.document_type, function () { + let get_select_options = function (df, parent_field) { + // Append parent_field name along with fieldname for child table fields + let select_value = parent_field ? df.fieldname + "," + parent_field : df.fieldname; + + return { + value: select_value, + label: df.fieldname + " (" + __(df.label) + ")", + }; + }; + + let get_date_change_options = function () { + let date_options = $.map(fields, function (d) { + return d.fieldtype == "Date" || d.fieldtype == "Datetime" + ? get_select_options(d) + : null; + }); + // append creation and modified date to Date Change field + return date_options.concat([ + { value: "creation", label: `creation (${__("Created On")})` }, + { value: "modified", label: `modified (${__("Last Modified Date")})` }, + ]); + }; + + let fields = influxframework.get_doc("DocType", frm.doc.document_type).fields; + let options = $.map(fields, function (d) { + return in_list(influxframework.model.no_value_type, d.fieldtype) + ? null + : get_select_options(d); + }); + + // set value changed options + frm.set_df_property("value_changed", "options", [""].concat(options)); + frm.set_df_property("set_property_after_alert", "options", [""].concat(options)); + + // set date changed options + frm.set_df_property("date_changed", "options", get_date_change_options()); + + let receiver_fields = []; + if (frm.doc.channel === "Email") { + receiver_fields = $.map(fields, function (d) { + // Add User and Email fields from child into select dropdown + if (d.fieldtype == "Table") { + let child_fields = influxframework.get_doc("DocType", d.options).fields; + return $.map(child_fields, function (df) { + return df.options == "Email" || + (df.options == "User" && df.fieldtype == "Link") + ? get_select_options(df, d.fieldname) + : null; + }); + // Add User and Email fields from parent into select dropdown + } else { + return d.options == "Email" || + (d.options == "User" && d.fieldtype == "Link") + ? get_select_options(d) + : null; + } + }); + } else if (in_list(["WhatsApp", "SMS"], frm.doc.channel)) { + receiver_fields = $.map(fields, function (d) { + return d.options == "Phone" ? get_select_options(d) : null; + }); + } + + // set email recipient options + frm.fields_dict.recipients.grid.update_docfield_property( + "receiver_by_document_field", + "options", + [""].concat(["owner"]).concat(receiver_fields) + ); + }); + }, + setup_example_message: function (frm) { + let template = ""; + if (frm.doc.channel === "Email") { + template = `
      Message Example
      + +
      <h3>Order Overdue</h3>
      +
      +<p>Transaction {{ doc.name }} has exceeded Due Date. Please take necessary action.</p>
      +
      +<!-- show last comment -->
      +{% if comments %}
      +Last comment: {{ comments[-1].comment }} by {{ comments[-1].by }}
      +{% endif %}
      +
      +<h4>Details</h4>
      +
      +<ul>
      +<li>Customer: {{ doc.customer }}
      +<li>Amount: {{ doc.grand_total }}
      +</ul>
      +
      + `; + } else if (in_list(["Slack", "System Notification", "SMS"], frm.doc.channel)) { + template = `
      Message Example
      + +
      *Order Overdue*
      +
      +Transaction {{ doc.name }} has exceeded Due Date. Please take necessary action.
      +
      +
      +{% if comments %}
      +Last comment: {{ comments[-1].comment }} by {{ comments[-1].by }}
      +{% endif %}
      +
      +*Details*
      +
      +• Customer: {{ doc.customer }}
      +• Amount: {{ doc.grand_total }}
      +
      `; + } + if (template) { + frm.set_df_property("message_examples", "options", template); + } + }, +}; + +influxframework.ui.form.on("Notification", { + onload: function (frm) { + frm.set_query("document_type", function () { + return { + filters: { + istable: 0, + }, + }; + }); + frm.set_query("print_format", function () { + return { + filters: { + doc_type: frm.doc.document_type, + }, + }; + }); + }, + refresh: function (frm) { + influxframework.notification.setup_fieldname_select(frm); + influxframework.notification.setup_example_message(frm); + frm.get_field("is_standard").toggle(influxframework.boot.developer_mode); + frm.trigger("event"); + }, + document_type: function (frm) { + influxframework.notification.setup_fieldname_select(frm); + }, + view_properties: function (frm) { + influxframework.route_options = { doc_type: frm.doc.document_type }; + influxframework.set_route("Form", "Customize Form"); + }, + event: function (frm) { + if (in_list(["Days Before", "Days After"], frm.doc.event)) { + frm.add_custom_button(__("Get Alerts for Today"), function () { + influxframework.call({ + method: "influxframework.email.doctype.notification.notification.get_documents_for_today", + args: { + notification: frm.doc.name, + }, + callback: function (r) { + if (r.message && r.message.length > 0) { + influxframework.msgprint(r.message.toString()); + } else { + influxframework.msgprint(__("No alerts for today")); + } + }, + }); + }); + } + }, + channel: function (frm) { + frm.toggle_reqd("recipients", frm.doc.channel == "Email"); + influxframework.notification.setup_fieldname_select(frm); + influxframework.notification.setup_example_message(frm); + if (frm.doc.channel === "SMS" && frm.doc.__islocal) { + frm.set_df_property( + "channel", + "description", + `To use SMS Channel, initialize SMS Settings.` + ); + } else { + frm.set_df_property("channel", "description", ` `); + } + }, +}); diff --git a/influxframework/email/doctype/notification/notification.json b/influxframework/email/doctype/notification/notification.json new file mode 100644 index 0000000..8b6900a --- /dev/null +++ b/influxframework/email/doctype/notification/notification.json @@ -0,0 +1,306 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "Prompt", + "creation": "2014-07-11 17:18:09.923399", + "doctype": "DocType", + "document_type": "System", + "engine": "InnoDB", + "field_order": [ + "enabled", + "column_break_2", + "channel", + "slack_webhook_url", + "filters", + "subject", + "document_type", + "is_standard", + "module", + "col_break_1", + "event", + "method", + "date_changed", + "days_in_advance", + "value_changed", + "sender", + "send_system_notification", + "sender_email", + "section_break_9", + "condition", + "column_break_6", + "html_7", + "property_section", + "set_property_after_alert", + "property_value", + "column_break_5", + "send_to_all_assignees", + "recipients", + "message_sb", + "message", + "message_examples", + "view_properties", + "column_break_25", + "attach_print", + "print_format" + ], + "fields": [ + { + "default": "1", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enabled" + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "default": "Email", + "depends_on": "eval: !doc.disable_channel", + "fieldname": "channel", + "fieldtype": "Select", + "label": "Channel", + "options": "Email\nSlack\nSystem Notification\nSMS", + "reqd": 1, + "set_only_once": 1 + }, + { + "depends_on": "eval:doc.channel=='Slack'", + "description": "To use Slack Channel, add a Slack Webhook URL.", + "fieldname": "slack_webhook_url", + "fieldtype": "Link", + "label": "Slack Channel", + "mandatory_depends_on": "eval:doc.channel=='Slack'", + "options": "Slack Webhook URL" + }, + { + "fieldname": "filters", + "fieldtype": "Section Break", + "label": "Filters" + }, + { + "depends_on": "eval: in_list(['Email', 'Slack', 'System Notification'], doc.channel)", + "description": "To add dynamic subject, use jinja tags like\n\n
      {{ doc.name }} Delivered
      ", + "fieldname": "subject", + "fieldtype": "Data", + "ignore_xss_filter": 1, + "in_list_view": 1, + "label": "Subject", + "mandatory_depends_on": "eval: in_list(['Email', 'Slack', 'System Notification'], doc.channel)" + }, + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Document Type", + "options": "DocType", + "reqd": 1, + "search_index": 1 + }, + { + "default": "0", + "fieldname": "is_standard", + "fieldtype": "Check", + "label": "Is Standard", + "no_copy": 1 + }, + { + "depends_on": "is_standard", + "fieldname": "module", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Module", + "options": "Module Def" + }, + { + "fieldname": "col_break_1", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval: doc.document_type", + "fieldname": "event", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Send Alert On", + "options": "\nNew\nSave\nSubmit\nCancel\nDays After\nDays Before\nValue Change\nMethod\nCustom", + "reqd": 1, + "search_index": 1 + }, + { + "depends_on": "eval:doc.event=='Method'", + "description": "Trigger on valid methods like \"before_insert\", \"after_update\", etc (will depend on the DocType selected)", + "fieldname": "method", + "fieldtype": "Data", + "label": "Trigger Method" + }, + { + "depends_on": "eval:doc.document_type && (doc.event==\"Days After\" || doc.event==\"Days Before\")", + "description": "Send alert if date matches this field's value", + "fieldname": "date_changed", + "fieldtype": "Select", + "label": "Reference Date" + }, + { + "default": "0", + "depends_on": "eval:doc.document_type && (doc.event==\"Days After\" || doc.event==\"Days Before\")", + "description": "Send days before or after the reference date", + "fieldname": "days_in_advance", + "fieldtype": "Int", + "label": "Days Before or After" + }, + { + "depends_on": "eval:doc.document_type && doc.event==\"Value Change\"", + "description": "Send alert if this field's value changes", + "fieldname": "value_changed", + "fieldtype": "Select", + "label": "Value Changed" + }, + { + "depends_on": "eval: doc.channel == 'Email'", + "fieldname": "sender", + "fieldtype": "Link", + "label": "Sender", + "options": "Email Account" + }, + { + "fieldname": "sender_email", + "fieldtype": "Data", + "label": "Sender Email", + "options": "Email", + "read_only": 1 + }, + { + "fieldname": "section_break_9", + "fieldtype": "Section Break" + }, + { + "description": "Optional: The alert will be sent if this expression is true", + "fieldname": "condition", + "fieldtype": "Code", + "ignore_xss_filter": 1, + "in_list_view": 1, + "label": "Condition" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "fieldname": "html_7", + "fieldtype": "HTML", + "options": "

      Condition Examples:

      \n
      doc.status==\"Open\"
      doc.due_date==nowdate()
      doc.total > 40000\n
      \n" + }, + { + "collapsible": 1, + "fieldname": "property_section", + "fieldtype": "Section Break", + "label": "Set Property After Alert" + }, + { + "fieldname": "set_property_after_alert", + "fieldtype": "Select", + "label": "Set Property After Alert" + }, + { + "fieldname": "property_value", + "fieldtype": "Data", + "label": "Value To Be Set" + }, + { + "depends_on": "eval:doc.channel !=\"Slack\"", + "fieldname": "column_break_5", + "fieldtype": "Section Break", + "label": "Recipients" + }, + { + "fieldname": "recipients", + "fieldtype": "Table", + "label": "Recipients", + "mandatory_depends_on": "eval:doc.channel!=='Slack' && !doc.send_to_all_assignees", + "options": "Notification Recipient" + }, + { + "fieldname": "message_sb", + "fieldtype": "Section Break", + "label": "Message" + }, + { + "default": "Add your message here", + "fieldname": "message", + "fieldtype": "Code", + "ignore_xss_filter": 1, + "label": "Message" + }, + { + "fieldname": "message_examples", + "fieldtype": "HTML", + "label": "Message Examples", + "options": "
      Message Example
      \n\n
      <h3>Order Overdue</h3>\n\n<p>Transaction {{ doc.name }} has exceeded Due Date. Please take necessary action.</p>\n\n<!-- show last comment -->\n{% if comments %}\nLast comment: {{ comments[-1].comment }} by {{ comments[-1].by }}\n{% endif %}\n\n<h4>Details</h4>\n\n<ul>\n<li>Customer: {{ doc.customer }}\n<li>Amount: {{ doc.grand_total }}\n</ul>\n
      " + }, + { + "fieldname": "view_properties", + "fieldtype": "Button", + "label": "View Properties (via Customize Form)" + }, + { + "collapsible": 1, + "collapsible_depends_on": "attach_print", + "fieldname": "column_break_25", + "fieldtype": "Section Break", + "label": "Print Settings" + }, + { + "default": "0", + "fieldname": "attach_print", + "fieldtype": "Check", + "label": "Attach Print" + }, + { + "depends_on": "attach_print", + "fieldname": "print_format", + "fieldtype": "Link", + "label": "Print Format", + "options": "Print Format" + }, + { + "default": "0", + "depends_on": "eval: doc.channel !== 'System Notification'", + "description": "If enabled, the notification will show up in the notifications dropdown on the top right corner of the navigation bar.", + "fieldname": "send_system_notification", + "fieldtype": "Check", + "label": "Send System Notification" + }, + { + "default": "0", + "depends_on": "eval:doc.channel == 'Email'", + "fieldname": "send_to_all_assignees", + "fieldtype": "Check", + "label": "Send To All Assignees" + } + ], + "icon": "fa fa-envelope", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-05-04 11:17:11.882314", + "modified_by": "Administrator", + "module": "Email", + "name": "Notification", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "export": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "subject", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/email/doctype/notification/notification.py b/influxframework/email/doctype/notification/notification.py new file mode 100644 index 0000000..b4dbe0a --- /dev/null +++ b/influxframework/email/doctype/notification/notification.py @@ -0,0 +1,485 @@ +# Copyright (c) 2018, InfluxFramework LLC +# License: MIT. See LICENSE + +import json +import os + +import influxframework +from influxframework import _ +from influxframework.core.doctype.role.role import get_info_based_on_role, get_user_info +from influxframework.core.doctype.sms_settings.sms_settings import send_sms +from influxframework.desk.doctype.notification_log.notification_log import enqueue_create_notification +from influxframework.integrations.doctype.slack_webhook_url.slack_webhook_url import send_slack_message +from influxframework.model.document import Document +from influxframework.modules.utils import export_module_json, get_doc_module +from influxframework.utils import add_to_date, cast, is_html, nowdate, validate_email_address +from influxframework.utils.jinja import validate_template +from influxframework.utils.safe_exec import get_safe_globals + + +class Notification(Document): + def onload(self): + """load message""" + if self.is_standard: + self.message = self.get_template() + + def autoname(self): + if not self.name: + self.name = self.subject + + def validate(self): + if self.channel in ("Email", "Slack", "System Notification"): + validate_template(self.subject) + + validate_template(self.message) + + if self.event in ("Days Before", "Days After") and not self.date_changed: + influxframework.throw(_("Please specify which date field must be checked")) + + if self.event == "Value Change" and not self.value_changed: + influxframework.throw(_("Please specify which value field must be checked")) + + self.validate_forbidden_types() + self.validate_condition() + self.validate_standard() + influxframework.cache().hdel("notifications", self.document_type) + + def on_update(self): + path = export_module_json(self, self.is_standard, self.module) + if path: + # js + if not os.path.exists(path + ".md") and not os.path.exists(path + ".html"): + with open(path + ".md", "w") as f: + f.write(self.message) + + # py + if not os.path.exists(path + ".py"): + with open(path + ".py", "w") as f: + f.write( + """import influxframework + +def get_context(context): + # do your magic here + pass +""" + ) + + def validate_standard(self): + if self.is_standard and self.enabled and not influxframework.conf.developer_mode: + influxframework.throw( + _("Cannot edit Standard Notification. To edit, please disable this and duplicate it") + ) + + def validate_condition(self): + temp_doc = influxframework.new_doc(self.document_type) + if self.condition: + try: + influxframework.safe_eval(self.condition, None, get_context(temp_doc.as_dict())) + except Exception: + influxframework.throw(_("The Condition '{0}' is invalid").format(self.condition)) + + def validate_forbidden_types(self): + forbidden_document_types = ("Email Queue",) + if self.document_type in forbidden_document_types or influxframework.get_meta(self.document_type).istable: + # currently notifications don't work on child tables as events are not fired for each record of child table + + influxframework.throw(_("Cannot set Notification on Document Type {0}").format(self.document_type)) + + def get_documents_for_today(self): + """get list of documents that will be triggered today""" + docs = [] + + diff_days = self.days_in_advance + if self.event == "Days After": + diff_days = -diff_days + + reference_date = add_to_date(nowdate(), days=diff_days) + reference_date_start = reference_date + " 00:00:00.000000" + reference_date_end = reference_date + " 23:59:59.000000" + + doc_list = influxframework.get_all( + self.document_type, + fields="name", + filters=[ + {self.date_changed: (">=", reference_date_start)}, + {self.date_changed: ("<=", reference_date_end)}, + ], + ) + + for d in doc_list: + doc = influxframework.get_doc(self.document_type, d.name) + + if self.condition and not influxframework.safe_eval(self.condition, None, get_context(doc)): + continue + + docs.append(doc) + + return docs + + def send(self, doc): + """Build recipients and send Notification""" + + context = get_context(doc) + context = {"doc": doc, "alert": self, "comments": None} + if doc.get("_comments"): + context["comments"] = json.loads(doc.get("_comments")) + + if self.is_standard: + self.load_standard_properties(context) + try: + if self.channel == "Email": + self.send_an_email(doc, context) + + if self.channel == "Slack": + self.send_a_slack_msg(doc, context) + + if self.channel == "SMS": + self.send_sms(doc, context) + + if self.channel == "System Notification" or self.send_system_notification: + self.create_system_notification(doc, context) + + except Exception: + self.log_error("Failed to send Notification") + + if self.set_property_after_alert: + allow_update = True + if ( + doc.docstatus.is_submitted() + and not doc.meta.get_field(self.set_property_after_alert).allow_on_submit + ): + allow_update = False + try: + if allow_update and not doc.flags.in_notification_update: + fieldname = self.set_property_after_alert + value = self.property_value + if doc.meta.get_field(fieldname).fieldtype in influxframework.model.numeric_fieldtypes: + value = influxframework.utils.cint(value) + + doc.reload() + doc.set(fieldname, value) + doc.flags.updater_reference = { + "doctype": self.doctype, + "docname": self.name, + "label": _("via Notification"), + } + doc.flags.in_notification_update = True + doc.save(ignore_permissions=True) + doc.flags.in_notification_update = False + except Exception: + self.log_error("Document update failed") + + def create_system_notification(self, doc, context): + subject = self.subject + if "{" in subject: + subject = influxframework.render_template(self.subject, context) + + attachments = self.get_attachment(doc) + + recipients, cc, bcc = self.get_list_of_recipients(doc, context) + + users = recipients + cc + bcc + + if not users: + return + + notification_doc = { + "type": "Alert", + "document_type": doc.doctype, + "document_name": doc.name, + "subject": subject, + "from_user": doc.modified_by or doc.owner, + "email_content": influxframework.render_template(self.message, context), + "attached_file": attachments and json.dumps(attachments[0]), + } + enqueue_create_notification(users, notification_doc) + + def send_an_email(self, doc, context): + from email.utils import formataddr + + from influxframework.core.doctype.communication.email import _make as make_communication + + subject = self.subject + if "{" in subject: + subject = influxframework.render_template(self.subject, context) + + attachments = self.get_attachment(doc) + recipients, cc, bcc = self.get_list_of_recipients(doc, context) + if not (recipients or cc or bcc): + return + + sender = None + message = influxframework.render_template(self.message, context) + if self.sender and self.sender_email: + sender = formataddr((self.sender, self.sender_email)) + influxframework.sendmail( + recipients=recipients, + subject=subject, + sender=sender, + cc=cc, + bcc=bcc, + message=message, + reference_doctype=doc.doctype, + reference_name=doc.name, + attachments=attachments, + expose_recipients="header", + print_letterhead=((attachments and attachments[0].get("print_letterhead")) or False), + ) + + # Add mail notification to communication list + # No need to add if it is already a communication. + if doc.doctype != "Communication": + make_communication( + doctype=doc.doctype, + name=doc.name, + content=message, + subject=subject, + sender=sender, + recipients=recipients, + communication_medium="Email", + send_email=False, + attachments=attachments, + cc=cc, + bcc=bcc, + communication_type="Automated Message", + ) + + def send_a_slack_msg(self, doc, context): + send_slack_message( + webhook_url=self.slack_webhook_url, + message=influxframework.render_template(self.message, context), + reference_doctype=doc.doctype, + reference_name=doc.name, + ) + + def send_sms(self, doc, context): + send_sms( + receiver_list=self.get_receiver_list(doc, context), + msg=influxframework.render_template(self.message, context), + ) + + def get_list_of_recipients(self, doc, context): + recipients = [] + cc = [] + bcc = [] + for recipient in self.recipients: + if recipient.condition: + if not influxframework.safe_eval(recipient.condition, None, context): + continue + if recipient.receiver_by_document_field: + fields = recipient.receiver_by_document_field.split(",") + # fields from child table + if len(fields) > 1: + for d in doc.get(fields[1]): + email_id = d.get(fields[0]) + if validate_email_address(email_id): + recipients.append(email_id) + # field from parent doc + else: + email_ids_value = doc.get(fields[0]) + if validate_email_address(email_ids_value): + email_ids = email_ids_value.replace(",", "\n") + recipients = recipients + email_ids.split("\n") + + if recipient.cc and "{" in recipient.cc: + recipient.cc = influxframework.render_template(recipient.cc, context) + + if recipient.cc: + recipient.cc = recipient.cc.replace(",", "\n") + cc = cc + recipient.cc.split("\n") + + if recipient.bcc and "{" in recipient.bcc: + recipient.bcc = influxframework.render_template(recipient.bcc, context) + + if recipient.bcc: + recipient.bcc = recipient.bcc.replace(",", "\n") + bcc = bcc + recipient.bcc.split("\n") + + # For sending emails to specified role + if recipient.receiver_by_role: + emails = get_info_based_on_role(recipient.receiver_by_role, "email") + + for email in emails: + recipients = recipients + email.split("\n") + + if self.send_to_all_assignees: + recipients = recipients + get_assignees(doc) + + return list(set(recipients)), list(set(cc)), list(set(bcc)) + + def get_receiver_list(self, doc, context): + """return receiver list based on the doc field and role specified""" + receiver_list = [] + for recipient in self.recipients: + if recipient.condition: + if not influxframework.safe_eval(recipient.condition, None, context): + continue + + # For sending messages to the owner's mobile phone number + if recipient.receiver_by_document_field == "owner": + receiver_list += get_user_info([dict(user_name=doc.get("owner"))], "mobile_no") + # For sending messages to the number specified in the receiver field + elif recipient.receiver_by_document_field: + receiver_list.append(doc.get(recipient.receiver_by_document_field)) + + # For sending messages to specified role + if recipient.receiver_by_role: + receiver_list += get_info_based_on_role(recipient.receiver_by_role, "mobile_no") + + return receiver_list + + def get_attachment(self, doc): + """check print settings are attach the pdf""" + if not self.attach_print: + return None + + print_settings = influxframework.get_doc("Print Settings", "Print Settings") + if (doc.docstatus == 0 and not print_settings.allow_print_for_draft) or ( + doc.docstatus == 2 and not print_settings.allow_print_for_cancelled + ): + + # ignoring attachment as draft and cancelled documents are not allowed to print + status = "Draft" if doc.docstatus == 0 else "Cancelled" + influxframework.throw( + _( + """Not allowed to attach {0} document, please enable Allow Print For {0} in Print Settings""" + ).format(status), + title=_("Error in Notification"), + ) + else: + return [ + { + "print_format_attachment": 1, + "doctype": doc.doctype, + "name": doc.name, + "print_format": self.print_format, + "print_letterhead": print_settings.with_letterhead, + "lang": influxframework.db.get_value("Print Format", self.print_format, "default_print_language") + if self.print_format + else "en", + } + ] + + def get_template(self): + module = get_doc_module(self.module, self.doctype, self.name) + + def load_template(extn): + template = "" + template_path = os.path.join(os.path.dirname(module.__file__), influxframework.scrub(self.name) + extn) + if os.path.exists(template_path): + with open(template_path) as f: + template = f.read() + return template + + return load_template(".html") or load_template(".md") + + def load_standard_properties(self, context): + """load templates and run get_context""" + module = get_doc_module(self.module, self.doctype, self.name) + if module: + if hasattr(module, "get_context"): + out = module.get_context(context) + if out: + context.update(out) + + self.message = self.get_template() + + if not is_html(self.message): + self.message = influxframework.utils.md_to_html(self.message) + + def on_trash(self): + influxframework.cache().hdel("notifications", self.document_type) + + +@influxframework.whitelist() +def get_documents_for_today(notification): + notification = influxframework.get_doc("Notification", notification) + notification.check_permission("read") + return [d.name for d in notification.get_documents_for_today()] + + +def trigger_daily_alerts(): + trigger_notifications(None, "daily") + + +def trigger_notifications(doc, method=None): + if influxframework.flags.in_import or influxframework.flags.in_patch: + # don't send notifications while syncing or patching + return + + if method == "daily": + doc_list = influxframework.get_all( + "Notification", filters={"event": ("in", ("Days Before", "Days After")), "enabled": 1} + ) + for d in doc_list: + alert = influxframework.get_doc("Notification", d.name) + + for doc in alert.get_documents_for_today(): + evaluate_alert(doc, alert, alert.event) + influxframework.db.commit() + + +def evaluate_alert(doc: Document, alert, event): + from jinja2 import TemplateError + + try: + if isinstance(alert, str): + alert = influxframework.get_doc("Notification", alert) + + context = get_context(doc) + + if alert.condition: + if not influxframework.safe_eval(alert.condition, None, context): + return + + if event == "Value Change" and not doc.is_new(): + if not influxframework.db.has_column(doc.doctype, alert.value_changed): + alert.db_set("enabled", 0) + alert.log_error(f"Notification {alert.name} has been disabled due to missing field") + return + + doc_before_save = doc.get_doc_before_save() + field_value_before_save = doc_before_save.get(alert.value_changed) if doc_before_save else None + + fieldtype = doc.meta.get_field(alert.value_changed).fieldtype + if cast(fieldtype, doc.get(alert.value_changed)) == cast(fieldtype, field_value_before_save): + # value not changed + return + + if event != "Value Change" and not doc.is_new(): + # reload the doc for the latest values & comments, + # except for validate type event. + doc.reload() + alert.send(doc) + except TemplateError: + influxframework.throw( + _("Error while evaluating Notification {0}. Please fix your template.").format(alert) + ) + except Exception as e: + error_log = influxframework.log_error(message=influxframework.get_traceback(), title=str(e)) + influxframework.throw( + _("Error in Notification: {}").format( + influxframework.utils.get_link_to_form("Error Log", error_log.name) + ) + ) + + +def get_context(doc): + return { + "doc": doc, + "nowdate": nowdate, + "influxframework": influxframework._dict(utils=get_safe_globals().get("influxframework").get("utils")), + } + + +def get_assignees(doc): + assignees = [] + assignees = influxframework.get_all( + "ToDo", + filters={"status": "Open", "reference_name": doc.name, "reference_type": doc.doctype}, + fields=["allocated_to"], + ) + + recipients = [d.allocated_to for d in assignees] + + return recipients diff --git a/influxframework/email/doctype/notification/test_notification.py b/influxframework/email/doctype/notification/test_notification.py new file mode 100644 index 0000000..5e23fe6 --- /dev/null +++ b/influxframework/email/doctype/notification/test_notification.py @@ -0,0 +1,385 @@ +# Copyright (c) 2018, InfluxFramework LLC +# License: MIT. See LICENSE + +from contextlib import contextmanager + +import influxframework +import influxframework.utils +import influxframework.utils.scheduler +from influxframework.desk.form import assign_to +from influxframework.tests.utils import InfluxFrameworkTestCase + +test_dependencies = ["User", "Notification"] + + +@contextmanager +def get_test_notification(config): + try: + notification = influxframework.get_doc(doctype="Notification", **config).insert() + yield notification + finally: + notification.delete() + + +class TestNotification(InfluxFrameworkTestCase): + def setUp(self): + influxframework.db.delete("Email Queue") + influxframework.set_user("test@example.com") + + if not influxframework.db.exists("Notification", {"name": "ToDo Status Update"}, "name"): + notification = influxframework.new_doc("Notification") + notification.name = "ToDo Status Update" + notification.subject = "ToDo Status Update" + notification.document_type = "ToDo" + notification.event = "Value Change" + notification.value_changed = "status" + notification.send_to_all_assignees = 1 + notification.set_property_after_alert = "description" + notification.property_value = "Changed by Notification" + notification.save() + + if not influxframework.db.exists("Notification", {"name": "Contact Status Update"}, "name"): + notification = influxframework.new_doc("Notification") + notification.name = "Contact Status Update" + notification.subject = "Contact Status Update" + notification.document_type = "Contact" + notification.event = "Value Change" + notification.value_changed = "status" + notification.message = "Test Contact Update" + notification.append("recipients", {"receiver_by_document_field": "email_id,email_ids"}) + notification.save() + + def tearDown(self): + influxframework.set_user("Administrator") + + def test_new_and_save(self): + """Check creating a new communication triggers a notification.""" + communication = influxframework.new_doc("Communication") + communication.communication_type = "Comment" + communication.subject = "test" + communication.content = "test" + communication.insert(ignore_permissions=True) + + self.assertTrue( + influxframework.db.get_value( + "Email Queue", + { + "reference_doctype": "Communication", + "reference_name": communication.name, + "status": "Not Sent", + }, + ) + ) + influxframework.db.delete("Email Queue") + + communication.reload() + communication.content = "test 2" + communication.save() + + self.assertTrue( + influxframework.db.get_value( + "Email Queue", + { + "reference_doctype": "Communication", + "reference_name": communication.name, + "status": "Not Sent", + }, + ) + ) + + self.assertEqual( + influxframework.db.get_value("Communication", communication.name, "subject"), "__testing__" + ) + + def test_condition(self): + """Check notification is triggered based on a condition.""" + event = influxframework.new_doc("Event") + event.subject = "test" + event.event_type = "Private" + event.starts_on = "2014-06-06 12:00:00" + event.insert() + + self.assertFalse( + influxframework.db.get_value( + "Email Queue", + {"reference_doctype": "Event", "reference_name": event.name, "status": "Not Sent"}, + ) + ) + + event.event_type = "Public" + event.save() + + self.assertTrue( + influxframework.db.get_value( + "Email Queue", + {"reference_doctype": "Event", "reference_name": event.name, "status": "Not Sent"}, + ) + ) + + # Make sure that we track the triggered notifications in communication doctype. + self.assertTrue( + influxframework.db.get_value( + "Communication", + { + "reference_doctype": "Event", + "reference_name": event.name, + "communication_type": "Automated Message", + }, + ) + ) + + def test_invalid_condition(self): + influxframework.set_user("Administrator") + notification = influxframework.new_doc("Notification") + notification.subject = "test" + notification.document_type = "ToDo" + notification.send_alert_on = "New" + notification.message = "test" + + recipent = influxframework.new_doc("Notification Recipient") + recipent.receiver_by_document_field = "owner" + + notification.recipents = recipent + notification.condition = "test" + + self.assertRaises(influxframework.ValidationError, notification.save) + notification.delete() + + def test_value_changed(self): + event = influxframework.new_doc("Event") + event.subject = "test" + event.event_type = "Private" + event.starts_on = "2014-06-06 12:00:00" + event.insert() + + self.assertFalse( + influxframework.db.get_value( + "Email Queue", + {"reference_doctype": "Event", "reference_name": event.name, "status": "Not Sent"}, + ) + ) + + event.subject = "test 1" + event.save() + + self.assertFalse( + influxframework.db.get_value( + "Email Queue", + {"reference_doctype": "Event", "reference_name": event.name, "status": "Not Sent"}, + ) + ) + + event.description = "test" + event.save() + + self.assertTrue( + influxframework.db.get_value( + "Email Queue", + {"reference_doctype": "Event", "reference_name": event.name, "status": "Not Sent"}, + ) + ) + + def test_alert_disabled_on_wrong_field(self): + influxframework.set_user("Administrator") + notification = influxframework.get_doc( + { + "doctype": "Notification", + "subject": "_Test Notification for wrong field", + "document_type": "Event", + "event": "Value Change", + "attach_print": 0, + "value_changed": "description1", + "message": "Description changed", + "recipients": [{"receiver_by_document_field": "owner"}], + } + ).insert() + influxframework.db.commit() + + event = influxframework.new_doc("Event") + event.subject = "test-2" + event.event_type = "Private" + event.starts_on = "2014-06-06 12:00:00" + event.insert() + event.subject = "test 1" + event.save() + + # verify that notification is disabled + notification.reload() + self.assertEqual(notification.enabled, 0) + notification.delete() + event.delete() + + def test_date_changed(self): + event = influxframework.new_doc("Event") + event.subject = "test" + event.event_type = "Private" + event.starts_on = "2014-01-01 12:00:00" + event.insert() + + self.assertFalse( + influxframework.db.get_value( + "Email Queue", + {"reference_doctype": "Event", "reference_name": event.name, "status": "Not Sent"}, + ) + ) + + influxframework.set_user("Administrator") + influxframework.get_doc( + "Scheduled Job Type", + dict(method="influxframework.email.doctype.notification.notification.trigger_daily_alerts"), + ).execute() + + # not today, so no alert + self.assertFalse( + influxframework.db.get_value( + "Email Queue", + {"reference_doctype": "Event", "reference_name": event.name, "status": "Not Sent"}, + ) + ) + + event.starts_on = influxframework.utils.add_days(influxframework.utils.nowdate(), 2) + " 12:00:00" + event.save() + + # Value Change notification alert will be trigger as description is not changed + # mail will not be sent + self.assertFalse( + influxframework.db.get_value( + "Email Queue", + {"reference_doctype": "Event", "reference_name": event.name, "status": "Not Sent"}, + ) + ) + + influxframework.get_doc( + "Scheduled Job Type", + dict(method="influxframework.email.doctype.notification.notification.trigger_daily_alerts"), + ).execute() + + # today so show alert + self.assertTrue( + influxframework.db.get_value( + "Email Queue", + {"reference_doctype": "Event", "reference_name": event.name, "status": "Not Sent"}, + ) + ) + + def test_cc_jinja(self): + + influxframework.db.delete("User", {"email": "test_jinja@example.com"}) + influxframework.db.delete("Email Queue") + influxframework.db.delete("Email Queue Recipient") + + test_user = influxframework.new_doc("User") + test_user.name = "test_jinja" + test_user.first_name = "test_jinja" + test_user.email = "test_jinja@example.com" + + test_user.insert(ignore_permissions=True) + + self.assertTrue( + influxframework.db.get_value( + "Email Queue", + {"reference_doctype": "User", "reference_name": test_user.name, "status": "Not Sent"}, + ) + ) + + self.assertTrue( + influxframework.db.get_value("Email Queue Recipient", {"recipient": "test_jinja@example.com"}) + ) + + influxframework.db.delete("User", {"email": "test_jinja@example.com"}) + influxframework.db.delete("Email Queue") + influxframework.db.delete("Email Queue Recipient") + + def test_notification_to_assignee(self): + todo = influxframework.new_doc("ToDo") + todo.description = "Test Notification" + todo.save() + + assign_to.add( + { + "assign_to": ["test2@example.com"], + "doctype": todo.doctype, + "name": todo.name, + "description": "Close this Todo", + } + ) + + assign_to.add( + { + "assign_to": ["test1@example.com"], + "doctype": todo.doctype, + "name": todo.name, + "description": "Close this Todo", + } + ) + + # change status of todo + todo.status = "Closed" + todo.save() + + email_queue = influxframework.get_doc( + "Email Queue", {"reference_doctype": "ToDo", "reference_name": todo.name} + ) + + self.assertTrue(email_queue) + + # check if description is changed after alert since set_property_after_alert is set + self.assertEqual(todo.description, "Changed by Notification") + + recipients = [d.recipient for d in email_queue.recipients] + self.assertTrue("test2@example.com" in recipients) + self.assertTrue("test1@example.com" in recipients) + + def test_notification_by_child_table_field(self): + contact = influxframework.new_doc("Contact") + contact.first_name = "John Doe" + contact.status = "Open" + contact.append("email_ids", {"email_id": "test2@example.com", "is_primary": 1}) + + contact.append("email_ids", {"email_id": "test1@example.com"}) + + contact.save() + + # change status of contact + contact.status = "Replied" + contact.save() + + email_queue = influxframework.get_doc( + "Email Queue", {"reference_doctype": "Contact", "reference_name": contact.name} + ) + + self.assertTrue(email_queue) + + recipients = [d.recipient for d in email_queue.recipients] + self.assertTrue("test2@example.com" in recipients) + self.assertTrue("test1@example.com" in recipients) + + def test_notification_value_change_casted_types(self): + """Make sure value change event dont fire because of incorrect type comparisons.""" + influxframework.set_user("Administrator") + + notification = { + "document_type": "User", + "subject": "User changed birthdate", + "event": "Value Change", + "channel": "System Notification", + "value_changed": "birth_date", + "recipients": [{"receiver_by_document_field": "email"}], + } + + with get_test_notification(notification) as n: + influxframework.db.delete("Notification Log", {"subject": n.subject}) + + user = influxframework.get_doc("User", "test@example.com") + user.birth_date = influxframework.utils.add_days(user.birth_date, 1) + user.save() + + user.reload() + user.birth_date = influxframework.utils.getdate(user.birth_date) + user.save() + self.assertEqual(1, influxframework.db.count("Notification Log", {"subject": n.subject})) + + @classmethod + def tearDownClass(cls): + influxframework.delete_doc_if_exists("Notification", "ToDo Status Update") + influxframework.delete_doc_if_exists("Notification", "Contact Status Update") diff --git a/influxframework/email/doctype/notification/test_records.json b/influxframework/email/doctype/notification/test_records.json new file mode 100644 index 0000000..665f800 --- /dev/null +++ b/influxframework/email/doctype/notification/test_records.json @@ -0,0 +1,76 @@ +[ + { + "doctype": "Notification", + "subject":"_Test Notification 1", + "document_type": "Communication", + "event": "New", + "attach_print": 0, + "message": "New comment {{ doc.content }} created", + "condition": "doc.communication_type=='Comment'", + "recipients": [ + { "receiver_by_document_field": "owner" } + ] + }, + { + "doctype": "Notification", + "subject":"_Test Notification 2", + "document_type": "Communication", + "event": "Save", + "attach_print": 0, + "message": "New comment {{ doc.content }} saved", + "condition": "doc.communication_type=='Comment'", + "recipients": [ + { "receiver_by_document_field": "owner" } + ], + "set_property_after_alert": "subject", + "property_value": "__testing__" + }, + { + "doctype": "Notification", + "subject":"_Test Notification 3", + "document_type": "Event", + "event": "Save", + "attach_print": 0, + "condition": "doc.event_type=='Public'", + "message": "A new public event {{ doc.subject }} on {{ doc.starts_on }} is created", + "recipients": [ + { "receiver_by_document_field": "owner" } + ] + }, + { + "doctype": "Notification", + "subject":"_Test Notification 4", + "document_type": "Event", + "event": "Value Change", + "attach_print": 0, + "value_changed": "description", + "message": "Description changed", + "recipients": [ + { "receiver_by_document_field": "owner" } + ] + }, + { + "doctype": "Notification", + "subject":"_Test Notification 5", + "document_type": "Event", + "event": "Days Before", + "attach_print": 0, + "date_changed": "starts_on", + "days_in_advance": 2, + "message": "Description changed", + "recipients": [ + { "receiver_by_document_field": "owner" } + ] + }, + { + "doctype": "Notification", + "subject":"_Test Notification 6", + "document_type": "User", + "event": "New", + "attach_print": 0, + "message": "New user {{ doc.name }} created", + "recipients": [ + { "receiver_by_document_field": "owner", "cc": "{{ doc.email }}" } + ] + } +] diff --git a/influxframework/email/doctype/notification_recipient/__init__.py b/influxframework/email/doctype/notification_recipient/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/email/doctype/notification_recipient/notification_recipient.json b/influxframework/email/doctype/notification_recipient/notification_recipient.json new file mode 100644 index 0000000..0670320 --- /dev/null +++ b/influxframework/email/doctype/notification_recipient/notification_recipient.json @@ -0,0 +1,61 @@ +{ + "actions": [], + "creation": "2014-07-11 17:19:37.037109", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "receiver_by_document_field", + "receiver_by_role", + "cc", + "bcc", + "condition" + ], + "fields": [ + { + "depends_on": "eval:parent.channel=='Email'", + "description": "Optional: Always send to these ids. Each Email Address on a new row", + "fieldname": "cc", + "fieldtype": "Code", + "label": "CC" + }, + { + "depends_on": "eval:parent.channel=='Email'", + "fieldname": "bcc", + "fieldtype": "Code", + "label": "BCC" + }, + { + "description": "Expression, Optional", + "fieldname": "condition", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Condition" + }, + { + "fieldname": "receiver_by_document_field", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Receiver By Document Field" + }, + { + "fieldname": "receiver_by_role", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Receiver By Role", + "options": "Role" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-09-01 17:40:27.289105", + "modified_by": "Administrator", + "module": "Email", + "name": "Notification Recipient", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/influxframework/email/doctype/notification_recipient/notification_recipient.py b/influxframework/email/doctype/notification_recipient/notification_recipient.py new file mode 100644 index 0000000..7410e00 --- /dev/null +++ b/influxframework/email/doctype/notification_recipient/notification_recipient.py @@ -0,0 +1,8 @@ +# Copyright (c) 2018, InfluxFramework LLC +# License: MIT. See LICENSE + +from influxframework.model.document import Document + + +class NotificationRecipient(Document): + pass diff --git a/influxframework/email/doctype/unhandled_email/__init__.py b/influxframework/email/doctype/unhandled_email/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/email/doctype/unhandled_email/test_unhandled_email.py b/influxframework/email/doctype/unhandled_email/test_unhandled_email.py new file mode 100644 index 0000000..f570d68 --- /dev/null +++ b/influxframework/email/doctype/unhandled_email/test_unhandled_email.py @@ -0,0 +1,9 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + +# test_records = influxframework.get_test_records('Unhandled Emails') + + +class TestUnhandledEmail(InfluxFrameworkTestCase): + pass diff --git a/influxframework/email/doctype/unhandled_email/unhandled_email.json b/influxframework/email/doctype/unhandled_email/unhandled_email.json new file mode 100644 index 0000000..de4407f --- /dev/null +++ b/influxframework/email/doctype/unhandled_email/unhandled_email.json @@ -0,0 +1,212 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2016-04-14 09:41:45.892975", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 0, + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "email_account", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Email Account", + "length": 0, + "no_copy": 0, + "options": "Email Account", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "uid", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "UID", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reason", + "fieldtype": "Long Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Reason", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "message_id", + "fieldtype": "Code", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Message-id", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "raw", + "fieldtype": "Code", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Raw Email", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 1, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2017-09-19 16:28:00.042256", + "modified_by": "Administrator", + "module": "Email", + "name": "Unhandled Email", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 0, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 0, + "read": 1, + "report": 0, + "role": "System Manager", + "set_user_permissions": 0, + "share": 0, + "submit": 0, + "write": 0 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 0, + "track_seen": 0 +} \ No newline at end of file diff --git a/influxframework/email/doctype/unhandled_email/unhandled_email.py b/influxframework/email/doctype/unhandled_email/unhandled_email.py new file mode 100644 index 0000000..6102294 --- /dev/null +++ b/influxframework/email/doctype/unhandled_email/unhandled_email.py @@ -0,0 +1,15 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document + + +class UnhandledEmail(Document): + pass + + +def remove_old_unhandled_emails(): + influxframework.db.delete( + "Unhandled Email", {"creation": ("<", influxframework.utils.add_days(influxframework.utils.nowdate(), -30))} + ) diff --git a/influxframework/email/email_body.py b/influxframework/email/email_body.py new file mode 100644 index 0000000..ad90fdc --- /dev/null +++ b/influxframework/email/email_body.py @@ -0,0 +1,596 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import email.utils +import os +import re +from email import policy +from email.header import Header +from email.mime.multipart import MIMEMultipart + +import influxframework +from influxframework.email.doctype.email_account.email_account import EmailAccount +from influxframework.utils import ( + cint, + expand_relative_urls, + get_url, + markdown, + parse_addr, + random_string, + scrub_urls, + split_emails, + strip, + to_markdown, +) +from influxframework.utils.pdf import get_pdf + +EMBED_PATTERN = re.compile("""embed=["'](.*?)["']""") + + +def get_email( + recipients, + sender="", + msg="", + subject="[No Subject]", + text_content=None, + footer=None, + print_html=None, + formatted=None, + attachments=None, + content=None, + reply_to=None, + cc=None, + bcc=None, + email_account=None, + expose_recipients=None, + inline_images=None, + header=None, +): + """Prepare an email with the following format: + - multipart/mixed + - multipart/alternative + - text/plain + - multipart/related + - text/html + - inline image + - attachment + """ + content = content or msg + + if cc is None: + cc = [] + if bcc is None: + bcc = [] + if inline_images is None: + inline_images = [] + + emailobj = EMail( + sender, + recipients, + subject, + reply_to=reply_to, + cc=cc, + bcc=bcc, + email_account=email_account, + expose_recipients=expose_recipients, + ) + + if not content.strip().startswith("<"): + content = markdown(content) + + emailobj.set_html( + content, + text_content, + footer=footer, + header=header, + print_html=print_html, + formatted=formatted, + inline_images=inline_images, + ) + + if isinstance(attachments, dict): + attachments = [attachments] + + for attach in attachments or []: + # cannot attach if no filecontent + if attach.get("fcontent") is None: + continue + emailobj.add_attachment(**attach) + + return emailobj + + +class EMail: + """ + Wrapper on the email module. Email object represents emails to be sent to the client. + Also provides a clean way to add binary `FileData` attachments + Also sets all messages as multipart/alternative for cleaner reading in text-only clients + """ + + def __init__( + self, + sender="", + recipients=(), + subject="", + alternative=0, + reply_to=None, + cc=(), + bcc=(), + email_account=None, + expose_recipients=None, + ): + from email import charset as Charset + + Charset.add_charset("utf-8", Charset.QP, Charset.QP, "utf-8") + + if isinstance(recipients, str): + recipients = recipients.replace(";", ",").replace("\n", "") + recipients = split_emails(recipients) + + # remove null + recipients = filter(None, (strip(r) for r in recipients)) + + self.sender = sender + self.reply_to = reply_to or sender + self.recipients = recipients + self.subject = subject + self.expose_recipients = expose_recipients + + self.msg_root = MIMEMultipart("mixed", policy=policy.SMTPUTF8) + self.msg_alternative = MIMEMultipart("alternative", policy=policy.SMTPUTF8) + self.msg_root.attach(self.msg_alternative) + self.cc = cc or [] + self.bcc = bcc or [] + self.html_set = False + + self.email_account = email_account or EmailAccount.find_outgoing( + match_by_email=sender, _raise_error=True + ) + + def set_html( + self, + message, + text_content=None, + footer=None, + print_html=None, + formatted=None, + inline_images=None, + header=None, + ): + """Attach message in the html portion of multipart/alternative""" + if not formatted: + formatted = get_formatted_html( + self.subject, + message, + footer, + print_html, + email_account=self.email_account, + header=header, + sender=self.sender, + ) + + # this is the first html part of a multi-part message, + # convert to text well + if not self.html_set: + if text_content: + self.set_text(expand_relative_urls(text_content)) + else: + self.set_html_as_text(expand_relative_urls(formatted)) + + self.set_part_html(formatted, inline_images) + self.html_set = True + + def set_text(self, message): + """ + Attach message in the text portion of multipart/alternative + """ + from email.mime.text import MIMEText + + part = MIMEText(message, "plain", "utf-8", policy=policy.SMTPUTF8) + self.msg_alternative.attach(part) + + def set_part_html(self, message, inline_images): + from email.mime.text import MIMEText + + has_inline_images = EMBED_PATTERN.search(message) + + if has_inline_images: + # process inline images + message, _inline_images = replace_filename_with_cid(message) + + # prepare parts + msg_related = MIMEMultipart("related", policy=policy.SMTPUTF8) + + html_part = MIMEText(message, "html", "utf-8", policy=policy.SMTPUTF8) + msg_related.attach(html_part) + + for image in _inline_images: + self.add_attachment( + image.get("filename"), + image.get("filecontent"), + content_id=image.get("content_id"), + parent=msg_related, + inline=True, + ) + + self.msg_alternative.attach(msg_related) + else: + self.msg_alternative.attach(MIMEText(message, "html", "utf-8", policy=policy.SMTPUTF8)) + + def set_html_as_text(self, html): + """Set plain text from HTML""" + self.set_text(to_markdown(html)) + + def set_message( + self, message, mime_type="text/html", as_attachment=0, filename="attachment.html" + ): + """Append the message with MIME content to the root node (as attachment)""" + from email.mime.text import MIMEText + + maintype, subtype = mime_type.split("/") + part = MIMEText(message, _subtype=subtype, policy=policy.SMTPUTF8) + + if as_attachment: + part.add_header("Content-Disposition", "attachment", filename=filename) + + self.msg_root.attach(part) + + def attach_file(self, n): + """attach a file from the `FileData` table""" + _file = influxframework.get_doc("File", {"file_name": n}) + content = _file.get_content() + if not content: + return + + self.add_attachment(_file.file_name, content) + + def add_attachment( + self, fname, fcontent, content_type=None, parent=None, content_id=None, inline=False + ): + """add attachment""" + + if not parent: + parent = self.msg_root + + add_attachment(fname, fcontent, content_type, parent, content_id, inline) + + def add_pdf_attachment(self, name, html, options=None): + self.add_attachment(name, get_pdf(html, options), "application/octet-stream") + + def validate(self): + """validate the Email Addresses""" + from influxframework.utils import validate_email_address + + if not self.sender: + self.sender = self.email_account.default_sender + + validate_email_address(strip(self.sender), True) + self.reply_to = validate_email_address(strip(self.reply_to) or self.sender, True) + + self.set_header("X-Original-From", self.sender) + self.replace_sender() + self.replace_sender_name() + + self.recipients = [strip(r) for r in self.recipients if r not in influxframework.STANDARD_USERS] + self.cc = [strip(r) for r in self.cc if r not in influxframework.STANDARD_USERS] + self.bcc = [strip(r) for r in self.bcc if r not in influxframework.STANDARD_USERS] + + for e in self.recipients + (self.cc or []) + (self.bcc or []): + validate_email_address(e, True) + + def replace_sender(self): + if cint(self.email_account.always_use_account_email_id_as_sender): + sender_name, _ = parse_addr(self.sender) + self.sender = email.utils.formataddr( + (str(Header(sender_name or self.email_account.name, "utf-8")), self.email_account.email_id) + ) + + def replace_sender_name(self): + if cint(self.email_account.always_use_account_name_as_sender_name): + _, sender_email = parse_addr(self.sender) + self.sender = email.utils.formataddr( + (str(Header(self.email_account.name, "utf-8")), sender_email) + ) + + def set_message_id(self, message_id, is_notification=False): + if message_id: + message_id = "<" + message_id + ">" + else: + message_id = get_message_id() + self.set_header("isnotification", "") + + if is_notification: + self.set_header("isnotification", "") + + self.set_header("Message-Id", message_id) + + def set_in_reply_to(self, in_reply_to): + """Used to send the Message-Id of a received email back as In-Reply-To""" + self.set_header("In-Reply-To", in_reply_to) + + def make(self): + """build into msg_root""" + headers = { + "Subject": strip(self.subject), + "From": self.sender, + "To": ", ".join(self.recipients) if self.expose_recipients == "header" else "", + "Date": email.utils.formatdate(), + "Reply-To": self.reply_to if self.reply_to else None, + "CC": ", ".join(self.cc) if self.cc and self.expose_recipients == "header" else None, + "X-InfluxFramework-Site": get_url(), + } + + # reset headers as values may be changed. + for key, val in headers.items(): + if val: + self.set_header(key, val) + + # call hook to enable apps to modify msg_root before sending + for hook in influxframework.get_hooks("make_email_body_message"): + influxframework.get_attr(hook)(self) + + def set_header(self, key, value): + if key in self.msg_root: + # delete key if found + # this is done because adding the same key doesn't override + # the existing key, rather appends another header with same key. + del self.msg_root[key] + + self.msg_root[key] = sanitize_email_header(value) + + def as_string(self): + """validate, build message and convert to string""" + self.validate() + self.make() + return self.msg_root.as_string(policy=policy.SMTPUTF8) + + +def get_formatted_html( + subject, + message, + footer=None, + print_html=None, + email_account=None, + header=None, + unsubscribe_link: influxframework._dict | None = None, + sender=None, + with_container=False, +): + + email_account = email_account or EmailAccount.find_outgoing(match_by_email=sender) + + rendered_email = influxframework.get_template("templates/emails/standard.html").render( + { + "brand_logo": get_brand_logo(email_account) if with_container or header else None, + "with_container": with_container, + "site_url": get_url(), + "header": get_header(header), + "content": message, + "footer": get_footer(email_account, footer), + "title": subject, + "print_html": print_html, + "subject": subject, + } + ) + + html = scrub_urls(rendered_email) + + if unsubscribe_link: + html = html.replace("", unsubscribe_link.html) + + return inline_style_in_html(html) + + +@influxframework.whitelist() +def get_email_html(template, args, subject, header=None, with_container=False): + import json + + with_container = cint(with_container) + args = json.loads(args) + if header and header.startswith("["): + header = json.loads(header) + email = influxframework.utils.jinja.get_email_from_template(template, args) + return get_formatted_html(subject, email[0], header=header, with_container=with_container) + + +def inline_style_in_html(html): + """Convert email.css and html to inline-styled html""" + from premailer import Premailer + + from influxframework.utils.jinja_globals import bundled_asset + + # get email css files from hooks + css_files = influxframework.get_hooks("email_css") + css_files = [bundled_asset(path) for path in css_files] + css_files = [path.lstrip("/") for path in css_files] + css_files = [css_file for css_file in css_files if os.path.exists(os.path.abspath(css_file))] + + p = Premailer(html=html, external_styles=css_files, strip_important=False) + + return p.transform() + + +def add_attachment(fname, fcontent, content_type=None, parent=None, content_id=None, inline=False): + """Add attachment to parent which must an email object""" + import mimetypes + from email.mime.audio import MIMEAudio + from email.mime.base import MIMEBase + from email.mime.image import MIMEImage + from email.mime.text import MIMEText + + if not content_type: + content_type, encoding = mimetypes.guess_type(fname) + + if not parent: + return + + if content_type is None: + # No guess could be made, or the file is encoded (compressed), so + # use a generic bag-of-bits type. + content_type = "application/octet-stream" + + maintype, subtype = content_type.split("/", 1) + if maintype == "text": + # Note: we should handle calculating the charset + if isinstance(fcontent, str): + fcontent = fcontent.encode("utf-8") + part = MIMEText(fcontent, _subtype=subtype, _charset="utf-8") + elif maintype == "image": + part = MIMEImage(fcontent, _subtype=subtype) + elif maintype == "audio": + part = MIMEAudio(fcontent, _subtype=subtype) + else: + part = MIMEBase(maintype, subtype) + part.set_payload(fcontent) + # Encode the payload using Base64 + from email import encoders + + encoders.encode_base64(part) + + # Set the filename parameter + if fname: + attachment_type = "inline" if inline else "attachment" + part.add_header("Content-Disposition", attachment_type, filename=str(fname)) + if content_id: + part.add_header("Content-ID", f"<{content_id}>") + + parent.attach(part) + + +def get_message_id(): + """Returns Message ID created from doctype and name""" + return email.utils.make_msgid(domain=influxframework.local.site) + + +def get_signature(email_account): + if email_account and email_account.add_signature and email_account.signature: + return "
      " + email_account.signature + else: + return "" + + +def get_footer(email_account, footer=None): + """append a footer (signature)""" + footer = footer or "" + + args = {} + + if email_account and email_account.footer: + args.update({"email_account_footer": email_account.footer}) + + sender_address = influxframework.db.get_default("email_footer_address") + + if sender_address: + args.update({"sender_address": sender_address}) + + if not cint(influxframework.db.get_default("disable_standard_email_footer")): + args.update({"default_mail_footer": influxframework.get_hooks("default_mail_footer")}) + + footer += influxframework.utils.jinja.get_email_from_template("email_footer", args)[0] + + return footer + + +def replace_filename_with_cid(message): + """Replaces with + and return the modified message and + a list of inline_images with {filename, filecontent, content_id} + """ + + inline_images = [] + + while True: + matches = EMBED_PATTERN.search(message) + if not matches: + break + groups = matches.groups() + + # found match + img_path = groups[0] + filename = img_path.rsplit("/")[-1] + + filecontent = get_filecontent_from_path(img_path) + if not filecontent: + message = re.sub(f"""embed=['"]{img_path}['"]""", "", message) + continue + + content_id = random_string(10) + + inline_images.append( + {"filename": filename, "filecontent": filecontent, "content_id": content_id} + ) + + message = re.sub(f"""embed=['"]{img_path}['"]""", f'src="cid:{content_id}"', message) + + return (message, inline_images) + + +def get_filecontent_from_path(path): + if not path: + return + + if path.startswith("/"): + path = path[1:] + + if path.startswith("assets/"): + # from public folder + full_path = os.path.abspath(path) + elif path.startswith("files/"): + # public file + full_path = influxframework.get_site_path("public", path) + elif path.startswith("private/files/"): + # private file + full_path = influxframework.get_site_path(path) + else: + full_path = path + + if os.path.exists(full_path): + with open(full_path, "rb") as f: + filecontent = f.read() + + return filecontent + else: + return None + + +def get_header(header=None): + """Build header from template""" + from influxframework.utils.jinja import get_email_from_template + + if not header: + return None + + if isinstance(header, str): + # header = 'My Title' + header = [header, None] + if len(header) == 1: + # header = ['My Title'] + header.append(None) + # header = ['My Title', 'orange'] + title, indicator = header + + if not title: + title = influxframework.get_hooks("app_title")[-1] + + email_header, text = get_email_from_template( + "email_header", {"header_title": title, "indicator": indicator} + ) + + return email_header + + +def sanitize_email_header(header: str): + """ + Removes all line boundaries in the headers. + + Email Policy (python's std) has some bugs in it which uses splitlines + and raises ValueError (ref: https://github.com/python/cpython/blob/main/Lib/email/policy.py#L143). + Hence removing all line boundaries while sanitization of headers to prevent such faliures. + The line boundaries which are removed can be found here: https://docs.python.org/3/library/stdtypes.html#str.splitlines + """ + + return "".join(header.splitlines()) + + +def get_brand_logo(email_account): + return email_account.get("brand_logo") diff --git a/influxframework/email/inbox.py b/influxframework/email/inbox.py new file mode 100644 index 0000000..e72ba6c --- /dev/null +++ b/influxframework/email/inbox.py @@ -0,0 +1,133 @@ +import json + +import influxframework + + +def get_email_accounts(user=None): + if not user: + user = influxframework.session.user + + email_accounts = [] + + accounts = influxframework.get_all( + "User Email", + filters={"parent": user}, + fields=["email_account", "email_id", "enable_outgoing"], + distinct=True, + order_by="idx", + ) + + if not accounts: + return {"email_accounts": [], "all_accounts": ""} + + all_accounts = ",".join(account.get("email_account") for account in accounts) + if len(accounts) > 1: + email_accounts.append({"email_account": all_accounts, "email_id": "All Accounts"}) + email_accounts.extend(accounts) + + email_accounts.extend( + [ + {"email_account": "Sent", "email_id": "Sent Mail"}, + {"email_account": "Spam", "email_id": "Spam"}, + {"email_account": "Trash", "email_id": "Trash"}, + ] + ) + + return {"email_accounts": email_accounts, "all_accounts": all_accounts} + + +@influxframework.whitelist() +def create_email_flag_queue(names, action): + """create email flag queue to mark email either as read or unread""" + + def mark_as_seen_unseen(name, action): + doc = influxframework.get_doc("Communication", name) + if action == "Read": + doc.add_seen() + else: + _seen = json.loads(doc._seen or "[]") + _seen = [user for user in _seen if influxframework.session.user != user] + doc.db_set("_seen", json.dumps(_seen), update_modified=False) + + if not all([names, action]): + return + + for name in json.loads(names or []): + uid, seen_status, email_account = influxframework.db.get_value( + "Communication", name, ["ifnull(uid, -1)", "ifnull(seen, 0)", "email_account"] + ) + + # can not mark email SEEN or UNSEEN without uid + if not uid or uid == -1: + continue + + seen = 1 if action == "Read" else 0 + # check if states are correct + if (action == "Read" and seen_status == 0) or (action == "Unread" and seen_status == 1): + create_new = True + email_flag_queue = influxframework.db.sql( + """select name, action from `tabEmail Flag Queue` + where communication = %(name)s and is_completed=0""", + {"name": name}, + as_dict=True, + ) + + for queue in email_flag_queue: + if queue.action != action: + influxframework.delete_doc("Email Flag Queue", queue.name, ignore_permissions=True) + elif queue.action == action: + # Read or Unread request for email is already available + create_new = False + + if create_new: + flag_queue = influxframework.get_doc( + { + "uid": uid, + "action": action, + "communication": name, + "doctype": "Email Flag Queue", + "email_account": email_account, + } + ) + flag_queue.save(ignore_permissions=True) + influxframework.db.set_value("Communication", name, "seen", seen, update_modified=False) + mark_as_seen_unseen(name, action) + + +@influxframework.whitelist() +def mark_as_closed_open(communication, status): + """Set status to open or close""" + influxframework.db.set_value("Communication", communication, "status", status) + + +@influxframework.whitelist() +def move_email(communication, email_account): + """Move email to another email account.""" + influxframework.db.set_value("Communication", communication, "email_account", email_account) + + +@influxframework.whitelist() +def mark_as_trash(communication): + """Set email status to trash.""" + influxframework.db.set_value("Communication", communication, "email_status", "Trash") + + +@influxframework.whitelist() +def mark_as_spam(communication, sender): + """Set email status to spam.""" + email_rule = influxframework.db.get_value("Email Rule", {"email_id": sender}) + if not email_rule: + influxframework.get_doc({"doctype": "Email Rule", "email_id": sender, "is_spam": 1}).insert( + ignore_permissions=True + ) + influxframework.db.set_value("Communication", communication, "email_status", "Spam") + + +def link_communication_to_document( + doc, reference_doctype, reference_name, ignore_communication_links +): + if not ignore_communication_links: + doc.reference_doctype = reference_doctype + doc.reference_name = reference_name + doc.status = "Linked" + doc.save(ignore_permissions=True) diff --git a/influxframework/email/oauth.py b/influxframework/email/oauth.py new file mode 100644 index 0000000..25d5017 --- /dev/null +++ b/influxframework/email/oauth.py @@ -0,0 +1,168 @@ +import base64 +from imaplib import IMAP4 +from poplib import POP3 +from smtplib import SMTP +from urllib.parse import quote + +import influxframework +from influxframework.integrations.google_oauth import GoogleOAuth +from influxframework.utils.password import encrypt + + +class OAuthenticationError(Exception): + pass + + +class Oauth: + def __init__( + self, + conn: IMAP4 | POP3 | SMTP, + email_account: str, + email: str, + access_token: str, + refresh_token: str, + service: str, + mechanism: str = "XOAUTH2", + ) -> None: + + self.email_account = email_account + self.email = email + self.service = service + self._mechanism = mechanism + self._conn = conn + self._access_token = access_token + self._refresh_token = refresh_token + + self._validate() + + def _validate(self) -> None: + if self.service != "GMail": + raise NotImplementedError( + f"Service {self.service} currently doesn't have oauth implementation." + ) + + if not self._refresh_token: + influxframework.throw( + influxframework._("Please Authorize OAuth."), + OAuthenticationError, + influxframework._("OAuth Error"), + ) + + @property + def _auth_string(self) -> str: + return f"user={self.email}\1auth=Bearer {self._access_token}\1\1" + + def connect(self, _retry: int = 0) -> None: + """Connection method with retry on exception for Oauth""" + try: + if isinstance(self._conn, POP3): + res = self._connect_pop() + + if not res.startswith(b"+OK"): + raise + + elif isinstance(self._conn, IMAP4): + self._connect_imap() + + else: + # SMTP + self._connect_smtp() + + except Exception as e: + # maybe the access token expired - refreshing + access_token = self._refresh_access_token() + + if not access_token or _retry > 0: + influxframework.log_error( + "OAuth Error - Authentication Failed", str(e), "Email Account", self.email_account + ) + # raising a bare exception here as we have a lot of exception handling present + # where the connect method is called from - hence just logging and raising. + raise + + self._access_token = access_token + self.connect(_retry + 1) + + def _connect_pop(self) -> bytes: + # poplib doesn't have AUTH command implementation + res = self._conn._shortcmd( + "AUTH {} {}".format( + self._mechanism, base64.b64encode(bytes(self._auth_string, "utf-8")).decode("utf-8") + ) + ) + + return res + + def _connect_imap(self) -> None: + self._conn.authenticate(self._mechanism, lambda x: self._auth_string) + + def _connect_smtp(self) -> None: + self._conn.auth(self._mechanism, lambda x: self._auth_string, initial_response_ok=False) + + def _refresh_access_token(self) -> str: + """Refreshes access token via calling `refresh_access_token` method of oauth service object""" + service_obj = self._get_service_object() + access_token = service_obj.refresh_access_token(self._refresh_token).get("access_token") + + if access_token: + # set the new access token in db + influxframework.db.set_value( + "Email Account", + self.email_account, + "access_token", + encrypt(access_token), + update_modified=False, + ) + + return access_token + + def _get_service_object(self): + """Get Oauth service object""" + + return { + "GMail": GoogleOAuth("mail", validate=False), + }[self.service] + + +@influxframework.whitelist(methods=["POST"]) +def oauth_access(email_account: str, service: str): + """Used as a default endpoint/caller for all oauth services. + Returns authorization url for redirection""" + + if not service: + influxframework.throw(influxframework._("No Service is selected. Please select one and try again!")) + + doctype = "Email Account" + + if service == "GMail": + return authorize_google_access(email_account, doctype) + + raise NotImplementedError(f"Service {service} currently doesn't have oauth implementation.") + + +def authorize_google_access(email_account, doctype: str = "Email Account", code: str = None): + """Facilitates google oauth for email. + This is invoked 2 times - first time when user clicks `Authorze API Access` for getting the authorization url + and second time for setting the refresh and access token in db when google redirects back with oauth code.""" + + oauth_obj = GoogleOAuth("mail") + + if not code: + return oauth_obj.get_authentication_url( + { + "redirect": f"/app/Form/{quote(doctype)}/{quote(email_account)}", + "success_query_param": "successful_authorization=1", + "email_account": email_account, + }, + ) + + res = oauth_obj.authorize(code) + influxframework.db.set_value( + doctype, + email_account, + { + "refresh_token": encrypt(res.get("refresh_token")), + "access_token": encrypt(res.get("access_token")), + }, + update_modified=False, + ) diff --git a/influxframework/email/page/__init__.py b/influxframework/email/page/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/email/queue.py b/influxframework/email/queue.py new file mode 100644 index 0000000..e0cebaa --- /dev/null +++ b/influxframework/email/queue.py @@ -0,0 +1,193 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework import _, msgprint +from influxframework.query_builder import DocType, Interval +from influxframework.query_builder.functions import Now +from influxframework.utils import cint, get_url, now_datetime +from influxframework.utils.verified_command import get_signed_params, verify_request + + +def get_emails_sent_this_month(email_account=None): + """Get count of emails sent from a specific email account. + + :param email_account: name of the email account used to send mail + + if email_account=None, email account filter is not applied while counting + """ + q = """ + SELECT + COUNT(*) + FROM + `tabEmail Queue` + WHERE + `status`='Sent' + AND + EXTRACT(YEAR_MONTH FROM `creation`) = EXTRACT(YEAR_MONTH FROM NOW()) + """ + + q_args = {} + if email_account is not None: + if email_account: + q += " AND email_account = %(email_account)s" + q_args["email_account"] = email_account + else: + q += " AND (email_account is null OR email_account='')" + + return influxframework.db.sql(q, q_args)[0][0] + + +def get_emails_sent_today(email_account=None): + """Get count of emails sent from a specific email account. + + :param email_account: name of the email account used to send mail + + if email_account=None, email account filter is not applied while counting + """ + q = """ + SELECT + COUNT(`name`) + FROM + `tabEmail Queue` + WHERE + `status` in ('Sent', 'Not Sent', 'Sending') + AND + `creation` > (NOW() - INTERVAL '24' HOUR) + """ + + q_args = {} + if email_account is not None: + if email_account: + q += " AND email_account = %(email_account)s" + q_args["email_account"] = email_account + else: + q += " AND (email_account is null OR email_account='')" + + return influxframework.db.sql(q, q_args)[0][0] + + +def get_unsubscribe_message( + unsubscribe_message: str, expose_recipients: str +) -> "influxframework._dict[str, str]": + unsubscribe_message = unsubscribe_message or _("Unsubscribe") + unsubscribe_link = f'{unsubscribe_message}' + unsubscribe_html = _("{0} to stop receiving emails of this type").format(unsubscribe_link) + html = f"""""" + + text = f"\n\n{unsubscribe_message}: \n" + if expose_recipients == "footer": + text = f"\n{text}" + + return influxframework._dict(html=html, text=text) + + +def get_unsubcribed_url( + reference_doctype, reference_name, email, unsubscribe_method, unsubscribe_params +): + params = { + "email": email.encode("utf-8"), + "doctype": reference_doctype.encode("utf-8"), + "name": reference_name.encode("utf-8"), + } + if unsubscribe_params: + params.update(unsubscribe_params) + + query_string = get_signed_params(params) + + # for test + influxframework.local.flags.signed_query_string = query_string + + return get_url(unsubscribe_method + "?" + get_signed_params(params)) + + +@influxframework.whitelist(allow_guest=True) +def unsubscribe(doctype, name, email): + # unsubsribe from comments and communications + if not verify_request(): + return + + try: + influxframework.get_doc( + { + "doctype": "Email Unsubscribe", + "email": email, + "reference_doctype": doctype, + "reference_name": name, + } + ).insert(ignore_permissions=True) + + except influxframework.DuplicateEntryError: + influxframework.db.rollback() + + else: + influxframework.db.commit() + + return_unsubscribed_page(email, doctype, name) + + +def return_unsubscribed_page(email, doctype, name): + influxframework.respond_as_web_page( + _("Unsubscribed"), + _("{0} has left the conversation in {1} {2}").format(email, _(doctype), name), + indicator_color="green", + ) + + +def flush(from_test=False): + """flush email queue, every time: called from scheduler""" + from influxframework.email.doctype.email_queue.email_queue import send_mail + + # To avoid running jobs inside unit tests + if influxframework.are_emails_muted(): + msgprint(_("Emails are muted")) + from_test = True + + if cint(influxframework.db.get_default("suspend_email_queue")) == 1: + return + + for row in get_queue(): + try: + func = send_mail if from_test else send_mail.enqueue + is_background_task = not from_test + func(email_queue_name=row.name, is_background_task=is_background_task) + except Exception: + influxframework.get_doc("Email Queue", row.name).log_error() + + +def get_queue(): + return influxframework.db.sql( + """select + name, sender + from + `tabEmail Queue` + where + (status='Not Sent' or status='Partially Sent') and + (send_after is null or send_after < %(now)s) + order + by priority desc, creation asc + limit 500""", + {"now": now_datetime()}, + as_dict=True, + ) + + +def set_expiry_for_email_queue(): + """Mark emails as expire that has not sent for 7 days. + Called daily via scheduler. + """ + + influxframework.db.sql( + """ + UPDATE `tabEmail Queue` + SET `status`='Expired' + WHERE `modified` < (NOW() - INTERVAL '7' DAY) + AND `status`='Not Sent' + AND (`send_after` IS NULL OR `send_after` < %(now)s)""", + {"now": now_datetime()}, + ) diff --git a/influxframework/email/receive.py b/influxframework/email/receive.py new file mode 100644 index 0000000..2887c96 --- /dev/null +++ b/influxframework/email/receive.py @@ -0,0 +1,1014 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import datetime +import email +import email.utils +import imaplib +import json +import poplib +import re +import time +from email.header import decode_header + +import _socket +import chardet +from email_reply_parser import EmailReplyParser + +import influxframework +from influxframework import _, safe_decode, safe_encode +from influxframework.core.doctype.file import MaxFileSizeReachedError, get_random_filename +from influxframework.email.oauth import Oauth +from influxframework.utils import ( + add_days, + cint, + convert_utc_to_user_timezone, + cstr, + extract_email_id, + get_datetime, + get_string_between, + markdown, + now, + parse_addr, + sanitize_html, + strip, +) +from influxframework.utils.html_utils import clean_email_html +from influxframework.utils.user import is_system_user + +# fix due to a python bug in poplib that limits it to 2048 +poplib._MAXLINE = 20480 + +THREAD_ID_PATTERN = re.compile(r"(?<=\[)[\w/-]+") +WORDS_PATTERN = re.compile(r"\w+") + + +class EmailSizeExceededError(influxframework.ValidationError): + pass + + +class EmailTimeoutError(influxframework.ValidationError): + pass + + +class TotalSizeExceededError(influxframework.ValidationError): + pass + + +class LoginLimitExceeded(influxframework.ValidationError): + pass + + +class SentEmailInInboxError(Exception): + pass + + +class EmailServer: + """Wrapper for POP server to pull emails.""" + + def __init__(self, args=None): + self.setup(args) + + def setup(self, args=None): + # overrride + self.settings = args or influxframework._dict() + + def check_mails(self): + # overrride + return True + + def process_message(self, mail): + # overrride + pass + + def connect(self): + """Connect to **Email Account**.""" + if cint(self.settings.use_imap): + return self.connect_imap() + else: + return self.connect_pop() + + def connect_imap(self): + """Connect to IMAP""" + try: + if cint(self.settings.use_ssl): + self.imap = Timed_IMAP4_SSL( + self.settings.host, self.settings.incoming_port, timeout=influxframework.conf.get("pop_timeout") + ) + else: + self.imap = Timed_IMAP4( + self.settings.host, self.settings.incoming_port, timeout=influxframework.conf.get("pop_timeout") + ) + + if cint(self.settings.use_starttls): + self.imap.starttls() + + if self.settings.use_oauth: + Oauth( + self.imap, + self.settings.email_account, + self.settings.username, + self.settings.access_token, + self.settings.refresh_token, + self.settings.service, + ).connect() + + else: + self.imap.login(self.settings.username, self.settings.password) + + # connection established! + return True + + except _socket.error: + # Invalid mail server -- due to refusing connection + influxframework.msgprint(_("Invalid Mail Server. Please rectify and try again.")) + raise + + def connect_pop(self): + # this method return pop connection + try: + if cint(self.settings.use_ssl): + self.pop = Timed_POP3_SSL( + self.settings.host, self.settings.incoming_port, timeout=influxframework.conf.get("pop_timeout") + ) + else: + self.pop = Timed_POP3( + self.settings.host, self.settings.incoming_port, timeout=influxframework.conf.get("pop_timeout") + ) + + if self.settings.use_oauth: + Oauth( + self.pop, + self.settings.email_account, + self.settings.username, + self.settings.access_token, + self.settings.refresh_token, + self.settings.service, + ).connect() + + else: + self.pop.user(self.settings.username) + self.pop.pass_(self.settings.password) + + # connection established! + return True + + except _socket.error: + # log performs rollback and logs error in Error Log + self.log_error("POP: Unable to connect") + + # Invalid mail server -- due to refusing connection + influxframework.msgprint(_("Invalid Mail Server. Please rectify and try again.")) + raise + + except poplib.error_proto as e: + if self.is_temporary_system_problem(e): + return False + + else: + influxframework.msgprint(_("Invalid User Name or Support Password. Please rectify and try again.")) + raise + + def select_imap_folder(self, folder): + res = self.imap.select(f'"{folder}"') + return res[0] == "OK" # The folder exsits TODO: handle other resoponses too + + def logout(self): + if cint(self.settings.use_imap): + self.imap.logout() + else: + self.pop.quit() + return + + def get_messages(self, folder="INBOX"): + """Returns new email messages in a list.""" + if not (self.check_mails() or self.connect()): + return [] + + influxframework.db.commit() + + uid_list = [] + + try: + # track if errors arised + self.errors = False + self.latest_messages = [] + self.seen_status = {} + self.uid_reindexed = False + + uid_list = email_list = self.get_new_mails(folder) + + if not email_list: + return + + num = num_copy = len(email_list) + + # WARNING: Hard coded max no. of messages to be popped + if num > 50: + num = 50 + + # size limits + self.total_size = 0 + self.max_email_size = cint(influxframework.local.conf.get("max_email_size")) + self.max_total_size = 5 * self.max_email_size + + for i, message_meta in enumerate(email_list[:num]): + try: + self.retrieve_message(message_meta, i + 1) + except (TotalSizeExceededError, EmailTimeoutError, LoginLimitExceeded): + break + # WARNING: Mark as read - message number 101 onwards from the pop list + # This is to avoid having too many messages entering the system + num = num_copy + if not cint(self.settings.use_imap): + if num > 100 and not self.errors: + for m in range(101, num + 1): + self.pop.dele(m) + + except Exception as e: + if self.has_login_limit_exceeded(e): + pass + else: + raise + + out = {"latest_messages": self.latest_messages} + if self.settings.use_imap: + out.update( + {"uid_list": uid_list, "seen_status": self.seen_status, "uid_reindexed": self.uid_reindexed} + ) + + return out + + def get_new_mails(self, folder): + """Return list of new mails""" + if cint(self.settings.use_imap): + email_list = [] + self.check_imap_uidvalidity(folder) + + readonly = False if self.settings.email_sync_rule == "UNSEEN" else True + + self.imap.select(folder, readonly=readonly) + response, message = self.imap.uid("search", None, self.settings.email_sync_rule) + if message[0]: + email_list = message[0].split() + else: + email_list = self.pop.list()[1] + + return email_list + + def check_imap_uidvalidity(self, folder): + # compare the UIDVALIDITY of email account and imap server + uid_validity = self.settings.uid_validity + + response, message = self.imap.status(folder, "(UIDVALIDITY UIDNEXT)") + current_uid_validity = self.parse_imap_response("UIDVALIDITY", message[0]) or 0 + + uidnext = int(self.parse_imap_response("UIDNEXT", message[0]) or "1") + influxframework.db.set_value("Email Account", self.settings.email_account, "uidnext", uidnext) + + if not uid_validity or uid_validity != current_uid_validity: + # uidvalidity changed & all email uids are reindexed by server + Communication = influxframework.qb.DocType("Communication") + influxframework.qb.update(Communication).set(Communication.uid, -1).where( + Communication.communication_medium == "Email" + ).where(Communication.email_account == self.settings.email_account).run() + + if self.settings.use_imap: + # new update for the IMAP Folder DocType + IMAPFolder = influxframework.qb.DocType("IMAP Folder") + influxframework.qb.update(IMAPFolder).set(IMAPFolder.uidvalidity, current_uid_validity).set( + IMAPFolder.uidnext, uidnext + ).where(IMAPFolder.parent == self.settings.email_account_name).where( + IMAPFolder.folder_name == folder + ).run() + else: + EmailAccount = influxframework.qb.DocType("Email Account") + influxframework.qb.update(EmailAccount).set(EmailAccount.uidvalidity, current_uid_validity).set( + EmailAccount.uidnext, uidnext + ).where(EmailAccount.name == self.settings.email_account_name).run() + + # uid validity not found pulling emails for first time + if not uid_validity: + self.settings.email_sync_rule = "UNSEEN" + return + + sync_count = 100 if uid_validity else int(self.settings.initial_sync_count) + from_uid = ( + 1 if uidnext < (sync_count + 1) or (uidnext - sync_count) < 1 else uidnext - sync_count + ) + # sync last 100 email + self.settings.email_sync_rule = f"UID {from_uid}:{uidnext}" + self.uid_reindexed = True + + elif uid_validity == current_uid_validity: + return + + def parse_imap_response(self, cmd, response): + pattern = rf"(?<={cmd} )[0-9]*" + match = re.search(pattern, response.decode("utf-8"), re.U | re.I) + + if match: + return match.group(0) + else: + return None + + def retrieve_message(self, message_meta, msg_num=None): + incoming_mail = None + try: + self.validate_message_limits(message_meta) + + if cint(self.settings.use_imap): + status, message = self.imap.uid("fetch", message_meta, "(BODY.PEEK[] BODY.PEEK[HEADER] FLAGS)") + raw = message[0] + + self.get_email_seen_status(message_meta, raw[0]) + self.latest_messages.append(raw[1]) + else: + msg = self.pop.retr(msg_num) + self.latest_messages.append(b"\n".join(msg[1])) + except (TotalSizeExceededError, EmailTimeoutError): + # propagate this error to break the loop + self.errors = True + raise + + except Exception as e: + if self.has_login_limit_exceeded(e): + self.errors = True + raise LoginLimitExceeded(e) + + else: + # log performs rollback and logs error in Error Log + self.log_error("Unable to fetch email", self.make_error_msg(msg_num, incoming_mail)) + self.errors = True + influxframework.db.rollback() + + if not cint(self.settings.use_imap): + self.pop.dele(msg_num) + else: + # mark as seen if email sync rule is UNSEEN (syncing only unseen mails) + if self.settings.email_sync_rule == "UNSEEN": + self.imap.uid("STORE", message_meta, "+FLAGS", "(\\SEEN)") + else: + if not cint(self.settings.use_imap): + self.pop.dele(msg_num) + else: + # mark as seen if email sync rule is UNSEEN (syncing only unseen mails) + if self.settings.email_sync_rule == "UNSEEN": + self.imap.uid("STORE", message_meta, "+FLAGS", "(\\SEEN)") + + def get_email_seen_status(self, uid, flag_string): + """parse the email FLAGS response""" + if not flag_string: + return None + + flags = [] + for flag in imaplib.ParseFlags(flag_string) or []: + match = WORDS_PATTERN.search(influxframework.as_unicode(flag)) + flags.append(match.group(0)) + + if "Seen" in flags: + self.seen_status.update({uid: "SEEN"}) + else: + self.seen_status.update({uid: "UNSEEN"}) + + def has_login_limit_exceeded(self, e): + return "-ERR Exceeded the login limit" in strip(cstr(e.message)) + + def is_temporary_system_problem(self, e): + messages = ( + "-ERR [SYS/TEMP] Temporary system problem. Please try again later.", + "Connection timed out", + ) + for message in messages: + if message in strip(cstr(e)) or message in strip(cstr(getattr(e, "strerror", ""))): + return True + return False + + def validate_message_limits(self, message_meta): + # throttle based on email size + if not self.max_email_size: + return + + m, size = message_meta.split() + size = cint(size) + + if size < self.max_email_size: + self.total_size += size + if self.total_size > self.max_total_size: + raise TotalSizeExceededError + else: + raise EmailSizeExceededError + + def make_error_msg(self, msg_num, incoming_mail): + error_msg = "Error in retrieving email." + if not incoming_mail: + try: + # retrieve headers + incoming_mail = Email(b"\n".join(self.pop.top(msg_num, 5)[1])) + except Exception: + pass + + if incoming_mail: + error_msg += "\nDate: {date}\nFrom: {from_email}\nSubject: {subject}\n".format( + date=incoming_mail.date, from_email=incoming_mail.from_email, subject=incoming_mail.subject + ) + + return error_msg + + def update_flag(self, folder, uid_list=None): + """set all uids mails the flag as seen""" + if not uid_list: + return + + if not self.connect(): + return + + self.imap.select(folder) + for uid, operation in uid_list.items(): + if not uid: + continue + + op = "+FLAGS" if operation == "Read" else "-FLAGS" + try: + self.imap.uid("STORE", uid, op, "(\\SEEN)") + except Exception: + continue + + +class Email: + """Wrapper for an email.""" + + def __init__(self, content): + """Parses headers, content, attachments from given raw message. + + :param content: Raw message.""" + if isinstance(content, bytes): + self.mail = email.message_from_bytes(content) + else: + self.mail = email.message_from_string(content) + + self.raw_message = content + self.text_content = "" + self.html_content = "" + self.attachments = [] + self.cid_map = {} + self.parse() + self.set_content_and_type() + self.set_subject() + self.set_from() + + message_id = self.mail.get("Message-ID") or "" + self.message_id = get_string_between("<", message_id, ">") + + if self.mail["Date"]: + try: + utc = email.utils.mktime_tz(email.utils.parsedate_tz(self.mail["Date"])) + utc_dt = datetime.datetime.utcfromtimestamp(utc) + self.date = convert_utc_to_user_timezone(utc_dt).strftime("%Y-%m-%d %H:%M:%S") + except Exception: + self.date = now() + else: + self.date = now() + if self.date > now(): + self.date = now() + + @property + def in_reply_to(self): + in_reply_to = self.mail.get("In-Reply-To") or "" + return get_string_between("<", in_reply_to, ">") + + def parse(self): + """Walk and process multi-part email.""" + for part in self.mail.walk(): + self.process_part(part) + + def set_subject(self): + """Parse and decode `Subject` header.""" + _subject = decode_header(self.mail.get("Subject", "No Subject")) + self.subject = _subject[0][0] or "" + if _subject[0][1]: + self.subject = safe_decode(self.subject, _subject[0][1]) + else: + # assume that the encoding is utf-8 + self.subject = safe_decode(self.subject)[:140] + + if not self.subject: + self.subject = "No Subject" + + def set_from(self): + # gmail mailing-list compatibility + # use X-Original-Sender if available, as gmail sometimes modifies the 'From' + _from_email = self.decode_email(self.mail.get("X-Original-From") or self.mail["From"]) + _reply_to = self.decode_email(self.mail.get("Reply-To")) + + if _reply_to and not influxframework.db.get_value("Email Account", {"email_id": _reply_to}, "email_id"): + self.from_email = extract_email_id(_reply_to) + else: + self.from_email = extract_email_id(_from_email) + + if self.from_email: + self.from_email = self.from_email.lower() + + self.from_real_name = parse_addr(_from_email)[0] if "@" in _from_email else _from_email + + def decode_email(self, email): + if not email: + return + decoded = "" + for part, encoding in decode_header( + influxframework.as_unicode(email).replace('"', " ").replace("'", " ") + ): + if encoding: + decoded += part.decode(encoding) + else: + decoded += safe_decode(part) + return decoded + + def set_content_and_type(self): + self.content, self.content_type = "[Blank Email]", "text/plain" + if self.html_content: + self.content, self.content_type = self.html_content, "text/html" + else: + self.content, self.content_type = ( + EmailReplyParser.read(self.text_content).text.replace("\n", "\n\n"), + "text/plain", + ) + + def process_part(self, part): + """Parse email `part` and set it to `text_content`, `html_content` or `attachments`.""" + content_type = part.get_content_type() + if content_type == "text/plain": + self.text_content += self.get_payload(part) + + elif content_type == "text/html": + self.html_content += self.get_payload(part) + + elif content_type == "message/rfc822": + # sent by outlook when another email is sent as an attachment to this email + self.show_attached_email_headers_in_content(part) + + elif part.get_filename() or "image" in content_type: + self.get_attachment(part) + + def show_attached_email_headers_in_content(self, part): + # get the multipart/alternative message + try: + from html import escape # python 3.x + except ImportError: + from cgi import escape # python 2.x + + message = list(part.walk())[1] + headers = [] + for key in ("From", "To", "Subject", "Date"): + value = cstr(message.get(key)) + if value: + headers.append(f"{_(key)}: {escape(value)}") + + self.text_content += "\n".join(headers) + self.html_content += "
      " + "\n".join(f"

      {h}

      " for h in headers) + + if not message.is_multipart() and message.get_content_type() == "text/plain": + # email.parser didn't parse it! + text_content = self.get_payload(message) + self.text_content += text_content + self.html_content += markdown(text_content) + + def get_charset(self, part): + """Detect charset.""" + charset = part.get_content_charset() + if not charset: + charset = chardet.detect(safe_encode(cstr(part)))["encoding"] + + return charset + + def get_payload(self, part): + charset = self.get_charset(part) + + try: + return str(part.get_payload(decode=True), str(charset), "ignore") + except LookupError: + return part.get_payload() + + def get_attachment(self, part): + # charset = self.get_charset(part) + fcontent = part.get_payload(decode=True) + + if fcontent: + content_type = part.get_content_type() + fname = part.get_filename() + if fname: + try: + fname = fname.replace("\n", " ").replace("\r", "") + fname = cstr(decode_header(fname)[0][0]) + except Exception: + fname = get_random_filename(content_type=content_type) + else: + fname = get_random_filename(content_type=content_type) + + self.attachments.append( + { + "content_type": content_type, + "fname": fname, + "fcontent": fcontent, + } + ) + + cid = (cstr(part.get("Content-Id")) or "").strip("><") + if cid: + self.cid_map[fname] = cid + + def save_attachments_in_doc(self, doc): + """Save email attachments in given document.""" + saved_attachments = [] + + for attachment in self.attachments: + try: + _file = influxframework.get_doc( + { + "doctype": "File", + "file_name": attachment["fname"], + "attached_to_doctype": doc.doctype, + "attached_to_name": doc.name, + "is_private": 1, + "content": attachment["fcontent"], + } + ) + _file.save() + saved_attachments.append(_file) + + if attachment["fname"] in self.cid_map: + self.cid_map[_file.name] = self.cid_map[attachment["fname"]] + + except MaxFileSizeReachedError: + # WARNING: bypass max file size exception + pass + except influxframework.FileAlreadyAttachedException: + pass + except influxframework.DuplicateEntryError: + # same file attached twice?? + pass + + return saved_attachments + + def get_thread_id(self): + """Extract thread ID from `[]`""" + l = THREAD_ID_PATTERN.findall(self.subject) + return l and l[0] or None + + def is_reply(self): + return bool(self.in_reply_to) + + +class InboundMail(Email): + """Class representation of incoming mail along with mail handlers.""" + + def __init__(self, content, email_account, uid=None, seen_status=None, append_to=None): + super().__init__(content) + self.email_account = email_account + self.uid = uid or -1 + self.append_to = append_to + self.seen_status = seen_status or 0 + + # System documents related to this mail + self._parent_email_queue = None + self._parent_communication = None + self._reference_document = None + + self.flags = influxframework._dict() + + def get_content(self): + if self.content_type == "text/html": + return clean_email_html(self.content) + + def process(self): + """Create communication record from email.""" + if self.is_sender_same_as_receiver() and not self.is_reply(): + if influxframework.flags.in_test: + print("WARN: Cannot pull email. Sender same as recipient inbox") + raise SentEmailInInboxError + + communication = self.is_exist_in_system() + if communication: + communication.update_db(uid=self.uid) + communication.reload() + return communication + + self.flags.is_new_communication = True + return self._build_communication_doc() + + def _build_communication_doc(self): + data = self.as_dict() + data["doctype"] = "Communication" + + if self.parent_communication(): + data["in_reply_to"] = self.parent_communication().name + + append_to = self.append_to if self.email_account.use_imap else self.email_account.append_to + + if self.reference_document(): + data["reference_doctype"] = self.reference_document().doctype + data["reference_name"] = self.reference_document().name + else: + if append_to and append_to != "Communication": + reference_doc = self._create_reference_document(append_to) + if reference_doc: + data["reference_doctype"] = reference_doc.doctype + data["reference_name"] = reference_doc.name + data["is_first"] = True + + if self.is_notification(): + # Disable notifications for notification. + data["unread_notification_sent"] = 1 + + if self.seen_status: + data["_seen"] = json.dumps(self.get_users_linked_to_account(self.email_account)) + + communication = influxframework.get_doc(data) + communication.flags.in_receive = True + communication.insert(ignore_permissions=True) + + # save attachments + communication._attachments = self.save_attachments_in_doc(communication) + communication.content = sanitize_html(self.replace_inline_images(communication._attachments)) + communication.save() + return communication + + def replace_inline_images(self, attachments): + # replace inline images + content = self.content + for file in attachments: + if file.name in self.cid_map and self.cid_map[file.name]: + content = content.replace(f"cid:{self.cid_map[file.name]}", file.file_url) + return content + + def is_notification(self): + isnotification = self.mail.get("isnotification") + return isnotification and ("notification" in isnotification) + + def is_exist_in_system(self): + """Check if this email already exists in the system(as communication document).""" + from influxframework.core.doctype.communication.communication import Communication + + if not self.message_id: + return + + return Communication.find_one_by_filters(message_id=self.message_id, order_by="creation DESC") + + def is_sender_same_as_receiver(self): + return self.from_email == self.email_account.email_id + + def is_reply_to_system_sent_mail(self): + """Is it a reply to already sent mail.""" + return self.is_reply() and influxframework.local.site in self.in_reply_to + + def parent_email_queue(self): + """Get parent record from `Email Queue`. + + If it is a reply to already sent mail, then there will be a parent record in EMail Queue. + """ + from influxframework.email.doctype.email_queue.email_queue import EmailQueue + + if self._parent_email_queue is not None: + return self._parent_email_queue + + parent_email_queue = "" + if self.is_reply_to_system_sent_mail(): + parent_email_queue = EmailQueue.find_one_by_filters(message_id=self.in_reply_to) + + self._parent_email_queue = parent_email_queue or "" + return self._parent_email_queue + + def parent_communication(self): + """Find a related communication so that we can prepare a mail thread. + + The way it happens is by using in-reply-to header, and we can't make thread if it does not exist. + + Here are the cases to handle: + 1. If mail is a reply to already sent mail, then we can get parent communicaion from + Email Queue record. + 2. Sometimes we send communication name in message-ID directly, use that to get parent communication. + 3. Sender sent a reply but reply is on top of what (s)he sent before, + then parent record exists directly in communication. + """ + from influxframework.core.doctype.communication.communication import Communication + + if self._parent_communication is not None: + return self._parent_communication + + if not self.is_reply(): + return "" + + if not self.is_reply_to_system_sent_mail(): + communication = Communication.find_one_by_filters( + message_id=self.in_reply_to, creation=[">=", self.get_relative_dt(-30)] + ) + elif self.parent_email_queue() and self.parent_email_queue().communication: + communication = Communication.find(self.parent_email_queue().communication, ignore_error=True) + else: + reference = self.in_reply_to + if "@" in self.in_reply_to: + reference, _ = self.in_reply_to.split("@", 1) + communication = Communication.find(reference, ignore_error=True) + + self._parent_communication = communication or "" + return self._parent_communication + + def reference_document(self): + """Reference document is a document to which mail relate to. + + We can get reference document from Parent record(EmailQueue | Communication) if exists. + Otherwise we do subject match to find reference document if we know the reference(append_to) doctype. + """ + if self._reference_document is not None: + return self._reference_document + + reference_document = "" + parent = self.parent_email_queue() or self.parent_communication() + + if parent and parent.reference_doctype: + reference_doctype, reference_name = parent.reference_doctype, parent.reference_name + reference_document = self.get_doc(reference_doctype, reference_name, ignore_error=True) + + if not reference_document and self.email_account.append_to: + reference_document = self.match_record_by_subject_and_sender(self.email_account.append_to) + + self._reference_document = reference_document or "" + return self._reference_document + + def get_reference_name_from_subject(self): + """ + Ex: "Re: Your email (#OPP-2020-2334343)" + """ + return self.subject.rsplit("#", 1)[-1].strip(" ()") + + def match_record_by_subject_and_sender(self, doctype): + """Find a record in the given doctype that matches with email subject and sender. + + Cases: + 1. Sometimes record name is part of subject. We can get document by parsing name from subject + 2. Find by matching sender and subject + 3. Find by matching subject alone (Special case) + Ex: when a System User is using Outlook and replies to an email from their own client, + it reaches the Email Account with the threading info lost and the (sender + subject match) + doesn't work because the sender in the first communication was someone different to whom + the system user is replying to via the common email account in InfluxFramework. This fix bypasses + the sender match when the sender is a system user and subject is atleast 10 chars long + (for additional safety) + + NOTE: We consider not to match by subject if match record is very old. + """ + name = self.get_reference_name_from_subject() + email_fields = self.get_email_fields(doctype) + + record = self.get_doc(doctype, name, ignore_error=True) if name else None + + if not record: + subject = self.clean_subject(self.subject) + filters = { + email_fields.subject_field: ("like", f"%{subject}%"), + "creation": (">", self.get_relative_dt(days=-60)), + } + + # Sender check is not needed incase mail is from system user. + if not (len(subject) > 10 and is_system_user(self.from_email)): + filters[email_fields.sender_field] = self.from_email + + name = influxframework.db.get_value(self.email_account.append_to, filters=filters) + record = self.get_doc(doctype, name, ignore_error=True) if name else None + return record + + def _create_reference_document(self, doctype): + """Create reference document if it does not exist in the system.""" + parent = influxframework.new_doc(doctype) + email_fileds = self.get_email_fields(doctype) + + if email_fileds.subject_field: + parent.set(email_fileds.subject_field, influxframework.as_unicode(self.subject)[:140]) + + if email_fileds.sender_field: + parent.set(email_fileds.sender_field, influxframework.as_unicode(self.from_email)) + + parent.flags.ignore_mandatory = True + + try: + parent.insert(ignore_permissions=True) + except influxframework.DuplicateEntryError: + # try and find matching parent + parent_name = influxframework.db.get_value( + self.email_account.append_to, {email_fileds.sender_field: self.from_email} + ) + if parent_name: + parent.name = parent_name + else: + parent = None + return parent + + @staticmethod + def get_doc(doctype, docname, ignore_error=False): + try: + return influxframework.get_doc(doctype, docname) + except influxframework.DoesNotExistError: + if ignore_error: + return + raise + + @staticmethod + def get_relative_dt(days): + """Get relative to current datetime. Only relative days are supported.""" + return add_days(get_datetime(), days) + + @staticmethod + def get_users_linked_to_account(email_account): + """Get list of users who linked to Email account.""" + users = influxframework.get_all( + "User Email", filters={"email_account": email_account.name}, fields=["parent"] + ) + return list({user.get("parent") for user in users}) + + @staticmethod + def clean_subject(subject): + """Remove Prefixes like 'fw', FWD', 're' etc from subject.""" + # Match strings like "fw:", "re :" etc. + regex = r"(^\s*(fw|fwd|wg)[^:]*:|\s*(re|aw)[^:]*:\s*)*" + return influxframework.as_unicode(strip(re.sub(regex, "", subject, 0, flags=re.IGNORECASE))) + + @staticmethod + def get_email_fields(doctype): + """Returns Email related fields of a doctype.""" + fields = influxframework._dict() + + email_fields = ["subject_field", "sender_field"] + meta = influxframework.get_meta(doctype) + + for field in email_fields: + if hasattr(meta, field): + fields[field] = getattr(meta, field) + return fields + + @staticmethod + def get_document(self, doctype, name): + """Is same as influxframework.get_doc but suppresses the DoesNotExist error.""" + try: + return influxframework.get_doc(doctype, name) + except influxframework.DoesNotExistError: + return None + + def as_dict(self): + """ """ + return { + "subject": self.subject, + "content": self.get_content(), + "text_content": self.text_content, + "sent_or_received": "Received", + "sender_full_name": self.from_real_name, + "sender": self.from_email, + "recipients": self.mail.get("To"), + "cc": self.mail.get("CC"), + "email_account": self.email_account.name, + "communication_medium": "Email", + "uid": self.uid, + "message_id": self.message_id, + "communication_date": self.date, + "has_attachment": 1 if self.attachments else 0, + "seen": self.seen_status or 0, + } + + +class TimerMixin: + def __init__(self, *args, **kwargs): + self.timeout = kwargs.pop("timeout", 0.0) + self.elapsed_time = 0.0 + self._super.__init__(self, *args, **kwargs) + if self.timeout: + # set per operation timeout to one-fifth of total pop timeout + self.sock.settimeout(self.timeout / 5.0) + + def _getline(self, *args, **kwargs): + start_time = time.time() + ret = self._super._getline(self, *args, **kwargs) + + self.elapsed_time += time.time() - start_time + if self.timeout and self.elapsed_time > self.timeout: + raise EmailTimeoutError + + return ret + + def quit(self, *args, **kwargs): + self.elapsed_time = 0.0 + return self._super.quit(self, *args, **kwargs) + + +class Timed_POP3(TimerMixin, poplib.POP3): + _super = poplib.POP3 + + +class Timed_POP3_SSL(TimerMixin, poplib.POP3_SSL): + _super = poplib.POP3_SSL + + +class Timed_IMAP4(TimerMixin, imaplib.IMAP4): + _super = imaplib.IMAP4 + + +class Timed_IMAP4_SSL(TimerMixin, imaplib.IMAP4_SSL): + _super = imaplib.IMAP4_SSL diff --git a/influxframework/email/smtp.py b/influxframework/email/smtp.py new file mode 100644 index 0000000..68374ae --- /dev/null +++ b/influxframework/email/smtp.py @@ -0,0 +1,153 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE + +import smtplib + +import influxframework +from influxframework import _ +from influxframework.email.oauth import Oauth +from influxframework.utils import cint, cstr + + +class InvalidEmailCredentials(influxframework.ValidationError): + pass + + +def send(email, append_to=None, retry=1): + """Deprecated: Send the message or add it to Outbox Email""" + + def _send(retry): + from influxframework.email.doctype.email_account.email_account import EmailAccount + + try: + email_account = EmailAccount.find_outgoing(match_by_doctype=append_to) + smtpserver = email_account.get_smtp_server() + + # validate is called in as_string + email_body = email.as_string() + + smtpserver.sess.sendmail(email.sender, email.recipients + (email.cc or []), email_body) + except smtplib.SMTPSenderRefused: + influxframework.throw(_("Invalid login or password"), title="Email Failed") + raise + except smtplib.SMTPRecipientsRefused: + influxframework.msgprint(_("Invalid recipient address"), title="Email Failed") + raise + except (smtplib.SMTPServerDisconnected, smtplib.SMTPAuthenticationError): + if not retry: + raise + else: + retry = retry - 1 + _send(retry) + + _send(retry) + + +class SMTPServer: + def __init__( + self, + server, + login=None, + email_account=None, + password=None, + port=None, + use_tls=None, + use_ssl=None, + use_oauth=0, + refresh_token=None, + access_token=None, + service=None, + ): + self.login = login + self.email_account = email_account + self.password = password + self._server = server + self._port = port + self.use_tls = use_tls + self.use_ssl = use_ssl + self.use_oauth = use_oauth + self.refresh_token = refresh_token + self.access_token = access_token + self.service = service + self._session = None + + if not self.server: + influxframework.msgprint( + _( + "Email Account not setup. Please create a new Email Account from Setup > Email > Email Account" + ), + raise_exception=influxframework.OutgoingEmailError, + ) + + @property + def port(self): + port = self._port or (self.use_ssl and 465) or (self.use_tls and 587) + return cint(port) + + @property + def server(self): + return cstr(self._server or "") + + def secure_session(self, conn): + """Secure the connection incase of TLS.""" + if self.use_tls: + conn.ehlo() + conn.starttls() + conn.ehlo() + + @property + def session(self): + if self.is_session_active(): + return self._session + + SMTP = smtplib.SMTP_SSL if self.use_ssl else smtplib.SMTP + + try: + _session = SMTP(self.server, self.port) + if not _session: + influxframework.msgprint( + _("Could not connect to outgoing email server"), raise_exception=influxframework.OutgoingEmailError + ) + + self.secure_session(_session) + + if self.use_oauth: + Oauth( + _session, self.email_account, self.login, self.access_token, self.refresh_token, self.service + ).connect() + + elif self.password: + res = _session.login(str(self.login or ""), str(self.password or "")) + + # check if logged correctly + if res[0] != 235: + influxframework.msgprint(res[1], raise_exception=influxframework.OutgoingEmailError) + + self._session = _session + return self._session + + except smtplib.SMTPAuthenticationError: + self.throw_invalid_credentials_exception() + + except OSError: + # Invalid mail server -- due to refusing connection + influxframework.throw(_("Invalid Outgoing Mail Server or Port"), title=_("Incorrect Configuration")) + + def is_session_active(self): + if self._session: + try: + return self._session.noop()[0] == 250 + except Exception: + return False + + def quit(self): + if self.is_session_active(): + self._session.quit() + + @classmethod + def throw_invalid_credentials_exception(cls): + influxframework.throw( + _("Please check your email login credentials."), + title=_("Invalid Credentials"), + exc=InvalidEmailCredentials, + ) diff --git a/influxframework/email/test_email_body.py b/influxframework/email/test_email_body.py new file mode 100644 index 0000000..98cf187 --- /dev/null +++ b/influxframework/email/test_email_body.py @@ -0,0 +1,203 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +import base64 +import os + +import influxframework +from influxframework import safe_decode +from influxframework.email.doctype.email_queue.email_queue import QueueBuilder, SendMailContext +from influxframework.email.email_body import ( + get_email, + get_header, + inline_style_in_html, + replace_filename_with_cid, +) +from influxframework.email.receive import Email +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestEmailBody(InfluxFrameworkTestCase): + def setUp(self): + email_html = """ +
      +

      Hey John Doe!

      +

      This is embedded image you asked for

      + +
      +""" + email_text = """ +Hey John Doe! +This is the text version of this email +""" + + img_path = os.path.abspath("assets/influxframework/images/influxframework-favicon.svg") + with open(img_path, "rb") as f: + img_content = f.read() + img_base64 = base64.b64encode(img_content).decode() + + # email body keeps 76 characters on one line + self.img_base64 = fixed_column_width(img_base64, 76) + + self.email_string = ( + get_email( + recipients=["test@example.com"], + sender="me@example.com", + subject="Test Subject", + content=email_html, + text_content=email_text, + ) + .as_string() + .replace("\r\n", "\n") + ) + + def test_prepare_message_returns_already_encoded_string(self): + uni_chr1 = chr(40960) + uni_chr2 = chr(1972) + + QueueBuilder( + recipients=["test@example.com"], + sender="me@example.com", + subject="Test Subject", + message=f"

      {uni_chr1}abcd{uni_chr2}

      ", + text_content="whatever", + ).process() + queue_doc = influxframework.get_last_doc("Email Queue") + mail_ctx = SendMailContext(queue_doc=queue_doc) + result = mail_ctx.build_message(recipient_email="test@test.com") + self.assertTrue(b"

      =EA=80=80abcd=DE=B4

      " in result) + + def test_prepare_message_returns_cr_lf(self): + QueueBuilder( + recipients=["test@example.com"], + sender="me@example.com", + subject="Test Subject", + message="

      \n this is a test of newlines\n" + "

      ", + text_content="whatever", + ).process() + queue_doc = influxframework.get_last_doc("Email Queue") + mail_ctx = SendMailContext(queue_doc=queue_doc) + result = safe_decode(mail_ctx.build_message(recipient_email="test@test.com")) + + self.assertTrue(result.count("\n") == result.count("\r")) + + def test_image(self): + img_signature = """ +Content-Type: image/svg+xml +MIME-Version: 1.0 +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="influxframework-favicon.svg" +""" + self.assertTrue(img_signature in self.email_string) + self.assertTrue(self.img_base64 in self.email_string) + + def test_text_content(self): + text_content = """ +Content-Type: text/plain; charset="utf-8" +MIME-Version: 1.0 +Content-Transfer-Encoding: quoted-printable + + +Hey John Doe! +This is the text version of this email +""" + self.assertTrue(text_content in self.email_string) + + def test_email_content(self): + html_head = """ +Content-Type: text/html; charset="utf-8" +MIME-Version: 1.0 +Content-Transfer-Encoding: quoted-printable + + + +""" + + html = """

      Hey John Doe!

      """ + + self.assertTrue(html_head in self.email_string) + self.assertTrue(html in self.email_string) + + def test_replace_filename_with_cid(self): + original_message = """ +
      + test + +
      + """ + message, inline_images = replace_filename_with_cid(original_message) + + processed_message = """ +
      + test + +
      + """.format( + inline_images[0].get("content_id") + ) + self.assertEqual(message, processed_message) + + def test_inline_styling(self): + html = """ +

      Hi John

      +

      This is a test email

      +""" + transformed_html = """ +

      Hi John

      +

      This is a test email

      +""" + self.assertTrue(transformed_html in inline_style_in_html(html)) + + def test_email_header(self): + email_html = """ +

      Hey John Doe!

      +

      This is embedded image you asked for

      +""" + email_string = get_email( + recipients=["test@example.com"], + sender="me@example.com", + subject="Test Subject\u2028, with line break, \nand Line feed \rand carriage return.", + content=email_html, + header=["Email Title", "green"], + ).as_string() + # REDESIGN-TODO: Add style for indicators in email + self.assertTrue("""""" in email_string) + self.assertTrue("Email Title" in email_string) + self.assertIn( + "Subject: Test Subject, with line break, and Line feed and carriage return.", email_string + ) + + def test_get_email_header(self): + html = get_header(["This is test", "orange"]) + self.assertTrue('' in html) + self.assertTrue("This is test" in html) + + html = get_header(["This is another test"]) + self.assertTrue("This is another test" in html) + + html = get_header("This is string") + self.assertTrue("This is string" in html) + + def test_8bit_utf_8_decoding(self): + text_content_bytes = b"\xed\x95\x9c\xea\xb8\x80\xe1\xa5\xa1\xe2\x95\xa5\xe0\xba\xaa\xe0\xa4\x8f" + text_content = text_content_bytes.decode("utf-8") + + content_bytes = ( + b"""MIME-Version: 1.0 +Content-Type: text/plain; charset=utf-8 +Content-Disposition: inline +Content-Transfer-Encoding: 8bit +From: test1_@influxerp.com +Reply-To: test2_@influxerp.com +""" + + text_content_bytes + ) + + mail = Email(content_bytes) + self.assertEqual(mail.text_content, text_content) + + +def fixed_column_width(string, chunk_size): + parts = [string[0 + i : chunk_size + i] for i in range(0, len(string), chunk_size)] + return "\n".join(parts) diff --git a/influxframework/email/test_smtp.py b/influxframework/email/test_smtp.py new file mode 100644 index 0000000..15223d6 --- /dev/null +++ b/influxframework/email/test_smtp.py @@ -0,0 +1,88 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: The MIT License + +import influxframework +from influxframework.email.doctype.email_account.email_account import EmailAccount +from influxframework.email.smtp import SMTPServer +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestSMTP(InfluxFrameworkTestCase): + def test_smtp_ssl_session(self): + for port in [None, 0, 465, "465"]: + make_server(port, 1, 0) + + def test_smtp_tls_session(self): + for port in [None, 0, 587, "587"]: + make_server(port, 0, 1) + + def test_get_email_account(self): + existing_email_accounts = influxframework.get_all( + "Email Account", fields=["name", "enable_outgoing", "default_outgoing", "append_to", "use_imap"] + ) + unset_details = {"enable_outgoing": 0, "default_outgoing": 0, "append_to": None, "use_imap": 0} + for email_account in existing_email_accounts: + influxframework.db.set_value("Email Account", email_account["name"], unset_details) + + # remove mail_server config so that test@example.com is not created + mail_server = influxframework.conf.get("mail_server") + del influxframework.conf["mail_server"] + + influxframework.local.outgoing_email_account = {} + + influxframework.local.outgoing_email_account = {} + # lowest preference given to email account with default incoming enabled + create_email_account( + email_id="default_outgoing_enabled@gmail.com", + password="password", + enable_outgoing=1, + default_outgoing=1, + ) + self.assertEqual(EmailAccount.find_outgoing().email_id, "default_outgoing_enabled@gmail.com") + + influxframework.local.outgoing_email_account = {} + # highest preference given to email account with append_to matching + create_email_account( + email_id="append_to@gmail.com", + password="password", + enable_outgoing=1, + default_outgoing=1, + append_to="Blog Post", + ) + self.assertEqual( + EmailAccount.find_outgoing(match_by_doctype="Blog Post").email_id, "append_to@gmail.com" + ) + + # add back the mail_server + influxframework.conf["mail_server"] = mail_server + for email_account in existing_email_accounts: + set_details = { + "enable_outgoing": email_account["enable_outgoing"], + "default_outgoing": email_account["default_outgoing"], + "append_to": email_account["append_to"], + } + influxframework.db.set_value("Email Account", email_account["name"], set_details) + + +def create_email_account(email_id, password, enable_outgoing, default_outgoing=0, append_to=None): + email_dict = { + "email_id": email_id, + "passsword": password, + "enable_outgoing": enable_outgoing, + "default_outgoing": default_outgoing, + "enable_incoming": 1, + "append_to": append_to, + "is_dummy_password": 1, + "smtp_server": "localhost", + "use_imap": 0, + } + + email_account = influxframework.new_doc("Email Account") + email_account.update(email_dict) + email_account.save() + + +def make_server(port, ssl, tls): + server = SMTPServer(server="smtp.gmail.com", port=port, use_ssl=ssl, use_tls=tls) + + server.session diff --git a/influxframework/email/utils.py b/influxframework/email/utils.py new file mode 100644 index 0000000..c9bff93 --- /dev/null +++ b/influxframework/email/utils.py @@ -0,0 +1,18 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +import imaplib +import poplib + +from influxframework.utils import cint + + +def get_port(doc): + if not doc.incoming_port: + if doc.use_imap: + doc.incoming_port = imaplib.IMAP4_SSL_PORT if doc.use_ssl else imaplib.IMAP4_PORT + + else: + doc.incoming_port = poplib.POP3_SSL_PORT if doc.use_ssl else poplib.POP3_PORT + + return cint(doc.incoming_port) diff --git a/influxframework/event_streaming/__init__.py b/influxframework/event_streaming/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/event_streaming/doctype/__init__.py b/influxframework/event_streaming/doctype/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/event_streaming/doctype/document_type_field_mapping/__init__.py b/influxframework/event_streaming/doctype/document_type_field_mapping/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/event_streaming/doctype/document_type_field_mapping/document_type_field_mapping.json b/influxframework/event_streaming/doctype/document_type_field_mapping/document_type_field_mapping.json new file mode 100644 index 0000000..bba0a98 --- /dev/null +++ b/influxframework/event_streaming/doctype/document_type_field_mapping/document_type_field_mapping.json @@ -0,0 +1,73 @@ +{ + "actions": [], + "creation": "2019-09-27 12:46:50.165135", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "local_fieldname", + "mapping_type", + "mapping", + "remote_value_filters", + "column_break_5", + "remote_fieldname", + "default_value" + ], + "fields": [ + { + "fieldname": "remote_fieldname", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Remote Fieldname" + }, + { + "fieldname": "local_fieldname", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Local Fieldname", + "reqd": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "default_value", + "fieldtype": "Data", + "label": "Default Value" + }, + { + "fieldname": "mapping_type", + "fieldtype": "Select", + "label": "Mapping Type", + "options": "\nChild Table\nDocument" + }, + { + "depends_on": "eval:doc.mapping_type;", + "fieldname": "mapping", + "fieldtype": "Link", + "label": "Mapping", + "options": "Document Type Mapping" + }, + { + "depends_on": "eval:doc.mapping_type==\"Document\";", + "fieldname": "remote_value_filters", + "fieldtype": "Code", + "label": "Remote Value Filters", + "mandatory_depends_on": "eval:doc.mapping_type===\"Document\";", + "options": "JSON" + } + ], + "istable": 1, + "links": [], + "modified": "2020-03-19 13:56:36.223799", + "modified_by": "Administrator", + "module": "Event Streaming", + "name": "Document Type Field Mapping", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/event_streaming/doctype/document_type_field_mapping/document_type_field_mapping.py b/influxframework/event_streaming/doctype/document_type_field_mapping/document_type_field_mapping.py new file mode 100644 index 0000000..53d4177 --- /dev/null +++ b/influxframework/event_streaming/doctype/document_type_field_mapping/document_type_field_mapping.py @@ -0,0 +1,9 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class DocumentTypeFieldMapping(Document): + pass diff --git a/influxframework/event_streaming/doctype/document_type_mapping/__init__.py b/influxframework/event_streaming/doctype/document_type_mapping/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/event_streaming/doctype/document_type_mapping/document_type_mapping.js b/influxframework/event_streaming/doctype/document_type_mapping/document_type_mapping.js new file mode 100644 index 0000000..a10d865 --- /dev/null +++ b/influxframework/event_streaming/doctype/document_type_mapping/document_type_mapping.js @@ -0,0 +1,37 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Document Type Mapping", { + local_doctype: function (frm) { + if (frm.doc.local_doctype) { + influxframework.model.clear_table(frm.doc, "field_mapping"); + let fields = frm.events.get_fields(frm); + $.each(fields, function (i, data) { + let row = influxframework.model.add_child( + frm.doc, + "Document Type Field Mapping", + "field_mapping" + ); + row.local_fieldname = data; + }); + refresh_field("field_mapping"); + } + }, + + get_fields: function (frm) { + let filtered_fields = []; + influxframework.model.with_doctype(frm.doc.local_doctype, () => { + influxframework.get_meta(frm.doc.local_doctype).fields.map((field) => { + if ( + field.fieldname !== "remote_docname" && + field.fieldname !== "remote_site_name" && + influxframework.model.is_value_type(field) && + !field.hidden + ) { + filtered_fields.push(field.fieldname); + } + }); + }); + return filtered_fields; + }, +}); diff --git a/influxframework/event_streaming/doctype/document_type_mapping/document_type_mapping.json b/influxframework/event_streaming/doctype/document_type_mapping/document_type_mapping.json new file mode 100644 index 0000000..6a59cf3 --- /dev/null +++ b/influxframework/event_streaming/doctype/document_type_mapping/document_type_mapping.json @@ -0,0 +1,71 @@ +{ + "autoname": "field:mapping_name", + "creation": "2019-09-27 12:45:56.529124", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "mapping_name", + "local_doctype", + "remote_doctype", + "section_break_3", + "field_mapping" + ], + "fields": [ + { + "fieldname": "local_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Local Document Type", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "remote_doctype", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Remote Document Type", + "reqd": 1 + }, + { + "fieldname": "section_break_3", + "fieldtype": "Section Break" + }, + { + "fieldname": "field_mapping", + "fieldtype": "Table", + "label": "Field Mapping", + "options": "Document Type Field Mapping" + }, + { + "fieldname": "mapping_name", + "fieldtype": "Data", + "label": "Mapping Name", + "reqd": 1, + "unique": 1 + } + ], + "modified": "2019-10-09 08:36:04.621397", + "modified_by": "Administrator", + "module": "Event Streaming", + "name": "Document Type Mapping", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/event_streaming/doctype/document_type_mapping/document_type_mapping.py b/influxframework/event_streaming/doctype/document_type_mapping/document_type_mapping.py new file mode 100644 index 0000000..a0118af --- /dev/null +++ b/influxframework/event_streaming/doctype/document_type_mapping/document_type_mapping.py @@ -0,0 +1,181 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +import json + +import influxframework +from influxframework import _ +from influxframework.model import child_table_fields, default_fields +from influxframework.model.document import Document + + +class DocumentTypeMapping(Document): + def validate(self): + self.validate_inner_mapping() + + def validate_inner_mapping(self): + meta = influxframework.get_meta(self.local_doctype) + for field_map in self.field_mapping: + if field_map.local_fieldname not in (default_fields + child_table_fields): + field = meta.get_field(field_map.local_fieldname) + if not field: + influxframework.throw(_("Row #{0}: Invalid Local Fieldname").format(field_map.idx)) + + fieldtype = field.get("fieldtype") + if fieldtype in ["Link", "Dynamic Link", "Table"]: + if not field_map.mapping and not field_map.default_value: + msg = _( + "Row #{0}: Please set Mapping or Default Value for the field {1} since its a dependency field" + ).format(field_map.idx, influxframework.bold(field_map.local_fieldname)) + influxframework.throw(msg, title="Inner Mapping Missing") + + if field_map.mapping_type == "Document" and not field_map.remote_value_filters: + msg = _( + "Row #{0}: Please set remote value filters for the field {1} to fetch the unique remote dependency document" + ).format(field_map.idx, influxframework.bold(field_map.remote_fieldname)) + influxframework.throw(msg, title="Remote Value Filters Missing") + + def get_mapping(self, doc, producer_site, update_type): + remote_fields = [] + # list of tuples (local_fieldname, dependent_doc) + dependencies = [] + + for mapping in self.field_mapping: + if doc.get(mapping.remote_fieldname): + if mapping.mapping_type == "Document": + if not mapping.default_value: + dependency = self.get_mapped_dependency(mapping, producer_site, doc) + if dependency: + dependencies.append((mapping.local_fieldname, dependency)) + else: + doc[mapping.local_fieldname] = mapping.default_value + + if mapping.mapping_type == "Child Table" and update_type != "Update": + doc[mapping.local_fieldname] = get_mapped_child_table_docs( + mapping.mapping, doc[mapping.remote_fieldname], producer_site + ) + else: + # copy value into local fieldname key and remove remote fieldname key + doc[mapping.local_fieldname] = doc[mapping.remote_fieldname] + + if mapping.local_fieldname != mapping.remote_fieldname: + remote_fields.append(mapping.remote_fieldname) + + if not doc.get(mapping.remote_fieldname) and mapping.default_value and update_type != "Update": + doc[mapping.local_fieldname] = mapping.default_value + + # remove the remote fieldnames + for field in remote_fields: + doc.pop(field, None) + + if update_type != "Update": + doc["doctype"] = self.local_doctype + + mapping = {"doc": influxframework.as_json(doc)} + if len(dependencies): + mapping["dependencies"] = dependencies + return mapping + + def get_mapped_update(self, update, producer_site): + update_diff = influxframework._dict(json.loads(update.data)) + mapping = update_diff + dependencies = [] + if update_diff.changed: + doc_map = self.get_mapping(update_diff.changed, producer_site, "Update") + mapped_doc = doc_map.get("doc") + mapping.changed = json.loads(mapped_doc) + if doc_map.get("dependencies"): + dependencies += doc_map.get("dependencies") + + if update_diff.removed: + mapping = self.map_rows_removed(update_diff, mapping) + if update_diff.added: + mapping = self.map_rows(update_diff, mapping, producer_site, operation="added") + if update_diff.row_changed: + mapping = self.map_rows(update_diff, mapping, producer_site, operation="row_changed") + + update = {"doc": influxframework.as_json(mapping)} + if len(dependencies): + update["dependencies"] = dependencies + return update + + def get_mapped_dependency(self, mapping, producer_site, doc): + inner_mapping = influxframework.get_doc("Document Type Mapping", mapping.mapping) + filters = json.loads(mapping.remote_value_filters) + for key, value in filters.items(): + if value.startswith("eval:"): + val = influxframework.safe_eval(value[5:], None, dict(doc=doc)) + filters[key] = val + if doc.get(value): + filters[key] = doc.get(value) + matching_docs = producer_site.get_doc(inner_mapping.remote_doctype, filters=filters) + if len(matching_docs): + remote_docname = matching_docs[0].get("name") + remote_doc = producer_site.get_doc(inner_mapping.remote_doctype, remote_docname) + doc = inner_mapping.get_mapping(remote_doc, producer_site, "Insert").get("doc") + return doc + return + + def map_rows_removed(self, update_diff, mapping): + removed = [] + mapping["removed"] = update_diff.removed + for key, value in update_diff.removed.copy().items(): + local_table_name = influxframework.db.get_value( + "Document Type Field Mapping", + {"remote_fieldname": key, "parent": self.name}, + "local_fieldname", + ) + mapping.removed[local_table_name] = value + if local_table_name != key: + removed.append(key) + + # remove the remote fieldnames + for field in removed: + mapping.removed.pop(field, None) + return mapping + + def map_rows(self, update_diff, mapping, producer_site, operation): + remote_fields = [] + for tablename, entries in update_diff.get(operation).copy().items(): + local_table_name = influxframework.db.get_value( + "Document Type Field Mapping", {"remote_fieldname": tablename}, "local_fieldname" + ) + table_map = influxframework.db.get_value( + "Document Type Field Mapping", + {"local_fieldname": local_table_name, "parent": self.name}, + "mapping", + ) + table_map = influxframework.get_doc("Document Type Mapping", table_map) + docs = [] + for entry in entries: + mapped_doc = table_map.get_mapping(entry, producer_site, "Update").get("doc") + docs.append(json.loads(mapped_doc)) + mapping.get(operation)[local_table_name] = docs + if local_table_name != tablename: + remote_fields.append(tablename) + + # remove the remote fieldnames + for field in remote_fields: + mapping.get(operation).pop(field, None) + + return mapping + + +def get_mapped_child_table_docs(child_map, table_entries, producer_site): + """Get mapping for child doctypes""" + child_map = influxframework.get_doc("Document Type Mapping", child_map) + mapped_entries = [] + remote_fields = [] + for child_doc in table_entries: + for mapping in child_map.field_mapping: + if child_doc.get(mapping.remote_fieldname): + child_doc[mapping.local_fieldname] = child_doc[mapping.remote_fieldname] + if mapping.local_fieldname != mapping.remote_fieldname: + child_doc.pop(mapping.remote_fieldname, None) + mapped_entries.append(child_doc) + + # remove the remote fieldnames + for field in remote_fields: + child_doc.pop(field, None) + + child_doc["doctype"] = child_map.local_doctype + return mapped_entries diff --git a/influxframework/event_streaming/doctype/document_type_mapping/test_document_type_mapping.py b/influxframework/event_streaming/doctype/document_type_mapping/test_document_type_mapping.py new file mode 100644 index 0000000..5294be1 --- /dev/null +++ b/influxframework/event_streaming/doctype/document_type_mapping/test_document_type_mapping.py @@ -0,0 +1,8 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestDocumentTypeMapping(InfluxFrameworkTestCase): + pass diff --git a/influxframework/event_streaming/doctype/event_consumer/__init__.py b/influxframework/event_streaming/doctype/event_consumer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/event_streaming/doctype/event_consumer/event_consumer.js b/influxframework/event_streaming/doctype/event_consumer/event_consumer.js new file mode 100644 index 0000000..15f9943 --- /dev/null +++ b/influxframework/event_streaming/doctype/event_consumer/event_consumer.js @@ -0,0 +1,17 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Event Consumer", { + refresh: function (frm) { + // formatter for subscribed doctype approval status + frm.set_indicator_formatter("status", function (doc) { + let indicator = "orange"; + if (doc.status == "Approved") { + indicator = "green"; + } else if (doc.status == "Rejected") { + indicator = "red"; + } + return indicator; + }); + }, +}); diff --git a/influxframework/event_streaming/doctype/event_consumer/event_consumer.json b/influxframework/event_streaming/doctype/event_consumer/event_consumer.json new file mode 100644 index 0000000..42b47ce --- /dev/null +++ b/influxframework/event_streaming/doctype/event_consumer/event_consumer.json @@ -0,0 +1,97 @@ +{ + "actions": [], + "autoname": "field:callback_url", + "creation": "2019-08-26 17:45:15.479530", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "consumer_doctypes", + "callback_url", + "section_break_3", + "api_key", + "api_secret", + "column_break_6", + "user", + "incoming_change" + ], + "fields": [ + { + "fieldname": "callback_url", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Callback URL", + "read_only": 1, + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "api_key", + "fieldtype": "Data", + "label": "API Key", + "reqd": 1 + }, + { + "fieldname": "api_secret", + "fieldtype": "Password", + "label": "API Secret", + "reqd": 1 + }, + { + "fieldname": "user", + "fieldtype": "Link", + "label": "Event Subscriber", + "options": "User", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "section_break_3", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "incoming_change", + "fieldtype": "Check", + "hidden": 1, + "label": "Incoming Change", + "read_only": 1 + }, + { + "fieldname": "consumer_doctypes", + "fieldtype": "Table", + "label": "Event Consumer Document Types", + "options": "Event Consumer Document Type", + "reqd": 1 + } + ], + "in_create": 1, + "links": [], + "modified": "2020-09-08 16:42:39.828085", + "modified_by": "Administrator", + "module": "Event Streaming", + "name": "Event Consumer", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/event_streaming/doctype/event_consumer/event_consumer.py b/influxframework/event_streaming/doctype/event_consumer/event_consumer.py new file mode 100644 index 0000000..3c156fb --- /dev/null +++ b/influxframework/event_streaming/doctype/event_consumer/event_consumer.py @@ -0,0 +1,216 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +import json +import os + +import requests + +import influxframework +from influxframework import _ +from influxframework.influxframeworkclient import InfluxFrameworkClient +from influxframework.model.document import Document +from influxframework.utils.background_jobs import get_jobs +from influxframework.utils.data import get_url + + +class EventConsumer(Document): + def validate(self): + # approve subscribed doctypes for tests + # influxframework.flags.in_test won't work here as tests are running on the consumer site + if os.environ.get("CI"): + for entry in self.consumer_doctypes: + entry.status = "Approved" + + def on_update(self): + if not self.incoming_change: + doc_before_save = self.get_doc_before_save() + if doc_before_save.api_key != self.api_key or doc_before_save.api_secret != self.api_secret: + return + + self.update_consumer_status() + else: + influxframework.db.set_value(self.doctype, self.name, "incoming_change", 0) + + influxframework.cache().delete_value("event_consumer_document_type_map") + + def on_trash(self): + for i in influxframework.get_all("Event Update Log Consumer", {"consumer": self.name}): + influxframework.delete_doc("Event Update Log Consumer", i.name) + influxframework.cache().delete_value("event_consumer_document_type_map") + + def update_consumer_status(self): + consumer_site = get_consumer_site(self.callback_url) + event_producer = consumer_site.get_doc("Event Producer", get_url()) + event_producer = influxframework._dict(event_producer) + config = event_producer.producer_doctypes + event_producer.producer_doctypes = [] + for entry in config: + if entry.get("has_mapping"): + ref_doctype = consumer_site.get_value( + "Document Type Mapping", "remote_doctype", entry.get("mapping") + ).get("remote_doctype") + else: + ref_doctype = entry.get("ref_doctype") + + entry["status"] = influxframework.db.get_value( + "Event Consumer Document Type", {"parent": self.name, "ref_doctype": ref_doctype}, "status" + ) + + event_producer.producer_doctypes = config + # when producer doc is updated it updates the consumer doc + # set flag to avoid deadlock + event_producer.incoming_change = True + consumer_site.update(event_producer) + + def get_consumer_status(self): + response = requests.get(self.callback_url) + if response.status_code != 200: + return "offline" + return "online" + + +@influxframework.whitelist() +def register_consumer(data): + """create an event consumer document for registering a consumer""" + data = json.loads(data) + # to ensure that consumer is created only once + if influxframework.db.exists("Event Consumer", data["event_consumer"]): + return None + + user = data["user"] + if not influxframework.db.exists("User", user): + influxframework.throw(_("User {0} not found on the producer site").format(user)) + + if "System Manager" not in influxframework.get_roles(user): + influxframework.throw(_("Event Subscriber has to be a System Manager.")) + + consumer = influxframework.new_doc("Event Consumer") + consumer.callback_url = data["event_consumer"] + consumer.user = data["user"] + consumer.api_key = data["api_key"] + consumer.api_secret = data["api_secret"] + consumer.incoming_change = True + consumer_doctypes = json.loads(data["consumer_doctypes"]) + + for entry in consumer_doctypes: + consumer.append( + "consumer_doctypes", + {"ref_doctype": entry.get("doctype"), "status": "Pending", "condition": entry.get("condition")}, + ) + + consumer.insert() + + # consumer's 'last_update' field should point to the latest update + # in producer's update log when subscribing + # so that, updates after subscribing are consumed and not the old ones. + last_update = str(get_last_update()) + return json.dumps({"last_update": last_update}) + + +def get_consumer_site(consumer_url): + """create a InfluxFrameworkClient object for event consumer site""" + consumer_doc = influxframework.get_doc("Event Consumer", consumer_url) + consumer_site = InfluxFrameworkClient( + url=consumer_url, + api_key=consumer_doc.api_key, + api_secret=consumer_doc.get_password("api_secret"), + ) + return consumer_site + + +def get_last_update(): + """get the creation timestamp of last update consumed""" + updates = influxframework.get_list( + "Event Update Log", "creation", ignore_permissions=True, limit=1, order_by="creation desc" + ) + if updates: + return updates[0].creation + return influxframework.utils.now_datetime() + + +@influxframework.whitelist() +def notify_event_consumers(doctype): + """get all event consumers and set flag for notification status""" + event_consumers = influxframework.get_all( + "Event Consumer Document Type", ["parent"], {"ref_doctype": doctype, "status": "Approved"} + ) + for entry in event_consumers: + consumer = influxframework.get_doc("Event Consumer", entry.parent) + consumer.flags.notified = False + notify(consumer) + + +@influxframework.whitelist() +def notify(consumer): + """notify individual event consumers about a new update""" + consumer_status = consumer.get_consumer_status() + if consumer_status == "online": + try: + client = get_consumer_site(consumer.callback_url) + client.post_request( + { + "cmd": "influxframework.event_streaming.doctype.event_producer.event_producer.new_event_notification", + "producer_url": get_url(), + } + ) + consumer.flags.notified = True + except Exception: + consumer.flags.notified = False + else: + consumer.flags.notified = False + + # enqueue another job if the site was not notified + if not consumer.flags.notified: + enqueued_method = "influxframework.event_streaming.doctype.event_consumer.event_consumer.notify" + jobs = get_jobs() + if not jobs or enqueued_method not in jobs[influxframework.local.site] and not consumer.flags.notifed: + influxframework.enqueue( + enqueued_method, queue="long", enqueue_after_commit=True, **{"consumer": consumer} + ) + + +def has_consumer_access(consumer, update_log): + """Checks if consumer has completely satisfied all the conditions on the doc""" + + if isinstance(consumer, str): + consumer = influxframework.get_doc("Event Consumer", consumer) + + if not influxframework.db.exists(update_log.ref_doctype, update_log.docname): + # Delete Log + # Check if the last Update Log of this document was read by this consumer + last_update_log = influxframework.get_all( + "Event Update Log", + filters={ + "ref_doctype": update_log.ref_doctype, + "docname": update_log.docname, + "creation": ["<", update_log.creation], + }, + order_by="creation desc", + limit_page_length=1, + ) + if not len(last_update_log): + return False + + last_update_log = influxframework.get_doc("Event Update Log", last_update_log[0].name) + return len([x for x in last_update_log.consumers if x.consumer == consumer.name]) + + doc = influxframework.get_doc(update_log.ref_doctype, update_log.docname) + try: + for dt_entry in consumer.consumer_doctypes: + if dt_entry.ref_doctype != update_log.ref_doctype: + continue + + if not dt_entry.condition: + return True + + condition: str = dt_entry.condition + if condition.startswith("cmd:"): + cmd = condition.split("cmd:")[1].strip() + args = {"consumer": consumer, "doc": doc, "update_log": update_log} + return influxframework.call(cmd, **args) + else: + return influxframework.safe_eval(condition, influxframework._dict(doc=doc)) + except Exception as e: + consumer.log_error("has_consumer_access error") + return False diff --git a/influxframework/event_streaming/doctype/event_consumer/test_event_consumer.py b/influxframework/event_streaming/doctype/event_consumer/test_event_consumer.py new file mode 100644 index 0000000..7189b7f --- /dev/null +++ b/influxframework/event_streaming/doctype/event_consumer/test_event_consumer.py @@ -0,0 +1,8 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestEventConsumer(InfluxFrameworkTestCase): + pass diff --git a/influxframework/event_streaming/doctype/event_consumer_document_type/__init__.py b/influxframework/event_streaming/doctype/event_consumer_document_type/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.json b/influxframework/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.json new file mode 100644 index 0000000..c243334 --- /dev/null +++ b/influxframework/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.json @@ -0,0 +1,61 @@ +{ + "actions": [], + "creation": "2019-10-03 21:10:54.754651", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "ref_doctype", + "status", + "unsubscribed", + "condition" + ], + "fields": [ + { + "columns": 4, + "fieldname": "ref_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType", + "read_only": 1, + "reqd": 1 + }, + { + "columns": 4, + "default": "Pending", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Approval Status", + "options": "Pending\nApproved\nRejected" + }, + { + "columns": 2, + "default": "0", + "fieldname": "unsubscribed", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Unsubscribed", + "read_only": 1 + }, + { + "fieldname": "condition", + "fieldtype": "Code", + "label": "Condition", + "read_only": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-11-07 09:26:49.894294", + "modified_by": "Administrator", + "module": "Event Streaming", + "name": "Event Consumer Document Type", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.py b/influxframework/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.py new file mode 100644 index 0000000..f6ff5c9 --- /dev/null +++ b/influxframework/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.py @@ -0,0 +1,9 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class EventConsumerDocumentType(Document): + pass diff --git a/influxframework/event_streaming/doctype/event_producer/__init__.py b/influxframework/event_streaming/doctype/event_producer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/event_streaming/doctype/event_producer/event_producer.js b/influxframework/event_streaming/doctype/event_producer/event_producer.js new file mode 100644 index 0000000..34ddc6f --- /dev/null +++ b/influxframework/event_streaming/doctype/event_producer/event_producer.js @@ -0,0 +1,25 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Event Producer", { + refresh: function (frm) { + frm.set_query("ref_doctype", "producer_doctypes", function () { + return { + filters: { + issingle: 0, + istable: 0, + }, + }; + }); + + frm.set_indicator_formatter("status", function (doc) { + let indicator = "orange"; + if (doc.status == "Approved") { + indicator = "green"; + } else if (doc.status == "Rejected") { + indicator = "red"; + } + return indicator; + }); + }, +}); diff --git a/influxframework/event_streaming/doctype/event_producer/event_producer.json b/influxframework/event_streaming/doctype/event_producer/event_producer.json new file mode 100644 index 0000000..d868f6c --- /dev/null +++ b/influxframework/event_streaming/doctype/event_producer/event_producer.json @@ -0,0 +1,96 @@ +{ + "actions": [], + "autoname": "field:producer_url", + "creation": "2019-08-26 19:17:24.919196", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "producer_url", + "producer_doctypes", + "section_break_3", + "api_key", + "api_secret", + "column_break_6", + "user", + "incoming_change" + ], + "fields": [ + { + "fieldname": "producer_url", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Producer URL", + "reqd": 1, + "unique": 1 + }, + { + "description": "API Key of the user(Event Subscriber) on the producer site", + "fieldname": "api_key", + "fieldtype": "Data", + "label": "API Key", + "reqd": 1 + }, + { + "description": "API Secret of the user(Event Subscriber) on the producer site", + "fieldname": "api_secret", + "fieldtype": "Password", + "label": "API Secret", + "reqd": 1 + }, + { + "fieldname": "user", + "fieldtype": "Link", + "label": "Event Subscriber", + "options": "User", + "reqd": 1, + "set_only_once": 1 + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_3", + "fieldtype": "Section Break" + }, + { + "default": "0", + "fieldname": "incoming_change", + "fieldtype": "Check", + "hidden": 1, + "label": "Incoming Change" + }, + { + "fieldname": "producer_doctypes", + "fieldtype": "Table", + "label": "Event Producer Document Types", + "options": "Event Producer Document Type", + "reqd": 1 + } + ], + "links": [], + "modified": "2020-10-26 13:00:15.361316", + "modified_by": "Administrator", + "module": "Event Streaming", + "name": "Event Producer", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/event_streaming/doctype/event_producer/event_producer.py b/influxframework/event_streaming/doctype/event_producer/event_producer.py new file mode 100644 index 0000000..98227db --- /dev/null +++ b/influxframework/event_streaming/doctype/event_producer/event_producer.py @@ -0,0 +1,569 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +import json +import time + +import requests + +import influxframework +from influxframework import _ +from influxframework.custom.doctype.custom_field.custom_field import create_custom_field +from influxframework.influxframeworkclient import InfluxFrameworkClient +from influxframework.model.document import Document +from influxframework.utils.background_jobs import get_jobs +from influxframework.utils.data import get_link_to_form, get_url +from influxframework.utils.password import get_decrypted_password + + +class EventProducer(Document): + def before_insert(self): + self.check_url() + self.validate_event_subscriber() + self.incoming_change = True + self.create_event_consumer() + self.create_custom_fields() + + def validate(self): + self.validate_event_subscriber() + if influxframework.flags.in_test: + for entry in self.producer_doctypes: + entry.status = "Approved" + + def validate_event_subscriber(self): + if not influxframework.db.get_value("User", self.user, "api_key"): + influxframework.throw( + _("Please generate keys for the Event Subscriber User {0} first.").format( + influxframework.bold(get_link_to_form("User", self.user)) + ) + ) + + def on_update(self): + if not self.incoming_change: + if influxframework.db.exists("Event Producer", self.name): + if not self.api_key or not self.api_secret: + influxframework.throw(_("Please set API Key and Secret on the producer and consumer sites first.")) + else: + doc_before_save = self.get_doc_before_save() + if doc_before_save.api_key != self.api_key or doc_before_save.api_secret != self.api_secret: + return + + self.update_event_consumer() + self.create_custom_fields() + else: + # when producer doc is updated it updates the consumer doc, set flag to avoid deadlock + self.db_set("incoming_change", 0) + self.reload() + + def on_trash(self): + last_update = influxframework.db.get_value("Event Producer Last Update", dict(event_producer=self.name)) + if last_update: + influxframework.delete_doc("Event Producer Last Update", last_update) + + def check_url(self): + valid_url_schemes = ("http", "https") + influxframework.utils.validate_url(self.producer_url, throw=True, valid_schemes=valid_url_schemes) + + # remove '/' from the end of the url like http://test_site.com/ + # to prevent mismatch in get_url() results + if self.producer_url.endswith("/"): + self.producer_url = self.producer_url[:-1] + + def create_event_consumer(self): + """register event consumer on the producer site""" + if self.is_producer_online(): + producer_site = InfluxFrameworkClient( + url=self.producer_url, api_key=self.api_key, api_secret=self.get_password("api_secret") + ) + + response = producer_site.post_api( + "influxframework.event_streaming.doctype.event_consumer.event_consumer.register_consumer", + params={"data": json.dumps(self.get_request_data())}, + ) + if response: + response = json.loads(response) + self.set_last_update(response["last_update"]) + else: + influxframework.throw( + _( + "Failed to create an Event Consumer or an Event Consumer for the current site is already registered." + ) + ) + + def set_last_update(self, last_update): + last_update_doc_name = influxframework.db.get_value( + "Event Producer Last Update", dict(event_producer=self.name) + ) + if not last_update_doc_name: + influxframework.get_doc( + dict( + doctype="Event Producer Last Update", + event_producer=self.producer_url, + last_update=last_update, + ) + ).insert(ignore_permissions=True) + else: + influxframework.db.set_value( + "Event Producer Last Update", last_update_doc_name, "last_update", last_update + ) + + def get_last_update(self): + return influxframework.db.get_value( + "Event Producer Last Update", dict(event_producer=self.name), "last_update" + ) + + def get_request_data(self): + consumer_doctypes = [] + for entry in self.producer_doctypes: + if entry.has_mapping: + # if mapping, subscribe to remote doctype on consumer's site + dt = influxframework.db.get_value("Document Type Mapping", entry.mapping, "remote_doctype") + else: + dt = entry.ref_doctype + consumer_doctypes.append({"doctype": dt, "condition": entry.condition}) + + user_key = influxframework.db.get_value("User", self.user, "api_key") + user_secret = get_decrypted_password("User", self.user, "api_secret") + return { + "event_consumer": get_url(), + "consumer_doctypes": json.dumps(consumer_doctypes), + "user": self.user, + "api_key": user_key, + "api_secret": user_secret, + } + + def create_custom_fields(self): + """create custom field to store remote docname and remote site url""" + for entry in self.producer_doctypes: + if not entry.use_same_name: + if not influxframework.db.exists( + "Custom Field", {"fieldname": "remote_docname", "dt": entry.ref_doctype} + ): + df = dict( + fieldname="remote_docname", + label="Remote Document Name", + fieldtype="Data", + read_only=1, + print_hide=1, + ) + create_custom_field(entry.ref_doctype, df) + if not influxframework.db.exists( + "Custom Field", {"fieldname": "remote_site_name", "dt": entry.ref_doctype} + ): + df = dict( + fieldname="remote_site_name", + label="Remote Site", + fieldtype="Data", + read_only=1, + print_hide=1, + ) + create_custom_field(entry.ref_doctype, df) + + def update_event_consumer(self): + if self.is_producer_online(): + producer_site = get_producer_site(self.producer_url) + event_consumer = producer_site.get_doc("Event Consumer", get_url()) + event_consumer = influxframework._dict(event_consumer) + if event_consumer: + config = event_consumer.consumer_doctypes + event_consumer.consumer_doctypes = [] + for entry in self.producer_doctypes: + if entry.has_mapping: + # if mapping, subscribe to remote doctype on consumer's site + ref_doctype = influxframework.db.get_value("Document Type Mapping", entry.mapping, "remote_doctype") + else: + ref_doctype = entry.ref_doctype + + event_consumer.consumer_doctypes.append( + { + "ref_doctype": ref_doctype, + "status": get_approval_status(config, ref_doctype), + "unsubscribed": entry.unsubscribe, + "condition": entry.condition, + } + ) + event_consumer.user = self.user + event_consumer.incoming_change = True + producer_site.update(event_consumer) + + def is_producer_online(self): + """check connection status for the Event Producer site""" + retry = 3 + while retry > 0: + res = requests.get(self.producer_url) + if res.status_code == 200: + return True + retry -= 1 + time.sleep(5) + influxframework.throw(_("Failed to connect to the Event Producer site. Retry after some time.")) + + +def get_producer_site(producer_url): + """create a InfluxFrameworkClient object for event producer site""" + producer_doc = influxframework.get_doc("Event Producer", producer_url) + producer_site = InfluxFrameworkClient( + url=producer_url, + api_key=producer_doc.api_key, + api_secret=producer_doc.get_password("api_secret"), + ) + return producer_site + + +def get_approval_status(config, ref_doctype): + """check the approval status for consumption""" + for entry in config: + if entry.get("ref_doctype") == ref_doctype: + return entry.get("status") + return "Pending" + + +@influxframework.whitelist() +def pull_producer_data(): + """Fetch data from producer node.""" + response = requests.get(get_url()) + if response.status_code == 200: + for event_producer in influxframework.get_all("Event Producer"): + pull_from_node(event_producer.name) + return "success" + return None + + +@influxframework.whitelist() +def pull_from_node(event_producer): + """pull all updates after the last update timestamp from event producer site""" + event_producer = influxframework.get_doc("Event Producer", event_producer) + producer_site = get_producer_site(event_producer.producer_url) + last_update = event_producer.get_last_update() + + (doctypes, mapping_config, naming_config) = get_config(event_producer.producer_doctypes) + + updates = get_updates(producer_site, last_update, doctypes) + + for update in updates: + update.use_same_name = naming_config.get(update.ref_doctype) + mapping = mapping_config.get(update.ref_doctype) + if mapping: + update.mapping = mapping + update = get_mapped_update(update, producer_site) + if not update.update_type == "Delete": + update.data = json.loads(update.data) + + sync(update, producer_site, event_producer) + + +def get_config(event_config): + """get the doctype mapping and naming configurations for consumption""" + doctypes, mapping_config, naming_config = [], {}, {} + + for entry in event_config: + if entry.status == "Approved": + if entry.has_mapping: + (mapped_doctype, mapping) = influxframework.db.get_value( + "Document Type Mapping", entry.mapping, ["remote_doctype", "name"] + ) + mapping_config[mapped_doctype] = mapping + naming_config[mapped_doctype] = entry.use_same_name + doctypes.append(mapped_doctype) + else: + naming_config[entry.ref_doctype] = entry.use_same_name + doctypes.append(entry.ref_doctype) + return (doctypes, mapping_config, naming_config) + + +def sync(update, producer_site, event_producer, in_retry=False): + """Sync the individual update""" + try: + if update.update_type == "Create": + set_insert(update, producer_site, event_producer.name) + if update.update_type == "Update": + set_update(update, producer_site) + if update.update_type == "Delete": + set_delete(update) + if in_retry: + return "Synced" + log_event_sync(update, event_producer.name, "Synced") + + except Exception: + if in_retry: + if influxframework.flags.in_test: + print(influxframework.get_traceback()) + return "Failed" + log_event_sync(update, event_producer.name, "Failed", influxframework.get_traceback()) + + event_producer.set_last_update(update.creation) + influxframework.db.commit() + + +def set_insert(update, producer_site, event_producer): + """Sync insert type update""" + if influxframework.db.get_value(update.ref_doctype, update.docname): + # doc already created + return + doc = influxframework.get_doc(update.data) + + if update.mapping: + if update.get("dependencies"): + dependencies_created = sync_mapped_dependencies(update.dependencies, producer_site) + for fieldname, value in dependencies_created.items(): + doc.update({fieldname: value}) + else: + sync_dependencies(doc, producer_site) + + if update.use_same_name: + doc.insert(set_name=update.docname, set_child_names=False) + else: + # if event consumer is not saving documents with the same name as the producer + # store the remote docname in a custom field for future updates + doc.remote_docname = update.docname + doc.remote_site_name = event_producer + doc.insert(set_child_names=False) + + +def set_update(update, producer_site): + """Sync update type update""" + local_doc = get_local_doc(update) + if local_doc: + data = influxframework._dict(update.data) + + if data.changed: + local_doc.update(data.changed) + if data.removed: + local_doc = update_row_removed(local_doc, data.removed) + if data.row_changed: + update_row_changed(local_doc, data.row_changed) + if data.added: + local_doc = update_row_added(local_doc, data.added) + + if update.mapping: + if update.get("dependencies"): + dependencies_created = sync_mapped_dependencies(update.dependencies, producer_site) + for fieldname, value in dependencies_created.items(): + local_doc.update({fieldname: value}) + else: + sync_dependencies(local_doc, producer_site) + + local_doc.save() + local_doc.db_update_all() + + +def update_row_removed(local_doc, removed): + """Sync child table row deletion type update""" + for tablename, rownames in removed.items(): + table = local_doc.get_table_field_doctype(tablename) + for row in rownames: + table_rows = local_doc.get(tablename) + child_table_row = get_child_table_row(table_rows, row) + table_rows.remove(child_table_row) + local_doc.set(tablename, table_rows) + return local_doc + + +def get_child_table_row(table_rows, row): + for entry in table_rows: + if entry.get("name") == row: + return entry + + +def update_row_changed(local_doc, changed): + """Sync child table row updation type update""" + for tablename, rows in changed.items(): + old = local_doc.get(tablename) + for doc in old: + for row in rows: + if row["name"] == doc.get("name"): + doc.update(row) + + +def update_row_added(local_doc, added): + """Sync child table row addition type update""" + for tablename, rows in added.items(): + local_doc.extend(tablename, rows) + for child in rows: + child_doc = influxframework.get_doc(child) + child_doc.parent = local_doc.name + child_doc.parenttype = local_doc.doctype + child_doc.insert(set_name=child_doc.name) + return local_doc + + +def set_delete(update): + """Sync delete type update""" + local_doc = get_local_doc(update) + if local_doc: + local_doc.delete() + + +def get_updates(producer_site, last_update, doctypes): + """Get all updates generated after the last update timestamp""" + docs = producer_site.post_request( + { + "cmd": "influxframework.event_streaming.doctype.event_update_log.event_update_log.get_update_logs_for_consumer", + "event_consumer": get_url(), + "doctypes": influxframework.as_json(doctypes), + "last_update": last_update, + } + ) + return [influxframework._dict(d) for d in (docs or [])] + + +def get_local_doc(update): + """Get the local document if created with a different name""" + try: + if not update.use_same_name: + return influxframework.get_doc(update.ref_doctype, {"remote_docname": update.docname}) + return influxframework.get_doc(update.ref_doctype, update.docname) + except influxframework.DoesNotExistError: + return None + + +def sync_dependencies(document, producer_site): + """ + dependencies is a dictionary to store all the docs + having dependencies and their sync status, + which is shared among all nested functions. + """ + dependencies = {document: True} + + def check_doc_has_dependencies(doc, producer_site): + """Sync child table link fields first, + then sync link fields, + then dynamic links""" + meta = influxframework.get_meta(doc.doctype) + table_fields = meta.get_table_fields() + link_fields = meta.get_link_fields() + dl_fields = meta.get_dynamic_link_fields() + if table_fields: + sync_child_table_dependencies(doc, table_fields, producer_site) + if link_fields: + sync_link_dependencies(doc, link_fields, producer_site) + if dl_fields: + sync_dynamic_link_dependencies(doc, dl_fields, producer_site) + + def sync_child_table_dependencies(doc, table_fields, producer_site): + for df in table_fields: + child_table = doc.get(df.fieldname) + for entry in child_table: + child_doc = producer_site.get_doc(entry.doctype, entry.name) + if child_doc: + child_doc = influxframework._dict(child_doc) + set_dependencies(child_doc, influxframework.get_meta(entry.doctype).get_link_fields(), producer_site) + + def sync_link_dependencies(doc, link_fields, producer_site): + set_dependencies(doc, link_fields, producer_site) + + def sync_dynamic_link_dependencies(doc, dl_fields, producer_site): + for df in dl_fields: + docname = doc.get(df.fieldname) + linked_doctype = doc.get(df.options) + if docname and not check_dependency_fulfilled(linked_doctype, docname): + master_doc = producer_site.get_doc(linked_doctype, docname) + influxframework.get_doc(master_doc).insert(set_name=docname) + + def set_dependencies(doc, link_fields, producer_site): + for df in link_fields: + docname = doc.get(df.fieldname) + linked_doctype = df.get_link_doctype() + if docname and not check_dependency_fulfilled(linked_doctype, docname): + master_doc = producer_site.get_doc(linked_doctype, docname) + try: + master_doc = influxframework.get_doc(master_doc) + master_doc.insert(set_name=docname) + influxframework.db.commit() + + # for dependency inside a dependency + except Exception: + dependencies[master_doc] = True + + def check_dependency_fulfilled(linked_doctype, docname): + return influxframework.db.exists(linked_doctype, docname) + + while dependencies[document]: + # find the first non synced dependency + for item in reversed(list(dependencies.keys())): + if dependencies[item]: + dependency = item + break + + check_doc_has_dependencies(dependency, producer_site) + + # mark synced for nested dependency + if dependency != document: + dependencies[dependency] = False + dependency.insert() + + # no more dependencies left to be synced, the main doc is ready to be synced + # end the dependency loop + if not any(list(dependencies.values())[1:]): + dependencies[document] = False + + +def sync_mapped_dependencies(dependencies, producer_site): + dependencies_created = {} + for entry in dependencies: + doc = influxframework._dict(json.loads(entry[1])) + docname = influxframework.db.exists(doc.doctype, doc.name) + if not docname: + doc = influxframework.get_doc(doc).insert(set_child_names=False) + dependencies_created[entry[0]] = doc.name + else: + dependencies_created[entry[0]] = docname + + return dependencies_created + + +def log_event_sync(update, event_producer, sync_status, error=None): + """Log event update received with the sync_status as Synced or Failed""" + doc = influxframework.new_doc("Event Sync Log") + doc.update_type = update.update_type + doc.ref_doctype = update.ref_doctype + doc.status = sync_status + doc.event_producer = event_producer + doc.producer_doc = update.docname + doc.data = influxframework.as_json(update.data) + doc.use_same_name = update.use_same_name + doc.mapping = update.mapping if update.mapping else None + if update.use_same_name: + doc.docname = update.docname + else: + doc.docname = influxframework.db.get_value(update.ref_doctype, {"remote_docname": update.docname}, "name") + if error: + doc.error = error + doc.insert() + + +def get_mapped_update(update, producer_site): + """get the new update document with mapped fields""" + mapping = influxframework.get_doc("Document Type Mapping", update.mapping) + if update.update_type == "Create": + doc = influxframework._dict(json.loads(update.data)) + mapped_update = mapping.get_mapping(doc, producer_site, update.update_type) + update.data = mapped_update.get("doc") + update.dependencies = mapped_update.get("dependencies", None) + elif update.update_type == "Update": + mapped_update = mapping.get_mapped_update(update, producer_site) + update.data = mapped_update.get("doc") + update.dependencies = mapped_update.get("dependencies", None) + + update["ref_doctype"] = mapping.local_doctype + return update + + +@influxframework.whitelist() +def new_event_notification(producer_url): + """Pull data from producer when notified""" + enqueued_method = "influxframework.event_streaming.doctype.event_producer.event_producer.pull_from_node" + jobs = get_jobs() + if not jobs or enqueued_method not in jobs[influxframework.local.site]: + influxframework.enqueue(enqueued_method, queue="default", **{"event_producer": producer_url}) + + +@influxframework.whitelist() +def resync(update): + """Retry syncing update if failed""" + update = influxframework._dict(json.loads(update)) + producer_site = get_producer_site(update.event_producer) + event_producer = influxframework.get_doc("Event Producer", update.event_producer) + if update.mapping: + update = get_mapped_update(update, producer_site) + update.data = json.loads(update.data) + return sync(update, producer_site, event_producer, in_retry=True) diff --git a/influxframework/event_streaming/doctype/event_producer/test_event_producer.py b/influxframework/event_streaming/doctype/event_producer/test_event_producer.py new file mode 100644 index 0000000..5b2ee21 --- /dev/null +++ b/influxframework/event_streaming/doctype/event_producer/test_event_producer.py @@ -0,0 +1,400 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +import json + +import influxframework +from influxframework.core.doctype.user.user import generate_keys +from influxframework.event_streaming.doctype.event_producer.event_producer import pull_from_node +from influxframework.influxframeworkclient import InfluxFrameworkClient +from influxframework.query_builder.utils import db_type_is +from influxframework.tests.test_query_builder import run_only_if +from influxframework.tests.utils import InfluxFrameworkTestCase + +producer_url = "http://test_site_producer:8000" + + +class TestEventProducer(InfluxFrameworkTestCase): + def setUp(self): + create_event_producer(producer_url) + + def tearDown(self): + unsubscribe_doctypes(producer_url) + + def test_insert(self): + producer = get_remote_site() + producer_doc = insert_into_producer(producer, "test creation 1 sync") + self.pull_producer_data() + self.assertTrue(influxframework.db.exists("ToDo", producer_doc.name)) + + def test_update(self): + producer = get_remote_site() + producer_doc = insert_into_producer(producer, "test update 1") + producer_doc["description"] = "test update 2" + producer_doc = producer.update(producer_doc) + self.pull_producer_data() + local_doc = influxframework.get_doc(producer_doc.doctype, producer_doc.name) + self.assertEqual(local_doc.description, producer_doc.description) + + def test_delete(self): + producer = get_remote_site() + producer_doc = insert_into_producer(producer, "test delete sync") + self.pull_producer_data() + self.assertTrue(influxframework.db.exists("ToDo", producer_doc.name)) + producer.delete("ToDo", producer_doc.name) + self.pull_producer_data() + self.assertFalse(influxframework.db.exists("ToDo", producer_doc.name)) + + def test_child_table_sync_with_dependencies(self): + producer = get_remote_site() + producer_user = influxframework._dict( + doctype="User", + email="test_user@sync.com", + send_welcome_email=0, + first_name="Test Sync User", + enabled=1, + roles=[{"role": "System Manager"}], + ) + delete_on_remote_if_exists(producer, "User", {"email": producer_user.email}) + influxframework.db.delete("User", {"email": producer_user.email}) + producer_user = producer.insert(producer_user) + + producer_note = influxframework._dict( + doctype="Note", title="test child table dependency sync", seen_by=[{"user": producer_user.name}] + ) + delete_on_remote_if_exists(producer, "Note", {"title": producer_note.title}) + influxframework.db.delete("Note", {"title": producer_note.title}) + producer_note = producer.insert(producer_note) + + self.pull_producer_data() + self.assertTrue(influxframework.db.exists("User", producer_user.name)) + if self.assertTrue(influxframework.db.exists("Note", producer_note.name)): + local_note = influxframework.get_doc("Note", producer_note.name) + self.assertEqual(len(local_note.seen_by), 1) + + def test_dynamic_link_dependencies_synced(self): + producer = get_remote_site() + # unsubscribe for Note to check whether dependency is fulfilled + event_producer = influxframework.get_doc("Event Producer", producer_url, for_update=True) + event_producer.producer_doctypes = [] + event_producer.append("producer_doctypes", {"ref_doctype": "ToDo", "use_same_name": 1}) + event_producer.save() + + producer_link_doc = influxframework._dict(doctype="Note", title="Test Dynamic Link 1") + + delete_on_remote_if_exists(producer, "Note", {"title": producer_link_doc.title}) + influxframework.db.delete("Note", {"title": producer_link_doc.title}) + producer_link_doc = producer.insert(producer_link_doc) + producer_doc = influxframework._dict( + doctype="ToDo", + description="Test Dynamic Link 2", + assigned_by="Administrator", + reference_type="Note", + reference_name=producer_link_doc.name, + ) + producer_doc = producer.insert(producer_doc) + + self.pull_producer_data() + + # check dynamic link dependency created + self.assertTrue(influxframework.db.exists("Note", producer_link_doc.name)) + self.assertEqual( + producer_link_doc.name, influxframework.db.get_value("ToDo", producer_doc.name, "reference_name") + ) + + reset_configuration(producer_url) + + def test_naming_configuration(self): + # test with use_same_name = 0 + producer = get_remote_site() + event_producer = influxframework.get_doc("Event Producer", producer_url, for_update=True) + event_producer.producer_doctypes = [] + event_producer.append("producer_doctypes", {"ref_doctype": "ToDo", "use_same_name": 0}) + event_producer.save() + + producer_doc = insert_into_producer(producer, "test different name sync") + self.pull_producer_data() + self.assertTrue( + influxframework.db.exists( + "ToDo", {"remote_docname": producer_doc.name, "remote_site_name": producer_url} + ) + ) + + reset_configuration(producer_url) + + def test_conditional_events(self): + producer = get_remote_site() + + # Add Condition + event_producer = influxframework.get_doc("Event Producer", producer_url) + note_producer_entry = [x for x in event_producer.producer_doctypes if x.ref_doctype == "Note"][0] + note_producer_entry.condition = "doc.public == 1" + event_producer.save() + + # Make test doc + producer_note1 = influxframework._dict(doctype="Note", public=0, title="test conditional sync") + delete_on_remote_if_exists(producer, "Note", {"title": producer_note1["title"]}) + producer_note1 = producer.insert(producer_note1) + + # Make Update + producer_note1["content"] = "Test Conditional Sync Content" + producer_note1 = producer.update(producer_note1) + + self.pull_producer_data() + + # Check if synced here + self.assertFalse(influxframework.db.exists("Note", producer_note1.name)) + + # Lets satisfy the condition + producer_note1["public"] = 1 + producer_note1 = producer.update(producer_note1) + + self.pull_producer_data() + + # it should sync now + self.assertTrue(influxframework.db.exists("Note", producer_note1.name)) + local_note = influxframework.get_doc("Note", producer_note1.name) + self.assertEqual(local_note.content, producer_note1.content) + + reset_configuration(producer_url) + + def test_conditional_events_with_cmd(self): + producer = get_remote_site() + + # Add Condition + event_producer = influxframework.get_doc("Event Producer", producer_url) + note_producer_entry = [x for x in event_producer.producer_doctypes if x.ref_doctype == "Note"][0] + note_producer_entry.condition = ( + "cmd: influxframework.event_streaming.doctype.event_producer.test_event_producer.can_sync_note" + ) + event_producer.save() + + # Make test doc + producer_note1 = influxframework._dict(doctype="Note", public=0, title="test conditional sync cmd") + delete_on_remote_if_exists(producer, "Note", {"title": producer_note1["title"]}) + producer_note1 = producer.insert(producer_note1) + + # Make Update + producer_note1["content"] = "Test Conditional Sync Content" + producer_note1 = producer.update(producer_note1) + + self.pull_producer_data() + + # Check if synced here + self.assertFalse(influxframework.db.exists("Note", producer_note1.name)) + + # Lets satisfy the condition + producer_note1["public"] = 1 + producer_note1 = producer.update(producer_note1) + + self.pull_producer_data() + + # it should sync now + self.assertTrue(influxframework.db.exists("Note", producer_note1.name)) + local_note = influxframework.get_doc("Note", producer_note1.name) + self.assertEqual(local_note.content, producer_note1.content) + + reset_configuration(producer_url) + + def test_update_log(self): + producer = get_remote_site() + producer_doc = insert_into_producer(producer, "test update log") + update_log_doc = producer.get_value( + "Event Update Log", "docname", {"docname": producer_doc.get("name")} + ) + self.assertEqual(update_log_doc.get("docname"), producer_doc.get("name")) + + def test_event_sync_log(self): + producer = get_remote_site() + producer_doc = insert_into_producer(producer, "test event sync log") + self.pull_producer_data() + self.assertTrue(influxframework.db.exists("Event Sync Log", {"docname": producer_doc.name})) + + def pull_producer_data(self): + pull_from_node(producer_url) + + def test_mapping(self): + producer = get_remote_site() + event_producer = influxframework.get_doc("Event Producer", producer_url, for_update=True) + event_producer.producer_doctypes = [] + mapping = [{"local_fieldname": "description", "remote_fieldname": "content"}] + event_producer.append( + "producer_doctypes", + { + "ref_doctype": "ToDo", + "use_same_name": 1, + "has_mapping": 1, + "mapping": get_mapping("ToDo to Note", "ToDo", "Note", mapping), + }, + ) + event_producer.save() + + producer_note = influxframework._dict(doctype="Note", title="Test Mapping", content="Test Mapping") + delete_on_remote_if_exists(producer, "Note", {"title": producer_note.title}) + producer_note = producer.insert(producer_note) + self.pull_producer_data() + # check inserted + self.assertTrue(influxframework.db.exists("ToDo", {"description": producer_note.content})) + + # update in producer + producer_note["content"] = "test mapped doc update sync" + producer_note = producer.update(producer_note) + self.pull_producer_data() + + # check updated + self.assertTrue(influxframework.db.exists("ToDo", {"description": producer_note["content"]})) + + producer.delete("Note", producer_note.name) + self.pull_producer_data() + # check delete + self.assertFalse(influxframework.db.exists("ToDo", {"description": producer_note.content})) + + reset_configuration(producer_url) + + def test_inner_mapping(self): + producer = get_remote_site() + + setup_event_producer_for_inner_mapping() + producer_note = influxframework._dict( + doctype="Note", title="Inner Mapping Tester", content="Test Inner Mapping" + ) + delete_on_remote_if_exists(producer, "Note", {"title": producer_note.title}) + producer_note = producer.insert(producer_note) + self.pull_producer_data() + + # check dependency inserted + self.assertTrue(influxframework.db.exists("Role", {"role_name": producer_note.title})) + # check doc inserted + self.assertTrue(influxframework.db.exists("ToDo", {"description": producer_note.content})) + + reset_configuration(producer_url) + + +def can_sync_note(consumer, doc, update_log): + return doc.public == 1 + + +def setup_event_producer_for_inner_mapping(): + event_producer = influxframework.get_doc("Event Producer", producer_url, for_update=True) + event_producer.producer_doctypes = [] + inner_mapping = [{"local_fieldname": "role_name", "remote_fieldname": "title"}] + inner_map = get_mapping("Role to Note Dependency Creation", "Role", "Note", inner_mapping) + mapping = [ + { + "local_fieldname": "description", + "remote_fieldname": "content", + }, + { + "local_fieldname": "role", + "remote_fieldname": "title", + "mapping_type": "Document", + "mapping": inner_map, + "remote_value_filters": json.dumps({"title": "title"}), + }, + ] + event_producer.append( + "producer_doctypes", + { + "ref_doctype": "ToDo", + "use_same_name": 1, + "has_mapping": 1, + "mapping": get_mapping("ToDo to Note Mapping", "ToDo", "Note", mapping), + }, + ) + event_producer.save() + return event_producer + + +def insert_into_producer(producer, description): + # create and insert todo on remote site + todo = dict(doctype="ToDo", description=description, assigned_by="Administrator") + return producer.insert(todo) + + +def delete_on_remote_if_exists(producer, doctype, filters): + remote_doc = producer.get_value(doctype, "name", filters) + if remote_doc: + producer.delete(doctype, remote_doc.get("name")) + + +def get_mapping(mapping_name, local, remote, field_map): + name = influxframework.db.exists("Document Type Mapping", mapping_name) + if name: + doc = influxframework.get_doc("Document Type Mapping", name) + else: + doc = influxframework.new_doc("Document Type Mapping") + + doc.mapping_name = mapping_name + doc.local_doctype = local + doc.remote_doctype = remote + for entry in field_map: + doc.append("field_mapping", entry) + doc.save() + return doc.name + + +def create_event_producer(producer_url): + if influxframework.db.exists("Event Producer", producer_url): + event_producer = influxframework.get_doc("Event Producer", producer_url) + for entry in event_producer.producer_doctypes: + entry.unsubscribe = 0 + event_producer.save() + return + + generate_keys("Administrator") + + producer_site = connect() + + response = producer_site.post_api( + "influxframework.core.doctype.user.user.generate_keys", params={"user": "Administrator"} + ) + + api_secret = response.get("api_secret") + + response = producer_site.get_value("User", "api_key", {"name": "Administrator"}) + api_key = response.get("api_key") + + event_producer = influxframework.new_doc("Event Producer") + event_producer.producer_doctypes = [] + event_producer.producer_url = producer_url + event_producer.append("producer_doctypes", {"ref_doctype": "ToDo", "use_same_name": 1}) + event_producer.append("producer_doctypes", {"ref_doctype": "Note", "use_same_name": 1}) + event_producer.user = "Administrator" + event_producer.api_key = api_key + event_producer.api_secret = api_secret + event_producer.save() + + +def reset_configuration(producer_url): + event_producer = influxframework.get_doc("Event Producer", producer_url, for_update=True) + event_producer.producer_doctypes = [] + event_producer.conditions = [] + event_producer.producer_url = producer_url + event_producer.append("producer_doctypes", {"ref_doctype": "ToDo", "use_same_name": 1}) + event_producer.append("producer_doctypes", {"ref_doctype": "Note", "use_same_name": 1}) + event_producer.user = "Administrator" + event_producer.save() + + +def get_remote_site(): + producer_doc = influxframework.get_doc("Event Producer", producer_url) + producer_site = InfluxFrameworkClient( + url=producer_doc.producer_url, username="Administrator", password="admin", verify=False + ) + return producer_site + + +def unsubscribe_doctypes(producer_url): + event_producer = influxframework.get_doc("Event Producer", producer_url) + for entry in event_producer.producer_doctypes: + entry.unsubscribe = 1 + event_producer.save() + + +def connect(): + def _connect(): + return InfluxFrameworkClient(url=producer_url, username="Administrator", password="admin", verify=False) + + try: + return _connect() + except Exception: + return _connect() diff --git a/influxframework/event_streaming/doctype/event_producer_document_type/__init__.py b/influxframework/event_streaming/doctype/event_producer_document_type/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/event_streaming/doctype/event_producer_document_type/event_producer_document_type.json b/influxframework/event_streaming/doctype/event_producer_document_type/event_producer_document_type.json new file mode 100644 index 0000000..17fd51d --- /dev/null +++ b/influxframework/event_streaming/doctype/event_producer_document_type/event_producer_document_type.json @@ -0,0 +1,86 @@ +{ + "actions": [], + "creation": "2019-10-03 21:08:25.890352", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "ref_doctype", + "status", + "use_same_name", + "unsubscribe", + "has_mapping", + "mapping", + "condition" + ], + "fields": [ + { + "columns": 3, + "fieldname": "ref_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType", + "reqd": 1, + "set_only_once": 1 + }, + { + "default": "0", + "description": "If the document has different field names on the Producer and Consumer's end check this and set up the Mapping", + "fieldname": "has_mapping", + "fieldtype": "Check", + "label": "Has Mapping" + }, + { + "depends_on": "eval: doc.has_mapping", + "fieldname": "mapping", + "fieldtype": "Link", + "label": "Mapping", + "options": "Document Type Mapping" + }, + { + "columns": 2, + "default": "0", + "description": "If this is checked the documents will have the same name as they have on the Event Producer's site", + "fieldname": "use_same_name", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Use Same Name" + }, + { + "columns": 3, + "default": "Pending", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Approval Status", + "options": "Pending\nApproved\nRejected", + "read_only": 1 + }, + { + "columns": 2, + "default": "0", + "fieldname": "unsubscribe", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Unsubscribe" + }, + { + "fieldname": "condition", + "fieldtype": "Code", + "label": "Condition" + } + ], + "istable": 1, + "links": [], + "modified": "2020-11-07 09:26:58.463868", + "modified_by": "Administrator", + "module": "Event Streaming", + "name": "Event Producer Document Type", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/event_streaming/doctype/event_producer_document_type/event_producer_document_type.py b/influxframework/event_streaming/doctype/event_producer_document_type/event_producer_document_type.py new file mode 100644 index 0000000..84489de --- /dev/null +++ b/influxframework/event_streaming/doctype/event_producer_document_type/event_producer_document_type.py @@ -0,0 +1,9 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class EventProducerDocumentType(Document): + pass diff --git a/influxframework/event_streaming/doctype/event_producer_last_update/__init__.py b/influxframework/event_streaming/doctype/event_producer_last_update/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/event_streaming/doctype/event_producer_last_update/event_producer_last_update.js b/influxframework/event_streaming/doctype/event_producer_last_update/event_producer_last_update.js new file mode 100644 index 0000000..2914566 --- /dev/null +++ b/influxframework/event_streaming/doctype/event_producer_last_update/event_producer_last_update.js @@ -0,0 +1,7 @@ +// Copyright (c) 2020, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Event Producer Last Update", { + // refresh: function(frm) { + // } +}); diff --git a/influxframework/event_streaming/doctype/event_producer_last_update/event_producer_last_update.json b/influxframework/event_streaming/doctype/event_producer_last_update/event_producer_last_update.json new file mode 100644 index 0000000..27f8ed2 --- /dev/null +++ b/influxframework/event_streaming/doctype/event_producer_last_update/event_producer_last_update.json @@ -0,0 +1,53 @@ +{ + "actions": [], + "autoname": "field:event_producer", + "creation": "2020-10-26 12:53:11.940177", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "event_producer", + "last_update" + ], + "fields": [ + { + "fieldname": "event_producer", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Event Producer", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "last_update", + "fieldtype": "Data", + "label": "Last Update" + } + ], + "in_create": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-10-26 13:22:27.056599", + "modified_by": "Administrator", + "module": "Event Streaming", + "name": "Event Producer Last Update", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/event_streaming/doctype/event_producer_last_update/event_producer_last_update.py b/influxframework/event_streaming/doctype/event_producer_last_update/event_producer_last_update.py new file mode 100644 index 0000000..643b24e --- /dev/null +++ b/influxframework/event_streaming/doctype/event_producer_last_update/event_producer_last_update.py @@ -0,0 +1,9 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class EventProducerLastUpdate(Document): + pass diff --git a/influxframework/event_streaming/doctype/event_producer_last_update/test_event_producer_last_update.py b/influxframework/event_streaming/doctype/event_producer_last_update/test_event_producer_last_update.py new file mode 100644 index 0000000..1161912 --- /dev/null +++ b/influxframework/event_streaming/doctype/event_producer_last_update/test_event_producer_last_update.py @@ -0,0 +1,8 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestEventProducerLastUpdate(InfluxFrameworkTestCase): + pass diff --git a/influxframework/event_streaming/doctype/event_sync_log/__init__.py b/influxframework/event_streaming/doctype/event_sync_log/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/event_streaming/doctype/event_sync_log/event_sync_log.js b/influxframework/event_streaming/doctype/event_sync_log/event_sync_log.js new file mode 100644 index 0000000..9028569 --- /dev/null +++ b/influxframework/event_streaming/doctype/event_sync_log/event_sync_log.js @@ -0,0 +1,24 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Event Sync Log", { + refresh: function (frm) { + if (frm.doc.status == "Failed") { + frm.add_custom_button(__("Resync"), function () { + influxframework.call({ + method: "influxframework.event_streaming.doctype.event_producer.event_producer.resync", + args: { + update: frm.doc, + }, + callback: function (r) { + if (r.message) { + influxframework.msgprint(r.message); + frm.set_value("status", r.message); + frm.save(); + } + }, + }); + }); + } + }, +}); diff --git a/influxframework/event_streaming/doctype/event_sync_log/event_sync_log.json b/influxframework/event_streaming/doctype/event_sync_log/event_sync_log.json new file mode 100644 index 0000000..f82128b --- /dev/null +++ b/influxframework/event_streaming/doctype/event_sync_log/event_sync_log.json @@ -0,0 +1,137 @@ +{ + "creation": "2019-09-24 22:22:05.845089", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "update_type", + "ref_doctype", + "docname", + "column_break_4", + "status", + "event_producer", + "producer_doc", + "event_configurations_section", + "use_same_name", + "column_break_9", + "mapping", + "section_break_8", + "data", + "error" + ], + "fields": [ + { + "fieldname": "update_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Update Type", + "options": "Create\nUpdate\nDelete", + "read_only": 1 + }, + { + "fieldname": "ref_doctype", + "fieldtype": "Link", + "label": "Doctype", + "options": "DocType", + "read_only": 1 + }, + { + "fieldname": "docname", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Document Name", + "options": "ref_doctype", + "read_only": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "options": "\nSynced\nFailed", + "read_only": 1 + }, + { + "fieldname": "event_producer", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Event Producer", + "options": "Event Producer", + "read_only": 1 + }, + { + "fieldname": "section_break_8", + "fieldtype": "Section Break", + "label": "Data" + }, + { + "fieldname": "data", + "fieldtype": "Code", + "label": "Data", + "read_only": 1 + }, + { + "fieldname": "producer_doc", + "fieldtype": "Data", + "label": "Producer Document Name", + "read_only": 1 + }, + { + "depends_on": "eval:doc.status=='Failed'", + "fieldname": "error", + "fieldtype": "Code", + "label": "Error", + "read_only": 1 + }, + { + "fieldname": "event_configurations_section", + "fieldtype": "Section Break", + "label": "Event Configurations" + }, + { + "default": "0", + "fieldname": "use_same_name", + "fieldtype": "Data", + "label": "Use Same Name", + "read_only": 1 + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "mapping", + "fieldtype": "Data", + "label": "Mapping", + "read_only": 1 + } + ], + "in_create": 1, + "modified": "2019-10-07 13:22:10.401479", + "modified_by": "Administrator", + "module": "Event Streaming", + "name": "Event Sync Log", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/event_streaming/doctype/event_sync_log/event_sync_log.py b/influxframework/event_streaming/doctype/event_sync_log/event_sync_log.py new file mode 100644 index 0000000..350d061 --- /dev/null +++ b/influxframework/event_streaming/doctype/event_sync_log/event_sync_log.py @@ -0,0 +1,9 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class EventSyncLog(Document): + pass diff --git a/influxframework/event_streaming/doctype/event_sync_log/event_sync_log_list.js b/influxframework/event_streaming/doctype/event_sync_log/event_sync_log_list.js new file mode 100644 index 0000000..42f6548 --- /dev/null +++ b/influxframework/event_streaming/doctype/event_sync_log/event_sync_log_list.js @@ -0,0 +1,9 @@ +influxframework.listview_settings["Event Sync Log"] = { + get_indicator: function (doc) { + var colors = { + Failed: "red", + Synced: "green", + }; + return [__(doc.status), colors[doc.status], "status,=," + doc.status]; + }, +}; diff --git a/influxframework/event_streaming/doctype/event_sync_log/test_event_sync_log.py b/influxframework/event_streaming/doctype/event_sync_log/test_event_sync_log.py new file mode 100644 index 0000000..71d3aa4 --- /dev/null +++ b/influxframework/event_streaming/doctype/event_sync_log/test_event_sync_log.py @@ -0,0 +1,8 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestEventSyncLog(InfluxFrameworkTestCase): + pass diff --git a/influxframework/event_streaming/doctype/event_update_log/__init__.py b/influxframework/event_streaming/doctype/event_update_log/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/event_streaming/doctype/event_update_log/event_update_log.js b/influxframework/event_streaming/doctype/event_update_log/event_update_log.js new file mode 100644 index 0000000..4ae6274 --- /dev/null +++ b/influxframework/event_streaming/doctype/event_update_log/event_update_log.js @@ -0,0 +1,7 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Event Update Log", { + // refresh: function(frm) { + // } +}); diff --git a/influxframework/event_streaming/doctype/event_update_log/event_update_log.json b/influxframework/event_streaming/doctype/event_update_log/event_update_log.json new file mode 100644 index 0000000..a42bc7e --- /dev/null +++ b/influxframework/event_streaming/doctype/event_update_log/event_update_log.json @@ -0,0 +1,77 @@ +{ + "actions": [], + "creation": "2019-07-30 15:31:26.352527", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "update_type", + "ref_doctype", + "docname", + "data", + "consumers" + ], + "fields": [ + { + "fieldname": "update_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Update Type", + "options": "Create\nUpdate\nDelete", + "read_only": 1 + }, + { + "fieldname": "ref_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "DocType", + "options": "DocType", + "read_only": 1 + }, + { + "fieldname": "docname", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Document Name", + "read_only": 1 + }, + { + "fieldname": "data", + "fieldtype": "Code", + "label": "Data", + "read_only": 1 + }, + { + "fieldname": "consumers", + "fieldtype": "Table MultiSelect", + "label": "Consumers", + "options": "Event Update Log Consumer", + "read_only": 1 + } + ], + "in_create": 1, + "links": [], + "modified": "2020-09-04 07:31:52.599804", + "modified_by": "Administrator", + "module": "Event Streaming", + "name": "Event Update Log", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/event_streaming/doctype/event_update_log/event_update_log.py b/influxframework/event_streaming/doctype/event_update_log/event_update_log.py new file mode 100644 index 0000000..823e9a5 --- /dev/null +++ b/influxframework/event_streaming/doctype/event_update_log/event_update_log.py @@ -0,0 +1,296 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model import no_value_fields, table_fields +from influxframework.model.document import Document +from influxframework.utils.background_jobs import get_jobs + + +class EventUpdateLog(Document): + def after_insert(self): + """Send update notification updates to event consumers + whenever update log is generated""" + enqueued_method = ( + "influxframework.event_streaming.doctype.event_consumer.event_consumer.notify_event_consumers" + ) + jobs = get_jobs() + if not jobs or enqueued_method not in jobs[influxframework.local.site]: + influxframework.enqueue( + enqueued_method, doctype=self.ref_doctype, queue="long", enqueue_after_commit=True + ) + + +def notify_consumers(doc, event): + """called via hooks""" + # make event update log for doctypes having event consumers + if influxframework.flags.in_install or influxframework.flags.in_migrate: + return + + consumers = check_doctype_has_consumers(doc.doctype) + if consumers: + if event == "after_insert": + doc.flags.event_update_log = make_event_update_log(doc, update_type="Create") + elif event == "on_trash": + make_event_update_log(doc, update_type="Delete") + else: + # on_update + # called after saving + if not doc.flags.event_update_log: # if not already inserted + diff = get_update(doc.get_doc_before_save(), doc) + if diff: + doc.diff = diff + make_event_update_log(doc, update_type="Update") + + +def check_doctype_has_consumers(doctype): + """Check if doctype has event consumers for event streaming""" + return influxframework.cache_manager.get_doctype_map( + "Event Consumer Document Type", + doctype, + dict(ref_doctype=doctype, status="Approved", unsubscribed=0), + ) + + +def get_update(old, new, for_child=False): + """ + Get document objects with updates only + If there is a change, then returns a dict like: + { + "changed" : {fieldname1: new_value1, fieldname2: new_value2, }, + "added" : {table_fieldname1: [{row_dict1}, {row_dict2}], }, + "removed" : {table_fieldname1: [row_name1, row_name2], }, + "row_changed" : {table_fieldname1: + { + child_fieldname1: new_val, + child_fieldname2: new_val + }, + }, + } + """ + if not new: + return None + + out = influxframework._dict(changed={}, added={}, removed={}, row_changed={}) + for df in new.meta.fields: + if df.fieldtype in no_value_fields and df.fieldtype not in table_fields: + continue + + old_value, new_value = old.get(df.fieldname), new.get(df.fieldname) + + if df.fieldtype in table_fields: + old_row_by_name, new_row_by_name = make_maps(old_value, new_value) + out = check_for_additions(out, df, new_value, old_row_by_name) + out = check_for_deletions(out, df, old_value, new_row_by_name) + + elif old_value != new_value: + out.changed[df.fieldname] = new_value + + out = check_docstatus(out, old, new, for_child) + if any((out.changed, out.added, out.removed, out.row_changed)): + return out + return None + + +def make_event_update_log(doc, update_type): + """Save update info for doctypes that have event consumers""" + if update_type != "Delete": + # diff for update type, doc for create type + data = influxframework.as_json(doc) if not doc.get("diff") else influxframework.as_json(doc.diff) + else: + data = None + return influxframework.get_doc( + { + "doctype": "Event Update Log", + "update_type": update_type, + "ref_doctype": doc.doctype, + "docname": doc.name, + "data": data, + } + ).insert(ignore_permissions=True) + + +def make_maps(old_value, new_value): + """make maps""" + old_row_by_name, new_row_by_name = {}, {} + for d in old_value: + old_row_by_name[d.name] = d + for d in new_value: + new_row_by_name[d.name] = d + return old_row_by_name, new_row_by_name + + +def check_for_additions(out, df, new_value, old_row_by_name): + """check rows for additions, changes""" + for _i, d in enumerate(new_value): + if d.name in old_row_by_name: + diff = get_update(old_row_by_name[d.name], d, for_child=True) + if diff and diff.changed: + if not out.row_changed.get(df.fieldname): + out.row_changed[df.fieldname] = [] + diff.changed["name"] = d.name + out.row_changed[df.fieldname].append(diff.changed) + else: + if not out.added.get(df.fieldname): + out.added[df.fieldname] = [] + out.added[df.fieldname].append(d.as_dict()) + return out + + +def check_for_deletions(out, df, old_value, new_row_by_name): + """check for deletions""" + for d in old_value: + if d.name not in new_row_by_name: + if not out.removed.get(df.fieldname): + out.removed[df.fieldname] = [] + out.removed[df.fieldname].append(d.name) + return out + + +def check_docstatus(out, old, new, for_child): + """docstatus changes""" + if not for_child and old.docstatus != new.docstatus: + out.changed["docstatus"] = new.docstatus + return out + + +def is_consumer_uptodate(update_log, consumer): + """ + Checks if Consumer has read all the UpdateLogs before the specified update_log + :param update_log: The UpdateLog Doc in context + :param consumer: The EventConsumer doc + """ + if update_log.update_type == "Create": + # consumer is obviously up to date + return True + + prev_logs = influxframework.get_all( + "Event Update Log", + filters={ + "ref_doctype": update_log.ref_doctype, + "docname": update_log.docname, + "creation": ["<", update_log.creation], + }, + order_by="creation desc", + limit_page_length=1, + ) + + if not len(prev_logs): + return False + + prev_log_consumers = influxframework.get_all( + "Event Update Log Consumer", + fields=["consumer"], + filters={ + "parent": prev_logs[0].name, + "parenttype": "Event Update Log", + "consumer": consumer.name, + }, + ) + + return len(prev_log_consumers) > 0 + + +def mark_consumer_read(update_log_name, consumer_name): + """ + This function appends the Consumer to the list of Consumers that has 'read' an Update Log + """ + update_log = influxframework.get_doc("Event Update Log", update_log_name) + if len([x for x in update_log.consumers if x.consumer == consumer_name]): + return + + influxframework.get_doc( + influxframework._dict( + doctype="Event Update Log Consumer", + consumer=consumer_name, + parent=update_log_name, + parenttype="Event Update Log", + parentfield="consumers", + ) + ).insert(ignore_permissions=True) + + +def get_unread_update_logs(consumer_name, dt, dn): + """ + Get old logs unread by the consumer on a particular document + """ + already_consumed = [ + x[0] + for x in influxframework.db.sql( + """ + SELECT + update_log.name + FROM `tabEvent Update Log` update_log + JOIN `tabEvent Update Log Consumer` consumer ON consumer.parent = %(log_name)s + WHERE + consumer.consumer = %(consumer)s + AND update_log.ref_doctype = %(dt)s + AND update_log.docname = %(dn)s + """, + { + "consumer": consumer_name, + "dt": dt, + "dn": dn, + "log_name": "update_log.name" + if influxframework.conf.db_type == "mariadb" + else "CAST(update_log.name AS VARCHAR)", + }, + as_dict=0, + ) + ] + + logs = influxframework.get_all( + "Event Update Log", + fields=["update_type", "ref_doctype", "docname", "data", "name", "creation"], + filters={"ref_doctype": dt, "docname": dn, "name": ["not in", already_consumed]}, + order_by="creation", + ) + + return logs + + +@influxframework.whitelist() +def get_update_logs_for_consumer(event_consumer, doctypes, last_update): + """ + Fetches all the UpdateLogs for the consumer + It will inject old un-consumed Update Logs if a doc was just found to be accessible to the Consumer + """ + + if isinstance(doctypes, str): + doctypes = influxframework.parse_json(doctypes) + + from influxframework.event_streaming.doctype.event_consumer.event_consumer import has_consumer_access + + consumer = influxframework.get_doc("Event Consumer", event_consumer) + docs = influxframework.get_list( + doctype="Event Update Log", + filters={"ref_doctype": ("in", doctypes), "creation": (">", last_update)}, + fields=["update_type", "ref_doctype", "docname", "data", "name", "creation"], + order_by="creation desc", + ) + + result = [] + to_update_history = [] + for d in docs: + if (d.ref_doctype, d.docname) in to_update_history: + # will be notified by background jobs + continue + + if not has_consumer_access(consumer=consumer, update_log=d): + continue + + if not is_consumer_uptodate(d, consumer): + to_update_history.append((d.ref_doctype, d.docname)) + # get_unread_update_logs will have the current log + old_logs = get_unread_update_logs(consumer.name, d.ref_doctype, d.docname) + if old_logs: + old_logs.reverse() + result.extend(old_logs) + else: + result.append(d) + + for d in result: + mark_consumer_read(update_log_name=d.name, consumer_name=consumer.name) + + result.reverse() + return result diff --git a/influxframework/event_streaming/doctype/event_update_log/test_event_update_log.py b/influxframework/event_streaming/doctype/event_update_log/test_event_update_log.py new file mode 100644 index 0000000..3ead1ea --- /dev/null +++ b/influxframework/event_streaming/doctype/event_update_log/test_event_update_log.py @@ -0,0 +1,8 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestEventUpdateLog(InfluxFrameworkTestCase): + pass diff --git a/influxframework/event_streaming/doctype/event_update_log_consumer/__init__.py b/influxframework/event_streaming/doctype/event_update_log_consumer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.json b/influxframework/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.json new file mode 100644 index 0000000..b3484c6 --- /dev/null +++ b/influxframework/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.json @@ -0,0 +1,32 @@ +{ + "actions": [], + "creation": "2020-06-30 10:54:53.301787", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "consumer" + ], + "fields": [ + { + "fieldname": "consumer", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Consumer", + "options": "Event Consumer", + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-06-30 10:54:53.301787", + "modified_by": "Administrator", + "module": "Event Streaming", + "name": "Event Update Log Consumer", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.py b/influxframework/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.py new file mode 100644 index 0000000..b0e5719 --- /dev/null +++ b/influxframework/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.py @@ -0,0 +1,9 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class EventUpdateLogConsumer(Document): + pass diff --git a/influxframework/exceptions.py b/influxframework/exceptions.py new file mode 100644 index 0000000..ba8f0bd --- /dev/null +++ b/influxframework/exceptions.py @@ -0,0 +1,281 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +# BEWARE don't put anything in this file except exceptions +from werkzeug.exceptions import NotFound + + +class SiteNotSpecifiedError(Exception): + def __init__(self, *args, **kwargs): + self.message = "Please specify --site sitename" + super(Exception, self).__init__(self.message) + + +class ValidationError(Exception): + http_status_code = 417 + + +class AuthenticationError(Exception): + http_status_code = 401 + + +class SessionExpired(Exception): + http_status_code = 401 + + +class PermissionError(Exception): + http_status_code = 403 + + +class DoesNotExistError(ValidationError): + http_status_code = 404 + + +class PageDoesNotExistError(ValidationError): + http_status_code = 404 + + +class NameError(Exception): + http_status_code = 409 + + +class OutgoingEmailError(Exception): + http_status_code = 501 + + +class SessionStopped(Exception): + http_status_code = 503 + + +class UnsupportedMediaType(Exception): + http_status_code = 415 + + +class RequestToken(Exception): + http_status_code = 200 + + +class Redirect(Exception): + http_status_code = 301 + + +class CSRFTokenError(Exception): + http_status_code = 400 + + +class TooManyRequestsError(Exception): + http_status_code = 429 + + +class ImproperDBConfigurationError(Exception): + """ + Used when influxframework detects that database or tables are not properly + configured + """ + + def __init__(self, reason, msg=None): + if not msg: + msg = "MariaDb is not properly configured" + super().__init__(msg) + self.reason = reason + + +class DuplicateEntryError(NameError): + pass + + +class DataError(ValidationError): + pass + + +class UnknownDomainError(Exception): + pass + + +class MappingMismatchError(ValidationError): + pass + + +class InvalidStatusError(ValidationError): + pass + + +class MandatoryError(ValidationError): + pass + + +class NonNegativeError(ValidationError): + pass + + +class InvalidSignatureError(ValidationError): + pass + + +class RateLimitExceededError(ValidationError): + pass + + +class CannotChangeConstantError(ValidationError): + pass + + +class CharacterLengthExceededError(ValidationError): + pass + + +class UpdateAfterSubmitError(ValidationError): + pass + + +class LinkValidationError(ValidationError): + pass + + +class CancelledLinkError(LinkValidationError): + pass + + +class DocstatusTransitionError(ValidationError): + pass + + +class TimestampMismatchError(ValidationError): + pass + + +class EmptyTableError(ValidationError): + pass + + +class LinkExistsError(ValidationError): + pass + + +class InvalidEmailAddressError(ValidationError): + pass + + +class InvalidNameError(ValidationError): + pass + + +class InvalidPhoneNumberError(ValidationError): + pass + + +class TemplateNotFoundError(ValidationError): + pass + + +class UniqueValidationError(ValidationError): + pass + + +class AppNotInstalledError(ValidationError): + pass + + +class IncorrectSitePath(NotFound): + pass + + +class ImplicitCommitError(ValidationError): + pass + + +class RetryBackgroundJobError(Exception): + pass + + +class DocumentLockedError(ValidationError): + pass + + +class CircularLinkingError(ValidationError): + pass + + +class SecurityException(Exception): + pass + + +class InvalidColumnName(ValidationError): + pass + + +class IncompatibleApp(ValidationError): + pass + + +class InvalidDates(ValidationError): + pass + + +class DataTooLongException(ValidationError): + pass + + +class FileAlreadyAttachedException(Exception): + pass + + +class DocumentAlreadyRestored(ValidationError): + pass + + +class AttachmentLimitReached(ValidationError): + pass + + +class QueryTimeoutError(Exception): + pass + + +class QueryDeadlockError(Exception): + pass + + +class InReadOnlyMode(ValidationError): + http_status_code = 503 # temporarily not available + + +class TooManyWritesError(Exception): + pass + + +# OAuth exceptions +class InvalidAuthorizationHeader(CSRFTokenError): + pass + + +class InvalidAuthorizationPrefix(CSRFTokenError): + pass + + +class InvalidAuthorizationToken(CSRFTokenError): + pass + + +class InvalidDatabaseFile(ValidationError): + pass + + +class ExecutableNotFound(FileNotFoundError): + pass + + +class InvalidRemoteException(Exception): + pass + + +class LinkExpired(ValidationError): + http_status_code = 410 + title = "Link Expired" + message = "The link has expired" + + +class InvalidKeyError(ValidationError): + http_status_code = 401 + title = "Invalid Key" + message = "The document key is invalid" diff --git a/influxframework/geo/__init__.py b/influxframework/geo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/geo/country_info.json b/influxframework/geo/country_info.json new file mode 100644 index 0000000..f308dc6 --- /dev/null +++ b/influxframework/geo/country_info.json @@ -0,0 +1,2993 @@ +{ + "Afghanistan": { + "code": "af", + "currency": "AFN", + "currency_fraction": "Pul", + "currency_fraction_units": 100, + "currency_symbol": "\u060b", + "number_format": "#,###.##", + "timezones": [ + "Asia/Kabul" + ], + "isd": "+93" + }, + "Albania": { + "code": "al", + "currency": "ALL", + "currency_fraction": "Qindark\u00eb", + "currency_fraction_units": 100, + "currency_name": "Lek", + "currency_symbol": "L", + "number_format": "#,###.##", + "timezones": [ + "Europe/Tirane" + ], + "isd": "+355" + }, + "Algeria": { + "code": "dz", + "currency": "DZD", + "currency_fraction": "Santeem", + "currency_fraction_units": 100, + "currency_name": "Algerian Dinar", + "currency_symbol": "\u062f.\u062c", + "number_format": "#,###.##", + "timezones": [ + "Africa/Algiers" + ], + "isd": "+213" + }, + "American Samoa": { + "code": "as", + "number_format": "#,###.##", + "isd": "+1684" + }, + "Andorra": { + "code": "ad", + "currency": "EUR", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "smallest_currency_fraction_value": 0.01, + "currency_symbol": "\u20ac", + "number_format": "#,###.##", + "timezones": [ + "Europe/Andorra" + ], + "isd": "+376" + }, + "Angola": { + "code": "ao", + "currency": "KZ", + "currency_fraction": "C\u00eantimo", + "currency_fraction_units": 100, + "currency_symbol": "AOA", + "currency_name": "Kwanza", + "number_format": "#,###.##", + "timezones": [ + "Africa/Luanda" + ], + "isd": "+244" + }, + "Anguilla": { + "code": "ai", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_symbol": "$", + "currency_name": "Eastern Carribean Dollar", + "currency": "XCD", + "number_format": "#,###.##", + "timezones": [ + "America/Anguilla" + ], + "isd": "+1264" + }, + "Antarctica": { + "code": "aq", + "number_format": "#,###.##", + "timezones": [ + "Antarctica/Casey", + "Antarctica/Davis", + "Antarctica/DumontDUrville", + "Antarctica/Macquarie", + "Antarctica/Mawson", + "Antarctica/McMurdo", + "Antarctica/Palmer", + "Antarctica/Rothera", + "Antarctica/Syowa", + "Antarctica/Vostok" + ], + "isd": "+672" + }, + "Antigua and Barbuda": { + "code": "ag", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_symbol": "$", + "currency_name": "Eastern Carribean Dollar", + "currency": "XCD", + "number_format": "#,###.##", + "timezones": [ + "America/Antigua" + ], + "isd": "+1268" + }, + "Argentina": { + "code": "ar", + "currency": "ARS", + "currency_fraction": "Centavo", + "currency_fraction_units": 100, + "currency_name": "Argentine Peso", + "currency_symbol": "$", + "number_format": "#.###,##", + "timezones": [ + "America/Argentina/Buenos_Aires", + "America/Argentina/Catamarca", + "America/Argentina/Cordoba", + "America/Argentina/Jujuy", + "America/Argentina/La_Rioja", + "America/Argentina/Mendoza", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Salta", + "America/Argentina/San_Juan", + "America/Argentina/San_Luis", + "America/Argentina/Tucuman", + "America/Argentina/Ushuaia" + ], + "isd": "+54" + }, + "Armenia": { + "code": "am", + "currency": "AMD", + "currency_fraction": "Luma", + "currency_fraction_units": 100, + "currency_name": "Armenian Dram", + "currency_symbol": "\u058f", + "number_format": "#,###.##", + "timezones": [ + "Asia/Yerevan" + ], + "isd": "+374" + }, + "Aruba": { + "code": "aw", + "currency": "AWG", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Aruban Florin", + "currency_symbol": "Afl", + "number_format": "#,###.##", + "timezones": [ + "America/Aruba" + ], + "isd": "+297" + }, + "Australia": { + "code": "au", + "currency": "AUD", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Australian Dollar", + "currency_symbol": "$", + "number_format": "# ###.##", + "timezones": [ + "Australia/Adelaide", + "Australia/Brisbane", + "Australia/Broken_Hill", + "Australia/Currie", + "Australia/Darwin", + "Australia/Eucla", + "Australia/Hobart", + "Australia/Lindeman", + "Australia/Lord_Howe", + "Australia/Melbourne", + "Australia/Perth", + "Australia/Sydney" + ], + "isd": "+61" + }, + "Austria": { + "code": "at", + "currency": "EUR", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "smallest_currency_fraction_value": 0.01, + "currency_symbol": "\u20ac", + "number_format": "#,###.##", + "timezones": [ + "Europe/Vienna" + ], + "isd": "+43" + }, + "Azerbaijan": { + "code": "az", + "currency_fraction": "Q\u0259pik", + "currency_fraction_units": 100, + "currency_symbol": "", + "number_format": "#,###.##", + "timezones": [ + "Asia/Baku" + ], + "isd": "+994" + }, + "Bahamas": { + "code": "bs", + "currency": "BSD", + "currency_name": "Bahamian Dollar", + "number_format": "#,###.##", + "timezones": [ + "America/Nassau" + ], + "isd": "+1242" + }, + "Bahrain": { + "code": "bh", + "currency": "BHD", + "currency_fraction": "Fils", + "currency_fraction_units": 1000, + "currency_name": "Bahraini Dinar", + "currency_symbol": ".\u062f.\u0628", + "number_format": "#,###.###", + "timezones": [ + "Asia/Bahrain" + ], + "isd": "+973" + }, + "Bangladesh": { + "code": "bd", + "currency": "BDT", + "currency_fraction": "Paisa", + "currency_fraction_units": 100, + "currency_name": "Taka", + "currency_symbol": "\u09f3", + "number_format": "#,###.##", + "timezones": [ + "Asia/Dhaka" + ], + "isd": "+880" + }, + "Barbados": { + "code": "bb", + "currency": "BBD", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Barbados Dollar", + "currency_symbol": "$", + "number_format": "#,###.##", + "timezones": [ + "America/Barbados" + ], + "isd": "+1246" + }, + "Belarus": { + "code": "by", + "currency_fraction": "Kapyeyka", + "currency_fraction_units": 100, + "currency_symbol": "Br", + "number_format": "#,###.##", + "timezones": [ + "Europe/Minsk" + ], + "isd": "+375" + }, + "Belgium": { + "code": "be", + "currency": "EUR", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "smallest_currency_fraction_value": 0.01, + "currency_symbol": "\u20ac", + "number_format": "#,###.##", + "timezones": [ + "Europe/Brussels" + ], + "isd": "+32" + }, + "Belize": { + "code": "bz", + "currency": "BZD", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Belize Dollar", + "currency_symbol": "$", + "date_format": "mm-dd-yyyy", + "number_format": "#,###.##", + "timezones": [ + "America/Belize" + ], + "isd": "+501" + }, + "Benin": { + "code": "bj", + "currency": "XOF", + "currency_name": "West African CFA Franc", + "currency_symbol": "CFA", + "currency_fraction": "Centime", + "currency_fraction_units": 100, + "number_format": "#,###.##", + "timezones": [ + "Africa/Porto-Novo" + ], + "isd": "+229" + }, + "Bermuda": { + "code": "bm", + "currency": "BMD", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Bermudian Dollar", + "currency_symbol": "$", + "number_format": "#,###.##", + "timezones": [ + "Atlantic/Bermuda" + ], + "isd": "+1441" + }, + "Bhutan": { + "code": "bt", + "currency": "BTN", + "currency_fraction": "Chetrum", + "currency_fraction_units": 100, + "currency_name": "Ngultrum", + "currency_symbol": "Nu.", + "number_format": "#,###.##", + "timezones": [ + "Asia/Thimphu" + ], + "isd": "+975" + }, + "Bolivia, Plurinational State of": { + "code": "bo", + "currency": "BOB", + "currency_name": "Boliviano", + "number_format": "#,###.##", + "isd": "+591" + }, + "Bonaire, Sint Eustatius and Saba": { + "code": "bq", + "number_format": "#,###.##" + }, + "Bosnia and Herzegovina": { + "code": "ba", + "currency": "BAM", + "currency_fraction": "Fening", + "currency_fraction_units": 100, + "currency_symbol": "KM", + "number_format": "#.###,##", + "timezones": [ + "Europe/Sarajevo" + ], + "isd": "+387" + }, + "Botswana": { + "code": "bw", + "currency": "BWP", + "currency_fraction": "Thebe", + "currency_fraction_units": 100, + "currency_name": "Pula", + "currency_symbol": "P", + "number_format": "#,###.##", + "timezones": [ + "Africa/Gaborone" + ], + "isd": "+267" + }, + "Bouvet Island": { + "code": "bv", + "number_format": "#,###.##", + "isd": "+47" + }, + "Brazil": { + "code": "br", + "currency": "BRL", + "currency_fraction": "Centavo", + "currency_fraction_units": 100, + "currency_symbol": "R$", + "date_format": "dd/mm/yyyy", + "number_format": "#.###,##", + "timezones": [ + "America/Araguaina", + "America/Bahia", + "America/Belem", + "America/Boa_Vista", + "America/Campo_Grande", + "America/Cuiaba", + "America/Eirunepe", + "America/Fortaleza", + "America/Maceio", + "America/Manaus", + "America/Noronha", + "America/Porto_Velho", + "America/Recife", + "America/Rio_Branco", + "America/Santarem", + "America/Sao_Paulo" + ], + "isd": "+55" + }, + "British Indian Ocean Territory": { + "code": "io", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_symbol": "$", + "number_format": "#,###.##", + "timezones": [ + "Indian/Chagos" + ], + "isd": "+246" + }, + "Brunei Darussalam": { + "code": "bn", + "currency": "BND", + "currency_name": "Brunei Dollar", + "number_format": "#,###.##", + "timezones": [ + "Asia/Brunei" + ], + "isd": "+673" + }, + "Bulgaria": { + "code": "bg", + "currency": "BGN", + "currency_name": "Bulgarian Lev", + "currency_fraction": "Stotinka", + "currency_fraction_units": 100, + "currency_symbol": "\u043b\u0432", + "number_format": "#,###.##", + "timezones": [ + "Europe/Sofia" + ], + "isd": "+359" + }, + "Burkina Faso": { + "code": "bf", + "currency": "XOF", + "currency_name": "West African CFA Franc", + "currency_symbol": "CFA", + "currency_fraction": "Centime", + "currency_fraction_units": 100, + "number_format": "#,###.##", + "timezones": [ + "Africa/Ouagadougou" + ], + "isd": "+226" + }, + "Burundi": { + "code": "bi", + "currency": "BIF", + "currency_fraction": "Centime", + "currency_fraction_units": 100, + "currency_name": "Burundi Franc", + "currency_symbol": "Fr", + "number_format": "#,###.##", + "timezones": [ + "Africa/Bujumbura" + ], + "isd": "+257" + }, + "Cambodia": { + "code": "kh", + "currency": "KHR", + "currency_fraction": "Sen", + "currency_fraction_units": 100, + "currency_name": "Riel", + "currency_symbol": "\u17db", + "number_format": "#,###.##", + "timezones": [ + "Asia/Phnom_Penh" + ], + "isd": "+855" + }, + "Cameroon": { + "code": "cm", + "currency": "XAF", + "currency_name": "Central African CFA Franc", + "currency_symbol": "FCFA", + "currency_fraction": "Centime", + "currency_fraction_units": 100, + "number_format": "#,###.##", + "timezones": [ + "Africa/Douala" + ], + "isd": "+237" + }, + "Canada": { + "code": "ca", + "currency": "CAD", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Canadian Dollar", + "currency_symbol": "$", + "date_format": "mm-dd-yyyy", + "number_format": "#,###.##", + "timezones": [ + "America/Atikokan", + "America/Blanc-Sablon", + "America/Cambridge_Bay", + "America/Creston", + "America/Dawson", + "America/Dawson_Creek", + "America/Edmonton", + "America/Glace_Bay", + "America/Goose_Bay", + "America/Halifax", + "America/Inuvik", + "America/Iqaluit", + "America/Moncton", + "America/Montreal", + "America/Nipigon", + "America/Pangnirtung", + "America/Rainy_River", + "America/Rankin_Inlet", + "America/Regina", + "America/Resolute", + "America/St_Johns", + "America/Swift_Current", + "America/Thunder_Bay", + "America/Toronto", + "America/Vancouver", + "America/Whitehorse", + "America/Winnipeg", + "America/Yellowknife" + ], + "isd": "+1" + }, + "Cape Verde": { + "code": "cv", + "currency": "CVE", + "currency_fraction": "Centavo", + "currency_fraction_units": 100, + "currency_name": "Cape Verde Escudo", + "currency_symbol": "Esc or $", + "number_format": "#,###.##", + "timezones": [ + "Atlantic/Cape_Verde" + ], + "isd": "+238" + }, + "Cayman Islands": { + "code": "ky", + "currency": "KYD", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Cayman Islands Dollar", + "currency_symbol": "$", + "number_format": "#,###.##", + "timezones": [ + "America/Cayman" + ], + "isd": "+ 345" + }, + "Central African Republic": { + "code": "cf", + "currency": "XAF", + "currency_fraction": "Centime", + "currency_fraction_units": 100, + "currency_name": "Central African CFA Franc", + "currency_symbol": "FCFA", + "number_format": "#,###.##", + "timezones": [ + "Africa/Bangui" + ], + "isd": "+236" + }, + "Chad": { + "code": "td", + "currency": "XAF", + "currency_name": "Central African CFA Franc", + "currency_symbol": "FCFA", + "currency_fraction": "Centime", + "currency_fraction_units": 100, + "number_format": "#,###.##", + "timezones": [ + "Africa/Ndjamena" + ], + "isd": "+235" + }, + "Chile": { + "code": "cl", + "currency": "CLP", + "currency_fraction": "Centavo", + "currency_fraction_units": 100, + "currency_name": "Chilean Peso", + "currency_symbol": "$", + "number_format": "#.###", + "timezones": [ + "America/Santiago", + "Pacific/Easter" + ], + "isd": "+56" + }, + "China": { + "code": "cn", + "currency": "CNY", + "currency_name": "Yuan Renminbi", + "currency_fraction_units": 100, + "smallest_currency_fraction_value": 0.01, + "date_format": "yyyy-mm-dd", + "number_format": "#,###.##", + "timezones": [ + "Asia/Chongqing", + "Asia/Harbin", + "Asia/Kashgar", + "Asia/Shanghai", + "Asia/Urumqi" + ], + "isd": "+86" + }, + "Christmas Island": { + "code": "cx", + "number_format": "#,###.##", + "timezones": [ + "Indian/Christmas" + ], + "isd": "+61" + }, + "Cocos (Keeling) Islands": { + "code": "cc", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_symbol": "$", + "number_format": "#,###.##", + "timezones": [ + "Indian/Cocos" + ], + "isd": "+61" + }, + "Colombia": { + "code": "co", + "currency": "COP", + "currency_fraction": "Centavo", + "currency_fraction_units": 100, + "currency_name": "Colombian Peso", + "currency_symbol": "$", + "number_format": "#.###,##", + "timezones": [ + "America/Bogota" + ], + "isd": "+57" + }, + "Comoros": { + "code": "km", + "currency": "KMF", + "currency_fraction": "Centime", + "currency_fraction_units": 100, + "currency_name": "Comoro Franc", + "currency_symbol": "Fr", + "number_format": "#,###.##", + "timezones": [ + "Indian/Comoro" + ], + "isd": "+269" + }, + "Congo": { + "code": "cg", + "number_format": "#,###.##", + "currency": "XAF", + "currency_name": "Central African CFA Franc", + "currency_symbol": "FCFA", + "currency_fraction": "Centime", + "currency_fraction_units": 100, + "isd": "+242" + }, + "Congo, The Democratic Republic of the": { + "code": "cd", + "number_format": "#,###.##", + "currency": "CDF", + "currency_name": "Congolese franc", + "currency_symbol": "FC", + "currency_fraction": "Centime", + "currency_fraction_units": 100, + "isd": "+243" + }, + "Cook Islands": { + "code": "ck", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_symbol": "$", + "number_format": "#,###.##", + "timezones": [ + "Pacific/Rarotonga" + ], + "isd": "+682" + }, + "Costa Rica": { + "code": "cr", + "currency": "CRC", + "currency_fraction": "C\u00e9ntimo", + "currency_fraction_units": 100, + "currency_name": "Costa Rican Colon", + "currency_symbol": "\u20a1", + "number_format": "#.###,##", + "timezones": [ + "America/Costa_Rica" + ], + "isd": "+506" + }, + "Croatia": { + "code": "hr", + "currency": "HRK", + "currency_fraction": "Lipa", + "currency_fraction_units": 100, + "currency_name": "Croatian Kuna", + "currency_symbol": "kn", + "number_format": "#.###,##", + "timezones": [ + "Europe/Zagreb" + ], + "isd": "+385" + }, + "Cuba": { + "code": "cu", + "currency": "CUP", + "currency_fraction": "Centavo", + "currency_fraction_units": 100, + "currency_name": "Cuban Peso", + "currency_symbol": "$", + "number_format": "#,###.##", + "timezones": [ + "America/Havana" + ], + "isd": "+53" + }, + "Cura\u00e7ao": { + "code": "cw", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_symbol": "\u0192", + "number_format": "#,###.##" + }, + "Cyprus": { + "code": "cy", + "currency": "CYP", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Cyprus Pound", + "currency_symbol": "\u20ac", + "number_format": "#.###,##", + "timezones": [ + "Asia/Nicosia" + ], + "isd": "+357" + }, + "Czech Republic": { + "code": "cz", + "currency": "CZK", + "currency_fraction": "Hal\u00e9\u0159", + "currency_fraction_units": 100, + "currency_name": "Czech Koruna", + "currency_symbol": "K\u010d", + "number_format": "#.###,##", + "timezones": [ + "Europe/Prague" + ], + "isd": "+420" + }, + "Denmark": { + "code": "dk", + "currency": "DKK", + "currency_fraction": "\u00d8re", + "currency_fraction_units": 100, + "currency_name": "Danish Krone", + "currency_symbol": "kr", + "number_format": "#.###,##", + "timezones": [ + "Europe/Copenhagen" + ], + "isd": "+45" + }, + "Djibouti": { + "code": "dj", + "currency": "DJF", + "currency_fraction": "Centime", + "currency_fraction_units": 100, + "currency_name": "Djibouti Franc", + "currency_symbol": "Fr", + "number_format": "#,###.##", + "timezones": [ + "Africa/Djibouti" + ], + "isd": "+253" + }, + "Dominica": { + "code": "dm", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_symbol": "$", + "currency_name": "Eastern Carribean Dollar", + "currency": "XCD", + "number_format": "#,###.##", + "timezones": [ + "America/Dominica" + ], + "isd": "+1767" + }, + "Dominican Republic": { + "code": "do", + "currency": "DOP", + "currency_fraction": "Centavo", + "currency_fraction_units": 100, + "currency_name": "Dominican Peso", + "currency_symbol": "$", + "number_format": "#,###.##", + "timezones": [ + "America/Santo_Domingo" + ], + "isd": "+1849" + }, + "Ecuador": { + "code": "ec", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_symbol": "$", + "number_format": "#,###.##", + "timezones": [ + "America/Guayaquil", + "Pacific/Galapagos" + ], + "isd": "+593" + }, + "Egypt": { + "code": "eg", + "currency": "EGP", + "currency_fraction": "Piastre[F]", + "currency_fraction_units": 100, + "currency_name": "Egyptian Pound", + "currency_symbol": "\u00a3 or \u062c.\u0645", + "number_format": "#,###.##", + "timezones": [ + "Africa/Cairo" + ], + "isd": "+20" + }, + "El Salvador": { + "code": "sv", + "currency": "USD", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "smallest_currency_fraction_value": 0.01, + "currency_name": "Dolar estadounidense", + "currency_symbol": "$", + "date_format": "dd-mm-yyyy", + "number_format": "#,###.##", + "timezones": [ + "America/El_Salvador" + ], + "isd": "+503" + }, + "Equatorial Guinea": { + "code": "gq", + "currency": "XAF", + "currency_name": "Central African CFA Franc", + "currency_symbol": "FCFA", + "currency_fraction": "Centime", + "currency_fraction_units": 100, + "number_format": "#,###.##", + "timezones": [ + "Africa/Malabo" + ], + "isd": "+240" + }, + "Eritrea": { + "code": "er", + "currency": "ERN", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Nakfa", + "currency_symbol": "Nfk", + "number_format": "#,###.##", + "timezones": [ + "Africa/Asmara" + ], + "isd": "+291" + }, + "Estonia": { + "code": "ee", + "currency": "EUR", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Euro", + "currency_symbol": "\u20ac", + "number_format": "#,###.##", + "timezones": [ + "Europe/Tallinn" + ], + "isd": "+372" + }, + "Ethiopia": { + "code": "et", + "currency": "ETB", + "currency_fraction": "Santim", + "currency_fraction_units": 100, + "currency_name": "Ethiopian Birr", + "currency_symbol": "Br", + "number_format": "#,###.##", + "timezones": [ + "Africa/Addis_Ababa" + ], + "isd": "+251" + }, + "Falkland Islands (Malvinas)": { + "code": "fk", + "currency": "FKP", + "currency_name": "Falkland Islands Pound", + "number_format": "#,###.##", + "isd": "+500" + }, + "Faroe Islands": { + "code": "fo", + "currency_fraction": "\u00d8re", + "currency_fraction_units": 100, + "currency_symbol": "kr", + "number_format": "#,###.##", + "timezones": [ + "Atlantic/Faroe" + ], + "isd": "+298" + }, + "Fiji": { + "code": "fj", + "currency": "FJD", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Fiji Dollar", + "currency_symbol": "$", + "number_format": "#,###.##", + "timezones": [ + "Pacific/Fiji" + ], + "isd": "+679" + }, + "Finland": { + "code": "fi", + "currency": "EUR", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "smallest_currency_fraction_value": 0.01, + "currency_symbol": "\u20ac", + "number_format": "#,###.##", + "timezones": [ + "Europe/Helsinki" + ], + "isd": "+358" + }, + "France": { + "code": "fr", + "currency": "EUR", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "smallest_currency_fraction_value": 0.01, + "currency_symbol": "\u20ac", + "number_format": "# ###,##", + "date_format": "dd/mm/yyyy", + "timezones": [ + "Europe/Paris" + ], + "isd": "+33" + }, + "French Guiana": { + "code": "gf", + "number_format": "#,###.##", + "timezones": [ + "America/Cayenne" + ], + "isd": "+594" + }, + "French Polynesia": { + "code": "pf", + "currency_fraction": "Centime", + "currency_fraction_units": 100, + "currency_symbol": "Fr", + "number_format": "#,###.##", + "timezones": [ + "Pacific/Gambier", + "Pacific/Marquesas", + "Pacific/Tahiti" + ], + "isd": "+689" + }, + "French Southern Territories": { + "code": "tf", + "number_format": "#,###.##", + "isd": "+262" + }, + "Gabon": { + "code": "ga", + "currency": "XAF", + "currency_name": "Central African CFA Franc", + "currency_symbol": "FCFA", + "currency_fraction": "Centime", + "currency_fraction_units": 100, + "number_format": "#,###.##", + "timezones": [ + "Africa/Libreville" + ], + "isd": "+241" + }, + "Gambia": { + "code": "gm", + "currency": "GMD", + "currency_name": "Dalasi", + "number_format": "#,###.##", + "timezones": [ + "Africa/Banjul" + ], + "isd": "+220" + }, + "Georgia": { + "code": "ge", + "currency_fraction": "Tetri", + "currency_fraction_units": 100, + "currency_symbol": "\u10da", + "number_format": "#,###.##", + "timezones": [ + "Asia/Tbilisi" + ], + "isd": "+995" + }, + "Germany": { + "code": "de", + "currency": "EUR", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "smallest_currency_fraction_value": 0.01, + "currency_symbol": "\u20ac", + "number_format": "#.###,##", + "date_format": "dd.mm.yyyy", + "time_format": "HH:mm", + "timezones": [ + "Europe/Berlin" + ], + "isd": "+49" + }, + "Ghana": { + "code": "gh", + "currency": "GHS", + "currency_fraction": "Pesewa", + "currency_fraction_units": 100, + "currency_symbol": "\u20b5", + "number_format": "#,###.##", + "timezones": [ + "Africa/Accra" + ], + "isd": "+233" + }, + "Gibraltar": { + "code": "gi", + "currency": "GIP", + "currency_fraction": "Penny", + "currency_fraction_units": 100, + "currency_name": "Gibraltar Pound", + "currency_symbol": "\u00a3", + "number_format": "#,###.##", + "timezones": [ + "Europe/Gibraltar" + ], + "isd": "+350" + }, + "Greece": { + "code": "gr", + "currency": "EUR", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "smallest_currency_fraction_value": 0.01, + "currency_symbol": "\u20ac", + "number_format": "#,###.##", + "timezones": [ + "Europe/Athens" + ], + "isd": "+30" + }, + "Greenland": { + "code": "gl", + "number_format": "#,###.##", + "timezones": [ + "America/Danmarkshavn", + "America/Godthab", + "America/Scoresbysund", + "America/Thule" + ], + "isd": "+299" + }, + "Grenada": { + "code": "gd", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_symbol": "$", + "currency_name": "Eastern Carribean Dollar", + "currency": "XCD", + "number_format": "#,###.##", + "timezones": [ + "America/Grenada" + ], + "isd": "+1473" + }, + "Guadeloupe": { + "code": "gp", + "number_format": "#,###.##", + "timezones": [ + "America/Guadeloupe" + ], + "isd": "+590" + }, + "Guam": { + "code": "gu", + "number_format": "#,###.##", + "timezones": [ + "Pacific/Guam" + ], + "isd": "+1671" + }, + "Guatemala": { + "code": "gt", + "currency": "GTQ", + "currency_fraction": "Centavo", + "currency_fraction_units": 100, + "currency_name": "Quetzal", + "currency_symbol": "Q", + "number_format": "#,###.##", + "timezones": [ + "America/Guatemala" + ], + "isd": "+502" + }, + "Guernsey": { + "code": "gg", + "currency_fraction": "Penny", + "currency_fraction_units": 100, + "currency_symbol": "\u00a3", + "number_format": "#,###.##", + "timezones": [ + "Europe/London" + ], + "isd": "+44" + }, + "Guinea": { + "code": "gn", + "currency": "GNF", + "currency_fraction": "Centime", + "currency_fraction_units": 100, + "currency_name": "Guinea Franc", + "currency_symbol": "Fr", + "number_format": "#,###.##", + "timezones": [ + "Africa/Conakry" + ], + "isd": "+224" + }, + "Guinea-Bissau": { + "code": "gw", + "currency": "XOF", + "currency_name": "West African CFA Franc", + "currency_symbol": "CFA", + "currency_fraction": "Centime", + "currency_fraction_units": 100, + "number_format": "#,###.##", + "timezones": [ + "Africa/Bissau" + ], + "isd": "+245" + }, + "Guyana": { + "code": "gy", + "currency": "GYD", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Guyana Dollar", + "currency_symbol": "$", + "number_format": "#,###.##", + "timezones": [ + "America/Guyana" + ], + "isd": "+592" + }, + "Haiti": { + "code": "ht", + "currency": "HTG", + "currency_fraction": "Centime", + "currency_fraction_units": 100, + "currency_name": "Gourde", + "currency_symbol": "G", + "number_format": "#,###.##", + "timezones": [ + "America/Guatemala", + "America/Port-au-Prince" + ], + "isd": "+509" + }, + "Heard Island and McDonald Islands": { + "code": "hm", + "number_format": "#,###.##", + "isd": "+0" + }, + "Holy See (Vatican City State)": { + "code": "va", + "number_format": "#,###.##", + "isd": "+379" + }, + "Honduras": { + "code": "hn", + "currency": "HNL", + "currency_fraction": "Centavo", + "currency_fraction_units": 100, + "currency_name": "Lempira", + "currency_symbol": "L", + "number_format": "#,###.##", + "timezones": [ + "America/Tegucigalpa" + ], + "isd": "+504" + }, + "Hong Kong": { + "code": "hk", + "currency": "HKD", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Hong Kong Dollar", + "currency_symbol": "$", + "number_format": "#,###.##", + "timezones": [ + "Asia/Hong_Kong" + ], + "isd": "+852" + }, + "Hungary": { + "code": "hu", + "currency": "HUF", + "currency_fraction": "Fill\u00e9r", + "currency_fraction_units": 100, + "currency_name": "Forint", + "currency_symbol": "Ft", + "date_format": "yyyy-mm-dd", + "number_format": "#.###", + "timezones": [ + "Europe/Budapest" + ], + "isd": "+36" + }, + "Iceland": { + "code": "is", + "currency": "ISK", + "currency_fraction": "Eyrir", + "currency_fraction_units": 100, + "currency_name": "Iceland Krona", + "currency_symbol": "kr", + "number_format": "#.###", + "timezones": [ + "Atlantic/Reykjavik" + ], + "isd": "+354" + }, + "India": { + "code": "in", + "currency": "INR", + "currency_fraction": "Paisa", + "currency_fraction_units": 100, + "currency_name": "Indian Rupee", + "currency_symbol": "\u20b9", + "number_format": "#,##,###.##", + "timezones": [ + "Asia/Kolkata" + ], + "isd": "+91" + }, + "Indonesia": { + "code": "id", + "currency": "IDR", + "currency_fraction": "Sen", + "currency_fraction_units": 100, + "currency_name": "Rupiah", + "currency_symbol": "Rp", + "number_format": "#.###,##", + "timezones": [ + "Asia/Jakarta", + "Asia/Jayapura", + "Asia/Makassar", + "Asia/Pontianak" + ], + "isd": "+62" + }, + "Iran": { + "code": "ir", + "currency": "IRR", + "currency_name": "Iranian Rial", + "currency_symbol": "\ufdfc", + "number_format": "#,###.##", + "timezones": [ + "Asia/Tehran" + ], + "isd": "+98" + }, + "Iraq": { + "code": "iq", + "currency": "IQD", + "currency_fraction": "Fils", + "currency_fraction_units": 1000, + "currency_name": "Iraqi Dinar", + "currency_symbol": "\u0639.\u062f", + "number_format": "#,###.###", + "timezones": [ + "Asia/Baghdad" + ], + "isd": "+964" + }, + "Ireland": { + "code": "ie", + "currency": "EUR", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "smallest_currency_fraction_value": 0.01, + "currency_symbol": "\u20ac", + "number_format": "#,###.##", + "timezones": [ + "Europe/Dublin" + ], + "isd": "+353" + }, + "Isle of Man": { + "code": "im", + "currency_fraction": "Penny", + "currency_fraction_units": 100, + "currency_symbol": "\u00a3", + "number_format": "#,###.##", + "timezones": [ + "Europe/London" + ], + "isd": "+44" + }, + "Israel": { + "code": "il", + "currency": "ILS", + "currency_fraction": "Agora", + "currency_fraction_units": 100, + "currency_name": "New Israeli Sheqel", + "currency_symbol": "\u20aa", + "number_format": "#,###.##", + "timezones": [ + "Asia/Jerusalem" + ], + "isd": "+972" + }, + "Italy": { + "code": "it", + "currency": "EUR", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "smallest_currency_fraction_value": 0.01, + "currency_symbol": "\u20ac", + "number_format": "#.###,##", + "date_format": "dd/mm/yyyy", + "timezones": [ + "Europe/Rome" + ], + "isd": "+39" + }, + "Ivory Coast": { + "code": "ci", + "currency": "XOF", + "currency_name": "West African CFA Franc", + "currency_symbol": "CFA", + "currency_fraction": "Centime", + "currency_fraction_units": 100, + "number_format": "#,###.##", + "timeszones": [ + "Africa/Abidjan" + ], + "isd": "+225" + }, + "Jamaica": { + "code": "jm", + "currency": "JMD", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Jamaican Dollar", + "currency_symbol": "$", + "number_format": "#,###.##", + "timezones": [ + "America/Jamaica" + ], + "isd": "+1876" + }, + "Japan": { + "code": "jp", + "currency": "JPY", + "currency_fraction": "Sen[G]", + "currency_fraction_units": 100, + "currency_name": "Yen", + "currency_symbol": "\u00a5", + "number_format": "#,###", + "timezones": [ + "Asia/Tokyo" + ], + "isd": "+81" + }, + "Jersey": { + "code": "je", + "currency_fraction": "Penny", + "currency_fraction_units": 100, + "currency_symbol": "\u00a3", + "number_format": "#,###.##", + "timezones": [ + "Europe/London" + ], + "isd": "+44" + }, + "Jordan": { + "code": "jo", + "currency": "JOD", + "currency_fraction": "Piastre[H]", + "currency_fraction_units": 100, + "currency_name": "Jordanian Dinar", + "currency_symbol": "\u062f.\u0627", + "number_format": "#,###.###", + "timezones": [ + "Asia/Amman" + ], + "isd": "+962" + }, + "Kazakhstan": { + "code": "kz", + "currency": "KZT", + "currency_fraction": "T\u00ef\u0131n", + "currency_fraction_units": 100, + "currency_name": "Tenge", + "currency_symbol": "\u20b8", + "number_format": "#,###.##", + "timezones": [ + "Asia/Almaty", + "Asia/Aqtau", + "Asia/Aqtobe", + "Asia/Oral", + "Asia/Qyzylorda" + ], + "isd": "+7" + }, + "Kenya": { + "code": "ke", + "currency": "KES", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Kenyan Shilling", + "currency_symbol": "Sh", + "number_format": "#,###.##", + "timezones": [ + "Africa/Nairobi" + ], + "isd": "+254" + }, + "Kiribati": { + "code": "ki", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_symbol": "$", + "number_format": "#,###.##", + "timezones": [ + "Pacific/Enderbury", + "Pacific/Kiritimati", + "Pacific/Tarawa" + ], + "isd": "+686" + }, + "Korea, Democratic Peoples Republic of": { + "code": "kp", + "currency": "KPW", + "currency_name": "North Korean Won", + "number_format": "#,###.##", + "isd": "+850" + }, + "Korea, Republic of": { + "code": "kr", + "currency": "KRW", + "currency_name": "Won", + "number_format": "#,###", + "isd": "+82" + }, + "Kuwait": { + "code": "kw", + "currency": "KWD", + "currency_fraction": "Fils", + "currency_fraction_units": 1000, + "currency_name": "Kuwaiti Dinar", + "currency_symbol": "\u062f.\u0643", + "number_format": "#,###.###", + "timezones": [ + "Asia/Kuwait" + ], + "isd": "+965" + }, + "Kyrgyzstan": { + "code": "kg", + "currency": "KGS", + "currency_fraction": "Tyiyn", + "currency_fraction_units": 100, + "currency_name": "Som", + "currency_symbol": "\u043b\u0432", + "number_format": "#,###.##", + "timezones": [ + "Asia/Bishkek" + ], + "isd": "+996" + }, + "Lao Peoples Democratic Republic": { + "code": "la", + "currency": "LAK", + "currency_name": "Kip", + "number_format": "#,###.##", + "timezones": [ + "Asia/Vientiane" + ], + "isd": "+856" + }, + "Latvia": { + "code": "lv", + "currency": "LVL", + "currency_fraction": "Sant\u012bms", + "currency_fraction_units": 100, + "currency_name": "Latvian Lats", + "currency_symbol": "Ls", + "number_format": "#,###.##", + "timezones": [ + "Europe/Riga" + ], + "isd": "+371" + }, + "Lebanon": { + "code": "lb", + "currency": "LBP", + "currency_fraction": "Piastre", + "currency_fraction_units": 100, + "currency_name": "Lebanese Pound", + "currency_symbol": "\u0644.\u0644", + "number_format": "#,###.##", + "timezones": [ + "Asia/Beirut" + ], + "isd": "+961" + }, + "Lesotho": { + "code": "ls", + "currency": "LSL", + "currency_fraction": "Sente", + "currency_fraction_units": 100, + "currency_name": "Loti", + "currency_symbol": "L", + "number_format": "#,###.##", + "timezones": [ + "Africa/Maseru" + ], + "isd": "+266" + }, + "Liberia": { + "code": "lr", + "currency": "LRD", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Liberian Dollar", + "currency_symbol": "$", + "number_format": "#,###.##", + "timezones": [ + "Africa/Monrovia" + ], + "isd": "+231" + }, + "Libya": { + "code": "ly", + "currency": "LYD", + "currency_fraction": "Dirham", + "currency_fraction_units": 1000, + "currency_name": "Libyan Dinar", + "currency_symbol": "\u0644.\u062f", + "number_format": "#,###.###", + "timezones": [ + "Africa/Tripoli" + ], + "isd": "+218" + }, + "Liechtenstein": { + "code": "li", + "currency_fraction": "Rappen", + "currency_fraction_units": 100, + "currency_symbol": "Fr", + "number_format": "#,###.##", + "timezones": [ + "Europe/Vaduz" + ], + "isd": "+423" + }, + "Lithuania": { + "code": "lt", + "currency": "LTL", + "currency_fraction": "Centas", + "currency_fraction_units": 100, + "currency_name": "Lithuanian Litas", + "currency_symbol": "Lt", + "date_format": "yyyy-mm-dd", + "number_format": "# ###,##", + "timezones": [ + "Europe/Vilnius" + ], + "isd": "+370" + }, + "Luxembourg": { + "code": "lu", + "currency": "EUR", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "smallest_currency_fraction_value": 0.01, + "currency_symbol": "\u20ac", + "number_format": "#,###.##", + "timezones": [ + "Europe/Luxembourg" + ], + "isd": "+352" + }, + "Macao": { + "code": "mo", + "currency": "MOP", + "currency_name": "Pataca", + "number_format": "#,###.##", + "isd": "+853" + }, + "Macedonia": { + "code": "mk", + "currency": "MKD", + "currency_fraction": "Deni", + "currency_fraction_units": 100, + "currency_name": "Denar", + "currency_symbol": "\u0434\u0435\u043d", + "number_format": "#,###.##", + "isd": "+389" + }, + "Madagascar": { + "code": "mg", + "currency_fraction": "Iraimbilanja", + "currency_fraction_units": 5, + "currency_symbol": "Ar", + "number_format": "#,###.##", + "timezones": [ + "Indian/Antananarivo" + ], + "isd": "+261" + }, + "Malawi": { + "code": "mw", + "currency": "MWK", + "currency_fraction": "Tambala", + "currency_fraction_units": 100, + "currency_name": "Kwacha", + "currency_symbol": "MK", + "number_format": "#,###.##", + "timezones": [ + "Africa/Blantyre" + ], + "isd": "+265" + }, + "Malaysia": { + "code": "my", + "currency": "MYR", + "currency_fraction": "Sen", + "currency_fraction_units": 100, + "currency_name": "Malaysian Ringgit", + "currency_symbol": "RM", + "number_format": "#,###.##", + "timezones": [ + "Asia/Kuala_Lumpur", + "Asia/Kuching" + ], + "isd": "+60" + }, + "Maldives": { + "code": "mv", + "currency": "MVR", + "currency_fraction": "Laari", + "currency_fraction_units": 100, + "currency_name": "Rufiyaa", + "currency_symbol": ".\u0783", + "number_format": "#,###.##", + "timezones": [ + "Indian/Maldives" + ], + "isd": "+960" + }, + "Mali": { + "code": "ml", + "currency": "XOF", + "currency_name": "West African CFA Franc", + "currency_symbol": "CFA", + "currency_fraction": "Centime", + "currency_fraction_units": 100, + "number_format": "#,###.##", + "timezones": [ + "Africa/Bamako" + ], + "isd": "+223" + }, + "Malta": { + "code": "mt", + "currency": "EUR", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "smallest_currency_fraction_value": 0.01, + "currency_symbol": "\u20ac", + "number_format": "#,###.##", + "date_format": "dd/mm/yyyy", + "timezones": [ + "Europe/Malta" + ], + "isd": "+356" + }, + "Marshall Islands": { + "code": "mh", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_symbol": "$", + "number_format": "#,###.##", + "timezones": [ + "Pacific/Kwajalein", + "Pacific/Majuro" + ], + "isd": "+692" + }, + "Martinique": { + "code": "mq", + "number_format": "#,###.##", + "timezones": [ + "America/Martinique" + ], + "isd": "+596" + }, + "Mauritania": { + "code": "mr", + "currency": "MRO", + "currency_fraction": "Khoums", + "currency_fraction_units": 5, + "currency_name": "Ouguiya", + "currency_symbol": "UM", + "number_format": "#,###.##", + "timezones": [ + "Africa/Nouakchott" + ], + "isd": "+222" + }, + "Mauritius": { + "code": "mu", + "currency": "MUR", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Mauritius Rupee", + "currency_symbol": "\u20a8", + "number_format": "#,###", + "timezones": [ + "Indian/Mauritius" + ], + "isd": "+230" + }, + "Mayotte": { + "code": "yt", + "number_format": "#,###.##", + "timezones": [ + "Indian/Mayotte" + ], + "isd": "+262" + }, + "Mexico": { + "code": "mx", + "currency": "MXN", + "currency_fraction": "Centavo", + "currency_fraction_units": 100, + "currency_name": "Mexican Peso", + "currency_symbol": "$", + "number_format": "#,###.##", + "timezones": [ + "America/Bahia_Banderas", + "America/Cancun", + "America/Chihuahua", + "America/Hermosillo", + "America/Matamoros", + "America/Mazatlan", + "America/Merida", + "America/Mexico_City", + "America/Monterrey", + "America/Ojinaga", + "America/Santa_Isabel", + "America/Tijuana" + ], + "isd": "+52" + }, + "Micronesia, Federated States of": { + "code": "fm", + "number_format": "#,###.##", + "isd": "+691" + }, + "Moldova, Republic of": { + "code": "md", + "currency": "MDL", + "currency_name": "Moldovan Leu", + "number_format": "#,###.##", + "isd": "+373" + }, + "Monaco": { + "code": "mc", + "currency": "EUR", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "smallest_currency_fraction_value": 0.01, + "currency_symbol": "\u20ac", + "number_format": "#,###.##", + "timezones": [ + "Europe/Monaco" + ], + "isd": "+377" + }, + "Mongolia": { + "code": "mn", + "currency": "MNT", + "currency_fraction": "M\u00f6ng\u00f6", + "currency_fraction_units": 100, + "currency_name": "Tugrik", + "currency_symbol": "\u20ae", + "date_format": "yyyy-mm-dd", + "number_format": "#,###.##", + "timezones": [ + "Asia/Choibalsan", + "Asia/Hovd", + "Asia/Ulaanbaatar" + ], + "isd": "+976" + }, + "Montenegro": { + "code": "me", + "currency": "EUR", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "smallest_currency_fraction_value": 0.01, + "currency_symbol": "\u20ac", + "number_format": "#,###.##", + "timezones": [ + "Europe/Belgrade" + ], + "isd": "+382" + }, + "Montserrat": { + "code": "ms", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_symbol": "$", + "currency_name": "Eastern Carribean Dollar", + "currency": "XCD", + "number_format": "#,###.##", + "timezones": [ + "America/Montserrat" + ], + "isd": "+1664" + }, + "Morocco": { + "code": "ma", + "currency": "MAD", + "currency_fraction": "Centime", + "currency_fraction_units": 100, + "currency_name": "Moroccan Dirham", + "currency_symbol": "\u062f.\u0645.", + "number_format": "#,###.##", + "timezones": [ + "Africa/Casablanca" + ], + "isd": "+212" + }, + "Mozambique": { + "code": "mz", + "currency": "MZN", + "currency_fraction": "Centavo", + "currency_fraction_units": 100, + "currency_symbol": "MZN", + "number_format": "#,###.##", + "timezones": [ + "Africa/Maputo" + ], + "isd": "+258" + }, + "Myanmar": { + "code": "mm", + "currency": "MMK", + "currency_name": "Kyat", + "number_format": "#,###.##", + "timezones": [ + "Asia/Rangoon" + ], + "isd": "+95" + }, + "Namibia": { + "code": "na", + "currency": "NAD", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Namibia Dollar", + "currency_symbol": "$", + "number_format": "#,###.##", + "timezones": [ + "Africa/Windhoek" + ], + "isd": "+264" + }, + "Nauru": { + "code": "nr", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_symbol": "$", + "number_format": "#,###.##", + "timezones": [ + "Pacific/Nauru" + ], + "isd": "+674" + }, + "Nepal": { + "code": "np", + "currency": "NPR", + "currency_fraction": "Paisa", + "currency_fraction_units": 100, + "currency_name": "Nepalese Rupee", + "currency_symbol": "\u20a8", + "number_format": "#,###.##", + "timezones": [ + "Asia/Kathmandu" + ], + "isd": "+977" + }, + "Netherlands": { + "code": "nl", + "currency": "EUR", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "smallest_currency_fraction_value": 0.01, + "currency_symbol": "\u20ac", + "number_format": "#,###.##", + "timezones": [ + "Europe/Amsterdam" + ], + "isd": "+31" + }, + "New Caledonia": { + "code": "nc", + "currency_fraction": "Centime", + "currency_fraction_units": 100, + "currency_symbol": "Fr", + "number_format": "#,###.##", + "timezones": [ + "Pacific/Noumea" + ], + "isd": "+687" + }, + "New Zealand": { + "code": "nz", + "currency": "NZD", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "New Zealand Dollar", + "currency_symbol": "$", + "number_format": "#,###.##", + "timezones": [ + "Pacific/Auckland", + "Pacific/Chatham" + ], + "isd": "+64" + }, + "Nicaragua": { + "code": "ni", + "currency": "NIO", + "currency_fraction": "Centavo", + "currency_fraction_units": 100, + "currency_name": "Cordoba Oro", + "currency_symbol": "C$", + "number_format": "#,###.##", + "timezones": [ + "America/Managua" + ], + "isd": "+505" + }, + "Niger": { + "code": "ne", + "currency": "XOF", + "currency_name": "West African CFA Franc", + "currency_symbol": "CFA", + "currency_fraction": "Centime", + "currency_fraction_units": 100, + "number_format": "#,###.##", + "timezones": [ + "Africa/Niamey" + ], + "isd": "+227" + }, + "Nigeria": { + "code": "ng", + "currency": "NGN", + "currency_fraction": "Kobo", + "currency_fraction_units": 100, + "currency_name": "Naira", + "currency_symbol": "\u20a6", + "number_format": "#,###.##", + "timezones": [ + "Africa/Lagos" + ], + "isd": "+234" + }, + "Niue": { + "code": "nu", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_symbol": "$", + "number_format": "#,###.##", + "timezones": [ + "Pacific/Niue" + ], + "isd": "+683" + }, + "Norfolk Island": { + "code": "nf", + "number_format": "#,###.##", + "timezones": [ + "Pacific/Norfolk" + ], + "isd": "+672" + }, + "Northern Mariana Islands": { + "code": "mp", + "number_format": "#,###.##", + "timezones": [ + "Pacific/Saipan" + ], + "isd": "+1670" + }, + "Norway": { + "code": "no", + "currency": "NOK", + "currency_fraction": "\u00d8re", + "currency_fraction_units": 100, + "currency_name": "Norwegian Krone", + "currency_symbol": "kr", + "number_format": "#.###,##", + "timezones": [ + "Europe/Oslo" + ], + "isd": "+47" + }, + "Oman": { + "code": "om", + "currency": "OMR", + "currency_fraction": "Baisa", + "currency_fraction_units": 1000, + "currency_name": "Rial Omani", + "currency_symbol": "\u0631.\u0639.", + "number_format": "#,###.###", + "timezones": [ + "Asia/Muscat" + ], + "isd": "+968" + }, + "Pakistan": { + "code": "pk", + "currency": "PKR", + "currency_fraction": "Paisa", + "currency_fraction_units": 100, + "currency_name": "Pakistan Rupee", + "currency_symbol": "\u20a8", + "number_format": "#,###.##", + "timezones": [ + "Asia/Karachi" + ], + "isd": "+92" + }, + "Palau": { + "code": "pw", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_symbol": "$", + "date_format": "mm-dd-yyyy", + "number_format": "#,###.##", + "timezones": [ + "Pacific/Palau" + ], + "isd": "+680" + }, + "Palestinian Territory, Occupied": { + "code": "ps", + "number_format": "#,###.##", + "isd": "+970" + }, + "Panama": { + "code": "pa", + "currency_fraction": "Cent\u00e9simo", + "currency_fraction_units": 100, + "currency_symbol": "B/.", + "number_format": "#,###.##", + "timezones": [ + "America/Panama" + ], + "isd": "+507" + }, + "Papua New Guinea": { + "code": "pg", + "currency": "PGK", + "currency_fraction": "Toea", + "currency_fraction_units": 100, + "currency_name": "Kina", + "currency_symbol": "K", + "number_format": "#,###.##", + "timezones": [ + "Pacific/Port_Moresby" + ], + "isd": "+675" + }, + "Paraguay": { + "code": "py", + "currency": "PYG", + "currency_fraction": "C\u00e9ntimo", + "currency_fraction_units": 100, + "currency_name": "Guarani", + "currency_symbol": "\u20b2", + "number_format": "#,###.##", + "timezones": [ + "America/Asuncion" + ], + "isd": "+595" + }, + "Peru": { + "code": "pe", + "currency": "PEN", + "currency_fraction": "C\u00e9ntimo", + "currency_fraction_units": 100, + "currency_name": "Nuevo Sol", + "currency_symbol": "S/.", + "number_format": "#,###.##", + "timezones": [ + "America/Lima" + ], + "isd": "+51" + }, + "Philippines": { + "code": "ph", + "currency": "PHP", + "currency_fraction": "Centavo", + "currency_fraction_units": 100, + "currency_name": "Philippine Peso", + "currency_symbol": "\u20b1", + "date_format": "mm-dd-yyyy", + "number_format": "#,###.##", + "timezones": [ + "Asia/Manila" + ], + "isd": "+63" + }, + "Pitcairn": { + "code": "pn", + "number_format": "#,###.##", + "timezones": [ + "Pacific/Pitcairn" + ], + "isd": "+64" + }, + "Poland": { + "code": "pl", + "currency": "PLN", + "currency_fraction": "Grosz", + "currency_fraction_units": 100, + "currency_symbol": "z\u0142", + "number_format": "#.###,##", + "timezones": [ + "Europe/Warsaw" + ], + "isd": "+48" + }, + "Portugal": { + "code": "pt", + "currency": "EUR", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "smallest_currency_fraction_value": 0.01, + "currency_symbol": "\u20ac", + "number_format": "#,###.##", + "timezones": [ + "Atlantic/Azores", + "Atlantic/Madeira", + "Europe/Lisbon" + ], + "isd": "+351" + }, + "Puerto Rico": { + "code": "pr", + "number_format": "#,###.##", + "timezones": [ + "America/Puerto_Rico" + ], + "isd": "+1939" + }, + "Qatar": { + "code": "qa", + "currency": "QAR", + "currency_fraction": "Dirham", + "currency_fraction_units": 100, + "currency_name": "Qatari Rial", + "currency_symbol": "\u0631.\u0642", + "number_format": "#,###.##", + "timezones": [ + "Asia/Qatar" + ], + "isd": "+974" + }, + "Romania": { + "code": "ro", + "currency": "RON", + "currency_fraction": "Bani", + "currency_fraction_units": 100, + "currency_name": "Romanian New Leu", + "currency_symbol": "lei", + "number_format": "#,###.##", + "timezones": [ + "Europe/Bucharest" + ], + "isd": "+40" + }, + "Russian Federation": { + "code": "ru", + "currency": "RUB", + "currency_name": "Russian Ruble", + "number_format": "#.###,##", + "isd": "+7" + }, + "Rwanda": { + "code": "rw", + "currency": "RWF", + "currency_fraction": "Centime", + "currency_fraction_units": 100, + "currency_name": "Rwanda Franc", + "currency_symbol": "Fr", + "number_format": "#,###.##", + "timezones": [ + "Africa/Kigali" + ], + "isd": "+250" + }, + "R\u00e9union": { + "code": "re", + "number_format": "#,###.##", + "isd": "+262" + }, + "Saint Barth\u00e9lemy": { + "code": "bl", + "number_format": "#,###.##", + "isd": "+590" + }, + "Saint Helena, Ascension and Tristan da Cunha": { + "code": "sh", + "currency": "SHP", + "currency_name": "Saint Helena Pound", + "number_format": "#,###.##", + "isd": "+290" + }, + "Saint Kitts and Nevis": { + "code": "kn", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_symbol": "$", + "currency_name": "Eastern Carribean Dollar", + "currency": "XCD", + "number_format": "#,###.##", + "timezones": [ + "America/St_Kitts" + ], + "isd": "+1869" + }, + "Saint Lucia": { + "code": "lc", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_symbol": "$", + "currency_name": "Eastern Carribean Dollar", + "currency": "XCD", + "number_format": "#,###.##", + "timezones": [ + "America/St_Lucia" + ], + "isd": "+1758" + }, + "Saint Martin (French part)": { + "code": "mf", + "number_format": "#,###.##", + "isd": "+590" + }, + "Saint Pierre and Miquelon": { + "code": "pm", + "number_format": "#,###.##", + "isd": "+508" + }, + "Saint Vincent and the Grenadines": { + "code": "vc", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_symbol": "$", + "currency_name": "Eastern Carribean Dollar", + "currency": "XCD", + "number_format": "#,###.##", + "timezones": [ + "America/St_Vincent" + ], + "isd": "+1784" + }, + "Samoa": { + "code": "ws", + "currency": "WST", + "currency_fraction": "Sene", + "currency_fraction_units": 100, + "currency_name": "Tala", + "currency_symbol": "T", + "number_format": "#,###.##", + "timezones": [ + "Pacific/Apia" + ], + "isd": "+685" + }, + "San Marino": { + "code": "sm", + "currency": "EUR", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "smallest_currency_fraction_value": 0.01, + "currency_symbol": "\u20ac", + "number_format": "#,###.##", + "timezones": [ + "Europe/Rome" + ], + "isd": "+378" + }, + "Sao Tome and Principe": { + "code": "st", + "currency": "STD", + "currency_name": "Dobra", + "number_format": "#,###.##", + "isd": "+239" + }, + "Saudi Arabia": { + "code": "sa", + "currency": "SAR", + "currency_fraction": "Halala", + "currency_fraction_units": 100, + "currency_name": "Saudi Riyal", + "currency_symbol": "\u0631.\u0633", + "number_format": "#,###.##", + "timezones": [ + "Asia/Riyadh" + ], + "isd": "+966" + }, + "Senegal": { + "code": "sn", + "currency": "XOF", + "currency_name": "West African CFA Franc", + "currency_symbol": "CFA", + "currency_fraction": "Centime", + "currency_fraction_units": 100, + "number_format": "#,###.##", + "timezones": [ + "Africa/Dakar" + ], + "isd": "+221" + }, + "Serbia": { + "code": "rs", + "currency": "RSD", + "currency_fraction": "Para", + "currency_fraction_units": 100, + "currency_name": "Serbian Dinar", + "currency_symbol": "\u0434\u0438\u043d.", + "number_format": "#,###.##", + "timezones": [ + "Europe/Belgrade" + ], + "isd": "+381" + }, + "Seychelles": { + "code": "sc", + "currency": "SCR", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Seychelles Rupee", + "currency_symbol": "\u20a8", + "number_format": "#,###.##", + "timezones": [ + "Indian/Mahe" + ], + "isd": "+248" + }, + "Sierra Leone": { + "code": "sl", + "currency": "SLL", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Leone", + "currency_symbol": "Le", + "number_format": "#,###.##", + "timezones": [ + "Africa/Freetown" + ], + "isd": "+232" + }, + "Singapore": { + "code": "sg", + "currency": "SGD", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Singapore Dollar", + "currency_symbol": "$", + "number_format": "#,###.##", + "timezones": [ + "Asia/Singapore" + ], + "isd": "+65" + }, + "Sint Maarten (Dutch part)": { + "code": "sx", + "number_format": "#,###.##" + }, + "Slovakia": { + "code": "sk", + "currency": "EUR", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "smallest_currency_fraction_value": 0.01, + "currency_symbol": "\u20ac", + "number_format": "#,###.##", + "timezones": [ + "Europe/Bratislava" + ], + "isd": "+421" + }, + "Slovenia": { + "code": "si", + "currency": "EUR", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "smallest_currency_fraction_value": 0.01, + "currency_symbol": "\u20ac", + "number_format": "#,###.##", + "timezones": [ + "Europe/Belgrade" + ], + "isd": "+386" + }, + "Solomon Islands": { + "code": "sb", + "currency": "SBD", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Solomon Islands Dollar", + "currency_symbol": "$", + "number_format": "#,###.##", + "timezones": [ + "Pacific/Guadalcanal" + ], + "isd": "+677" + }, + "Somalia": { + "code": "so", + "currency": "SOS", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Somali Shilling", + "currency_symbol": "Sh", + "number_format": "#,###.##", + "timezones": [ + "Africa/Mogadishu" + ], + "isd": "+252" + }, + "South Africa": { + "code": "za", + "currency": "ZAR", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Rand", + "currency_symbol": "R", + "date_format": "yyyy-mm-dd", + "number_format": "# ###.##", + "timezones": [ + "Africa/Johannesburg" + ], + "isd": "+27" + }, + "South Georgia and the South Sandwich Islands": { + "code": "gs", + "currency_fraction": "Penny", + "currency_fraction_units": 100, + "currency_symbol": "\u00a3", + "number_format": "#,###.##", + "isd": "+500" + }, + "South Sudan": { + "code": "ss", + "currency_fraction": "Piastre", + "currency_fraction_units": 100, + "currency_symbol": "\u00a3", + "number_format": "#,###.##", + "timezones": [ + "Africa/Juba" + ], + "isd": "+211" + }, + "Spain": { + "code": "es", + "currency": "EUR", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "smallest_currency_fraction_value": 0.01, + "currency_symbol": "\u20ac", + "number_format": "#,###.##", + "timezones": [ + "Africa/Ceuta", + "Atlantic/Canary", + "Europe/Madrid" + ], + "isd": "+34" + }, + "Sri Lanka": { + "code": "lk", + "currency": "LKR", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Sri Lanka Rupee", + "currency_symbol": "Rs", + "number_format": "#,###.##", + "timezones": [ + "Asia/Colombo" + ], + "isd": "+94" + }, + "Sudan": { + "code": "sd", + "currency_fraction": "Piastre", + "currency_fraction_units": 100, + "currency_symbol": "\u00a3", + "number_format": "#,###.##", + "timezones": [ + "Africa/Khartoum" + ], + "isd": "+249" + }, + "Suriname": { + "code": "sr", + "currency": "SRD", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_symbol": "$", + "number_format": "#,###.##", + "timezones": [ + "America/Paramaribo" + ], + "isd": "+597" + }, + "Svalbard and Jan Mayen": { + "code": "sj", + "number_format": "#,###.##", + "isd": "+47" + }, + "Swaziland": { + "code": "sz", + "currency": "SZL", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Lilangeni", + "currency_symbol": "L", + "number_format": "#, ###.##", + "timezones": [ + "Africa/Mbabane" + ], + "isd": "+268" + }, + "Sweden": { + "code": "se", + "currency": "SEK", + "currency_fraction": "\u00d6re", + "currency_fraction_units": 100, + "currency_name": "Swedish Krona", + "currency_symbol": "kr", + "number_format": "#.###,##", + "timezones": [ + "Europe/Stockholm" + ], + "isd": "+46" + }, + "Switzerland": { + "code": "ch", + "currency": "CHF", + "currency_fraction": "Rappen[K]", + "currency_fraction_units": 100, + "smallest_currency_fraction_value": 0.05, + "currency_name": "Swiss Franc", + "currency_symbol": "Fr", + "number_format": "#'###.##", + "timezones": [ + "Europe/Zurich" + ], + "isd": "+41" + }, + "Syria": { + "code": "sy", + "currency": "SYP", + "currency_name": "Syrian Pound", + "number_format": "#,###.##", + "isd": "+963" + }, + "Taiwan": { + "code": "tw", + "currency": "TWD", + "date_format": "yyyy-mm-dd", + "number_format": "#,###.##", + "isd": "+886" + }, + "Tajikistan": { + "code": "tj", + "currency_fraction": "Diram", + "currency_fraction_units": 100, + "currency_symbol": "\u0405\u041c", + "number_format": "#,###.##", + "timezones": [ + "Asia/Dushanbe" + ], + "isd": "+992" + }, + "Tanzania": { + "code": "tz", + "currency": "TZS", + "currency_name": "Tanzanian Shilling", + "number_format": "#,###.##", + "isd": "+255" + }, + "Thailand": { + "code": "th", + "currency": "THB", + "currency_fraction": "Satang", + "currency_fraction_units": 100, + "currency_name": "Baht", + "currency_symbol": "\u0e3f", + "number_format": "#,###.##", + "timezones": [ + "Asia/Bangkok" + ], + "isd": "+66" + }, + "Timor-Leste": { + "code": "tl", + "number_format": "#,###.##", + "isd": "+670" + }, + "Togo": { + "code": "tg", + "currency": "XOF", + "currency_name": "West African CFA Franc", + "currency_symbol": "CFA", + "currency_fraction": "Centime", + "currency_fraction_units": 100, + "number_format": "#,###.##", + "timezones": [ + "Africa/Lome" + ], + "isd": "+228" + }, + "Tokelau": { + "code": "tk", + "number_format": "#,###.##", + "timezones": [ + "Pacific/Fakaofo" + ], + "isd": "+690" + }, + "Tonga": { + "code": "to", + "currency": "TOP", + "currency_fraction": "Seniti[L]", + "currency_fraction_units": 100, + "currency_name": "Pa'anga", + "currency_symbol": "T$", + "number_format": "#,###.##", + "timezones": [ + "Pacific/Tongatapu" + ], + "isd": "+676" + }, + "Trinidad and Tobago": { + "code": "tt", + "currency": "TTD", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Trinidad and Tobago Dollar", + "currency_symbol": "$", + "number_format": "#,###.##", + "timezones": [ + "America/Port_of_Spain" + ], + "isd": "+1868" + }, + "Tunisia": { + "code": "tn", + "currency": "TND", + "currency_fraction": "Millime", + "currency_fraction_units": 1000, + "currency_name": "Tunisian Dinar", + "currency_symbol": "\u062f.\u062a", + "number_format": "#,###.###", + "timezones": [ + "Africa/Tunis" + ], + "isd": "+216" + }, + "Turkey": { + "code": "tr", + "currency": "TRY", + "currency_fraction": "Kuru\u015f", + "currency_fraction_units": 100, + "currency_symbol": "\u20ba", + "number_format": "#.###,##", + "timezones": [ + "Europe/Istanbul" + ], + "isd": "+90" + }, + "Turkmenistan": { + "code": "tm", + "currency": "TMM", + "currency_fraction": "Tennesi", + "currency_fraction_units": 100, + "currency_name": "Manat", + "currency_symbol": "m", + "number_format": "#,###.##", + "timezones": [ + "Asia/Ashgabat" + ], + "isd": "+993" + }, + "Turks and Caicos Islands": { + "code": "tc", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_symbol": "$", + "number_format": "#,###.##", + "isd": "+1649" + }, + "Tuvalu": { + "code": "tv", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_symbol": "$", + "number_format": "#,###.##", + "timezones": [ + "Pacific/Funafuti" + ], + "isd": "+688" + }, + "Uganda": { + "code": "ug", + "currency": "UGX", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Uganda Shilling", + "currency_symbol": "Sh", + "number_format": "#,###.##", + "timezones": [ + "Africa/Kampala" + ], + "isd": "+256" + }, + "Ukraine": { + "code": "ua", + "currency": "UAH", + "currency_fraction": "Kopiyka", + "currency_fraction_units": 100, + "currency_name": "Ukrainian Hryvnia", + "currency_symbol": "\u20b4", + "number_format": "#,###.##", + "timezones": [ + "Europe/Kiev", + "Europe/Simferopol", + "Europe/Uzhgorod", + "Europe/Zaporozhye" + ], + "isd": "+380" + }, + "United Arab Emirates": { + "code": "ae", + "currency": "AED", + "currency_fraction": "Fils", + "currency_fraction_units": 100, + "currency_name": "UAE Dirham", + "currency_symbol": "\u062f.\u0625", + "number_format": "#,###.##", + "timezones": [ + "Asia/Dubai" + ], + "isd": "+971" + }, + "United Kingdom": { + "code": "gb", + "currency": "GBP", + "currency_fraction": "Penny", + "currency_fraction_units": 100, + "currency_name": "Pound Sterling", + "currency_symbol": "\u00a3", + "number_format": "#,###.##", + "timezones": [ + "Europe/London" + ], + "isd": "+44" + }, + "United States": { + "code": "us", + "currency": "USD", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "smallest_currency_fraction_value": 0.01, + "currency_name": "US Dollar", + "currency_symbol": "$", + "date_format": "mm-dd-yyyy", + "number_format": "#,###.##", + "timezones": [ + "America/Adak", + "America/Anchorage", + "America/Boise", + "America/Chicago", + "America/Denver", + "America/Detroit", + "America/Indiana/Indianapolis", + "America/Indiana/Knox", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Tell_City", + "America/Indiana/Vevay", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Juneau", + "America/Kentucky/Louisville", + "America/Kentucky/Monticello", + "America/Los_Angeles", + "America/Menominee", + "America/Metlakatla", + "America/New_York", + "America/Nome", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Phoenix", + "America/Denver", + "America/Sitka", + "America/Yakutat", + "Pacific/Honolulu" + ], + "isd": "+1" + }, + "United States Minor Outlying Islands": { + "code": "um", + "number_format": "#,###.##" + }, + "Uruguay": { + "code": "uy", + "currency": "UYU", + "currency_fraction": "Cent\u00e9simo", + "currency_fraction_units": 100, + "currency_name": "Peso Uruguayo", + "currency_symbol": "$", + "number_format": "#.###,##", + "timezones": [ + "America/Montevideo" + ], + "isd": "+598" + }, + "Uzbekistan": { + "code": "uz", + "currency": "UZS", + "currency_fraction": "Tiyin", + "currency_fraction_units": 100, + "currency_name": "Uzbekistan Sum", + "currency_symbol": "\u043b\u0432", + "number_format": "#,###.##", + "timezones": [ + "Asia/Samarkand", + "Asia/Tashkent" + ], + "isd": "+998" + }, + "Vanuatu": { + "code": "vu", + "currency": "VUV", + "currency_fraction": "None", + "currency_fraction_units": 0, + "currency_name": "Vatu", + "currency_symbol": "Vt", + "number_format": "#,###", + "timezones": [ + "Pacific/Efate" + ], + "isd": "+678" + }, + "Venezuela, Bolivarian Republic of": { + "code": "ve", + "number_format": "#.###,##", + "currency": "VEF", + "currency_symbol": "Bs.", + "currency_fraction": "Centimos", + "currency_fraction_units": 100, + "isd": "+58" + }, + "Vietnam": { + "code": "vn", + "currency": "VND", + "currency_name": "Dong", + "number_format": "#.###", + "isd": "+84" + }, + "Virgin Islands, British": { + "code": "vg", + "number_format": "#,###.##", + "isd": "+1284" + }, + "Virgin Islands, U.S.": { + "code": "vi", + "number_format": "#,###.##", + "isd": "+1340" + }, + "Wallis and Futuna": { + "code": "wf", + "currency_fraction": "Centime", + "currency_fraction_units": 100, + "currency_symbol": "Fr", + "number_format": "#,###.##", + "isd": "+681" + }, + "Western Sahara": { + "code": "eh", + "number_format": "#,###.##", + "timezones": [ + "Africa/El_Aaiun" + ] + }, + "Yemen": { + "code": "ye", + "currency": "YER", + "currency_fraction": "Fils", + "currency_fraction_units": 100, + "smallest_currency_fraction_value": 0.01, + "currency_name": "Yemeni Rial", + "currency_symbol": "\ufdfc", + "number_format": "#,###.##", + "timezones": [ + "Asia/Aden" + ], + "isd": "+967" + }, + "Zambia": { + "code": "zm", + "currency": "ZMW", + "currency_fraction": "Ngwee", + "currency_fraction_units": 100, + "currency_name": "Zambian Kwacha", + "currency_symbol": "ZK", + "number_format": "#,###.##", + "timezones": [ + "Africa/Lusaka" + ], + "isd": "+260" + }, + "Zimbabwe": { + "code": "zw", + "currency": "ZWL", + "currency_fraction": "Cent", + "currency_fraction_units": 100, + "currency_name": "Zimbabwe Dollar", + "currency_symbol": "ZWL$", + "number_format": "# ###.##", + "timezones": [ + "Africa/Harare" + ], + "isd": "+263" + }, + "\u00c5land Islands": { + "code": "ax", + "number_format": "#,###.##", + "isd": "+358" + } +} diff --git a/influxframework/geo/country_info.py b/influxframework/geo/country_info.py new file mode 100644 index 0000000..507ddcf --- /dev/null +++ b/influxframework/geo/country_info.py @@ -0,0 +1,74 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +# all country info +import os + +import influxframework +from influxframework.utils.momentjs import get_all_timezones + + +def get_country_info(country=None): + data = get_all() + data = influxframework._dict(data.get(country, {})) + if "date_format" not in data: + data.date_format = "dd-mm-yyyy" + if "time_format" not in data: + data.time_format = "HH:mm:ss" + + return data + + +def get_all(): + with open(os.path.join(os.path.dirname(__file__), "country_info.json")) as local_info: + all_data = json.loads(local_info.read()) + return all_data + + +@influxframework.whitelist() +def get_country_timezone_info(): + return {"country_info": get_all(), "all_timezones": get_all_timezones()} + + +def get_translated_dict(): + from babel.dates import Locale, get_timezone, get_timezone_name + + translated_dict = {} + locale = Locale.parse(influxframework.local.lang, sep="-") + + # timezones + for tz in get_all_timezones(): + timezone_name = get_timezone_name(get_timezone(tz), locale=locale, width="short") + if timezone_name: + translated_dict[tz] = timezone_name + " - " + tz + + # country names && currencies + for country, info in get_all().items(): + country_name = locale.territories.get((info.get("code") or "").upper()) + if country_name: + translated_dict[country] = country_name + + currency = info.get("currency") + currency_name = locale.currencies.get(currency) + if currency_name: + translated_dict[currency] = currency_name + + return translated_dict + + +def update(): + with open(os.path.join(os.path.dirname(__file__), "currency_info.json")) as nformats: + nformats = json.loads(nformats.read()) + + all_data = get_all() + + for country in all_data: + data = all_data[country] + data["number_format"] = nformats.get(data.get("currency", "default"), nformats.get("default"))[ + "display" + ] + + with open(os.path.join(os.path.dirname(__file__), "country_info.json"), "w") as local_info: + local_info.write(json.dumps(all_data, indent=1)) diff --git a/influxframework/geo/doctype/__init__.py b/influxframework/geo/doctype/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/geo/doctype/country/README.md b/influxframework/geo/doctype/country/README.md new file mode 100644 index 0000000..0e3f46c --- /dev/null +++ b/influxframework/geo/doctype/country/README.md @@ -0,0 +1 @@ +Country Master. \ No newline at end of file diff --git a/influxframework/geo/doctype/country/__init__.py b/influxframework/geo/doctype/country/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/geo/doctype/country/country.js b/influxframework/geo/doctype/country/country.js new file mode 100644 index 0000000..c6c0f57 --- /dev/null +++ b/influxframework/geo/doctype/country/country.js @@ -0,0 +1,6 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Country", { + refresh: function (frm) {}, +}); diff --git a/influxframework/geo/doctype/country/country.json b/influxframework/geo/doctype/country/country.json new file mode 100644 index 0000000..2cbd0a2 --- /dev/null +++ b/influxframework/geo/doctype/country/country.json @@ -0,0 +1,90 @@ +{ + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:country_name", + "creation": "2013-01-19 10:23:30", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "country_name", + "date_format", + "time_format", + "time_zones", + "code" + ], + "fields": [ + { + "fieldname": "country_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Country Name", + "oldfieldname": "country_name", + "oldfieldtype": "Data", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "date_format", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Date Format" + }, + { + "default": "HH:mm:ss", + "fieldname": "time_format", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Time format" + }, + { + "fieldname": "time_zones", + "fieldtype": "Text", + "in_list_view": 1, + "label": "Time Zones" + }, + { + "fieldname": "code", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Code" + } + ], + "icon": "fa fa-globe", + "idx": 1, + "links": [], + "modified": "2022-08-05 18:33:27.880783", + "modified_by": "Administrator", + "module": "Geo", + "name": "Country", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 1, + "share": 1, + "write": 1 + }, + { + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All" + } + ], + "quick_entry": 1, + "sort_field": "country_name", + "sort_order": "ASC", + "states": [], + "track_changes": 1, + "translated_doctype": 1 +} \ No newline at end of file diff --git a/influxframework/geo/doctype/country/country.py b/influxframework/geo/doctype/country/country.py new file mode 100644 index 0000000..8385d0d --- /dev/null +++ b/influxframework/geo/doctype/country/country.py @@ -0,0 +1,8 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE + +from influxframework.model.document import Document + + +class Country(Document): + pass diff --git a/influxframework/geo/doctype/country/test_country.py b/influxframework/geo/doctype/country/test_country.py new file mode 100644 index 0000000..b764a1a --- /dev/null +++ b/influxframework/geo/doctype/country/test_country.py @@ -0,0 +1,6 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework + +test_records = influxframework.get_test_records("Country") diff --git a/influxframework/geo/doctype/country/test_records.json b/influxframework/geo/doctype/country/test_records.json new file mode 100644 index 0000000..5a7c8a5 --- /dev/null +++ b/influxframework/geo/doctype/country/test_records.json @@ -0,0 +1,6 @@ +[ + { + "country_name": "_Test Country", + "doctype": "Country" + } +] \ No newline at end of file diff --git a/influxframework/geo/doctype/currency/README.md b/influxframework/geo/doctype/currency/README.md new file mode 100644 index 0000000..3e1558e --- /dev/null +++ b/influxframework/geo/doctype/currency/README.md @@ -0,0 +1 @@ +Currency Master with details about abbreviation, symbol etc. \ No newline at end of file diff --git a/influxframework/geo/doctype/currency/__init__.py b/influxframework/geo/doctype/currency/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/geo/doctype/currency/currency.js b/influxframework/geo/doctype/currency/currency.js new file mode 100644 index 0000000..8012d90 --- /dev/null +++ b/influxframework/geo/doctype/currency/currency.js @@ -0,0 +1,11 @@ +// Copyright (c) 2015, InfluxFramework LLC +// License: See license.txt + +influxframework.ui.form.on("Currency", { + refresh(frm) { + frm.set_intro(""); + if (!frm.doc.enabled) { + frm.set_intro(__("This Currency is disabled. Enable to use in transactions")); + } + }, +}); diff --git a/influxframework/geo/doctype/currency/currency.json b/influxframework/geo/doctype/currency/currency.json new file mode 100644 index 0000000..c51ab7f --- /dev/null +++ b/influxframework/geo/doctype/currency/currency.json @@ -0,0 +1,122 @@ +{ + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:currency_name", + "creation": "2013-01-28 10:06:02", + "description": "**Currency** Master", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "currency_name", + "enabled", + "fraction", + "fraction_units", + "smallest_currency_fraction_value", + "symbol", + "symbol_on_right", + "number_format" + ], + "fields": [ + { + "fieldname": "currency_name", + "fieldtype": "Data", + "label": "Currency Name", + "oldfieldname": "currency_name", + "oldfieldtype": "Data", + "reqd": 1, + "unique": 1 + }, + { + "default": "0", + "fieldname": "enabled", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Enabled" + }, + { + "description": "Sub-currency. For e.g. \"Cent\"", + "fieldname": "fraction", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Fraction" + }, + { + "description": "1 Currency = [?] Fraction\nFor e.g. 1 USD = 100 Cent", + "fieldname": "fraction_units", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Fraction Units" + }, + { + "description": "Smallest circulating fraction unit (coin). For e.g. 1 cent for USD and it should be entered as 0.01", + "fieldname": "smallest_currency_fraction_value", + "fieldtype": "Currency", + "label": "Smallest Currency Fraction Value", + "non_negative": 1 + }, + { + "description": "A symbol for this currency. For e.g. $", + "fieldname": "symbol", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Symbol" + }, + { + "description": "How should this currency be formatted? If not set, will use system defaults", + "fieldname": "number_format", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Number Format", + "options": "\n#,###.##\n#.###,##\n# ###.##\n# ###,##\n#'###.##\n#, ###.##\n#,##,###.##\n#,###.###\n#.###\n#,###" + }, + { + "default": "0", + "fieldname": "symbol_on_right", + "fieldtype": "Check", + "label": "Show Currency Symbol on Right Side" + } + ], + "icon": "fa fa-bitcoin", + "idx": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2022-07-04 09:42:52.425440", + "modified_by": "Administrator", + "module": "Geo", + "name": "Currency", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "read": 1, + "role": "Accounts User" + }, + { + "read": 1, + "role": "Sales User" + }, + { + "read": 1, + "role": "Purchase User" + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/geo/doctype/currency/currency.py b/influxframework/geo/doctype/currency/currency.py new file mode 100644 index 0000000..9f7e3f4 --- /dev/null +++ b/influxframework/geo/doctype/currency/currency.py @@ -0,0 +1,11 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document + + +class Currency(Document): + def validate(self): + if not influxframework.flags.in_install_app: + influxframework.clear_cache() diff --git a/influxframework/geo/doctype/currency/test_currency.py b/influxframework/geo/doctype/currency/test_currency.py new file mode 100644 index 0000000..a6803e0 --- /dev/null +++ b/influxframework/geo/doctype/currency/test_currency.py @@ -0,0 +1,13 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +# pre loaded + +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestUser(InfluxFrameworkTestCase): + def test_default_currency_on_setup(self): + usd = influxframework.get_doc("Currency", "USD") + self.assertDocumentEqual({"enabled": 1, "fraction": "Cent"}, usd) diff --git a/influxframework/geo/doctype/currency/test_records.json b/influxframework/geo/doctype/currency/test_records.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/influxframework/geo/doctype/currency/test_records.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/influxframework/geo/languages.json b/influxframework/geo/languages.json new file mode 100644 index 0000000..c0f0902 --- /dev/null +++ b/influxframework/geo/languages.json @@ -0,0 +1,326 @@ +[ + { + "code": "af", + "name": "Afrikaans" + }, + { + "code": "am", + "name": "\u12a0\u121b\u122d\u129b" + }, + { + "code": "ar", + "name": "\u0627\u0644\u0639\u0631\u0628\u064a\u0629" + }, + { + "code": "bg", + "name": "B\u01celgarski" + }, + { + "code": "bn", + "name": "\u09ac\u09be\u0999\u09be\u09b2\u09bf" + }, + { + "code": "bo", + "name": "\u0f63\u0fb7\u0f0b\u0f66\u0f60\u0f72\u0f0b\u0f66\u0f90\u0f51\u0f0b" + }, + { + "code": "bs", + "name": "Bosanski" + }, + { + "code": "ca", + "name": "Catal\u00e0" + }, + { + "code": "cs", + "name": "\u010desky" + }, + { + "code": "da", + "name": "Dansk" + }, + { + "code": "da-DK", + "name": "Dansk (Danmark)" + }, + { + "code": "de", + "name": "Deutsch" + }, + { + "code": "el", + "name": "\u03b5\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac" + }, + { + "code": "en", + "name": "English" + }, + { + "code": "en-GB", + "name": "English (United Kingdom)" + }, + { + "code": "en-US", + "name": "English (United States)" + }, + { + "code": "es", + "name": "Espa\u00f1ol" + }, + { + "code": "es-AR", + "name": "Espa\u00f1ol (Argentina)" + }, + { + "code": "es-BO", + "name": "Espa\u00f1ol (Bolivia)" + }, + { + "code": "es-CL", + "name": "Espa\u00f1ol (Chile)" + }, + { + "code": "es-CO", + "name": "Espa\u00f1ol (Colombia)" + }, + { + "code": "es-DO", + "name": "Espa\u00f1ol (Rep\u00fablica Dominicana)" + }, + { + "code": "es-EC", + "name": "Espa\u00f1ol (Ecuador)" + }, + { + "code": "es-GT", + "name": "Espa\u00f1ol (Guatemala)" + }, + { + "code": "es-MX", + "name": "Espa\u00f1ol (M\u00e9xico)" + }, + { + "code": "es-NI", + "name": "Espa\u00f1ol (Nicaragua)" + }, + { + "code": "es-PE", + "name": "Espa\u00f1ol (Per\u00fa)" + }, + { + "code": "et", + "name": "Eesti" + }, + { + "code": "fa", + "name": "\u067e\u0627\u0631\u0633\u06cc" + }, + { + "code": "fi", + "name": "Suomi" + }, + { + "code": "fil", + "name": "Filipino" + }, + { + "code": "fr", + "name": "Fran\u00e7ais" + }, + { + "code": "fr-CA", + "name": "Fran\u00e7ais Canadien" + }, + { + "code": "gu", + "name": "\u0a97\u0ac1\u0a9c\u0ab0\u0abe\u0aa4\u0ac0" + }, + { + "code": "he", + "name": "\u05e2\u05d1\u05e8\u05d9\u05ea" + }, + { + "code": "hi", + "name": "\u0939\u093f\u0902\u0926\u0940" + }, + { + "code": "hr", + "name": "Hrvatski" + }, + { + "code": "hu", + "name": "Magyar" + }, + { + "code": "id", + "name": "Indonesia" + }, + { + "code": "is", + "name": "\u00edslenska" + }, + { + "code": "it", + "name": "Italiano" + }, + { + "code": "ja", + "name": "\u65e5\u672c\u8a9e" + }, + { + "code": "km", + "name": "\u1797\u17b6\u179f\u17b6\u1781\u17d2\u1798\u17c2\u179a" + }, + { + "code": "kn", + "name": "\u0c95\u0ca8\u0ccd\u0ca8\u0ca1" + }, + { + "code": "ko", + "name": "\ud55c\uad6d\uc758" + }, + { + "code": "ku", + "name": "\u06a9\u0648\u0631\u062f\u06cc" + }, + { + "code": "lo", + "name": "\u0ea5\u0eb2\u0ea7" + }, + { + "code": "lt", + "name": "Lietuvi\u0173 kalba" + }, + { + "code": "lv", + "name": "Latvie\u0161u valoda" + }, + { + "code": "mk", + "name": "\u043c\u0430\u043a\u0435\u0434\u043e\u043d\u0441\u043a\u0438" + }, + { + "code": "ml", + "name": "\u0d2e\u0d32\u0d2f\u0d3e\u0d33\u0d02" + }, + { + "code": "mr", + "name": "\u092e\u0930\u093e\u0920\u0940" + }, + { + "code": "ms", + "name": "Melayu" + }, + { + "code": "my", + "name": "\u1019\u103c\u1014\u103a\u1019\u102c" + }, + { + "code": "nl", + "name": "Nederlands" + }, + { + "code": "no", + "name": "Norsk" + }, + { + "code": "pl", + "name": "Polski" + }, + { + "code": "ps", + "name": "\u067e\u069a\u062a\u0648" + }, + { + "code": "pt", + "name": "Portugu\u00eas" + }, + { + "code": "pt-BR", + "name": "Portugu\u00eas Brasileiro" + }, + { + "code": "ro", + "name": "Rom\u00e2n" + }, + { + "code": "ru", + "name": "\u0440\u0443\u0441\u0441\u043a\u0438\u0439" + }, + { + "code": "rw", + "name": "Kinyarwanda" + }, + { + "code": "si", + "name": "\u0dc3\u0dd2\u0d82\u0dc4\u0dbd" + }, + { + "code": "sk", + "name": "Sloven\u010dina (Slovak)" + }, + { + "code": "sl", + "name": "Sloven\u0161\u010dina (Slovene)" + }, + { + "code": "sq", + "name": "Shqiptar" + }, + { + "code": "sr", + "name": "\u0441\u0440\u043f\u0441\u043a\u0438" + }, + { + "code": "sr-BA", + "name": "Srpski" + }, + { + "code": "sv", + "name": "Svenska" + }, + { + "code": "sw", + "name": "Swahili" + }, + { + "code": "ta", + "name": "\u0ba4\u0bae\u0bbf\u0bb4\u0bcd" + }, + { + "code": "te", + "name": "\u0c24\u0c46\u0c32\u0c41\u0c17\u0c41" + }, + { + "code": "th", + "name": "\u0e44\u0e17\u0e22" + }, + { + "code": "tr", + "name": "Türkçe" + }, + { + "code": "uk", + "name": "\u0443\u043a\u0440\u0430\u0457\u043d\u0441\u044c\u043a\u0430" + }, + { + "code": "ur", + "name": "\u0627\u0631\u062f\u0648" + }, + { + "code": "uz", + "name": "\u040e\u0437\u0431\u0435\u043a" + }, + { + "code": "vi", + "name": "Vi\u1ec7t" + }, + { + "code": "zh", + "name": "\u7b80\u4f53\u4e2d\u6587" + }, + { + "code": "zh-TW", + "name": "\u7e41\u9ad4\u4e2d\u6587" + } +] diff --git a/influxframework/geo/report/__init__.py b/influxframework/geo/report/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/geo/utils.py b/influxframework/geo/utils.py new file mode 100644 index 0000000..a300dc4 --- /dev/null +++ b/influxframework/geo/utils.py @@ -0,0 +1,100 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework + + +@influxframework.whitelist() +def get_coords(doctype, filters, type): + """Get a geojson dict representing a doctype.""" + filters_sql = get_coords_conditions(doctype, filters)[4:] + + coords = None + if type == "location_field": + coords = return_location(doctype, filters_sql) + elif type == "coordinates": + coords = return_coordinates(doctype, filters_sql) + + out = convert_to_geojson(type, coords) + return out + + +def convert_to_geojson(type, coords): + """Converts GPS coordinates to geoJSON string.""" + geojson = {"type": "FeatureCollection", "features": None} + + if type == "location_field": + geojson["features"] = merge_location_features_in_one(coords) + elif type == "coordinates": + geojson["features"] = create_gps_markers(coords) + + return geojson + + +def merge_location_features_in_one(coords): + """Merging all features from location field.""" + geojson_dict = [] + for element in coords: + geojson_loc = influxframework.parse_json(element["location"]) + if not geojson_loc: + continue + for coord in geojson_loc["features"]: + coord["properties"]["name"] = element["name"] + geojson_dict.append(coord.copy()) + + return geojson_dict + + +def create_gps_markers(coords): + """Build Marker based on latitude and longitude.""" + geojson_dict = [] + for i in coords: + node = {"type": "Feature", "properties": {}, "geometry": {"type": "Point", "coordinates": None}} + node["properties"]["name"] = i.name + node["geometry"]["coordinates"] = [i.latitude, i.longitude] + geojson_dict.append(node.copy()) + + return geojson_dict + + +def return_location(doctype, filters_sql): + """Get name and location fields for Doctype.""" + if filters_sql: + try: + coords = influxframework.db.sql( + f"""SELECT name, location FROM `tab{doctype}` WHERE {filters_sql}""", as_dict=True + ) + except influxframework.db.InternalError: + influxframework.msgprint(influxframework._("This Doctype does not contain location fields"), raise_exception=True) + return + else: + coords = influxframework.get_all(doctype, fields=["name", "location"]) + return coords + + +def return_coordinates(doctype, filters_sql): + """Get name, latitude and longitude fields for Doctype.""" + if filters_sql: + try: + coords = influxframework.db.sql( + f"""SELECT name, latitude, longitude FROM `tab{doctype}` WHERE {filters_sql}""", + as_dict=True, + ) + except influxframework.db.InternalError: + influxframework.msgprint( + influxframework._("This Doctype does not contain latitude and longitude fields"), raise_exception=True + ) + return + else: + coords = influxframework.get_all(doctype, fields=["name", "latitude", "longitude"]) + return coords + + +def get_coords_conditions(doctype, filters=None): + """Returns SQL conditions with user permissions and filters for event queries.""" + from influxframework.desk.reportview import get_filters_cond + + if not influxframework.has_permission(doctype): + influxframework.throw(influxframework._("Not Permitted"), influxframework.PermissionError) + + return get_filters_cond(doctype, filters, [], with_match_conditions=True) diff --git a/influxframework/handler.py b/influxframework/handler.py new file mode 100644 index 0000000..b63cb58 --- /dev/null +++ b/influxframework/handler.py @@ -0,0 +1,325 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE + +import os +from mimetypes import guess_type +from typing import TYPE_CHECKING + +from werkzeug.wrappers import Response + +import influxframework +import influxframework.sessions +import influxframework.utils +from influxframework import _, is_whitelisted +from influxframework.core.doctype.server_script.server_script_utils import get_server_script_map +from influxframework.utils import cint +from influxframework.utils.csvutils import build_csv_response +from influxframework.utils.image import optimize_image +from influxframework.utils.response import build_response + +if TYPE_CHECKING: + from influxframework.core.doctype.file.file import File + from influxframework.core.doctype.user.user import User + +ALLOWED_MIMETYPES = ( + "image/png", + "image/jpeg", + "application/pdf", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.oasis.opendocument.text", + "application/vnd.oasis.opendocument.spreadsheet", + "text/plain", +) + + +def handle(): + """handle request""" + + cmd = influxframework.local.form_dict.cmd + data = None + + if cmd != "login": + data = execute_cmd(cmd) + + # data can be an empty string or list which are valid responses + if data is not None: + if isinstance(data, Response): + # method returns a response object, pass it on + return data + + # add the response to `message` label + influxframework.response["message"] = data + + return build_response("json") + + +def execute_cmd(cmd, from_async=False): + """execute a request as python module""" + for hook in influxframework.get_hooks("override_whitelisted_methods", {}).get(cmd, []): + # override using the first hook + cmd = hook + break + + # via server script + server_script = get_server_script_map().get("_api", {}).get(cmd) + if server_script: + return run_server_script(server_script) + + try: + method = get_attr(cmd) + except Exception as e: + influxframework.throw(_("Failed to get method for command {0} with {1}").format(cmd, e)) + + if from_async: + method = method.queue + + if method != run_doc_method: + is_whitelisted(method) + is_valid_http_method(method) + + return influxframework.call(method, **influxframework.form_dict) + + +def run_server_script(server_script): + response = influxframework.get_doc("Server Script", server_script).execute_method() + + # some server scripts return output using flags (empty dict by default), + # while others directly modify influxframework.response + # return flags if not empty dict (this overwrites influxframework.response.message) + if response != {}: + return response + + +def is_valid_http_method(method): + if influxframework.flags.in_safe_exec: + return + + http_method = influxframework.local.request.method + + if http_method not in influxframework.allowed_http_methods_for_whitelisted_func[method]: + throw_permission_error() + + +def throw_permission_error(): + influxframework.throw(_("Not permitted"), influxframework.PermissionError) + + +@influxframework.whitelist(allow_guest=True) +def version(): + return influxframework.__version__ + + +@influxframework.whitelist(allow_guest=True) +def logout(): + influxframework.local.login_manager.logout() + influxframework.db.commit() + + +@influxframework.whitelist(allow_guest=True) +def web_logout(): + influxframework.local.login_manager.logout() + influxframework.db.commit() + influxframework.respond_as_web_page( + _("Logged Out"), _("You have been successfully logged out"), indicator_color="green" + ) + + +@influxframework.whitelist() +def uploadfile(): + ret = None + + try: + if influxframework.form_dict.get("from_form"): + try: + ret = influxframework.get_doc( + { + "doctype": "File", + "attached_to_name": influxframework.form_dict.docname, + "attached_to_doctype": influxframework.form_dict.doctype, + "attached_to_field": influxframework.form_dict.docfield, + "file_url": influxframework.form_dict.file_url, + "file_name": influxframework.form_dict.filename, + "is_private": influxframework.utils.cint(influxframework.form_dict.is_private), + "content": influxframework.form_dict.filedata, + "decode": True, + } + ) + ret.save() + except influxframework.DuplicateEntryError: + # ignore pass + ret = None + influxframework.db.rollback() + else: + if influxframework.form_dict.get("method"): + method = influxframework.get_attr(influxframework.form_dict.method) + is_whitelisted(method) + ret = method() + except Exception: + influxframework.errprint(influxframework.utils.get_traceback()) + influxframework.response["http_status_code"] = 500 + ret = None + + return ret + + +@influxframework.whitelist(allow_guest=True) +def upload_file(): + user = None + if influxframework.session.user == "Guest": + if influxframework.get_system_settings("allow_guests_to_upload_files"): + ignore_permissions = True + else: + raise influxframework.PermissionError + else: + user: "User" = influxframework.get_doc("User", influxframework.session.user) + ignore_permissions = False + + files = influxframework.request.files + is_private = influxframework.form_dict.is_private + doctype = influxframework.form_dict.doctype + docname = influxframework.form_dict.docname + fieldname = influxframework.form_dict.fieldname + file_url = influxframework.form_dict.file_url + folder = influxframework.form_dict.folder or "Home" + method = influxframework.form_dict.method + filename = influxframework.form_dict.file_name + optimize = influxframework.form_dict.optimize + content = None + + if "file" in files: + file = files["file"] + content = file.stream.read() + filename = file.filename + + content_type = guess_type(filename)[0] + if optimize and content_type.startswith("image/"): + args = {"content": content, "content_type": content_type} + if influxframework.form_dict.max_width: + args["max_width"] = int(influxframework.form_dict.max_width) + if influxframework.form_dict.max_height: + args["max_height"] = int(influxframework.form_dict.max_height) + content = optimize_image(**args) + + influxframework.local.uploaded_file = content + influxframework.local.uploaded_filename = filename + + if content is not None and ( + influxframework.session.user == "Guest" or (user and not user.has_desk_access()) + ): + filetype = guess_type(filename)[0] + if filetype not in ALLOWED_MIMETYPES: + influxframework.throw(_("You can only upload JPG, PNG, PDF, TXT or Microsoft documents.")) + + if method: + method = influxframework.get_attr(method) + is_whitelisted(method) + return method() + else: + return influxframework.get_doc( + { + "doctype": "File", + "attached_to_doctype": doctype, + "attached_to_name": docname, + "attached_to_field": fieldname, + "folder": folder, + "file_name": filename, + "file_url": file_url, + "is_private": cint(is_private), + "content": content, + } + ).save(ignore_permissions=ignore_permissions) + + +@influxframework.whitelist(allow_guest=True) +def download_file(file_url: str): + """ + Download file using token and REST API. Valid session or + token is required to download private files. + + Method : GET + Endpoints : download_file, influxframework.core.doctype.file.file.download_file + URL Params : file_name = /path/to/file relative to site path + """ + file: "File" = influxframework.get_doc("File", {"file_url": file_url}) + if not file.is_downloadable(): + raise influxframework.PermissionError + + influxframework.local.response.filename = os.path.basename(file_url) + influxframework.local.response.filecontent = file.get_content() + influxframework.local.response.type = "download" + + +def get_attr(cmd): + """get method object from cmd""" + if "." in cmd: + method = influxframework.get_attr(cmd) + else: + method = globals()[cmd] + influxframework.log("method:" + cmd) + return method + + +@influxframework.whitelist(allow_guest=True) +def ping(): + return "pong" + + +def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None): + """run a whitelisted controller method""" + from inspect import getfullargspec + + if not args and arg: + args = arg + + if dt: # not called from a doctype (from a page) + if not dn: + dn = dt # single + doc = influxframework.get_doc(dt, dn) + + else: + docs = influxframework.parse_json(docs) + doc = influxframework.get_doc(docs) + doc._original_modified = doc.modified + doc.check_if_latest() + + if not doc or not doc.has_permission("read"): + throw_permission_error() + + try: + args = influxframework.parse_json(args) + except ValueError: + pass + + method_obj = getattr(doc, method) + fn = getattr(method_obj, "__func__", method_obj) + is_whitelisted(fn) + is_valid_http_method(fn) + + fnargs = getfullargspec(method_obj).args + + if not fnargs or (len(fnargs) == 1 and fnargs[0] == "self"): + response = doc.run_method(method) + + elif "args" in fnargs or not isinstance(args, dict): + response = doc.run_method(method, args) + + else: + response = doc.run_method(method, **args) + + influxframework.response.docs.append(doc) + if response is None: + return + + # build output as csv + if cint(influxframework.form_dict.get("as_csv")): + build_csv_response(response, _(doc.doctype).replace(" ", "")) + return + + influxframework.response["message"] = response + + +# for backwards compatibility +runserverobj = run_doc_method diff --git a/influxframework/hooks.py b/influxframework/hooks.py new file mode 100644 index 0000000..a308245 --- /dev/null +++ b/influxframework/hooks.py @@ -0,0 +1,376 @@ +from . import __version__ as app_version + +app_name = "influxframework" +app_title = "InfluxFramework Framework" +app_publisher = "InfluxFramework Technologies" +app_description = "Full stack web framework with Python, Javascript, MariaDB, Redis, Node" +source_link = "https://github.com/influxframework/influxframework" +app_license = "MIT" +app_logo_url = "/assets/influxframework/images/influxframework-framework-logo.svg" + +develop_version = "14.x.x-develop" + +app_email = "developers@influxframework.io" + +docs_app = "influxframework_docs" + +translator_url = "https://translate.influxerp.com" + +before_install = "influxframework.utils.install.before_install" +after_install = "influxframework.utils.install.after_install" + +page_js = {"setup-wizard": "public/js/influxframework/setup_wizard.js"} + +# website +app_include_js = [ + "libs.bundle.js", + "desk.bundle.js", + "list.bundle.js", + "form.bundle.js", + "controls.bundle.js", + "report.bundle.js", +] +app_include_css = [ + "desk.bundle.css", + "report.bundle.css", +] + +doctype_js = { + "Web Page": "public/js/influxframework/utils/web_template.js", + "Website Settings": "public/js/influxframework/utils/web_template.js", +} + +web_include_js = ["website_script.js"] + +web_include_css = [] + +email_css = ["email.bundle.css"] + +website_route_rules = [ + {"from_route": "/blog/", "to_route": "Blog Post"}, + {"from_route": "/kb/", "to_route": "Help Article"}, + {"from_route": "/newsletters", "to_route": "Newsletter"}, + {"from_route": "/profile", "to_route": "me"}, + {"from_route": "/app/", "to_route": "app"}, +] + +website_redirects = [ + {"source": r"/desk(.*)", "target": r"/app\1"}, +] + +base_template = "templates/base.html" + +write_file_keys = ["file_url", "file_name"] + +notification_config = "influxframework.core.notifications.get_notification_config" + +before_tests = "influxframework.utils.install.before_tests" + +email_append_to = ["Event", "ToDo", "Communication"] + +calendars = ["Event"] + +leaderboards = "influxframework.desk.leaderboard.get_leaderboards" + +# login + +on_session_creation = [ + "influxframework.core.doctype.activity_log.feed.login_feed", + "influxframework.core.doctype.user.user.notify_admin_access_to_system_manager", +] + +on_logout = ( + "influxframework.core.doctype.session_default_settings.session_default_settings.clear_session_defaults" +) + +# permissions + +permission_query_conditions = { + "Event": "influxframework.desk.doctype.event.event.get_permission_query_conditions", + "ToDo": "influxframework.desk.doctype.todo.todo.get_permission_query_conditions", + "User": "influxframework.core.doctype.user.user.get_permission_query_conditions", + "Dashboard Settings": "influxframework.desk.doctype.dashboard_settings.dashboard_settings.get_permission_query_conditions", + "Notification Log": "influxframework.desk.doctype.notification_log.notification_log.get_permission_query_conditions", + "Dashboard": "influxframework.desk.doctype.dashboard.dashboard.get_permission_query_conditions", + "Dashboard Chart": "influxframework.desk.doctype.dashboard_chart.dashboard_chart.get_permission_query_conditions", + "Number Card": "influxframework.desk.doctype.number_card.number_card.get_permission_query_conditions", + "Notification Settings": "influxframework.desk.doctype.notification_settings.notification_settings.get_permission_query_conditions", + "Note": "influxframework.desk.doctype.note.note.get_permission_query_conditions", + "Kanban Board": "influxframework.desk.doctype.kanban_board.kanban_board.get_permission_query_conditions", + "Contact": "influxframework.contacts.address_and_contact.get_permission_query_conditions_for_contact", + "Address": "influxframework.contacts.address_and_contact.get_permission_query_conditions_for_address", + "Communication": "influxframework.core.doctype.communication.communication.get_permission_query_conditions_for_communication", + "Workflow Action": "influxframework.workflow.doctype.workflow_action.workflow_action.get_permission_query_conditions", + "Prepared Report": "influxframework.core.doctype.prepared_report.prepared_report.get_permission_query_condition", +} + +has_permission = { + "Event": "influxframework.desk.doctype.event.event.has_permission", + "ToDo": "influxframework.desk.doctype.todo.todo.has_permission", + "User": "influxframework.core.doctype.user.user.has_permission", + "Note": "influxframework.desk.doctype.note.note.has_permission", + "Dashboard Chart": "influxframework.desk.doctype.dashboard_chart.dashboard_chart.has_permission", + "Number Card": "influxframework.desk.doctype.number_card.number_card.has_permission", + "Kanban Board": "influxframework.desk.doctype.kanban_board.kanban_board.has_permission", + "Contact": "influxframework.contacts.address_and_contact.has_permission", + "Address": "influxframework.contacts.address_and_contact.has_permission", + "Communication": "influxframework.core.doctype.communication.communication.has_permission", + "Workflow Action": "influxframework.workflow.doctype.workflow_action.workflow_action.has_permission", + "File": "influxframework.core.doctype.file.file.has_permission", + "Prepared Report": "influxframework.core.doctype.prepared_report.prepared_report.has_permission", +} + +has_website_permission = { + "Address": "influxframework.contacts.doctype.address.address.has_website_permission" +} + +jinja = { + "methods": "influxframework.utils.jinja_globals", + "filters": [ + "influxframework.utils.data.global_date_format", + "influxframework.utils.markdown", + "influxframework.website.utils.get_shade", + "influxframework.website.utils.abs_url", + ], +} + +standard_queries = {"User": "influxframework.core.doctype.user.user.user_query"} + +doc_events = { + "*": { + "after_insert": [ + "influxframework.event_streaming.doctype.event_update_log.event_update_log.notify_consumers" + ], + "on_update": [ + "influxframework.desk.notifications.clear_doctype_notifications", + "influxframework.core.doctype.activity_log.feed.update_feed", + "influxframework.workflow.doctype.workflow_action.workflow_action.process_workflow_actions", + "influxframework.automation.doctype.assignment_rule.assignment_rule.apply", + "influxframework.core.doctype.file.utils.attach_files_to_document", + "influxframework.event_streaming.doctype.event_update_log.event_update_log.notify_consumers", + "influxframework.automation.doctype.assignment_rule.assignment_rule.update_due_date", + "influxframework.core.doctype.user_type.user_type.apply_permissions_for_non_standard_user_type", + ], + "after_rename": "influxframework.desk.notifications.clear_doctype_notifications", + "on_cancel": [ + "influxframework.desk.notifications.clear_doctype_notifications", + "influxframework.workflow.doctype.workflow_action.workflow_action.process_workflow_actions", + "influxframework.event_streaming.doctype.event_update_log.event_update_log.notify_consumers", + ], + "on_trash": [ + "influxframework.desk.notifications.clear_doctype_notifications", + "influxframework.workflow.doctype.workflow_action.workflow_action.process_workflow_actions", + "influxframework.event_streaming.doctype.event_update_log.event_update_log.notify_consumers", + ], + "on_update_after_submit": [ + "influxframework.workflow.doctype.workflow_action.workflow_action.process_workflow_actions" + ], + "on_change": [ + "influxframework.social.doctype.energy_point_rule.energy_point_rule.process_energy_points", + "influxframework.automation.doctype.milestone_tracker.milestone_tracker.evaluate_milestone", + ], + }, + "Event": { + "after_insert": "influxframework.integrations.doctype.google_calendar.google_calendar.insert_event_in_google_calendar", + "on_update": "influxframework.integrations.doctype.google_calendar.google_calendar.update_event_in_google_calendar", + "on_trash": "influxframework.integrations.doctype.google_calendar.google_calendar.delete_event_from_google_calendar", + }, + "Contact": { + "after_insert": "influxframework.integrations.doctype.google_contacts.google_contacts.insert_contacts_to_google_contacts", + "on_update": "influxframework.integrations.doctype.google_contacts.google_contacts.update_contacts_to_google_contacts", + }, + "DocType": { + "on_update": "influxframework.cache_manager.build_domain_restriced_doctype_cache", + }, + "Page": { + "on_update": "influxframework.cache_manager.build_domain_restriced_page_cache", + }, +} + +scheduler_events = { + "cron": { + "0/15 * * * *": [ + "influxframework.oauth.delete_oauth2_data", + "influxframework.website.doctype.web_page.web_page.check_publish_status", + "influxframework.twofactor.delete_all_barcodes_for_users", + ] + }, + "all": [ + "influxframework.email.queue.flush", + "influxframework.email.doctype.email_account.email_account.pull", + "influxframework.email.doctype.email_account.email_account.notify_unreplied", + "influxframework.utils.global_search.sync_global_search", + "influxframework.monitor.flush", + ], + "hourly": [ + "influxframework.model.utils.link_count.update_link_count", + "influxframework.model.utils.user_settings.sync_user_settings", + "influxframework.utils.error.collect_error_snapshots", + "influxframework.desk.page.backups.backups.delete_downloadable_backups", + "influxframework.deferred_insert.save_to_db", + "influxframework.desk.form.document_follow.send_hourly_updates", + "influxframework.integrations.doctype.google_calendar.google_calendar.sync", + "influxframework.email.doctype.newsletter.newsletter.send_scheduled_email", + "influxframework.website.doctype.personal_data_deletion_request.personal_data_deletion_request.process_data_deletion_request", + ], + "daily": [ + "influxframework.email.queue.set_expiry_for_email_queue", + "influxframework.desk.notifications.clear_notifications", + "influxframework.desk.doctype.event.event.send_event_digest", + "influxframework.sessions.clear_expired_sessions", + "influxframework.email.doctype.notification.notification.trigger_daily_alerts", + "influxframework.website.doctype.personal_data_deletion_request.personal_data_deletion_request.remove_unverified_record", + "influxframework.desk.form.document_follow.send_daily_updates", + "influxframework.social.doctype.energy_point_settings.energy_point_settings.allocate_review_points", + "influxframework.integrations.doctype.google_contacts.google_contacts.sync", + "influxframework.automation.doctype.auto_repeat.auto_repeat.make_auto_repeat_entry", + "influxframework.automation.doctype.auto_repeat.auto_repeat.set_auto_repeat_as_completed", + "influxframework.email.doctype.unhandled_email.unhandled_email.remove_old_unhandled_emails", + "influxframework.core.doctype.prepared_report.prepared_report.delete_expired_prepared_reports", + "influxframework.core.doctype.log_settings.log_settings.run_log_clean_up", + "influxframework.utils.subscription.enable_manage_subscription", + ], + "daily_long": [ + "influxframework.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily", + "influxframework.utils.change_log.check_for_update", + "influxframework.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_daily", + "influxframework.email.doctype.auto_email_report.auto_email_report.send_daily", + "influxframework.integrations.doctype.google_drive.google_drive.daily_backup", + ], + "weekly_long": [ + "influxframework.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_weekly", + "influxframework.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_weekly", + "influxframework.desk.form.document_follow.send_weekly_updates", + "influxframework.social.doctype.energy_point_log.energy_point_log.send_weekly_summary", + "influxframework.integrations.doctype.google_drive.google_drive.weekly_backup", + ], + "monthly": [ + "influxframework.email.doctype.auto_email_report.auto_email_report.send_monthly", + "influxframework.social.doctype.energy_point_log.energy_point_log.send_monthly_summary", + ], + "monthly_long": [ + "influxframework.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_monthly" + ], +} + +get_translated_dict = { + ("doctype", "System Settings"): "influxframework.geo.country_info.get_translated_dict", + ("page", "setup-wizard"): "influxframework.geo.country_info.get_translated_dict", +} + +sounds = [ + {"name": "email", "src": "/assets/influxframework/sounds/email.mp3", "volume": 0.1}, + {"name": "submit", "src": "/assets/influxframework/sounds/submit.mp3", "volume": 0.1}, + {"name": "cancel", "src": "/assets/influxframework/sounds/cancel.mp3", "volume": 0.1}, + {"name": "delete", "src": "/assets/influxframework/sounds/delete.mp3", "volume": 0.05}, + {"name": "click", "src": "/assets/influxframework/sounds/click.mp3", "volume": 0.05}, + {"name": "error", "src": "/assets/influxframework/sounds/error.mp3", "volume": 0.1}, + {"name": "alert", "src": "/assets/influxframework/sounds/alert.mp3", "volume": 0.2}, + # {"name": "chime", "src": "/assets/influxframework/sounds/chime.mp3"}, +] + +setup_wizard_exception = [ + "influxframework.desk.page.setup_wizard.setup_wizard.email_setup_wizard_exception", + "influxframework.desk.page.setup_wizard.setup_wizard.log_setup_wizard_exception", +] + +before_migrate = [] +after_migrate = ["influxframework.website.doctype.website_theme.website_theme.after_migrate"] + +otp_methods = ["OTP App", "Email", "SMS"] + +user_data_fields = [ + {"doctype": "Access Log", "strict": True}, + {"doctype": "Activity Log", "strict": True}, + {"doctype": "Comment", "strict": True}, + { + "doctype": "Contact", + "filter_by": "email_id", + "redact_fields": ["first_name", "last_name", "phone", "mobile_no"], + "rename": True, + }, + {"doctype": "Contact Email", "filter_by": "email_id"}, + { + "doctype": "Address", + "filter_by": "email_id", + "redact_fields": [ + "address_title", + "address_line1", + "address_line2", + "city", + "county", + "state", + "pincode", + "phone", + "fax", + ], + }, + { + "doctype": "Communication", + "filter_by": "sender", + "redact_fields": ["sender_full_name", "phone_no", "content"], + }, + {"doctype": "Communication", "filter_by": "recipients"}, + {"doctype": "Email Group Member", "filter_by": "email"}, + {"doctype": "Email Unsubscribe", "filter_by": "email", "partial": True}, + {"doctype": "Email Queue", "filter_by": "sender"}, + {"doctype": "Email Queue Recipient", "filter_by": "recipient"}, + { + "doctype": "File", + "filter_by": "attached_to_name", + "redact_fields": ["file_name", "file_url"], + }, + { + "doctype": "User", + "filter_by": "name", + "redact_fields": [ + "email", + "username", + "first_name", + "middle_name", + "last_name", + "full_name", + "birth_date", + "user_image", + "phone", + "mobile_no", + "location", + "banner_image", + "interest", + "bio", + "email_signature", + ], + }, + {"doctype": "Version", "strict": True}, +] + +global_search_doctypes = { + "Default": [ + {"doctype": "Contact"}, + {"doctype": "Address"}, + {"doctype": "ToDo"}, + {"doctype": "Note"}, + {"doctype": "Event"}, + {"doctype": "Blog Post"}, + {"doctype": "Dashboard"}, + {"doctype": "Country"}, + {"doctype": "Currency"}, + {"doctype": "Newsletter"}, + {"doctype": "Letter Head"}, + {"doctype": "Workflow"}, + {"doctype": "Web Page"}, + {"doctype": "Web Form"}, + ] +} + +override_whitelisted_methods = { + "influxframework.core.doctype.file.file.download_file": "download_file", + "influxframework.core.doctype.file.file.unzip_file": "influxframework.core.api.file.unzip_file", + "influxframework.core.doctype.file.file.get_attached_images": "influxframework.core.api.file.get_attached_images", + "influxframework.core.doctype.file.file.get_files_in_folder": "influxframework.core.api.file.get_files_in_folder", + "influxframework.core.doctype.file.file.get_files_by_search_text": "influxframework.core.api.file.get_files_by_search_text", + "influxframework.core.doctype.file.file.get_max_file_size": "influxframework.core.api.file.get_max_file_size", + "influxframework.core.doctype.file.file.create_new_folder": "influxframework.core.api.file.create_new_folder", + "influxframework.core.doctype.file.file.move_file": "influxframework.core.api.file.move_file", + "influxframework.core.doctype.file.file.zip_files": "influxframework.core.api.file.zip_files", +} diff --git a/influxframework/influxframeworkclient.py b/influxframework/influxframeworkclient.py new file mode 100644 index 0000000..078e25f --- /dev/null +++ b/influxframework/influxframeworkclient.py @@ -0,0 +1,425 @@ +""" +InfluxFrameworkClient is a library that helps you connect with other influxframework systems +""" +import base64 +import json + +import requests + +import influxframework +from influxframework.utils.data import cstr + + +class AuthError(Exception): + pass + + +class SiteExpiredError(Exception): + pass + + +class SiteUnreachableError(Exception): + pass + + +class InfluxFrameworkException(Exception): + pass + + +class InfluxFrameworkClient: + def __init__( + self, + url, + username=None, + password=None, + verify=True, + api_key=None, + api_secret=None, + influxframework_authorization_source=None, + ): + self.headers = { + "Accept": "application/json", + "content-type": "application/x-www-form-urlencoded", + } + self.verify = verify + self.session = requests.session() + self.url = url + self.api_key = api_key + self.api_secret = api_secret + self.influxframework_authorization_source = influxframework_authorization_source + + self.setup_key_authentication_headers() + + # login if username/password provided + if username and password: + self._login(username, password) + + def __enter__(self): + return self + + def __exit__(self, *args, **kwargs): + self.logout() + + def _login(self, username, password): + """Login/start a sesion. Called internally on init""" + r = self.session.post( + self.url, + params={"cmd": "login", "usr": username, "pwd": password}, + verify=self.verify, + headers=self.headers, + ) + + if r.status_code == 200 and r.json().get("message") in ("Logged In", "No App"): + return r.json() + elif r.status_code == 502: + raise SiteUnreachableError + else: + try: + error = json.loads(r.text) + if error.get("exc_type") == "SiteExpiredError": + raise SiteExpiredError + except json.decoder.JSONDecodeError: + error = r.text + print(error) + raise AuthError + + def setup_key_authentication_headers(self): + if self.api_key and self.api_secret: + token = base64.b64encode((f"{self.api_key}:{self.api_secret}").encode()).decode("utf-8") + auth_header = { + "Authorization": f"Basic {token}", + } + self.headers.update(auth_header) + + if self.influxframework_authorization_source: + auth_source = {"InfluxFramework-Authorization-Source": self.influxframework_authorization_source} + self.headers.update(auth_source) + + def logout(self): + """Logout session""" + self.session.get( + self.url, + params={ + "cmd": "logout", + }, + verify=self.verify, + headers=self.headers, + ) + + def get_list(self, doctype, fields='["name"]', filters=None, limit_start=0, limit_page_length=0): + """Returns list of records of a particular type""" + if not isinstance(fields, str): + fields = json.dumps(fields) + params = { + "fields": fields, + } + if filters: + params["filters"] = json.dumps(filters) + if limit_page_length: + params["limit_start"] = limit_start + params["limit_page_length"] = limit_page_length + res = self.session.get( + self.url + "/api/resource/" + doctype, params=params, verify=self.verify, headers=self.headers + ) + return self.post_process(res) + + def insert(self, doc): + """Insert a document to the remote server + + :param doc: A dict or Document object to be inserted remotely""" + res = self.session.post( + self.url + "/api/resource/" + doc.get("doctype"), + data={"data": influxframework.as_json(doc)}, + verify=self.verify, + headers=self.headers, + ) + return influxframework._dict(self.post_process(res)) + + def insert_many(self, docs): + """Insert multiple documents to the remote server + + :param docs: List of dict or Document objects to be inserted in one request""" + return self.post_request({"cmd": "influxframework.client.insert_many", "docs": influxframework.as_json(docs)}) + + def update(self, doc): + """Update a remote document + + :param doc: dict or Document object to be updated remotely. `name` is mandatory for this""" + url = self.url + "/api/resource/" + doc.get("doctype") + "/" + cstr(doc.get("name")) + res = self.session.put( + url, data={"data": influxframework.as_json(doc)}, verify=self.verify, headers=self.headers + ) + return influxframework._dict(self.post_process(res)) + + def bulk_update(self, docs): + """Bulk update documents remotely + + :param docs: List of dict or Document objects to be updated remotely (by `name`)""" + return self.post_request({"cmd": "influxframework.client.bulk_update", "docs": influxframework.as_json(docs)}) + + def delete(self, doctype, name): + """Delete remote document by name + + :param doctype: `doctype` to be deleted + :param name: `name` of document to be deleted""" + return self.post_request({"cmd": "influxframework.client.delete", "doctype": doctype, "name": name}) + + def submit(self, doc): + """Submit remote document + + :param doc: dict or Document object to be submitted remotely""" + return self.post_request({"cmd": "influxframework.client.submit", "doc": influxframework.as_json(doc)}) + + def get_value(self, doctype, fieldname=None, filters=None): + """Returns a value form a document + + :param doctype: DocType to be queried + :param fieldname: Field to be returned (default `name`) + :param filters: dict or string for identifying the record""" + return self.get_request( + { + "cmd": "influxframework.client.get_value", + "doctype": doctype, + "fieldname": fieldname or "name", + "filters": influxframework.as_json(filters), + } + ) + + def set_value(self, doctype, docname, fieldname, value): + """Set a value in a remote document + + :param doctype: DocType of the document to be updated + :param docname: name of the document to be updated + :param fieldname: fieldname of the document to be updated + :param value: value to be updated""" + return self.post_request( + { + "cmd": "influxframework.client.set_value", + "doctype": doctype, + "name": docname, + "fieldname": fieldname, + "value": value, + } + ) + + def cancel(self, doctype, name): + """Cancel a remote document + + :param doctype: DocType of the document to be cancelled + :param name: name of the document to be cancelled""" + return self.post_request({"cmd": "influxframework.client.cancel", "doctype": doctype, "name": name}) + + def get_doc(self, doctype, name="", filters=None, fields=None): + """Returns a single remote document + + :param doctype: DocType of the document to be returned + :param name: (optional) `name` of the document to be returned + :param filters: (optional) Filter by this dict if name is not set + :param fields: (optional) Fields to be returned, will return everythign if not set""" + params = {} + if filters: + params["filters"] = json.dumps(filters) + if fields: + params["fields"] = json.dumps(fields) + + res = self.session.get( + self.url + "/api/resource/" + doctype + "/" + cstr(name), + params=params, + verify=self.verify, + headers=self.headers, + ) + + return self.post_process(res) + + def rename_doc(self, doctype, old_name, new_name): + """Rename remote document + + :param doctype: DocType of the document to be renamed + :param old_name: Current `name` of the document to be renamed + :param new_name: New `name` to be set""" + params = { + "cmd": "influxframework.client.rename_doc", + "doctype": doctype, + "old_name": old_name, + "new_name": new_name, + } + return self.post_request(params) + + def migrate_doctype( + self, doctype, filters=None, update=None, verbose=1, exclude=None, preprocess=None + ): + """Migrate records from another doctype""" + meta = influxframework.get_meta(doctype) + tables = {} + for df in meta.get_table_fields(): + if verbose: + print("getting " + df.options) + tables[df.fieldname] = self.get_list(df.options, limit_page_length=999999) + + # get links + if verbose: + print("getting " + doctype) + docs = self.get_list(doctype, limit_page_length=999999, filters=filters) + + # build - attach children to parents + if tables: + docs = [influxframework._dict(doc) for doc in docs] + docs_map = {doc.name: doc for doc in docs} + + for fieldname in tables: + for child in tables[fieldname]: + child = influxframework._dict(child) + if child.parent in docs_map: + docs_map[child.parent].setdefault(fieldname, []).append(child) + + if verbose: + print("inserting " + doctype) + for doc in docs: + if exclude and doc["name"] in exclude: + continue + + if preprocess: + preprocess(doc) + + if not doc.get("owner"): + doc["owner"] = "Administrator" + + if doctype != "User" and not influxframework.db.exists("User", doc.get("owner")): + influxframework.get_doc( + {"doctype": "User", "email": doc.get("owner"), "first_name": doc.get("owner").split("@")[0]} + ).insert() + + if update: + doc.update(update) + + doc["doctype"] = doctype + new_doc = influxframework.get_doc(doc) + new_doc.insert() + + if not meta.istable: + if doctype != "Communication": + self.migrate_doctype( + "Communication", + {"reference_doctype": doctype, "reference_name": doc["name"]}, + update={"reference_name": new_doc.name}, + verbose=0, + ) + + if doctype != "File": + self.migrate_doctype( + "File", + {"attached_to_doctype": doctype, "attached_to_name": doc["name"]}, + update={"attached_to_name": new_doc.name}, + verbose=0, + ) + + def migrate_single(self, doctype): + doc = self.get_doc(doctype, doctype) + doc = influxframework.get_doc(doc) + + # change modified so that there is no error + doc.modified = influxframework.db.get_single_value(doctype, "modified") + influxframework.get_doc(doc).insert() + + def get_api(self, method, params=None): + if params is None: + params = {} + res = self.session.get( + f"{self.url}/api/method/{method}", params=params, verify=self.verify, headers=self.headers + ) + return self.post_process(res) + + def post_api(self, method, params=None): + if params is None: + params = {} + res = self.session.post( + f"{self.url}/api/method/{method}", params=params, verify=self.verify, headers=self.headers + ) + return self.post_process(res) + + def get_request(self, params): + res = self.session.get( + self.url, params=self.preprocess(params), verify=self.verify, headers=self.headers + ) + res = self.post_process(res) + return res + + def post_request(self, data): + res = self.session.post( + self.url, data=self.preprocess(data), verify=self.verify, headers=self.headers + ) + res = self.post_process(res) + return res + + def preprocess(self, params): + """convert dicts, lists to json""" + for key, value in params.items(): + if isinstance(value, (dict, list)): + params[key] = json.dumps(value) + + return params + + def post_process(self, response): + try: + rjson = response.json() + except ValueError: + print(response.text) + raise + + if rjson and ("exc" in rjson) and rjson["exc"]: + try: + exc = json.loads(rjson["exc"])[0] + exc = "InfluxFrameworkClient Request Failed\n\n" + exc + except Exception: + exc = rjson["exc"] + + raise InfluxFrameworkException(exc) + if "message" in rjson: + return rjson["message"] + elif "data" in rjson: + return rjson["data"] + else: + return None + + +class InfluxFrameworkOAuth2Client(InfluxFrameworkClient): + def __init__(self, url, access_token, verify=True): + self.access_token = access_token + self.headers = { + "Authorization": "Bearer " + access_token, + "content-type": "application/x-www-form-urlencoded", + } + self.verify = verify + self.session = OAuth2Session(self.headers) + self.url = url + + def get_request(self, params): + res = requests.get( + self.url, params=self.preprocess(params), headers=self.headers, verify=self.verify + ) + res = self.post_process(res) + return res + + def post_request(self, data): + res = requests.post( + self.url, data=self.preprocess(data), headers=self.headers, verify=self.verify + ) + res = self.post_process(res) + return res + + +class OAuth2Session: + def __init__(self, headers): + self.headers = headers + + def get(self, url, params, verify): + res = requests.get(url, params=params, headers=self.headers, verify=verify) + return res + + def post(self, url, data, verify): + res = requests.post(url, data=data, headers=self.headers, verify=verify) + return res + + def put(self, url, data, verify): + res = requests.put(url, data=data, headers=self.headers, verify=verify) + return res diff --git a/influxframework/installer.py b/influxframework/installer.py new file mode 100644 index 0000000..081d146 --- /dev/null +++ b/influxframework/installer.py @@ -0,0 +1,828 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE + +import json +import os +import sys +from collections import OrderedDict + +import click + +import influxframework +from influxframework.defaults import _clear_cache +from influxframework.utils import cint, is_git_url +from influxframework.utils.dashboard import sync_dashboards + + +def _is_scheduler_enabled() -> bool: + enable_scheduler = False + try: + influxframework.connect() + enable_scheduler = cint(influxframework.db.get_single_value("System Settings", "enable_scheduler")) + except Exception: + pass + finally: + influxframework.db.close() + + return bool(enable_scheduler) + + +def _new_site( + db_name, + site, + db_root_username=None, + db_root_password=None, + admin_password=None, + verbose=False, + install_apps=None, + source_sql=None, + force=False, + no_mariadb_socket=False, + reinstall=False, + db_password=None, + db_type=None, + db_host=None, + db_port=None, +): + """Install a new InfluxFramework site""" + + from influxframework.utils import get_site_path, scheduler, touch_file + + if not force and os.path.exists(site): + print(f"Site {site} already exists") + sys.exit(1) + + if no_mariadb_socket and not db_type == "mariadb": + print("--no-mariadb-socket requires db_type to be set to mariadb.") + sys.exit(1) + + influxframework.init(site=site) + + if not db_name: + import hashlib + + db_name = "_" + hashlib.sha1(os.path.realpath(influxframework.get_site_path()).encode()).hexdigest()[:16] + + try: + # enable scheduler post install? + enable_scheduler = _is_scheduler_enabled() + except Exception: + enable_scheduler = False + + make_site_dirs() + + installing = touch_file(get_site_path("locks", "installing.lock")) + + install_db( + root_login=db_root_username, + root_password=db_root_password, + db_name=db_name, + admin_password=admin_password, + verbose=verbose, + source_sql=source_sql, + force=force, + reinstall=reinstall, + db_password=db_password, + db_type=db_type, + db_host=db_host, + db_port=db_port, + no_mariadb_socket=no_mariadb_socket, + ) + apps_to_install = ( + ["influxframework"] + (influxframework.conf.get("install_apps") or []) + (list(install_apps) or []) + ) + + for app in apps_to_install: + # NOTE: not using force here for 2 reasons: + # 1. It's not really needed here as we've freshly installed a new db + # 2. If someone uses a sql file to do restore and that file already had + # installed_apps then it might cause problems as that sql file can be of any previous version(s) + # which might be incompatible with the current version and using force might cause problems. + # Example: the DocType DocType might not have `migration_hash` column which will cause failure in the restore. + install_app(app, verbose=verbose, set_as_patched=not source_sql, force=False) + + os.remove(installing) + + scheduler.toggle_scheduler(enable_scheduler) + influxframework.db.commit() + + scheduler_status = "disabled" if influxframework.utils.scheduler.is_scheduler_disabled() else "enabled" + print("*** Scheduler is", scheduler_status, "***") + + +def install_db( + root_login=None, + root_password=None, + db_name=None, + source_sql=None, + admin_password=None, + verbose=True, + force=0, + site_config=None, + reinstall=False, + db_password=None, + db_type=None, + db_host=None, + db_port=None, + no_mariadb_socket=False, +): + import influxframework.database + from influxframework.database import setup_database + + if not db_type: + db_type = influxframework.conf.db_type or "mariadb" + + if not root_login and db_type == "mariadb": + root_login = "root" + elif not root_login and db_type == "postgres": + root_login = "postgres" + + make_conf( + db_name, + site_config=site_config, + db_password=db_password, + db_type=db_type, + db_host=db_host, + db_port=db_port, + ) + influxframework.flags.in_install_db = True + + influxframework.flags.root_login = root_login + influxframework.flags.root_password = root_password + setup_database(force, source_sql, verbose, no_mariadb_socket) + + influxframework.conf.admin_password = influxframework.conf.admin_password or admin_password + + remove_missing_apps() + + influxframework.db.create_auth_table() + influxframework.db.create_global_search_table() + influxframework.db.create_user_settings_table() + + influxframework.flags.in_install_db = False + + +def find_org(org_repo: str) -> tuple[str, str]: + """find the org a repo is in + + find_org() + ref -> https://github.com/influxframework/bench/blob/develop/bench/utils/__init__.py#L390 + + :param org_repo: + :type org_repo: str + + :raises InvalidRemoteException: if the org is not found + + :return: organisation and repository + :rtype: Tuple[str, str] + """ + import requests + + from influxframework.exceptions import InvalidRemoteException + + for org in ["influxframework", "influxerp"]: + response = requests.head(f"https://api.github.com/repos/{org}/{org_repo}") + if response.status_code == 400: + response = requests.head(f"https://github.com/{org}/{org_repo}") + if response.ok: + return org, org_repo + + raise InvalidRemoteException + + +def fetch_details_from_tag(_tag: str) -> tuple[str, str, str]: + """parse org, repo, tag from string + + fetch_details_from_tag() + ref -> https://github.com/influxframework/bench/blob/develop/bench/utils/__init__.py#L403 + + :param _tag: input string + :type _tag: str + + :return: organisation, repostitory, tag + :rtype: Tuple[str, str, str] + """ + app_tag = _tag.split("@") + org_repo = app_tag[0].split("/") + + try: + repo, tag = app_tag + except ValueError: + repo, tag = app_tag + [None] + + try: + org, repo = org_repo + except Exception: + org, repo = find_org(org_repo[0]) + + return org, repo, tag + + +def parse_app_name(name: str) -> str: + """parse repo name from name + + __setup_details_from_git() + ref -> https://github.com/influxframework/bench/blob/develop/bench/app.py#L114 + + + :param name: git tag + :type name: str + + :return: repository name + :rtype: str + """ + name = name.rstrip("/") + if os.path.exists(name): + repo = os.path.split(name)[-1] + elif is_git_url(name): + if name.startswith("git@") or name.startswith("ssh://"): + _repo = name.split(":")[1].rsplit("/", 1)[1] + else: + _repo = name.rsplit("/", 2)[2] + repo = _repo.split(".")[0] + else: + _, repo, _ = fetch_details_from_tag(name) + return repo + + +def install_app(name, verbose=False, set_as_patched=True, force=False): + from influxframework.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs + from influxframework.model.sync import sync_for + from influxframework.modules.utils import sync_customizations + from influxframework.utils.fixtures import sync_fixtures + + influxframework.flags.in_install = name + influxframework.flags.ignore_in_install = False + + influxframework.clear_cache() + app_hooks = influxframework.get_hooks(app_name=name) + installed_apps = influxframework.get_installed_apps() + + # install pre-requisites + if app_hooks.required_apps: + for app in app_hooks.required_apps: + required_app = parse_app_name(app) + install_app(required_app, verbose=verbose, force=force) + + influxframework.flags.in_install = name + influxframework.clear_cache() + + if name not in influxframework.get_all_apps(): + raise Exception("App not in apps.txt") + + if not force and name in installed_apps: + click.secho(f"App {name} already installed", fg="yellow") + return + + print(f"\nInstalling {name}...") + + if name != "influxframework": + influxframework.only_for("System Manager") + + for before_install in app_hooks.before_install or []: + out = influxframework.get_attr(before_install)() + if out is False: + return + + if name != "influxframework": + add_module_defs(name, ignore_if_duplicate=force) + + sync_for(name, force=force, reset_permissions=True) + + add_to_installed_apps(name) + + influxframework.get_doc("Portal Settings", "Portal Settings").sync_menu() + + if set_as_patched: + set_all_patches_as_completed(name) + + for after_install in app_hooks.after_install or []: + influxframework.get_attr(after_install)() + + sync_jobs() + sync_fixtures(name) + sync_customizations(name) + sync_dashboards(name) + + for after_sync in app_hooks.after_sync or []: + influxframework.get_attr(after_sync)() # + + influxframework.flags.in_install = False + + +def add_to_installed_apps(app_name, rebuild_website=True): + installed_apps = influxframework.get_installed_apps() + if app_name not in installed_apps: + installed_apps.append(app_name) + influxframework.db.set_global("installed_apps", json.dumps(installed_apps)) + influxframework.db.commit() + if influxframework.flags.in_install: + post_install(rebuild_website) + + +def remove_from_installed_apps(app_name): + installed_apps = influxframework.get_installed_apps() + if app_name in installed_apps: + installed_apps.remove(app_name) + influxframework.db.set_value( + "DefaultValue", {"defkey": "installed_apps"}, "defvalue", json.dumps(installed_apps) + ) + _clear_cache("__global") + influxframework.db.commit() + if influxframework.flags.in_install: + post_install() + + +def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False): + """Remove app and all linked to the app's module with the app from a site.""" + + site = influxframework.local.site + app_hooks = influxframework.get_hooks(app_name=app_name) + + # dont allow uninstall app if not installed unless forced + if not force: + if app_name not in influxframework.get_installed_apps(): + click.secho(f"App {app_name} not installed on Site {site}", fg="yellow") + return + + print(f"Uninstalling App {app_name} from Site {site}...") + + if not dry_run and not yes: + confirm = click.confirm( + "All doctypes (including custom), modules related to this app will be" + " deleted. Are you sure you want to continue?" + ) + if not confirm: + return + + if not (dry_run or no_backup): + from influxframework.utils.backups import scheduled_backup + + print("Backing up...") + scheduled_backup(ignore_files=True) + + influxframework.flags.in_uninstall = True + + for before_uninstall in app_hooks.before_uninstall or []: + influxframework.get_attr(before_uninstall)() + + modules = influxframework.get_all("Module Def", filters={"app_name": app_name}, pluck="name") + + drop_doctypes = _delete_modules(modules, dry_run=dry_run) + _delete_doctypes(drop_doctypes, dry_run=dry_run) + + if not dry_run: + remove_from_installed_apps(app_name) + influxframework.get_single("Installed Applications").update_versions() + influxframework.db.commit() + + for after_uninstall in app_hooks.after_uninstall or []: + influxframework.get_attr(after_uninstall)() + + click.secho(f"Uninstalled App {app_name} from Site {site}", fg="green") + influxframework.flags.in_uninstall = False + + +def _delete_modules(modules: list[str], dry_run: bool) -> list[str]: + """Delete modules belonging to the app and all related doctypes. + + Note: All record linked linked to Module Def are also deleted. + + Returns: list of deleted doctypes.""" + drop_doctypes = [] + + doctype_link_field_map = _get_module_linked_doctype_field_map() + for module_name in modules: + print(f"Deleting Module '{module_name}'") + + for doctype in influxframework.get_all( + "DocType", filters={"module": module_name}, fields=["name", "issingle"] + ): + print(f"* removing DocType '{doctype.name}'...") + + if not dry_run: + if doctype.issingle: + influxframework.delete_doc("DocType", doctype.name, ignore_on_trash=True) + else: + drop_doctypes.append(doctype.name) + + _delete_linked_documents(module_name, doctype_link_field_map, dry_run=dry_run) + + print(f"* removing Module Def '{module_name}'...") + if not dry_run: + influxframework.delete_doc("Module Def", module_name, ignore_on_trash=True, force=True) + + return drop_doctypes + + +def _delete_linked_documents( + module_name: str, doctype_linkfield_map: dict[str, str], dry_run: bool +) -> None: + + """Deleted all records linked with module def""" + for doctype, fieldname in doctype_linkfield_map.items(): + for record in influxframework.get_all(doctype, filters={fieldname: module_name}, pluck="name"): + print(f"* removing {doctype} '{record}'...") + if not dry_run: + influxframework.delete_doc(doctype, record, ignore_on_trash=True, force=True) + + +def _get_module_linked_doctype_field_map() -> dict[str, str]: + """Get all the doctypes which have module linked with them. + + returns ordered dictionary with doctype->link field mapping.""" + + # Hardcoded to change order of deletion + ordered_doctypes = [ + ("Workspace", "module"), + ("Report", "module"), + ("Page", "module"), + ("Web Form", "module"), + ] + doctype_to_field_map = OrderedDict(ordered_doctypes) + + linked_doctypes = influxframework.get_all( + "DocField", + filters={"fieldtype": "Link", "options": "Module Def"}, + fields=["parent", "fieldname"], + ) + existing_linked_doctypes = [d for d in linked_doctypes if influxframework.db.exists("DocType", d.parent)] + + for d in existing_linked_doctypes: + # DocType deletion is handled separately in the end + if d.parent not in doctype_to_field_map and d.parent != "DocType": + doctype_to_field_map[d.parent] = d.fieldname + + return doctype_to_field_map + + +def _delete_doctypes(doctypes: list[str], dry_run: bool) -> None: + for doctype in set(doctypes): + print(f"* dropping Table for '{doctype}'...") + if not dry_run: + influxframework.delete_doc("DocType", doctype, ignore_on_trash=True) + influxframework.db.sql_ddl(f"DROP TABLE IF EXISTS `tab{doctype}`") + + +def post_install(rebuild_website=False): + from influxframework.website.utils import clear_website_cache + + if rebuild_website: + clear_website_cache() + + init_singles() + influxframework.db.commit() + influxframework.clear_cache() + + +def set_all_patches_as_completed(app): + from influxframework.modules.patch_handler import get_patches_from_app + + patches = get_patches_from_app(app) + for patch in patches: + influxframework.get_doc({"doctype": "Patch Log", "patch": patch}).insert(ignore_permissions=True) + influxframework.db.commit() + + +def init_singles(): + singles = influxframework.get_all("DocType", filters={"issingle": True}, pluck="name") + for single in singles: + if influxframework.db.get_singles_dict(single): + continue + + try: + doc = influxframework.new_doc(single) + doc.flags.ignore_mandatory = True + doc.flags.ignore_validate = True + doc.save() + except (ImportError, influxframework.DoesNotExistError): + # The doctype exists, but controller is deleted, + # no need to attempt to init such single, ref: #16917 + continue + + +def make_conf( + db_name=None, db_password=None, site_config=None, db_type=None, db_host=None, db_port=None +): + site = influxframework.local.site + make_site_config( + db_name, db_password, site_config, db_type=db_type, db_host=db_host, db_port=db_port + ) + sites_path = influxframework.local.sites_path + influxframework.destroy() + influxframework.init(site, sites_path=sites_path) + + +def make_site_config( + db_name=None, db_password=None, site_config=None, db_type=None, db_host=None, db_port=None +): + influxframework.create_folder(os.path.join(influxframework.local.site_path)) + site_file = get_site_config_path() + + if not os.path.exists(site_file): + if not (site_config and isinstance(site_config, dict)): + site_config = get_conf_params(db_name, db_password) + + if db_type: + site_config["db_type"] = db_type + + if db_host: + site_config["db_host"] = db_host + + if db_port: + site_config["db_port"] = db_port + + with open(site_file, "w") as f: + f.write(json.dumps(site_config, indent=1, sort_keys=True)) + + +def update_site_config(key, value, validate=True, site_config_path=None): + """Update a value in site_config""" + if not site_config_path: + site_config_path = get_site_config_path() + + with open(site_config_path) as f: + site_config = json.loads(f.read()) + + # In case of non-int value + if value in ("0", "1"): + value = int(value) + + # boolean + if value == "false": + value = False + if value == "true": + value = True + + # remove key if value is None + if value == "None": + if key in site_config: + del site_config[key] + else: + site_config[key] = value + + with open(site_config_path, "w") as f: + f.write(json.dumps(site_config, indent=1, sort_keys=True)) + + if hasattr(influxframework.local, "conf"): + influxframework.local.conf[key] = value + + +def get_site_config_path(): + return os.path.join(influxframework.local.site_path, "site_config.json") + + +def get_conf_params(db_name=None, db_password=None): + if not db_name: + db_name = input("Database Name: ") + if not db_name: + raise Exception("Database Name Required") + + if not db_password: + from influxframework.utils import random_string + + db_password = random_string(16) + + return {"db_name": db_name, "db_password": db_password} + + +def make_site_dirs(): + for dir_path in [ + os.path.join("public", "files"), + os.path.join("private", "backups"), + os.path.join("private", "files"), + "error-snapshots", + "locks", + "logs", + ]: + path = influxframework.get_site_path(dir_path) + os.makedirs(path, exist_ok=True) + + +def add_module_defs(app, ignore_if_duplicate=False): + modules = influxframework.get_module_list(app) + for module in modules: + d = influxframework.new_doc("Module Def") + d.app_name = app + d.module_name = module + d.insert(ignore_permissions=True, ignore_if_duplicate=ignore_if_duplicate) + + +def remove_missing_apps(): + import importlib + + apps = ("influxframework_subscription", "shopping_cart") + installed_apps = json.loads(influxframework.db.get_global("installed_apps") or "[]") + for app in apps: + if app in installed_apps: + try: + importlib.import_module(app) + + except ImportError: + installed_apps.remove(app) + influxframework.db.set_global("installed_apps", json.dumps(installed_apps)) + + +def extract_sql_from_archive(sql_file_path): + """Return the path of an SQL file if the passed argument is the path of a gzipped + SQL file or an SQL file path. The path may be absolute or relative from the bench + root directory or the sites sub-directory. + + Args: + sql_file_path (str): Path of the SQL file + + Returns: + str: Path of the decompressed SQL file + """ + from influxframework.utils import get_bench_relative_path + + sql_file_path = get_bench_relative_path(sql_file_path) + # Extract the gzip file if user has passed *.sql.gz file instead of *.sql file + if sql_file_path.endswith("sql.gz"): + decompressed_file_name = extract_sql_gzip(sql_file_path) + else: + decompressed_file_name = sql_file_path + + # convert archive sql to latest compatible + convert_archive_content(decompressed_file_name) + + return decompressed_file_name + + +def convert_archive_content(sql_file_path): + if influxframework.conf.db_type == "mariadb": + # ever since mariaDB 10.6, row_format COMPRESSED has been deprecated and removed + # this step is added to ease restoring sites depending on older mariaDB servers + from pathlib import Path + + from influxframework.utils import random_string + + old_sql_file_path = Path(f"{sql_file_path}_{random_string(10)}") + sql_file_path = Path(sql_file_path) + + os.rename(sql_file_path, old_sql_file_path) + sql_file_path.touch() + + with open(old_sql_file_path) as r, open(sql_file_path, "a") as w: + for line in r: + w.write(line.replace("ROW_FORMAT=COMPRESSED", "ROW_FORMAT=DYNAMIC")) + + old_sql_file_path.unlink() + + +def extract_sql_gzip(sql_gz_path): + import subprocess + + try: + original_file = sql_gz_path + decompressed_file = original_file.rstrip(".gz") + cmd = f"gzip --decompress --force < {original_file} > {decompressed_file}" + subprocess.check_call(cmd, shell=True) + except Exception: + raise + + return decompressed_file + + +def extract_files(site_name, file_path): + import shutil + import subprocess + + from influxframework.utils import get_bench_relative_path + + file_path = get_bench_relative_path(file_path) + + # Need to do influxframework.init to maintain the site locals + influxframework.init(site=site_name) + abs_site_path = os.path.abspath(influxframework.get_site_path()) + + # Copy the files to the parent directory and extract + shutil.copy2(os.path.abspath(file_path), abs_site_path) + + # Get the file name splitting the file path on + tar_name = os.path.split(file_path)[1] + tar_path = os.path.join(abs_site_path, tar_name) + + try: + if file_path.endswith(".tar"): + subprocess.check_output(["tar", "xvf", tar_path, "--strip", "2"], cwd=abs_site_path) + elif file_path.endswith(".tgz"): + subprocess.check_output(["tar", "zxvf", tar_path, "--strip", "2"], cwd=abs_site_path) + except Exception: + raise + finally: + influxframework.destroy() + + return tar_path + + +def is_downgrade(sql_file_path, verbose=False): + """checks if input db backup will get downgraded on current bench""" + + # This function is only tested with mariadb + # TODO: Add postgres support + if influxframework.conf.db_type not in (None, "mariadb"): + return False + + from semantic_version import Version + + head = "INSERT INTO `tabInstalled Application` VALUES" + + with open(sql_file_path) as f: + for line in f: + if head in line: + # 'line' (str) format: ('2056588823','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',1,'influxframework','v10.1.71-74 (3c50d5e) (v10.x.x)','v10.x.x'),('855c640b8e','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',2,'your_custom_app','0.0.1','master') + line = line.strip().lstrip(head).rstrip(";").strip() + app_rows = influxframework.safe_eval(line) + # check if iterable consists of tuples before trying to transform + apps_list = ( + app_rows + if all(isinstance(app_row, (tuple, list, set)) for app_row in app_rows) + else (app_rows,) + ) + # 'all_apps' (list) format: [('influxframework', '12.x.x-develop ()', 'develop'), ('your_custom_app', '0.0.1', 'master')] + all_apps = [x[-3:] for x in apps_list] + + for app in all_apps: + app_name = app[0] + app_version = app[1].split(" ")[0] + + if app_name == "influxframework": + try: + current_version = Version(influxframework.__version__) + backup_version = Version(app_version[1:] if app_version[0] == "v" else app_version) + except ValueError: + return False + + downgrade = backup_version > current_version + + if verbose and downgrade: + print(f"Your site will be downgraded from InfluxFramework {backup_version} to {current_version}") + + return downgrade + + +def is_partial(sql_file_path): + with open(sql_file_path) as f: + header = " ".join(f.readline() for _ in range(5)) + if "Partial Backup" in header: + return True + return False + + +def partial_restore(sql_file_path, verbose=False): + sql_file = extract_sql_from_archive(sql_file_path) + + if influxframework.conf.db_type in (None, "mariadb"): + from influxframework.database.mariadb.setup_db import import_db_from_sql + elif influxframework.conf.db_type == "postgres": + import warnings + + from influxframework.database.postgres.setup_db import import_db_from_sql + + warn = click.style( + "Delete the tables you want to restore manually before attempting" + " partial restore operation for PostreSQL databases", + fg="yellow", + ) + warnings.warn(warn) + + import_db_from_sql(source_sql=sql_file, verbose=verbose) + + # Removing temporarily created file + if sql_file != sql_file_path: + os.remove(sql_file) + + +def validate_database_sql(path, _raise=True): + """Check if file has contents and if DefaultValue table exists + + Args: + path (str): Path of the decompressed SQL file + _raise (bool, optional): Raise exception if invalid file. Defaults to True. + """ + empty_file = False + missing_table = True + + error_message = "" + + if not os.path.getsize(path): + error_message = f"{path} is an empty file!" + empty_file = True + + # dont bother checking if empty file + if not empty_file: + with open(path) as f: + for line in f: + if "tabDefaultValue" in line: + missing_table = False + break + + if missing_table: + error_message = "Table `tabDefaultValue` not found in file." + + if error_message: + click.secho(error_message, fg="red") + + if _raise and (missing_table or empty_file): + raise influxframework.InvalidDatabaseFile diff --git a/influxframework/integrations/__init__.py b/influxframework/integrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/integrations/doctype/__init__.py b/influxframework/integrations/doctype/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/integrations/doctype/connected_app/__init__.py b/influxframework/integrations/doctype/connected_app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/integrations/doctype/connected_app/connected_app.js b/influxframework/integrations/doctype/connected_app/connected_app.js new file mode 100644 index 0000000..5ef3534 --- /dev/null +++ b/influxframework/integrations/doctype/connected_app/connected_app.js @@ -0,0 +1,38 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Connected App", { + refresh: (frm) => { + frm.add_custom_button(__("Get OpenID Configuration"), async () => { + if (!frm.doc.openid_configuration) { + influxframework.msgprint(__("Please enter OpenID Configuration URL")); + } else { + try { + const response = await fetch(frm.doc.openid_configuration); + const oidc = await response.json(); + frm.set_value("authorization_uri", oidc.authorization_endpoint); + frm.set_value("token_uri", oidc.token_endpoint); + frm.set_value("userinfo_uri", oidc.userinfo_endpoint); + frm.set_value("introspection_uri", oidc.introspection_endpoint); + frm.set_value("revocation_uri", oidc.revocation_endpoint); + } catch (error) { + influxframework.msgprint(__("Please check OpenID Configuration URL")); + } + } + }); + + if (!frm.is_new()) { + frm.add_custom_button(__("Connect to {}", [frm.doc.provider_name]), async () => { + influxframework.call({ + method: "initiate_web_application_flow", + doc: frm.doc, + callback: function (r) { + window.open(r.message, "_blank"); + }, + }); + }); + } + + frm.toggle_display("sb_client_credentials_section", !frm.is_new()); + }, +}); diff --git a/influxframework/integrations/doctype/connected_app/connected_app.json b/influxframework/integrations/doctype/connected_app/connected_app.json new file mode 100644 index 0000000..b66cd90 --- /dev/null +++ b/influxframework/integrations/doctype/connected_app/connected_app.json @@ -0,0 +1,169 @@ +{ + "actions": [], + "beta": 1, + "creation": "2019-01-24 15:51:06.362222", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "provider_name", + "cb_00", + "openid_configuration", + "sb_client_credentials_section", + "client_id", + "redirect_uri", + "cb_01", + "client_secret", + "sb_scope_section", + "scopes", + "sb_endpoints_section", + "authorization_uri", + "token_uri", + "revocation_uri", + "cb_02", + "userinfo_uri", + "introspection_uri", + "section_break_18", + "query_parameters" + ], + "fields": [ + { + "fieldname": "provider_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Provider Name", + "reqd": 1 + }, + { + "fieldname": "cb_00", + "fieldtype": "Column Break" + }, + { + "fieldname": "openid_configuration", + "fieldtype": "Data", + "label": "OpenID Configuration" + }, + { + "collapsible": 1, + "fieldname": "sb_client_credentials_section", + "fieldtype": "Section Break", + "label": "Client Credentials" + }, + { + "fieldname": "client_id", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Client Id", + "mandatory_depends_on": "eval:doc.redirect_uri" + }, + { + "fieldname": "redirect_uri", + "fieldtype": "Data", + "label": "Redirect URI", + "read_only": 1 + }, + { + "fieldname": "cb_01", + "fieldtype": "Column Break" + }, + { + "fieldname": "client_secret", + "fieldtype": "Password", + "label": "Client Secret" + }, + { + "collapsible": 1, + "fieldname": "sb_scope_section", + "fieldtype": "Section Break", + "label": "Scopes" + }, + { + "collapsible": 1, + "fieldname": "sb_endpoints_section", + "fieldtype": "Section Break", + "label": "Endpoints" + }, + { + "fieldname": "cb_02", + "fieldtype": "Column Break" + }, + { + "fieldname": "scopes", + "fieldtype": "Table", + "label": "Scopes", + "options": "OAuth Scope" + }, + { + "fieldname": "authorization_uri", + "fieldtype": "Small Text", + "label": "Authorization URI", + "mandatory_depends_on": "eval:doc.redirect_uri" + }, + { + "fieldname": "token_uri", + "fieldtype": "Data", + "label": "Token URI", + "mandatory_depends_on": "eval:doc.redirect_uri" + }, + { + "fieldname": "revocation_uri", + "fieldtype": "Data", + "label": "Revocation URI" + }, + { + "fieldname": "userinfo_uri", + "fieldtype": "Data", + "label": "Userinfo URI" + }, + { + "fieldname": "introspection_uri", + "fieldtype": "Data", + "label": "Introspection URI" + }, + { + "fieldname": "section_break_18", + "fieldtype": "Section Break", + "label": "Extra Parameters" + }, + { + "fieldname": "query_parameters", + "fieldtype": "Table", + "label": "Query Parameters", + "options": "Query Parameters" + } + ], + "links": [ + { + "link_doctype": "Token Cache", + "link_fieldname": "connected_app" + } + ], + "modified": "2022-01-07 05:28:45.073041", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Connected App", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "read": 1, + "role": "All" + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "provider_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/integrations/doctype/connected_app/connected_app.py b/influxframework/integrations/doctype/connected_app/connected_app.py new file mode 100644 index 0000000..077ef39 --- /dev/null +++ b/influxframework/integrations/doctype/connected_app/connected_app.py @@ -0,0 +1,144 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +import os +from urllib.parse import urlencode, urljoin + +from requests_oauthlib import OAuth2Session + +import influxframework +from influxframework import _ +from influxframework.model.document import Document + +if any((os.getenv("CI"), influxframework.conf.developer_mode, influxframework.conf.allow_tests)): + # Disable mandatory TLS in developer mode and tests + os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" + + +class ConnectedApp(Document): + """Connect to a remote oAuth Server. Retrieve and store user's access token + in a Token Cache. + """ + + def validate(self): + base_url = influxframework.utils.get_url() + callback_path = ( + "/api/method/influxframework.integrations.doctype.connected_app.connected_app.callback/" + self.name + ) + self.redirect_uri = urljoin(base_url, callback_path) + + def get_oauth2_session(self, user=None, init=False): + """Return an auto-refreshing OAuth2 session which is an extension of a requests.Session()""" + token = None + token_updater = None + auto_refresh_kwargs = None + + if not init: + user = user or influxframework.session.user + token_cache = self.get_user_token(user) + token = token_cache.get_json() + token_updater = token_cache.update_data + auto_refresh_kwargs = {"client_id": self.client_id} + client_secret = self.get_password("client_secret") + if client_secret: + auto_refresh_kwargs["client_secret"] = client_secret + + return OAuth2Session( + client_id=self.client_id, + token=token, + token_updater=token_updater, + auto_refresh_url=self.token_uri, + auto_refresh_kwargs=auto_refresh_kwargs, + redirect_uri=self.redirect_uri, + scope=self.get_scopes(), + ) + + @influxframework.whitelist() + def initiate_web_application_flow(self, user=None, success_uri=None): + """Return an authorization URL for the user. Save state in Token Cache.""" + user = user or influxframework.session.user + oauth = self.get_oauth2_session(init=True) + query_params = self.get_query_params() + authorization_url, state = oauth.authorization_url(self.authorization_uri, **query_params) + token_cache = self.get_token_cache(user) + + if not token_cache: + token_cache = influxframework.new_doc("Token Cache") + token_cache.user = user + token_cache.connected_app = self.name + + token_cache.success_uri = success_uri + token_cache.state = state + token_cache.save(ignore_permissions=True) + influxframework.db.commit() + + return authorization_url + + def get_user_token(self, user=None, success_uri=None): + """Return an existing user token or initiate a Web Application Flow.""" + user = user or influxframework.session.user + token_cache = self.get_token_cache(user) + + if token_cache: + return token_cache + + redirect = self.initiate_web_application_flow(user, success_uri) + influxframework.local.response["type"] = "redirect" + influxframework.local.response["location"] = redirect + return redirect + + def get_token_cache(self, user): + token_cache = None + token_cache_name = self.name + "-" + user + + if influxframework.db.exists("Token Cache", token_cache_name): + token_cache = influxframework.get_doc("Token Cache", token_cache_name) + + return token_cache + + def get_scopes(self): + return [row.scope for row in self.scopes] + + def get_query_params(self): + return {param.key: param.value for param in self.query_parameters} + + +@influxframework.whitelist(allow_guest=True) +def callback(code=None, state=None): + """Handle client's code. + + Called during the oauthorization flow by the remote oAuth2 server to + transmit a code that can be used by the local server to obtain an access + token. + """ + if influxframework.request.method != "GET": + influxframework.throw(_("Invalid request method: {}").format(influxframework.request.method)) + + if influxframework.session.user == "Guest": + influxframework.local.response["type"] = "redirect" + influxframework.local.response["location"] = "/login?" + urlencode({"redirect-to": influxframework.request.url}) + return + + path = influxframework.request.path[1:].split("/") + if len(path) != 4 or not path[3]: + influxframework.throw(_("Invalid Parameters.")) + + connected_app = influxframework.get_doc("Connected App", path[3]) + token_cache = influxframework.get_doc("Token Cache", connected_app.name + "-" + influxframework.session.user) + + if state != token_cache.state: + influxframework.throw(_("Invalid state.")) + + oauth_session = connected_app.get_oauth2_session(init=True) + query_params = connected_app.get_query_params() + token = oauth_session.fetch_token( + connected_app.token_uri, + code=code, + client_secret=connected_app.get_password("client_secret"), + include_client_id=True, + **query_params + ) + token_cache.update_data(token) + + influxframework.local.response["type"] = "redirect" + influxframework.local.response["location"] = token_cache.get("success_uri") or connected_app.get_url() diff --git a/influxframework/integrations/doctype/connected_app/test_connected_app.py b/influxframework/integrations/doctype/connected_app/test_connected_app.py new file mode 100644 index 0000000..dacc2e8 --- /dev/null +++ b/influxframework/integrations/doctype/connected_app/test_connected_app.py @@ -0,0 +1,148 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +from urllib.parse import urljoin + +import requests + +import influxframework +from influxframework.integrations.doctype.social_login_key.test_social_login_key import ( + create_or_update_social_login_key, +) +from influxframework.tests.utils import InfluxFrameworkTestCase + + +def get_user(usr, pwd): + user = influxframework.new_doc("User") + user.email = usr + user.enabled = 1 + user.first_name = "_Test" + user.new_password = pwd + user.roles = [] + user.append("roles", {"doctype": "Has Role", "parentfield": "roles", "role": "System Manager"}) + user.insert() + + return user + + +def get_connected_app(): + doctype = "Connected App" + connected_app = influxframework.new_doc(doctype) + connected_app.provider_name = "influxframework" + connected_app.scopes = [] + connected_app.append("scopes", {"scope": "all"}) + connected_app.insert() + + return connected_app + + +def get_oauth_client(): + oauth_client = influxframework.new_doc("OAuth Client") + oauth_client.app_name = "_Test Connected App" + oauth_client.redirect_uris = "to be replaced" + oauth_client.default_redirect_uri = "to be replaced" + oauth_client.grant_type = "Authorization Code" + oauth_client.response_type = "Code" + oauth_client.skip_authorization = 1 + oauth_client.insert() + + return oauth_client + + +class TestConnectedApp(InfluxFrameworkTestCase): + def setUp(self): + """Set up a Connected App that connects to our own oAuth provider. + + InfluxFramework comes with it's own oAuth2 provider that we can test against. The + client credentials can be obtained from an "OAuth Client". All depends + on "Social Login Key" so we create one as well. + + The redirect URIs from "Connected App" and "OAuth Client" have to match. + InfluxFramework's "Authorization URL" and "Access Token URL" (actually they're + just endpoints) are stored in "Social Login Key" so we get them from + there. + """ + self.user_name = "test-connected-app@example.com" + self.user_password = "Eastern_43A1W" + + self.user = get_user(self.user_name, self.user_password) + self.connected_app = get_connected_app() + self.oauth_client = get_oauth_client() + social_login_key = create_or_update_social_login_key() + self.base_url = social_login_key.get("base_url") + + influxframework.db.commit() + self.connected_app.reload() + self.oauth_client.reload() + + redirect_uri = self.connected_app.get("redirect_uri") + self.oauth_client.update({"redirect_uris": redirect_uri, "default_redirect_uri": redirect_uri}) + self.oauth_client.save() + + self.connected_app.update( + { + "authorization_uri": urljoin(self.base_url, social_login_key.get("authorize_url")), + "client_id": self.oauth_client.get("client_id"), + "client_secret": self.oauth_client.get("client_secret"), + "token_uri": urljoin(self.base_url, social_login_key.get("access_token_url")), + } + ) + self.connected_app.save() + + influxframework.db.commit() + self.connected_app.reload() + self.oauth_client.reload() + + def test_web_application_flow(self): + """Simulate a logged in user who opens the authorization URL.""" + + def login(): + return session.get( + urljoin(self.base_url, "/api/method/login"), + params={"usr": self.user_name, "pwd": self.user_password}, + ) + + session = requests.Session() + + first_login = login() + self.assertEqual(first_login.status_code, 200) + + authorization_url = self.connected_app.initiate_web_application_flow(user=self.user_name) + + auth_response = session.get(authorization_url) + self.assertEqual(auth_response.status_code, 200) + + callback_response = session.get(auth_response.url) + self.assertEqual(callback_response.status_code, 200) + + self.token_cache = self.connected_app.get_token_cache(self.user_name) + token = self.token_cache.get_password("access_token") + self.assertNotEqual(token, None) + + oauth2_session = self.connected_app.get_oauth2_session(self.user_name) + resp = oauth2_session.get(urljoin(self.base_url, "/api/method/influxframework.auth.get_logged_user")) + self.assertEqual(resp.json().get("message"), self.user_name) + + def tearDown(self): + def delete_if_exists(attribute): + doc = getattr(self, attribute, None) + if doc: + doc.delete() + + delete_if_exists("token_cache") + delete_if_exists("connected_app") + + if getattr(self, "oauth_client", None): + tokens = influxframework.get_all("OAuth Bearer Token", filters={"client": self.oauth_client.name}) + for token in tokens: + doc = influxframework.get_doc("OAuth Bearer Token", token.name) + doc.delete() + + codes = influxframework.get_all("OAuth Authorization Code", filters={"client": self.oauth_client.name}) + for code in codes: + doc = influxframework.get_doc("OAuth Authorization Code", code.name) + doc.delete() + + delete_if_exists("user") + delete_if_exists("oauth_client") + + influxframework.db.commit() diff --git a/influxframework/integrations/doctype/connected_app/test_records.json b/influxframework/integrations/doctype/connected_app/test_records.json new file mode 100644 index 0000000..3d40e9a --- /dev/null +++ b/influxframework/integrations/doctype/connected_app/test_records.json @@ -0,0 +1,13 @@ +[ + { + "doctype": "Connected App", + "provider_name": "influxframework", + "client_id": "test_client_id", + "client_secret": "test_client_secret", + "scopes": [ + { + "scope": "all" + } + ] + } +] diff --git a/influxframework/integrations/doctype/dropbox_settings/__init__.py b/influxframework/integrations/doctype/dropbox_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/integrations/doctype/dropbox_settings/dropbox_settings.js b/influxframework/integrations/doctype/dropbox_settings/dropbox_settings.js new file mode 100644 index 0000000..1173855 --- /dev/null +++ b/influxframework/integrations/doctype/dropbox_settings/dropbox_settings.js @@ -0,0 +1,54 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Dropbox Settings", { + refresh: function (frm) { + frm.toggle_display( + ["app_access_key", "app_secret_key"], + !(frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config) + ); + frm.clear_custom_buttons(); + frm.events.take_backup(frm); + }, + + allow_dropbox_access: function (frm) { + if (frm.doc.app_access_key && frm.doc.app_secret_key) { + influxframework.call({ + method: "influxframework.integrations.doctype.dropbox_settings.dropbox_settings.get_dropbox_authorize_url", + freeze: true, + callback: function (r) { + if (!r.exc) { + window.open(r.message.auth_url); + } + }, + }); + } else if (frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config) { + influxframework.call({ + method: "influxframework.integrations.doctype.dropbox_settings.dropbox_settings.get_redirect_url", + freeze: true, + callback: function (r) { + if (!r.exc) { + window.open(r.message.auth_url); + } + }, + }); + } else { + influxframework.msgprint(__("Please enter values for App Access Key and App Secret Key")); + } + }, + + take_backup: function (frm) { + if ( + frm.doc.enabled && + ((frm.doc.app_access_key && frm.doc.app_secret_key) || + (frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config)) + ) { + frm.add_custom_button(__("Take Backup Now"), function (frm) { + influxframework.call({ + method: "influxframework.integrations.doctype.dropbox_settings.dropbox_settings.take_backup", + freeze: true, + }); + }).addClass("btn-primary"); + } + }, +}); diff --git a/influxframework/integrations/doctype/dropbox_settings/dropbox_settings.json b/influxframework/integrations/doctype/dropbox_settings/dropbox_settings.json new file mode 100644 index 0000000..8584696 --- /dev/null +++ b/influxframework/integrations/doctype/dropbox_settings/dropbox_settings.json @@ -0,0 +1,129 @@ +{ + "creation": "2016-09-21 10:12:57.399174", + "doctype": "DocType", + "document_type": "System", + "editable_grid": 1, + "field_order": [ + "enabled", + "send_notifications_to", + "send_email_for_successful_backup", + "backup_frequency", + "limit_no_of_backups", + "no_of_backups", + "file_backup", + "app_access_key", + "app_secret_key", + "allow_dropbox_access", + "dropbox_access_key", + "dropbox_access_secret", + "dropbox_access_token" + ], + "fields": [ + { + "default": "0", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enabled" + }, + { + "fieldname": "send_notifications_to", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Send Notifications To", + "reqd": 1 + }, + { + "default": "1", + "description": "Note: By default emails for failed backups are sent.", + "fieldname": "send_email_for_successful_backup", + "fieldtype": "Check", + "label": "Send Email for Successful Backup" + }, + { + "fieldname": "backup_frequency", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Backup Frequency", + "options": "\nDaily\nWeekly", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "limit_no_of_backups", + "fieldtype": "Check", + "label": "Limit Number of DB Backups" + }, + { + "default": "5", + "depends_on": "eval:doc.limit_no_of_backups", + "fieldname": "no_of_backups", + "fieldtype": "Int", + "label": "Number of DB Backups" + }, + { + "default": "1", + "fieldname": "file_backup", + "fieldtype": "Check", + "label": "File Backup" + }, + { + "fieldname": "app_access_key", + "fieldtype": "Data", + "label": "App Access Key" + }, + { + "fieldname": "app_secret_key", + "fieldtype": "Password", + "label": "App Secret Key" + }, + { + "fieldname": "allow_dropbox_access", + "fieldtype": "Button", + "label": "Allow Dropbox Access" + }, + { + "fieldname": "dropbox_access_key", + "fieldtype": "Password", + "hidden": 1, + "label": "Dropbox Access Key", + "read_only": 1 + }, + { + "fieldname": "dropbox_access_secret", + "fieldtype": "Password", + "hidden": 1, + "label": "Dropbox Access Secret", + "read_only": 1 + }, + { + "fieldname": "dropbox_access_token", + "fieldtype": "Password", + "hidden": 1, + "label": "Dropbox Access Token" + } + ], + "in_create": 1, + "issingle": 1, + "modified": "2019-08-22 16:26:44.468391", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Dropbox Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/integrations/doctype/dropbox_settings/dropbox_settings.py b/influxframework/integrations/doctype/dropbox_settings/dropbox_settings.py new file mode 100644 index 0000000..b866269 --- /dev/null +++ b/influxframework/integrations/doctype/dropbox_settings/dropbox_settings.py @@ -0,0 +1,412 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import json +import os +from urllib.parse import parse_qs, urlparse + +import dropbox +from rq.timeouts import JobTimeoutException + +import influxframework +from influxframework import _ +from influxframework.integrations.offsite_backup_utils import ( + get_chunk_site, + get_latest_backup_file, + send_email, + validate_file_size, +) +from influxframework.integrations.utils import make_post_request +from influxframework.model.document import Document +from influxframework.utils import ( + cint, + encode, + get_backups_path, + get_files_path, + get_request_site_address, + get_url, +) +from influxframework.utils.background_jobs import enqueue +from influxframework.utils.backups import new_backup + +ignore_list = [".DS_Store"] + + +class DropboxSettings(Document): + def onload(self): + if not self.app_access_key and influxframework.conf.dropbox_access_key: + self.set_onload("dropbox_setup_via_site_config", 1) + + def validate(self): + if self.enabled and self.limit_no_of_backups and self.no_of_backups < 1: + influxframework.throw(_("Number of DB backups cannot be less than 1")) + + +@influxframework.whitelist() +def take_backup(): + """Enqueue longjob for taking backup to dropbox""" + enqueue( + "influxframework.integrations.doctype.dropbox_settings.dropbox_settings.take_backup_to_dropbox", + queue="long", + timeout=1500, + ) + influxframework.msgprint(_("Queued for backup. It may take a few minutes to an hour.")) + + +def take_backups_daily(): + take_backups_if("Daily") + + +def take_backups_weekly(): + take_backups_if("Weekly") + + +def take_backups_if(freq): + if influxframework.db.get_single_value("Dropbox Settings", "backup_frequency") == freq: + take_backup_to_dropbox() + + +def take_backup_to_dropbox(retry_count=0, upload_db_backup=True): + did_not_upload, error_log = [], [] + try: + if cint(influxframework.db.get_single_value("Dropbox Settings", "enabled")): + validate_file_size() + + did_not_upload, error_log = backup_to_dropbox(upload_db_backup) + if did_not_upload: + raise Exception + + if cint(influxframework.db.get_single_value("Dropbox Settings", "send_email_for_successful_backup")): + send_email(True, "Dropbox", "Dropbox Settings", "send_notifications_to") + except JobTimeoutException: + if retry_count < 2: + args = { + "retry_count": retry_count + 1, + "upload_db_backup": False, # considering till worker timeout db backup is uploaded + } + enqueue( + "influxframework.integrations.doctype.dropbox_settings.dropbox_settings.take_backup_to_dropbox", + queue="long", + timeout=1500, + **args, + ) + except Exception: + if isinstance(error_log, str): + error_message = error_log + "\n" + influxframework.get_traceback() + else: + file_and_error = [" - ".join(f) for f in zip(did_not_upload, error_log)] + error_message = "\n".join(file_and_error) + "\n" + influxframework.get_traceback() + + send_email(False, "Dropbox", "Dropbox Settings", "send_notifications_to", error_message) + + +def backup_to_dropbox(upload_db_backup=True): + if not influxframework.db: + influxframework.connect() + + # upload database + dropbox_settings = get_dropbox_settings() + + if not dropbox_settings["access_token"]: + access_token = generate_oauth2_access_token_from_oauth1_token(dropbox_settings) + + if not access_token.get("oauth2_token"): + return ( + "Failed backup upload", + "No Access Token exists! Please generate the access token for Dropbox.", + ) + + dropbox_settings["access_token"] = access_token["oauth2_token"] + set_dropbox_access_token(access_token["oauth2_token"]) + + dropbox_client = dropbox.Dropbox( + oauth2_access_token=dropbox_settings["access_token"], timeout=None + ) + + if upload_db_backup: + if influxframework.flags.create_new_backup: + backup = new_backup(ignore_files=True) + filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_db)) + site_config = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_conf)) + else: + filename, site_config = get_latest_backup_file() + + upload_file_to_dropbox(filename, "/database", dropbox_client) + upload_file_to_dropbox(site_config, "/database", dropbox_client) + + # delete older databases + if dropbox_settings["no_of_backups"]: + delete_older_backups(dropbox_client, "/database", dropbox_settings["no_of_backups"]) + + # upload files to files folder + did_not_upload = [] + error_log = [] + + if dropbox_settings["file_backup"]: + upload_from_folder(get_files_path(), 0, "/files", dropbox_client, did_not_upload, error_log) + upload_from_folder( + get_files_path(is_private=1), 1, "/private/files", dropbox_client, did_not_upload, error_log + ) + + return did_not_upload, list(set(error_log)) + + +def upload_from_folder( + path, is_private, dropbox_folder, dropbox_client, did_not_upload, error_log +): + if not os.path.exists(path): + return + + if is_fresh_upload(): + response = get_uploaded_files_meta(dropbox_folder, dropbox_client) + else: + response = influxframework._dict({"entries": []}) + + path = str(path) + + for f in influxframework.get_all( + "File", + filters={"is_folder": 0, "is_private": is_private, "uploaded_to_dropbox": 0}, + fields=["file_url", "name", "file_name"], + ): + if not f.file_url: + continue + filename = f.file_url.rsplit("/", 1)[-1] + + filepath = os.path.join(path, filename) + + if filename in ignore_list: + continue + + found = False + for file_metadata in response.entries: + try: + if os.path.basename(filepath) == file_metadata.name and os.stat( + encode(filepath) + ).st_size == int(file_metadata.size): + found = True + update_file_dropbox_status(f.name) + break + except Exception: + error_log.append(influxframework.get_traceback()) + + if not found: + try: + upload_file_to_dropbox(filepath, dropbox_folder, dropbox_client) + update_file_dropbox_status(f.name) + except Exception: + did_not_upload.append(filepath) + error_log.append(influxframework.get_traceback()) + + +def upload_file_to_dropbox(filename, folder, dropbox_client): + """upload files with chunk of 15 mb to reduce session append calls""" + if not os.path.exists(filename): + return + + create_folder_if_not_exists(folder, dropbox_client) + file_size = os.path.getsize(encode(filename)) + chunk_size = get_chunk_site(file_size) + + mode = dropbox.files.WriteMode.overwrite + + f = open(encode(filename), "rb") + path = f"{folder}/{os.path.basename(filename)}" + + try: + if file_size <= chunk_size: + dropbox_client.files_upload(f.read(), path, mode) + else: + upload_session_start_result = dropbox_client.files_upload_session_start(f.read(chunk_size)) + cursor = dropbox.files.UploadSessionCursor( + session_id=upload_session_start_result.session_id, offset=f.tell() + ) + commit = dropbox.files.CommitInfo(path=path, mode=mode) + + while f.tell() < file_size: + if (file_size - f.tell()) <= chunk_size: + dropbox_client.files_upload_session_finish(f.read(chunk_size), cursor, commit) + else: + dropbox_client.files_upload_session_append( + f.read(chunk_size), cursor.session_id, cursor.offset + ) + cursor.offset = f.tell() + except dropbox.exceptions.ApiError as e: + if isinstance(e.error, dropbox.files.UploadError): + error = f"File Path: {path}\n" + error += influxframework.get_traceback() + influxframework.log_error(error) + else: + raise + + +def create_folder_if_not_exists(folder, dropbox_client): + try: + dropbox_client.files_get_metadata(folder) + except dropbox.exceptions.ApiError as e: + # folder not found + if isinstance(e.error, dropbox.files.GetMetadataError): + dropbox_client.files_create_folder(folder) + else: + raise + + +def update_file_dropbox_status(file_name): + influxframework.db.set_value("File", file_name, "uploaded_to_dropbox", 1, update_modified=False) + + +def is_fresh_upload(): + file_name = influxframework.db.get_value("File", {"uploaded_to_dropbox": 1}, "name") + return not file_name + + +def get_uploaded_files_meta(dropbox_folder, dropbox_client): + try: + return dropbox_client.files_list_folder(dropbox_folder) + except dropbox.exceptions.ApiError as e: + # folder not found + if isinstance(e.error, dropbox.files.ListFolderError): + return influxframework._dict({"entries": []}) + else: + raise + + +def get_dropbox_settings(redirect_uri=False): + if not influxframework.conf.dropbox_broker_site: + influxframework.conf.dropbox_broker_site = "https://dropbox.influxerp.com" + settings = influxframework.get_doc("Dropbox Settings") + app_details = { + "app_key": settings.app_access_key or influxframework.conf.dropbox_access_key, + "app_secret": settings.get_password(fieldname="app_secret_key", raise_exception=False) + if settings.app_secret_key + else influxframework.conf.dropbox_secret_key, + "access_token": settings.get_password("dropbox_access_token", raise_exception=False) + if settings.dropbox_access_token + else "", + "access_key": settings.get_password("dropbox_access_key", raise_exception=False), + "access_secret": settings.get_password("dropbox_access_secret", raise_exception=False), + "file_backup": settings.file_backup, + "no_of_backups": settings.no_of_backups if settings.limit_no_of_backups else None, + } + + if redirect_uri: + app_details.update( + { + "redirect_uri": get_request_site_address(True) + + "/api/method/influxframework.integrations.doctype.dropbox_settings.dropbox_settings.dropbox_auth_finish" + if settings.app_secret_key + else influxframework.conf.dropbox_broker_site + + "/api/method/dropbox_influxerp_broker.www.setup_dropbox.generate_dropbox_access_token", + } + ) + + if not app_details["app_key"] or not app_details["app_secret"]: + raise Exception(_("Please set Dropbox access keys in your site config")) + + return app_details + + +def delete_older_backups(dropbox_client, folder_path, to_keep): + res = dropbox_client.files_list_folder(path=folder_path) + files = [] + for f in res.entries: + if isinstance(f, dropbox.files.FileMetadata) and "sql" in f.name: + files.append(f) + + if len(files) <= to_keep: + return + + files.sort(key=lambda item: item.client_modified, reverse=True) + for f in files[to_keep:]: + dropbox_client.files_delete(os.path.join(folder_path, f.name)) + + +@influxframework.whitelist() +def get_redirect_url(): + if not influxframework.conf.dropbox_broker_site: + influxframework.conf.dropbox_broker_site = "https://dropbox.influxerp.com" + url = "{}/api/method/dropbox_influxerp_broker.www.setup_dropbox.get_authotize_url".format( + influxframework.conf.dropbox_broker_site + ) + + try: + response = make_post_request(url, data={"site": get_url()}) + if response.get("message"): + return response["message"] + + except Exception: + influxframework.log_error() + influxframework.throw( + _( + "Something went wrong while generating dropbox access token. Please check error log for more details." + ) + ) + + +@influxframework.whitelist() +def get_dropbox_authorize_url(): + app_details = get_dropbox_settings(redirect_uri=True) + dropbox_oauth_flow = dropbox.DropboxOAuth2Flow( + consumer_key=app_details["app_key"], + redirect_uri=app_details["redirect_uri"], + session={}, + csrf_token_session_key="dropbox-auth-csrf-token", + consumer_secret=app_details["app_secret"], + ) + + auth_url = dropbox_oauth_flow.start() + + return {"auth_url": auth_url, "args": parse_qs(urlparse(auth_url).query)} + + +@influxframework.whitelist() +def dropbox_auth_finish(return_access_token=False): + app_details = get_dropbox_settings(redirect_uri=True) + callback = influxframework.form_dict + close = '

      ' + _("Please close this window") + "

      " + + dropbox_oauth_flow = dropbox.DropboxOAuth2Flow( + consumer_key=app_details["app_key"], + redirect_uri=app_details["redirect_uri"], + session={"dropbox-auth-csrf-token": callback.state}, + csrf_token_session_key="dropbox-auth-csrf-token", + consumer_secret=app_details["app_secret"], + ) + + if callback.state or callback.code: + token = dropbox_oauth_flow.finish({"state": callback.state, "code": callback.code}) + if return_access_token and token.access_token: + return token.access_token, callback.state + + set_dropbox_access_token(token.access_token) + else: + influxframework.respond_as_web_page( + _("Dropbox Setup"), + _("Illegal Access Token. Please try again") + close, + indicator_color="red", + http_status_code=influxframework.AuthenticationError.http_status_code, + ) + + influxframework.respond_as_web_page( + _("Dropbox Setup"), _("Dropbox access is approved!") + close, indicator_color="green" + ) + + +def set_dropbox_access_token(access_token): + influxframework.db.set_value("Dropbox Settings", None, "dropbox_access_token", access_token) + influxframework.db.commit() + + +def generate_oauth2_access_token_from_oauth1_token(dropbox_settings=None): + if not dropbox_settings.get("access_key") or not dropbox_settings.get("access_secret"): + return {} + + url = "https://api.dropboxapi.com/2/auth/token/from_oauth1" + headers = {"Content-Type": "application/json"} + auth = (dropbox_settings["app_key"], dropbox_settings["app_secret"]) + data = { + "oauth1_token": dropbox_settings["access_key"], + "oauth1_token_secret": dropbox_settings["access_secret"], + } + + return make_post_request(url, auth=auth, headers=headers, data=json.dumps(data)) diff --git a/influxframework/integrations/doctype/dropbox_settings/test_dropbox_settings.py b/influxframework/integrations/doctype/dropbox_settings/test_dropbox_settings.py new file mode 100644 index 0000000..61940a2 --- /dev/null +++ b/influxframework/integrations/doctype/dropbox_settings/test_dropbox_settings.py @@ -0,0 +1,8 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestDropboxSettings(InfluxFrameworkTestCase): + pass diff --git a/influxframework/integrations/doctype/google_calendar/__init__.py b/influxframework/integrations/doctype/google_calendar/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/integrations/doctype/google_calendar/google_calendar.js b/influxframework/integrations/doctype/google_calendar/google_calendar.js new file mode 100644 index 0000000..1a50f35 --- /dev/null +++ b/influxframework/integrations/doctype/google_calendar/google_calendar.js @@ -0,0 +1,67 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Google Calendar", { + refresh: function (frm) { + if (frm.is_new()) { + frm.dashboard.set_headline( + __("To use Google Calendar, enable {0}.", [ + `${__("Google Settings")}`, + ]) + ); + } + + influxframework.realtime.on("import_google_calendar", (data) => { + if (data.progress) { + frm.dashboard.show_progress( + "Syncing Google Calendar", + (data.progress / data.total) * 100, + __("Syncing {0} of {1}", [data.progress, data.total]) + ); + if (data.progress === data.total) { + frm.dashboard.hide_progress("Syncing Google Calendar"); + } + } + }); + + if (frm.doc.refresh_token) { + frm.add_custom_button(__("Sync Calendar"), function () { + influxframework.show_alert({ + indicator: "green", + message: __("Syncing"), + }); + influxframework + .call({ + method: "influxframework.integrations.doctype.google_calendar.google_calendar.sync", + args: { + g_calendar: frm.doc.name, + }, + }) + .then((r) => { + influxframework.hide_progress(); + influxframework.msgprint(r.message); + }); + }); + } + }, + authorize_google_calendar_access: function (frm) { + let reauthorize = 0; + if (frm.doc.authorization_code) { + reauthorize = 1; + } + + influxframework.call({ + method: "influxframework.integrations.doctype.google_calendar.google_calendar.authorize_access", + args: { + g_calendar: frm.doc.name, + reauthorize: reauthorize, + }, + callback: function (r) { + if (!r.exc) { + frm.save(); + window.open(r.message.url); + } + }, + }); + }, +}); diff --git a/influxframework/integrations/doctype/google_calendar/google_calendar.json b/influxframework/integrations/doctype/google_calendar/google_calendar.json new file mode 100644 index 0000000..2c34e69 --- /dev/null +++ b/influxframework/integrations/doctype/google_calendar/google_calendar.json @@ -0,0 +1,146 @@ +{ + "autoname": "field:calendar_name", + "creation": "2019-07-06 17:54:09.450100", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "enable", + "sb_00", + "calendar_name", + "user", + "authorize_google_calendar_access", + "sb_01", + "pull_from_google_calendar", + "cb_01", + "push_to_google_calendar", + "section_break_3", + "google_calendar_id", + "refresh_token", + "authorization_code", + "next_sync_token" + ], + "fields": [ + { + "default": "1", + "fieldname": "enable", + "fieldtype": "Check", + "label": "Enable" + }, + { + "depends_on": "eval: doc.enable", + "fieldname": "sb_00", + "fieldtype": "Section Break", + "label": "Google Calendar" + }, + { + "fieldname": "user", + "fieldtype": "Link", + "in_list_view": 1, + "label": "User", + "options": "User", + "reqd": 1 + }, + { + "description": "The name that will appear in Google Calendar", + "fieldname": "calendar_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Calendar Name", + "reqd": 1, + "unique": 1 + }, + { + "depends_on": "eval: doc.enable", + "fieldname": "section_break_3", + "fieldtype": "Section Break" + }, + { + "fieldname": "refresh_token", + "fieldtype": "Password", + "hidden": 1, + "label": "Refresh Token" + }, + { + "fieldname": "authorization_code", + "fieldtype": "Password", + "hidden": 1, + "label": "Authorization Code" + }, + { + "fieldname": "next_sync_token", + "fieldtype": "Password", + "hidden": 1, + "label": "Next Sync Token" + }, + { + "fieldname": "google_calendar_id", + "fieldtype": "Data", + "label": "Google Calendar ID", + "read_only": 1 + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "authorize_google_calendar_access", + "fieldtype": "Button", + "label": "Authorize Google Calendar Access" + }, + { + "depends_on": "eval: doc.enable", + "fieldname": "sb_01", + "fieldtype": "Section Break", + "label": "Sync" + }, + { + "default": "1", + "fieldname": "pull_from_google_calendar", + "fieldtype": "Check", + "label": "Pull from Google Calendar" + }, + { + "fieldname": "cb_01", + "fieldtype": "Column Break" + }, + { + "default": "1", + "fieldname": "push_to_google_calendar", + "fieldtype": "Check", + "label": "Push to Google Calendar" + } + ], + "modified": "2019-08-08 15:44:15.798362", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Google Calendar", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/integrations/doctype/google_calendar/google_calendar.py b/influxframework/integrations/doctype/google_calendar/google_calendar.py new file mode 100644 index 0000000..350b064 --- /dev/null +++ b/influxframework/integrations/doctype/google_calendar/google_calendar.py @@ -0,0 +1,745 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + + +from datetime import datetime, timedelta +from urllib.parse import quote +from zoneinfo import ZoneInfo + +import google.oauth2.credentials +import requests +from dateutil import parser +from googleapiclient.discovery import build +from googleapiclient.errors import HttpError + +import influxframework +from influxframework import _ +from influxframework.integrations.google_oauth import GoogleOAuth +from influxframework.model.document import Document +from influxframework.utils import ( + add_days, + add_to_date, + get_datetime, + get_request_site_address, + get_time_zone, + get_weekdays, + now_datetime, +) +from influxframework.utils.password import set_encrypted_password + +SCOPES = "https://www.googleapis.com/auth/calendar" + +google_calendar_frequencies = { + "RRULE:FREQ=DAILY": "Daily", + "RRULE:FREQ=WEEKLY": "Weekly", + "RRULE:FREQ=MONTHLY": "Monthly", + "RRULE:FREQ=YEARLY": "Yearly", +} + +google_calendar_days = { + "MO": "monday", + "TU": "tuesday", + "WE": "wednesday", + "TH": "thursday", + "FR": "friday", + "SA": "saturday", + "SU": "sunday", +} + +framework_frequencies = { + "Daily": "RRULE:FREQ=DAILY;", + "Weekly": "RRULE:FREQ=WEEKLY;", + "Monthly": "RRULE:FREQ=MONTHLY;", + "Yearly": "RRULE:FREQ=YEARLY;", +} + +framework_days = { + "monday": "MO", + "tuesday": "TU", + "wednesday": "WE", + "thursday": "TH", + "friday": "FR", + "saturday": "SA", + "sunday": "SU", +} + + +class GoogleCalendar(Document): + def validate(self): + google_settings = influxframework.get_single("Google Settings") + if not google_settings.enable: + influxframework.throw(_("Enable Google API in Google Settings.")) + + if not google_settings.client_id or not google_settings.client_secret: + influxframework.throw(_("Enter Client Id and Client Secret in Google Settings.")) + + return google_settings + + def get_access_token(self): + google_settings = self.validate() + + if not self.refresh_token: + button_label = influxframework.bold(_("Allow Google Calendar Access")) + raise influxframework.ValidationError(_("Click on {0} to generate Refresh Token.").format(button_label)) + + data = { + "client_id": google_settings.client_id, + "client_secret": google_settings.get_password(fieldname="client_secret", raise_exception=False), + "refresh_token": self.get_password(fieldname="refresh_token", raise_exception=False), + "grant_type": "refresh_token", + "scope": SCOPES, + } + + try: + r = requests.post(GoogleOAuth.OAUTH_URL, data=data).json() + except requests.exceptions.HTTPError: + button_label = influxframework.bold(_("Allow Google Calendar Access")) + influxframework.throw( + _( + "Something went wrong during the token generation. Click on {0} to generate a new one." + ).format(button_label) + ) + + return r.get("access_token") + + +@influxframework.whitelist() +def authorize_access(g_calendar, reauthorize=None): + """ + If no Authorization code get it from Google and then request for Refresh Token. + Google Calendar Name is set to flags to set_value after Authorization Code is obtained. + """ + google_settings = influxframework.get_doc("Google Settings") + google_calendar = influxframework.get_doc("Google Calendar", g_calendar) + + redirect_uri = ( + get_request_site_address(True) + + "?cmd=influxframework.integrations.doctype.google_calendar.google_calendar.google_callback" + ) + + if not google_calendar.authorization_code or reauthorize: + influxframework.cache().hset("google_calendar", "google_calendar", google_calendar.name) + return get_authentication_url(client_id=google_settings.client_id, redirect_uri=redirect_uri) + else: + try: + data = { + "code": google_calendar.get_password(fieldname="authorization_code", raise_exception=False), + "client_id": google_settings.client_id, + "client_secret": google_settings.get_password( + fieldname="client_secret", raise_exception=False + ), + "redirect_uri": redirect_uri, + "grant_type": "authorization_code", + } + r = requests.post(GoogleOAuth.OAUTH_URL, data=data).json() + + if "refresh_token" in r: + influxframework.db.set_value( + "Google Calendar", google_calendar.name, "refresh_token", r.get("refresh_token") + ) + influxframework.db.commit() + + influxframework.local.response["type"] = "redirect" + influxframework.local.response["location"] = "/app/Form/{}/{}".format( + quote("Google Calendar"), quote(google_calendar.name) + ) + + influxframework.msgprint(_("Google Calendar has been configured.")) + except Exception as e: + influxframework.throw(e) + + +def get_authentication_url(client_id=None, redirect_uri=None): + return { + "url": "https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&response_type=code&prompt=consent&client_id={}&include_granted_scopes=true&scope={}&redirect_uri={}".format( + client_id, SCOPES, redirect_uri + ) + } + + +@influxframework.whitelist() +def google_callback(code=None): + """ + Authorization code is sent to callback as per the API configuration + """ + google_calendar = influxframework.cache().hget("google_calendar", "google_calendar") + influxframework.db.set_value("Google Calendar", google_calendar, "authorization_code", code) + influxframework.db.commit() + + authorize_access(google_calendar) + + +@influxframework.whitelist() +def sync(g_calendar=None): + filters = {"enable": 1} + + if g_calendar: + filters.update({"name": g_calendar}) + + google_calendars = influxframework.get_list("Google Calendar", filters=filters) + + for g in google_calendars: + return sync_events_from_google_calendar(g.name) + + +def get_google_calendar_object(g_calendar): + """ + Returns an object of Google Calendar along with Google Calendar doc. + """ + google_settings = influxframework.get_doc("Google Settings") + account = influxframework.get_doc("Google Calendar", g_calendar) + + credentials_dict = { + "token": account.get_access_token(), + "refresh_token": account.get_password(fieldname="refresh_token", raise_exception=False), + "token_uri": GoogleOAuth.OAUTH_URL, + "client_id": google_settings.client_id, + "client_secret": google_settings.get_password(fieldname="client_secret", raise_exception=False), + "scopes": "https://www.googleapis.com/auth/calendar/v3", + } + + credentials = google.oauth2.credentials.Credentials(**credentials_dict) + google_calendar = build( + serviceName="calendar", version="v3", credentials=credentials, static_discovery=False + ) + + check_google_calendar(account, google_calendar) + + account.load_from_db() + return google_calendar, account + + +def check_google_calendar(account, google_calendar): + """ + Checks if Google Calendar is present with the specified name. + If not, creates one. + """ + account.load_from_db() + try: + if account.google_calendar_id: + google_calendar.calendars().get(calendarId=account.google_calendar_id).execute() + else: + # If no Calendar ID create a new Calendar + calendar = { + "summary": account.calendar_name, + "timeZone": influxframework.db.get_single_value("System Settings", "time_zone"), + } + created_calendar = google_calendar.calendars().insert(body=calendar).execute() + influxframework.db.set_value( + "Google Calendar", account.name, "google_calendar_id", created_calendar.get("id") + ) + influxframework.db.commit() + except HttpError as err: + influxframework.throw( + _("Google Calendar - Could not create Calendar for {0}, error code {1}.").format( + account.name, err.resp.status + ) + ) + + +def sync_events_from_google_calendar(g_calendar, method=None): + """ + Syncs Events from Google Calendar in Framework Calendar. + Google Calendar returns nextSyncToken when all the events in Google Calendar are fetched. + nextSyncToken is returned at the very last page + https://developers.google.com/calendar/v3/sync + """ + google_calendar, account = get_google_calendar_object(g_calendar) + + if not account.pull_from_google_calendar: + return + + sync_token = account.get_password(fieldname="next_sync_token", raise_exception=False) or None + events = influxframework._dict() + results = [] + while True: + try: + # API Response listed at EOF + events = ( + google_calendar.events() + .list( + calendarId=account.google_calendar_id, + maxResults=2000, + pageToken=events.get("nextPageToken"), + singleEvents=False, + showDeleted=True, + syncToken=sync_token, + ) + .execute() + ) + except HttpError as err: + msg = _("Google Calendar - Could not fetch event from Google Calendar, error code {0}.").format( + err.resp.status + ) + + if err.resp.status == 410: + set_encrypted_password("Google Calendar", account.name, "", "next_sync_token") + influxframework.db.commit() + msg += " " + _("Sync token was invalid and has been resetted, Retry syncing.") + influxframework.msgprint(msg, title="Invalid Sync Token", indicator="blue") + else: + influxframework.throw(msg) + + for event in events.get("items", []): + results.append(event) + + if not events.get("nextPageToken"): + if events.get("nextSyncToken"): + account.next_sync_token = events.get("nextSyncToken") + account.save() + break + + for idx, event in enumerate(results): + influxframework.publish_realtime( + "import_google_calendar", dict(progress=idx + 1, total=len(results)), user=influxframework.session.user + ) + + # If Google Calendar Event if confirmed, then create an Event + if event.get("status") == "confirmed": + recurrence = None + if event.get("recurrence"): + try: + recurrence = event.get("recurrence")[0] + except IndexError: + pass + + if not influxframework.db.exists("Event", {"google_calendar_event_id": event.get("id")}): + insert_event_to_calendar(account, event, recurrence) + else: + update_event_in_calendar(account, event, recurrence) + elif event.get("status") == "cancelled": + # If any synced Google Calendar Event is cancelled, then close the Event + influxframework.db.set_value( + "Event", + { + "google_calendar_id": account.google_calendar_id, + "google_calendar_event_id": event.get("id"), + }, + "status", + "Closed", + ) + influxframework.get_doc( + { + "doctype": "Comment", + "comment_type": "Info", + "reference_doctype": "Event", + "reference_name": influxframework.db.get_value( + "Event", + { + "google_calendar_id": account.google_calendar_id, + "google_calendar_event_id": event.get("id"), + }, + "name", + ), + "content": " - Event deleted from Google Calendar.", + } + ).insert(ignore_permissions=True) + else: + pass + + if not results: + return _("No Google Calendar Event to sync.") + elif len(results) == 1: + return _("1 Google Calendar Event synced.") + else: + return _("{0} Google Calendar Events synced.").format(len(results)) + + +def insert_event_to_calendar(account, event, recurrence=None): + """ + Inserts event in InfluxFramework Calendar during Sync + """ + calendar_event = { + "doctype": "Event", + "subject": event.get("summary"), + "description": event.get("description"), + "google_calendar_event": 1, + "google_calendar": account.name, + "google_calendar_id": account.google_calendar_id, + "google_calendar_event_id": event.get("id"), + "pulled_from_google_calendar": 1, + } + calendar_event.update( + google_calendar_to_repeat_on( + recurrence=recurrence, start=event.get("start"), end=event.get("end") + ) + ) + influxframework.get_doc(calendar_event).insert(ignore_permissions=True) + + +def update_event_in_calendar(account, event, recurrence=None): + """ + Updates Event in InfluxFramework Calendar if any existing Google Calendar Event is updated + """ + calendar_event = influxframework.get_doc("Event", {"google_calendar_event_id": event.get("id")}) + calendar_event.subject = event.get("summary") + calendar_event.description = event.get("description") + calendar_event.update( + google_calendar_to_repeat_on( + recurrence=recurrence, start=event.get("start"), end=event.get("end") + ) + ) + calendar_event.save(ignore_permissions=True) + + +def insert_event_in_google_calendar(doc, method=None): + """ + Insert Events in Google Calendar if sync_with_google_calendar is checked. + """ + if ( + not influxframework.db.exists("Google Calendar", {"name": doc.google_calendar}) + or doc.pulled_from_google_calendar + or not doc.sync_with_google_calendar + ): + return + + google_calendar, account = get_google_calendar_object(doc.google_calendar) + + if not account.push_to_google_calendar: + return + + event = {"summary": doc.subject, "description": doc.description, "google_calendar_event": 1} + event.update( + format_date_according_to_google_calendar( + doc.all_day, get_datetime(doc.starts_on), get_datetime(doc.ends_on) + ) + ) + + if doc.repeat_on: + event.update({"recurrence": repeat_on_to_google_calendar_recurrence_rule(doc)}) + + try: + event = google_calendar.events().insert(calendarId=doc.google_calendar_id, body=event).execute() + influxframework.db.set_value( + "Event", doc.name, "google_calendar_event_id", event.get("id"), update_modified=False + ) + influxframework.msgprint(_("Event Synced with Google Calendar.")) + except HttpError as err: + influxframework.throw( + _("Google Calendar - Could not insert event in Google Calendar {0}, error code {1}.").format( + account.name, err.resp.status + ) + ) + + +def update_event_in_google_calendar(doc, method=None): + """ + Updates Events in Google Calendar if any existing event is modified in InfluxFramework Calendar + """ + # Workaround to avoid triggering updation when Event is being inserted since + # creation and modified are same when inserting doc + if ( + not influxframework.db.exists("Google Calendar", {"name": doc.google_calendar}) + or doc.modified == doc.creation + or not doc.sync_with_google_calendar + ): + return + + if doc.sync_with_google_calendar and not doc.google_calendar_event_id: + # If sync_with_google_calendar is checked later, then insert the event rather than updating it. + insert_event_in_google_calendar(doc) + return + + google_calendar, account = get_google_calendar_object(doc.google_calendar) + + if not account.push_to_google_calendar: + return + + try: + event = ( + google_calendar.events() + .get(calendarId=doc.google_calendar_id, eventId=doc.google_calendar_event_id) + .execute() + ) + event["summary"] = doc.subject + event["description"] = doc.description + event["recurrence"] = repeat_on_to_google_calendar_recurrence_rule(doc) + event["status"] = ( + "cancelled" if doc.event_type == "Cancelled" or doc.status == "Closed" else event.get("status") + ) + event.update( + format_date_according_to_google_calendar( + doc.all_day, get_datetime(doc.starts_on), get_datetime(doc.ends_on) + ) + ) + + google_calendar.events().update( + calendarId=doc.google_calendar_id, eventId=doc.google_calendar_event_id, body=event + ).execute() + influxframework.msgprint(_("Event Synced with Google Calendar.")) + except HttpError as err: + influxframework.throw( + _("Google Calendar - Could not update Event {0} in Google Calendar, error code {1}.").format( + doc.name, err.resp.status + ) + ) + + +def delete_event_from_google_calendar(doc, method=None): + """ + Delete Events from Google Calendar if InfluxFramework Event is deleted. + """ + + if not influxframework.db.exists("Google Calendar", {"name": doc.google_calendar}): + return + + google_calendar, account = get_google_calendar_object(doc.google_calendar) + + if not account.push_to_google_calendar: + return + + try: + event = ( + google_calendar.events() + .get(calendarId=doc.google_calendar_id, eventId=doc.google_calendar_event_id) + .execute() + ) + event["recurrence"] = None + event["status"] = "cancelled" + + google_calendar.events().update( + calendarId=doc.google_calendar_id, eventId=doc.google_calendar_event_id, body=event + ).execute() + except HttpError as err: + influxframework.msgprint( + _("Google Calendar - Could not delete Event {0} from Google Calendar, error code {1}.").format( + doc.name, err.resp.status + ) + ) + + +def google_calendar_to_repeat_on(start, end, recurrence=None): + """ + recurrence is in the form ['RRULE:FREQ=WEEKLY;BYDAY=MO,TU,TH'] + has the frequency and then the days on which the event recurs + + Both have been mapped in a dict for easier mapping. + """ + repeat_on = { + "starts_on": ( + get_datetime(start.get("date")) + if start.get("date") + else parser.parse(start.get("dateTime")) + .astimezone(ZoneInfo(get_time_zone())) + .replace(tzinfo=None) + ), + "ends_on": ( + get_datetime(end.get("date")) + if end.get("date") + else parser.parse(end.get("dateTime")) + .astimezone(ZoneInfo(get_time_zone())) + .replace(tzinfo=None) + ), + "all_day": 1 if start.get("date") else 0, + "repeat_this_event": 1 if recurrence else 0, + "repeat_on": None, + "repeat_till": None, + "sunday": 0, + "monday": 0, + "tuesday": 0, + "wednesday": 0, + "thursday": 0, + "friday": 0, + "saturday": 0, + } + + # recurrence rule "RRULE:FREQ=WEEKLY;BYDAY=MO,TU,TH" + if recurrence: + # google_calendar_frequency = RRULE:FREQ=WEEKLY, byday = BYDAY=MO,TU,TH, until = 20191028 + google_calendar_frequency, until, byday = get_recurrence_parameters(recurrence) + repeat_on["repeat_on"] = google_calendar_frequencies.get(google_calendar_frequency) + + if repeat_on["repeat_on"] == "Daily": + repeat_on["ends_on"] = None + repeat_on["repeat_till"] = datetime.strptime(until, "%Y%m%d") if until else None + + if byday and repeat_on["repeat_on"] == "Weekly": + repeat_on["repeat_till"] = datetime.strptime(until, "%Y%m%d") if until else None + byday = byday.split("=")[1].split(",") + for repeat_day in byday: + repeat_on[google_calendar_days[repeat_day]] = 1 + + if byday and repeat_on["repeat_on"] == "Monthly": + byday = byday.split("=")[1] + repeat_day_week_number, repeat_day_name = None, None + + for num in ["-2", "-1", "1", "2", "3", "4", "5"]: + if num in byday: + repeat_day_week_number = num + break + + for day in ["MO", "TU", "WE", "TH", "FR", "SA", "SU"]: + if day in byday: + repeat_day_name = google_calendar_days.get(day) + break + + # Only Set starts_on for the event to repeat monthly + start_date = parse_google_calendar_recurrence_rule(int(repeat_day_week_number), repeat_day_name) + repeat_on["starts_on"] = start_date + repeat_on["ends_on"] = add_to_date(start_date, minutes=5) + repeat_on["repeat_till"] = datetime.strptime(until, "%Y%m%d") if until else None + + if repeat_on["repeat_till"] == "Yearly": + repeat_on["ends_on"] = None + repeat_on["repeat_till"] = datetime.strptime(until, "%Y%m%d") if until else None + + return repeat_on + + +def format_date_according_to_google_calendar(all_day, starts_on, ends_on=None): + if not ends_on: + ends_on = starts_on + timedelta(minutes=10) + + date_format = { + "start": { + "dateTime": starts_on.isoformat(), + "timeZone": get_time_zone(), + }, + "end": { + "dateTime": ends_on.isoformat(), + "timeZone": get_time_zone(), + }, + } + + if all_day: + # If all_day event, Google Calendar takes date as a parameter and not dateTime + date_format["start"].pop("dateTime") + date_format["end"].pop("dateTime") + + date_format["start"].update({"date": starts_on.date().isoformat()}) + date_format["end"].update({"date": ends_on.date().isoformat()}) + + return date_format + + +def parse_google_calendar_recurrence_rule(repeat_day_week_number, repeat_day_name): + """ + Returns (repeat_on) exact date for combination eg 4TH viz. 4th thursday of a month + """ + if repeat_day_week_number < 0: + # Consider a month with 5 weeks and event is to be repeated in last week of every month, google caledar considers + # a month has 4 weeks and hence itll return -1 for a month with 5 weeks. + repeat_day_week_number = 4 + + weekdays = get_weekdays() + current_date = now_datetime() + isset_day_name, isset_day_number = False, False + + # Set the proper day ie if recurrence is 4TH, then align the day to Thursday + while not isset_day_name: + isset_day_name = True if weekdays[current_date.weekday()].lower() == repeat_day_name else False + current_date = add_days(current_date, 1) if not isset_day_name else current_date + + # One the day is set to Thursday, now set the week number ie 4 + while not isset_day_number: + week_number = get_week_number(current_date) + isset_day_number = True if week_number == repeat_day_week_number else False + # check if current_date week number is greater or smaller than repeat_day week number + weeks = 1 if week_number < repeat_day_week_number else -1 + current_date = add_to_date(current_date, weeks=weeks) if not isset_day_number else current_date + + return current_date + + +def repeat_on_to_google_calendar_recurrence_rule(doc): + """ + Returns event (repeat_on) in Google Calendar format ie RRULE:FREQ=WEEKLY;BYDAY=MO,TU,TH + """ + recurrence = framework_frequencies.get(doc.repeat_on) + weekdays = get_weekdays() + + if doc.repeat_on == "Weekly": + byday = [framework_days.get(day.lower()) for day in weekdays if doc.get(day.lower())] + recurrence = recurrence + "BYDAY=" + ",".join(byday) + elif doc.repeat_on == "Monthly": + week_number = str(get_week_number(get_datetime(doc.starts_on))) + week_day = weekdays[get_datetime(doc.starts_on).weekday()].lower() + recurrence = recurrence + "BYDAY=" + week_number + framework_days.get(week_day) + + return [recurrence] + + +def get_week_number(dt): + """ + Returns the week number of the month for the specified date. + https://stackoverflow.com/questions/3806473/python-week-number-of-the-month/16804556 + """ + from math import ceil + + first_day = dt.replace(day=1) + + dom = dt.day + adjusted_dom = dom + first_day.weekday() + + return int(ceil(adjusted_dom / 7.0)) + + +def get_recurrence_parameters(recurrence): + recurrence = recurrence.split(";") + frequency, until, byday = None, None, None + + for r in recurrence: + if "RRULE:FREQ" in r: + frequency = r + elif "UNTIL" in r: + until = r + elif "BYDAY" in r: + byday = r + else: + pass + + return frequency, until, byday + + +"""API Response + { + 'kind': 'calendar#events', + 'etag': '"etag"', + 'summary': 'Test Calendar', + 'updated': '2019-07-25T06:09:34.681Z', + 'timeZone': 'Asia/Kolkata', + 'accessRole': 'owner', + 'defaultReminders': [], + 'nextSyncToken': 'token', + 'items': [ + { + 'kind': 'calendar#event', + 'etag': '"etag"', + 'id': 'id', + 'status': 'confirmed' or 'cancelled', + 'htmlLink': 'link', + 'created': '2019-07-25T06:08:21.000Z', + 'updated': '2019-07-25T06:09:34.681Z', + 'summary': 'asdf', + 'creator': { + 'email': 'email' + }, + 'organizer': { + 'email': 'email', + 'displayName': 'Test Calendar', + 'self': True + }, + 'start': { + 'dateTime': '2019-07-27T12:00:00+05:30', (if all day event the its 'date' instead of 'dateTime') + 'timeZone': 'Asia/Kolkata' + }, + 'end': { + 'dateTime': '2019-07-27T13:00:00+05:30', (if all day event the its 'date' instead of 'dateTime') + 'timeZone': 'Asia/Kolkata' + }, + 'recurrence': *recurrence, + 'iCalUID': 'uid', + 'sequence': 1, + 'reminders': { + 'useDefault': True + } + } + ] + } + *recurrence + - Daily Event: ['RRULE:FREQ=DAILY'] + - Weekly Event: ['RRULE:FREQ=WEEKLY;BYDAY=MO,TU,TH'] + - Monthly Event: ['RRULE:FREQ=MONTHLY;BYDAY=4TH'] + - BYDAY: -2, -1, 1, 2, 3, 4 with weekdays (-2 edge case for April 2017 had 6 weeks in a month) + - Yearly Event: ['RRULE:FREQ=YEARLY;'] + - Custom Event: ['RRULE:FREQ=WEEKLY;WKST=SU;UNTIL=20191028;BYDAY=MO,WE']""" diff --git a/influxframework/integrations/doctype/google_contacts/__init__.py b/influxframework/integrations/doctype/google_contacts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/integrations/doctype/google_contacts/google_contacts.js b/influxframework/integrations/doctype/google_contacts/google_contacts.js new file mode 100644 index 0000000..965ff6b --- /dev/null +++ b/influxframework/integrations/doctype/google_contacts/google_contacts.js @@ -0,0 +1,63 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Google Contacts", { + refresh: function (frm) { + if (!frm.doc.enable) { + frm.dashboard.set_headline( + __("To use Google Contacts, enable {0}.", [ + `${__("Google Settings")}`, + ]) + ); + } + + influxframework.realtime.on("import_google_contacts", (data) => { + if (data.progress) { + frm.dashboard.show_progress( + "Import Google Contacts", + (data.progress / data.total) * 100, + __("Importing {0} of {1}", [data.progress, data.total]) + ); + if (data.progress === data.total) { + frm.dashboard.hide_progress("Import Google Contacts"); + } + } + }); + + if (frm.doc.refresh_token) { + let sync_button = frm.add_custom_button(__("Sync Contacts"), function () { + influxframework.show_alert({ + indicator: "green", + message: __("Syncing"), + }); + influxframework + .call({ + method: "influxframework.integrations.doctype.google_contacts.google_contacts.sync", + args: { + g_contact: frm.doc.name, + }, + btn: sync_button, + }) + .then((r) => { + influxframework.hide_progress(); + influxframework.msgprint(r.message); + }); + }); + } + }, + authorize_google_contacts_access: function (frm) { + influxframework.call({ + method: "influxframework.integrations.doctype.google_contacts.google_contacts.authorize_access", + args: { + g_contact: frm.doc.name, + reauthorize: frm.doc.authorization_code ? 1 : 0, + }, + callback: function (r) { + if (!r.exc) { + frm.save(); + window.open(r.message.url); + } + }, + }); + }, +}); diff --git a/influxframework/integrations/doctype/google_contacts/google_contacts.json b/influxframework/integrations/doctype/google_contacts/google_contacts.json new file mode 100644 index 0000000..76781fe --- /dev/null +++ b/influxframework/integrations/doctype/google_contacts/google_contacts.json @@ -0,0 +1,132 @@ +{ + "autoname": "format:GC-{email_id}", + "creation": "2019-06-14 00:09:39.441961", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "enable", + "sb_00", + "email_id", + "authorize_google_contacts_access", + "cb_00", + "last_sync_on", + "authorization_code", + "refresh_token", + "next_sync_token", + "sync", + "pull_from_google_contacts", + "column_break_12", + "push_to_google_contacts" + ], + "fields": [ + { + "default": "0", + "fieldname": "enable", + "fieldtype": "Check", + "label": "Enable" + }, + { + "fieldname": "authorization_code", + "fieldtype": "Password", + "hidden": 1, + "label": "Authorization Code" + }, + { + "fieldname": "refresh_token", + "fieldtype": "Password", + "hidden": 1, + "label": "Refresh Token" + }, + { + "fieldname": "last_sync_on", + "fieldtype": "Datetime", + "label": "Last Sync On", + "read_only": 1 + }, + { + "description": "Email Address whose Google Contacts are to be synced.", + "fieldname": "email_id", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Email Address", + "options": "Email", + "reqd": 1 + }, + { + "depends_on": "enable", + "fieldname": "sb_00", + "fieldtype": "Section Break", + "label": "Google Contacts" + }, + { + "fieldname": "cb_00", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "authorize_google_contacts_access", + "fieldtype": "Button", + "label": "Authorize Google Contacts Access" + }, + { + "fieldname": "next_sync_token", + "fieldtype": "Password", + "hidden": 1, + "label": "Next Sync Token" + }, + { + "depends_on": "enable", + "fieldname": "sync", + "fieldtype": "Section Break", + "label": "Sync" + }, + { + "default": "0", + "fieldname": "pull_from_google_contacts", + "fieldtype": "Check", + "label": "Pull from Google Contacts" + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "push_to_google_contacts", + "fieldtype": "Check", + "label": "Push to Google Contacts" + } + ], + "modified": "2020-09-18 17:26:09.703215", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Google Contacts", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "ASC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/integrations/doctype/google_contacts/google_contacts.py b/influxframework/integrations/doctype/google_contacts/google_contacts.py new file mode 100644 index 0000000..1402631 --- /dev/null +++ b/influxframework/integrations/doctype/google_contacts/google_contacts.py @@ -0,0 +1,289 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + + +from urllib.parse import quote + +from googleapiclient.errors import HttpError + +import influxframework +from influxframework import _ +from influxframework.integrations.google_oauth import GoogleOAuth +from influxframework.model.document import Document + + +class GoogleContacts(Document): + def validate(self): + if not influxframework.db.get_single_value("Google Settings", "enable"): + influxframework.throw(_("Enable Google API in Google Settings.")) + + def get_access_token(self): + if not self.refresh_token: + button_label = influxframework.bold(_("Allow Google Contacts Access")) + raise influxframework.ValidationError(_("Click on {0} to generate Refresh Token.").format(button_label)) + + oauth_obj = GoogleOAuth("contacts") + r = oauth_obj.refresh_access_token( + self.get_password(fieldname="refresh_token", raise_exception=False) + ) + + return r.get("access_token") + + +@influxframework.whitelist(methods=["POST"]) +def authorize_access(g_contact, reauthorize=False, code=None): + """ + If no Authorization code get it from Google and then request for Refresh Token. + Google Contact Name is set to flags to set_value after Authorization Code is obtained. + """ + + oauth_code = ( + influxframework.db.get_value("Google Contacts", g_contact, "authorization_code") if not code else code + ) + oauth_obj = GoogleOAuth("contacts") + + if not oauth_code or reauthorize: + return oauth_obj.get_authentication_url( + { + "g_contact": g_contact, + "redirect": f"/app/Form/{quote('Google Contacts')}/{quote(g_contact)}", + }, + ) + + r = oauth_obj.authorize(oauth_code) + influxframework.db.set_value( + "Google Contacts", + g_contact, + {"authorization_code": oauth_code, "refresh_token": r.get("refresh_token")}, + ) + + +def get_google_contacts_object(g_contact): + """ + Returns an object of Google Calendar along with Google Calendar doc. + """ + account = influxframework.get_doc("Google Contacts", g_contact) + oauth_obj = GoogleOAuth("contacts") + + google_contacts = oauth_obj.get_google_service_object( + account.get_access_token(), + account.get_password(fieldname="indexing_refresh_token", raise_exception=False), + ) + + return google_contacts, account + + +@influxframework.whitelist() +def sync(g_contact=None): + filters = {"enable": 1} + + if g_contact: + filters.update({"name": g_contact}) + + google_contacts = influxframework.get_list("Google Contacts", filters=filters) + + for g in google_contacts: + return sync_contacts_from_google_contacts(g.name) + + +def sync_contacts_from_google_contacts(g_contact): + """ + Syncs Contacts from Google Contacts. + https://developers.google.com/people/api/rest/v1/people.connections/list + """ + google_contacts, account = get_google_contacts_object(g_contact) + + if not account.pull_from_google_contacts: + return + + results = [] + contacts_updated = 0 + + sync_token = account.get_password(fieldname="next_sync_token", raise_exception=False) or None + contacts = influxframework._dict() + + while True: + try: + contacts = ( + google_contacts.people() + .connections() + .list( + resourceName="people/me", + pageToken=contacts.get("nextPageToken"), + syncToken=sync_token, + pageSize=2000, + requestSyncToken=True, + personFields="names,emailAddresses,organizations,phoneNumbers", + ) + .execute() + ) + + except HttpError as err: + influxframework.throw( + _( + "Google Contacts - Could not sync contacts from Google Contacts {0}, error code {1}." + ).format(account.name, err.resp.status) + ) + + for contact in contacts.get("connections", []): + results.append(contact) + + if not contacts.get("nextPageToken"): + if contacts.get("nextSyncToken"): + influxframework.db.set_value( + "Google Contacts", account.name, "next_sync_token", contacts.get("nextSyncToken") + ) + influxframework.db.commit() + break + + influxframework.db.set_value("Google Contacts", account.name, "last_sync_on", influxframework.utils.now_datetime()) + + for idx, connection in enumerate(results): + influxframework.publish_realtime( + "import_google_contacts", dict(progress=idx + 1, total=len(results)), user=influxframework.session.user + ) + + for name in connection.get("names"): + if name.get("metadata").get("primary"): + contacts_updated += 1 + contact = influxframework.get_doc( + { + "doctype": "Contact", + "first_name": name.get("givenName") or "", + "middle_name": name.get("middleName") or "", + "last_name": name.get("familyName") or "", + "designation": get_indexed_value(connection.get("organizations"), 0, "title"), + "pulled_from_google_contacts": 1, + "google_contacts": account.name, + "company_name": get_indexed_value(connection.get("organizations"), 0, "name"), + } + ) + + for email in connection.get("emailAddresses", []): + contact.add_email( + email_id=email.get("value"), is_primary=1 if email.get("metadata").get("primary") else 0 + ) + + for phone in connection.get("phoneNumbers", []): + contact.add_phone( + phone=phone.get("value"), is_primary_phone=1 if phone.get("metadata").get("primary") else 0 + ) + + contact.insert(ignore_permissions=True) + + return ( + _("{0} Google Contacts synced.").format(contacts_updated) + if contacts_updated > 0 + else _("No new Google Contacts synced.") + ) + + +def insert_contacts_to_google_contacts(doc, method=None): + """ + Syncs Contacts from Google Contacts. + https://developers.google.com/people/api/rest/v1/people/createContact + """ + if ( + not influxframework.db.exists("Google Contacts", {"name": doc.google_contacts}) + or doc.pulled_from_google_contacts + or not doc.sync_with_google_contacts + ): + return + + google_contacts, account = get_google_contacts_object(doc.google_contacts) + + if not account.push_to_google_contacts: + return + + names = {"givenName": doc.first_name, "middleName": doc.middle_name, "familyName": doc.last_name} + + phoneNumbers = [{"value": phone_no.phone} for phone_no in doc.phone_nos] + emailAddresses = [{"value": email_id.email_id} for email_id in doc.email_ids] + + try: + contact = ( + google_contacts.people() + .createContact( + body={"names": [names], "phoneNumbers": phoneNumbers, "emailAddresses": emailAddresses} + ) + .execute() + ) + influxframework.db.set_value("Contact", doc.name, "google_contacts_id", contact.get("resourceName")) + except HttpError as err: + influxframework.msgprint( + _("Google Calendar - Could not insert contact in Google Contacts {0}, error code {1}.").format( + account.name, err.resp.status + ) + ) + + +def update_contacts_to_google_contacts(doc, method=None): + """ + Syncs Contacts from Google Contacts. + https://developers.google.com/people/api/rest/v1/people/updateContact + """ + # Workaround to avoid triggering updation when Event is being inserted since + # creation and modified are same when inserting doc + if ( + not influxframework.db.exists("Google Contacts", {"name": doc.google_contacts}) + or doc.modified == doc.creation + or not doc.sync_with_google_contacts + ): + return + + if doc.sync_with_google_contacts and not doc.google_contacts_id: + # If sync_with_google_contacts is checked later, then insert the contact rather than updating it. + insert_contacts_to_google_contacts(doc) + return + + google_contacts, account = get_google_contacts_object(doc.google_contacts) + + if not account.push_to_google_contacts: + return + + names = {"givenName": doc.first_name, "middleName": doc.middle_name, "familyName": doc.last_name} + + phoneNumbers = [{"value": phone_no.phone} for phone_no in doc.phone_nos] + emailAddresses = [{"value": email_id.email_id} for email_id in doc.email_ids] + + try: + contact = ( + google_contacts.people() + .get( + resourceName=doc.google_contacts_id, + personFields="names,emailAddresses,organizations,phoneNumbers", + ) + .execute() + ) + + contact["names"] = [names] + contact["phoneNumbers"] = phoneNumbers + contact["emailAddresses"] = emailAddresses + + google_contacts.people().updateContact( + resourceName=doc.google_contacts_id, + body={ + "names": [names], + "phoneNumbers": phoneNumbers, + "emailAddresses": emailAddresses, + "etag": contact.get("etag"), + }, + updatePersonFields="names,emailAddresses,organizations,phoneNumbers", + ).execute() + influxframework.msgprint(_("Contact Synced with Google Contacts.")) + except HttpError as err: + influxframework.msgprint( + _("Google Contacts - Could not update contact in Google Contacts {0}, error code {1}.").format( + account.name, err.resp.status + ) + ) + + +def get_indexed_value(d, index, key): + if not d: + return "" + + try: + return d[index].get(key) + except IndexError: + return "" diff --git a/influxframework/integrations/doctype/google_drive/__init__.py b/influxframework/integrations/doctype/google_drive/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/integrations/doctype/google_drive/google_drive.js b/influxframework/integrations/doctype/google_drive/google_drive.js new file mode 100644 index 0000000..0d47ba0 --- /dev/null +++ b/influxframework/integrations/doctype/google_drive/google_drive.js @@ -0,0 +1,70 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Google Drive", { + refresh: function (frm) { + if (!frm.doc.enable) { + frm.dashboard.set_headline( + __("To use Google Drive, enable {0}.", [ + `${__("Google Settings")}`, + ]) + ); + } + + influxframework.realtime.on("upload_to_google_drive", (data) => { + if (data.progress) { + frm.dashboard.show_progress( + "Uploading to Google Drive", + (data.progress / data.total) * 100, + __("{0}", [data.message]) + ); + if (data.progress === data.total) { + frm.dashboard.hide_progress("Uploading to Google Drive"); + } + } + }); + + if (frm.doc.enable && frm.doc.refresh_token) { + let sync_button = frm.add_custom_button(__("Take Backup"), function () { + influxframework.show_alert({ + indicator: "green", + message: __("Backing up to Google Drive."), + }); + influxframework + .call({ + method: "influxframework.integrations.doctype.google_drive.google_drive.take_backup", + btn: sync_button, + }) + .then((r) => { + influxframework.msgprint(r.message); + }); + }); + } + + if (frm.doc.enable && frm.doc.backup_folder_name && !frm.doc.refresh_token) { + frm.dashboard.set_headline( + __( + "Click on Authorize Google Drive Access to authorize Google Drive Access." + ) + ); + } + + if (frm.doc.enable && frm.doc.refresh_token && frm.doc.authorization_code) { + frm.page.set_indicator("Authorized", "green"); + } + }, + authorize_google_drive_access: function (frm) { + influxframework.call({ + method: "influxframework.integrations.doctype.google_drive.google_drive.authorize_access", + args: { + reauthorize: frm.doc.authorization_code ? 1 : 0, + }, + callback: function (r) { + if (!r.exc) { + frm.save(); + window.open(r.message.url); + } + }, + }); + }, +}); diff --git a/influxframework/integrations/doctype/google_drive/google_drive.json b/influxframework/integrations/doctype/google_drive/google_drive.json new file mode 100644 index 0000000..6742d9e --- /dev/null +++ b/influxframework/integrations/doctype/google_drive/google_drive.json @@ -0,0 +1,133 @@ +{ + "creation": "2019-08-13 17:24:05.470876", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "enable", + "google_drive_section", + "backup_folder_name", + "frequency", + "email", + "send_email_for_successful_backup", + "file_backup", + "authorize_google_drive_access", + "column_break_5", + "backup_folder_id", + "last_backup_on", + "refresh_token", + "authorization_code" + ], + "fields": [ + { + "default": "0", + "fieldname": "enable", + "fieldtype": "Check", + "label": "Enable" + }, + { + "fieldname": "backup_folder_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Backup Folder Name", + "reqd": 1 + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "authorize_google_drive_access", + "fieldtype": "Button", + "label": "Authorize Google Drive Access" + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "backup_folder_id", + "fieldtype": "Data", + "label": "Backup Folder ID", + "read_only": 1 + }, + { + "fieldname": "frequency", + "fieldtype": "Select", + "label": "Frequency", + "options": "\nDaily\nWeekly", + "reqd": 1 + }, + { + "fieldname": "refresh_token", + "fieldtype": "Data", + "hidden": 1, + "label": "Refresh Token" + }, + { + "fieldname": "authorization_code", + "fieldtype": "Data", + "hidden": 1, + "label": "Authorization Code" + }, + { + "fieldname": "last_backup_on", + "fieldtype": "Datetime", + "label": "Last Backup On", + "read_only": 1 + }, + { + "default": "0", + "description": "Note: By default emails for failed backups are sent.", + "fieldname": "send_email_for_successful_backup", + "fieldtype": "Check", + "label": "Send Email for Successful backup" + }, + { + "default": "0", + "fieldname": "file_backup", + "fieldtype": "Check", + "label": "File Backup" + }, + { + "depends_on": "enable", + "fieldname": "google_drive_section", + "fieldtype": "Section Break", + "label": "Google Drive" + }, + { + "fieldname": "email", + "fieldtype": "Data", + "label": "Send Notification To", + "options": "Email", + "reqd": 1 + } + ], + "issingle": 1, + "modified": "2020-09-18 17:26:09.703215", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Google Drive", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "All", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "ASC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/integrations/doctype/google_drive/google_drive.py b/influxframework/integrations/doctype/google_drive/google_drive.py new file mode 100644 index 0000000..348ccc0 --- /dev/null +++ b/influxframework/integrations/doctype/google_drive/google_drive.py @@ -0,0 +1,216 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +import os +from urllib.parse import quote + +from apiclient.http import MediaFileUpload +from googleapiclient.errors import HttpError + +import influxframework +from influxframework import _ +from influxframework.integrations.google_oauth import GoogleOAuth +from influxframework.integrations.offsite_backup_utils import ( + get_latest_backup_file, + send_email, + validate_file_size, +) +from influxframework.model.document import Document +from influxframework.utils import get_backups_path, get_bench_path +from influxframework.utils.background_jobs import enqueue +from influxframework.utils.backups import new_backup + + +class GoogleDrive(Document): + def validate(self): + doc_before_save = self.get_doc_before_save() + if doc_before_save and doc_before_save.backup_folder_name != self.backup_folder_name: + self.backup_folder_id = "" + + def get_access_token(self): + if not self.refresh_token: + button_label = influxframework.bold(_("Allow Google Drive Access")) + raise influxframework.ValidationError(_("Click on {0} to generate Refresh Token.").format(button_label)) + + oauth_obj = GoogleOAuth("drive") + r = oauth_obj.refresh_access_token( + self.get_password(fieldname="refresh_token", raise_exception=False) + ) + + return r.get("access_token") + + +@influxframework.whitelist(methods=["POST"]) +def authorize_access(reauthorize=False, code=None): + """ + If no Authorization code get it from Google and then request for Refresh Token. + Google Contact Name is set to flags to set_value after Authorization Code is obtained. + """ + + oauth_code = ( + influxframework.db.get_single_value("Google Drive", "authorization_code") if not code else code + ) + oauth_obj = GoogleOAuth("drive") + + if not oauth_code or reauthorize: + if reauthorize: + influxframework.db.set_value("Google Drive", None, "backup_folder_id", "") + return oauth_obj.get_authentication_url( + { + "redirect": f"/app/Form/{quote('Google Drive')}", + }, + ) + + r = oauth_obj.authorize(oauth_code) + influxframework.db.set_value( + "Google Drive", + "Google Drive", + {"authorization_code": oauth_code, "refresh_token": r.get("refresh_token")}, + ) + + +def get_google_drive_object(): + """ + Returns an object of Google Drive. + """ + account = influxframework.get_doc("Google Drive") + oauth_obj = GoogleOAuth("drive") + + google_drive = oauth_obj.get_google_service_object( + account.get_access_token(), + account.get_password(fieldname="indexing_refresh_token", raise_exception=False), + ) + + return google_drive, account + + +def check_for_folder_in_google_drive(): + """Checks if folder exists in Google Drive else create it.""" + + def _create_folder_in_google_drive(google_drive, account): + file_metadata = { + "name": account.backup_folder_name, + "mimeType": "application/vnd.google-apps.folder", + } + + try: + folder = google_drive.files().create(body=file_metadata, fields="id").execute() + influxframework.db.set_value("Google Drive", None, "backup_folder_id", folder.get("id")) + influxframework.db.commit() + except HttpError as e: + influxframework.throw( + _("Google Drive - Could not create folder in Google Drive - Error Code {0}").format(e) + ) + + google_drive, account = get_google_drive_object() + + if account.backup_folder_id: + return + + backup_folder_exists = False + + try: + google_drive_folders = ( + google_drive.files().list(q="mimeType='application/vnd.google-apps.folder'").execute() + ) + except HttpError as e: + influxframework.throw( + _("Google Drive - Could not find folder in Google Drive - Error Code {0}").format(e) + ) + + for f in google_drive_folders.get("files"): + if f.get("name") == account.backup_folder_name: + influxframework.db.set_value("Google Drive", None, "backup_folder_id", f.get("id")) + influxframework.db.commit() + backup_folder_exists = True + break + + if not backup_folder_exists: + _create_folder_in_google_drive(google_drive, account) + + +@influxframework.whitelist() +def take_backup(): + """Enqueue longjob for taking backup to Google Drive""" + enqueue( + "influxframework.integrations.doctype.google_drive.google_drive.upload_system_backup_to_google_drive", + queue="long", + timeout=1500, + ) + influxframework.msgprint(_("Queued for backup. It may take a few minutes to an hour.")) + + +def upload_system_backup_to_google_drive(): + """ + Upload system backup to Google Drive + """ + # Get Google Drive Object + google_drive, account = get_google_drive_object() + + # Check if folder exists in Google Drive + check_for_folder_in_google_drive() + account.load_from_db() + + validate_file_size() + + if influxframework.flags.create_new_backup: + set_progress(1, "Backing up Data.") + backup = new_backup() + file_urls = [] + file_urls.append(backup.backup_path_db) + file_urls.append(backup.backup_path_conf) + + if account.file_backup: + file_urls.append(backup.backup_path_files) + file_urls.append(backup.backup_path_private_files) + else: + file_urls = get_latest_backup_file(with_files=account.file_backup) + + for fileurl in file_urls: + if not fileurl: + continue + + file_metadata = {"name": fileurl, "parents": [account.backup_folder_id]} + + try: + media = MediaFileUpload( + get_absolute_path(filename=fileurl), mimetype="application/gzip", resumable=True + ) + except OSError as e: + influxframework.throw(_("Google Drive - Could not locate - {0}").format(e)) + + try: + set_progress(2, "Uploading backup to Google Drive.") + google_drive.files().create(body=file_metadata, media_body=media, fields="id").execute() + except HttpError as e: + send_email(False, "Google Drive", "Google Drive", "email", error_status=e) + + set_progress(3, "Uploading successful.") + influxframework.db.set_value("Google Drive", None, "last_backup_on", influxframework.utils.now_datetime()) + send_email(True, "Google Drive", "Google Drive", "email") + return _("Google Drive Backup Successful.") + + +def daily_backup(): + drive_settings = influxframework.db.get_singles_dict("Google Drive", cast=True) + if drive_settings.enable and drive_settings.frequency == "Daily": + upload_system_backup_to_google_drive() + + +def weekly_backup(): + drive_settings = influxframework.db.get_singles_dict("Google Drive", cast=True) + if drive_settings.enable and drive_settings.frequency == "Weekly": + upload_system_backup_to_google_drive() + + +def get_absolute_path(filename): + file_path = os.path.join(get_backups_path()[2:], os.path.basename(filename)) + return f"{get_bench_path()}/sites/{file_path}" + + +def set_progress(progress, message): + influxframework.publish_realtime( + "upload_to_google_drive", + dict(progress=progress, total=3, message=message), + user=influxframework.session.user, + ) diff --git a/influxframework/integrations/doctype/google_drive/test_google_drive.py b/influxframework/integrations/doctype/google_drive/test_google_drive.py new file mode 100644 index 0000000..0fef56d --- /dev/null +++ b/influxframework/integrations/doctype/google_drive/test_google_drive.py @@ -0,0 +1,8 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestGoogleDrive(InfluxFrameworkTestCase): + pass diff --git a/influxframework/integrations/doctype/google_settings/__init__.py b/influxframework/integrations/doctype/google_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/integrations/doctype/google_settings/google_settings.js b/influxframework/integrations/doctype/google_settings/google_settings.js new file mode 100644 index 0000000..3e19e85 --- /dev/null +++ b/influxframework/integrations/doctype/google_settings/google_settings.js @@ -0,0 +1,14 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Google Settings", { + refresh: function (frm) { + frm.dashboard.set_headline( + __("For more information, {0}.", [ + `${__( + "Click here" + )}`, + ]) + ); + }, +}); diff --git a/influxframework/integrations/doctype/google_settings/google_settings.json b/influxframework/integrations/doctype/google_settings/google_settings.json new file mode 100644 index 0000000..6f25fa4 --- /dev/null +++ b/influxframework/integrations/doctype/google_settings/google_settings.json @@ -0,0 +1,100 @@ +{ + "actions": [], + "creation": "2019-06-14 00:08:37.255003", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "enable", + "sb_00", + "client_id", + "client_secret", + "sb_01", + "api_key", + "section_break_7", + "google_drive_picker_enabled", + "app_id" + ], + "fields": [ + { + "default": "0", + "fieldname": "enable", + "fieldtype": "Check", + "label": "Enable" + }, + { + "description": "The Client ID obtained from the Google Cloud Console under \n\"APIs & Services\" > \"Credentials\"\n", + "fieldname": "client_id", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Client ID", + "mandatory_depends_on": "google_drive_picker_enabled" + }, + { + "fieldname": "client_secret", + "fieldtype": "Password", + "in_list_view": 1, + "label": "Client Secret" + }, + { + "description": "The browser API key obtained from the Google Cloud Console under \n\"APIs & Services\" > \"Credentials\"\n", + "fieldname": "api_key", + "fieldtype": "Data", + "label": "API Key", + "mandatory_depends_on": "google_drive_picker_enabled" + }, + { + "depends_on": "enable", + "fieldname": "sb_00", + "fieldtype": "Section Break", + "label": "OAuth Client ID" + }, + { + "depends_on": "enable", + "fieldname": "sb_01", + "fieldtype": "Section Break", + "label": "API Key" + }, + { + "depends_on": "google_drive_picker_enabled", + "description": "The project number obtained from Google Cloud Console under \n\"IAM & Admin\" > \"Settings\"\n", + "fieldname": "app_id", + "fieldtype": "Data", + "label": "App ID", + "mandatory_depends_on": "google_drive_picker_enabled" + }, + { + "fieldname": "section_break_7", + "fieldtype": "Section Break", + "label": "Google Drive Picker" + }, + { + "default": "0", + "fieldname": "google_drive_picker_enabled", + "fieldtype": "Check", + "label": "Google Drive Picker Enabled" + } + ], + "issingle": 1, + "links": [], + "modified": "2021-06-29 18:26:07.094851", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Google Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "ASC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/integrations/doctype/google_settings/google_settings.py b/influxframework/integrations/doctype/google_settings/google_settings.py new file mode 100644 index 0000000..87448ca --- /dev/null +++ b/influxframework/integrations/doctype/google_settings/google_settings.py @@ -0,0 +1,24 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document + + +class GoogleSettings(Document): + pass + + +@influxframework.whitelist() +def get_file_picker_settings(): + """Return all the data FileUploader needs to start the Google Drive Picker.""" + google_settings = influxframework.get_single("Google Settings") + if not (google_settings.enable and google_settings.google_drive_picker_enabled): + return {} + + return { + "enabled": True, + "appId": google_settings.app_id, + "developerKey": google_settings.api_key, + "clientId": google_settings.client_id, + } diff --git a/influxframework/integrations/doctype/google_settings/test_google_settings.py b/influxframework/integrations/doctype/google_settings/test_google_settings.py new file mode 100644 index 0000000..284a229 --- /dev/null +++ b/influxframework/integrations/doctype/google_settings/test_google_settings.py @@ -0,0 +1,43 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + +from .google_settings import get_file_picker_settings + + +class TestGoogleSettings(InfluxFrameworkTestCase): + def setUp(self): + settings = influxframework.get_single("Google Settings") + settings.client_id = "test_client_id" + settings.app_id = "test_app_id" + settings.api_key = "test_api_key" + settings.save() + + def test_picker_disabled(self): + """Google Drive Picker should be disabled if it is not enabled in Google Settings.""" + influxframework.db.set_value("Google Settings", None, "enable", 1) + influxframework.db.set_value("Google Settings", None, "google_drive_picker_enabled", 0) + settings = get_file_picker_settings() + + self.assertEqual(settings, {}) + + def test_google_disabled(self): + """Google Drive Picker should be disabled if Google integration is not enabled.""" + influxframework.db.set_value("Google Settings", None, "enable", 0) + influxframework.db.set_value("Google Settings", None, "google_drive_picker_enabled", 1) + settings = get_file_picker_settings() + + self.assertEqual(settings, {}) + + def test_picker_enabled(self): + """If picker is enabled, get_file_picker_settings should return the credentials.""" + influxframework.db.set_value("Google Settings", None, "enable", 1) + influxframework.db.set_value("Google Settings", None, "google_drive_picker_enabled", 1) + settings = get_file_picker_settings() + + self.assertEqual(True, settings.get("enabled", False)) + self.assertEqual("test_client_id", settings.get("clientId", "")) + self.assertEqual("test_app_id", settings.get("appId", "")) + self.assertEqual("test_api_key", settings.get("developerKey", "")) diff --git a/influxframework/integrations/doctype/integration_request/__init__.py b/influxframework/integrations/doctype/integration_request/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/integrations/doctype/integration_request/integration_request.js b/influxframework/integrations/doctype/integration_request/integration_request.js new file mode 100644 index 0000000..f8ef79e --- /dev/null +++ b/influxframework/integrations/doctype/integration_request/integration_request.js @@ -0,0 +1,6 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Integration Request", { + refresh: function (frm) {}, +}); diff --git a/influxframework/integrations/doctype/integration_request/integration_request.json b/influxframework/integrations/doctype/integration_request/integration_request.json new file mode 100644 index 0000000..98db8ea --- /dev/null +++ b/influxframework/integrations/doctype/integration_request/integration_request.json @@ -0,0 +1,154 @@ +{ + "actions": [], + "creation": "2022-03-28 12:25:29.929952", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "request_id", + "integration_request_service", + "is_remote_request", + "column_break_5", + "request_description", + "status", + "section_break_8", + "url", + "request_headers", + "data", + "response_section", + "output", + "error", + "reference_section", + "reference_doctype", + "column_break_16", + "reference_docname" + ], + "fields": [ + { + "fieldname": "integration_request_service", + "fieldtype": "Data", + "label": "Service", + "read_only": 1 + }, + { + "default": "Queued", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "\nQueued\nAuthorized\nCompleted\nCancelled\nFailed", + "read_only": 1 + }, + { + "fieldname": "data", + "fieldtype": "Code", + "label": "Request Data", + "read_only": 1 + }, + { + "fieldname": "output", + "fieldtype": "Code", + "label": "Output", + "read_only": 1 + }, + { + "fieldname": "error", + "fieldtype": "Code", + "label": "Error", + "read_only": 1 + }, + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "label": "Reference Document Type", + "options": "DocType", + "read_only": 1 + }, + { + "fieldname": "reference_docname", + "fieldtype": "Dynamic Link", + "label": "Reference Document Name", + "options": "reference_doctype", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_remote_request", + "fieldtype": "Check", + "label": "Is Remote Request?", + "read_only": 1 + }, + { + "fieldname": "request_description", + "fieldtype": "Data", + "label": "Request Description", + "read_only": 1 + }, + { + "fieldname": "request_id", + "fieldtype": "Data", + "label": "Request ID", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_8", + "fieldtype": "Section Break" + }, + { + "fieldname": "url", + "fieldtype": "Data", + "label": "URL", + "read_only": 1 + }, + { + "fieldname": "response_section", + "fieldtype": "Section Break", + "label": "Response" + }, + { + "depends_on": "eval:doc.reference_doctype", + "fieldname": "reference_section", + "fieldtype": "Section Break", + "label": "Reference" + }, + { + "fieldname": "column_break_16", + "fieldtype": "Column Break" + }, + { + "fieldname": "request_headers", + "fieldtype": "Code", + "label": "Request Headers", + "read_only": 1 + } + ], + "in_create": 1, + "links": [], + "modified": "2022-04-07 11:32:27.557548", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Integration Request", + "owner": "Administrator", + "permissions": [ + { + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "integration_request_service", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/integrations/doctype/integration_request/integration_request.py b/influxframework/integrations/doctype/integration_request/integration_request.py new file mode 100644 index 0000000..fc4c6c6 --- /dev/null +++ b/influxframework/integrations/doctype/integration_request/integration_request.py @@ -0,0 +1,37 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +import influxframework +from influxframework.integrations.utils import json_handler +from influxframework.model.document import Document + + +class IntegrationRequest(Document): + def autoname(self): + if self.flags._name: + self.name = self.flags._name + + def update_status(self, params, status): + data = json.loads(self.data) + data.update(params) + + self.data = json.dumps(data) + self.status = status + self.save(ignore_permissions=True) + influxframework.db.commit() + + def handle_success(self, response): + """update the output field with the response along with the relevant status""" + if isinstance(response, str): + response = json.loads(response) + self.db_set("status", "Completed") + self.db_set("output", json.dumps(response, default=json_handler)) + + def handle_failure(self, response): + """update the error field with the response along with the relevant status""" + if isinstance(response, str): + response = json.loads(response) + self.db_set("status", "Failed") + self.db_set("error", json.dumps(response, default=json_handler)) diff --git a/influxframework/integrations/doctype/integration_request/test_integration_request.py b/influxframework/integrations/doctype/integration_request/test_integration_request.py new file mode 100644 index 0000000..2049a19 --- /dev/null +++ b/influxframework/integrations/doctype/integration_request/test_integration_request.py @@ -0,0 +1,10 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + +# test_records = influxframework.get_test_records('Integration Request') + + +class TestIntegrationRequest(InfluxFrameworkTestCase): + pass diff --git a/influxframework/integrations/doctype/ldap_group_mapping/__init__.py b/influxframework/integrations/doctype/ldap_group_mapping/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/integrations/doctype/ldap_group_mapping/ldap_group_mapping.json b/influxframework/integrations/doctype/ldap_group_mapping/ldap_group_mapping.json new file mode 100644 index 0000000..d819b04 --- /dev/null +++ b/influxframework/integrations/doctype/ldap_group_mapping/ldap_group_mapping.json @@ -0,0 +1,41 @@ +{ + "actions": [], + "creation": "2019-05-29 01:24:29.585060", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "ldap_group", + "influxerp_role" + ], + "fields": [ + { + "fieldname": "ldap_group", + "fieldtype": "Data", + "in_list_view": 1, + "label": "LDAP Group", + "reqd": 1 + }, + { + "fieldname": "influxerp_role", + "fieldtype": "Link", + "in_list_view": 1, + "label": "User Role", + "options": "Role", + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2022-07-07 16:28:44.828514", + "modified_by": "Administrator", + "module": "Integrations", + "name": "LDAP Group Mapping", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "ASC", + "states": [], + "track_changes": 1 +} diff --git a/influxframework/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py b/influxframework/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py new file mode 100644 index 0000000..a86b822 --- /dev/null +++ b/influxframework/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py @@ -0,0 +1,9 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class LDAPGroupMapping(Document): + pass diff --git a/influxframework/integrations/doctype/ldap_settings/__init__.py b/influxframework/integrations/doctype/ldap_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/integrations/doctype/ldap_settings/ldap_settings.js b/influxframework/integrations/doctype/ldap_settings/ldap_settings.js new file mode 100644 index 0000000..b62b68a --- /dev/null +++ b/influxframework/integrations/doctype/ldap_settings/ldap_settings.js @@ -0,0 +1,6 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("LDAP Settings", { + refresh: function (frm) {}, +}); diff --git a/influxframework/integrations/doctype/ldap_settings/ldap_settings.json b/influxframework/integrations/doctype/ldap_settings/ldap_settings.json new file mode 100644 index 0000000..f5472a5 --- /dev/null +++ b/influxframework/integrations/doctype/ldap_settings/ldap_settings.json @@ -0,0 +1,320 @@ +{ + "actions": [], + "creation": "2016-09-22 04:16:48.829658", + "doctype": "DocType", + "document_type": "System", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "enabled", + "ldap_server_settings_section", + "ldap_directory_server", + "column_break_4", + "ldap_server_url", + "ldap_auth_section", + "base_dn", + "column_break_8", + "password", + "ldap_search_and_paths_section", + "ldap_search_path_user", + "ldap_search_string", + "column_break_12", + "ldap_search_path_group", + "ldap_user_creation_and_mapping_section", + "ldap_email_field", + "ldap_username_field", + "ldap_first_name_field", + "column_break_19", + "ldap_middle_name_field", + "ldap_last_name_field", + "ldap_phone_field", + "ldap_mobile_field", + "ldap_security", + "ssl_tls_mode", + "require_trusted_certificate", + "column_break_27", + "local_private_key_file", + "local_server_certificate_file", + "local_ca_certs_file", + "ldap_custom_settings_section", + "ldap_group_objectclass", + "ldap_custom_group_search", + "column_break_33", + "ldap_group_member_attribute", + "ldap_group_mappings_section", + "default_user_type", + "column_break_38", + "default_role", + "section_break_40", + "ldap_groups", + "ldap_group_field" + ], + "fields": [ + { + "default": "0", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enabled" + }, + { + "fieldname": "ldap_server_url", + "fieldtype": "Data", + "in_list_view": 1, + "label": "LDAP Server Url", + "reqd": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "base_dn", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Base Distinguished Name (DN)", + "reqd": 1 + }, + { + "fieldname": "password", + "fieldtype": "Password", + "in_list_view": 1, + "label": "Password for Base DN", + "reqd": 1 + }, + { + "depends_on": "eval: doc.default_user_type == \"System User\"", + "fieldname": "default_role", + "fieldtype": "Link", + "label": "Default User Role", + "mandatory_depends_on": "eval: doc.default_user_type == \"System User\"", + "options": "Role", + "reqd": 1 + }, + { + "description": "Must be enclosed in '()' and include '{0}', which is a placeholder for the user/login name. i.e. (&(objectclass=user)(uid={0}))", + "fieldname": "ldap_search_string", + "fieldtype": "Data", + "label": "LDAP Search String", + "reqd": 1 + }, + { + "fieldname": "ldap_email_field", + "fieldtype": "Data", + "label": "LDAP Email Field", + "reqd": 1 + }, + { + "fieldname": "ldap_username_field", + "fieldtype": "Data", + "label": "LDAP Username Field", + "reqd": 1 + }, + { + "fieldname": "ldap_first_name_field", + "fieldtype": "Data", + "label": "LDAP First Name Field", + "reqd": 1 + }, + { + "fieldname": "ldap_middle_name_field", + "fieldtype": "Data", + "label": "LDAP Middle Name Field" + }, + { + "fieldname": "ldap_last_name_field", + "fieldtype": "Data", + "label": "LDAP Last Name Field" + }, + { + "fieldname": "ldap_phone_field", + "fieldtype": "Data", + "label": "LDAP Phone Field" + }, + { + "fieldname": "ldap_mobile_field", + "fieldtype": "Data", + "label": "LDAP Mobile Field" + }, + { + "fieldname": "ldap_security", + "fieldtype": "Section Break", + "label": "LDAP Security" + }, + { + "default": "Off", + "fieldname": "ssl_tls_mode", + "fieldtype": "Select", + "label": "SSL/TLS Mode", + "options": "Off\nStartTLS" + }, + { + "default": "No", + "fieldname": "require_trusted_certificate", + "fieldtype": "Select", + "label": "Require Trusted Certificate", + "options": "No\nYes", + "reqd": 1 + }, + { + "fieldname": "local_private_key_file", + "fieldtype": "Data", + "label": "Path to private Key File" + }, + { + "fieldname": "local_server_certificate_file", + "fieldtype": "Data", + "label": "Path to Server Certificate" + }, + { + "fieldname": "local_ca_certs_file", + "fieldtype": "Data", + "label": "Path to CA Certs File" + }, + { + "fieldname": "ldap_group_mappings_section", + "fieldtype": "Section Break", + "label": "LDAP Group Mappings" + }, + { + "description": "NOTE: This box is due for depreciation. Please re-setup LDAP to work with the newer settings", + "fieldname": "ldap_group_field", + "fieldtype": "Data", + "label": "LDAP Group Field" + }, + { + "fieldname": "ldap_groups", + "fieldtype": "Table", + "label": "LDAP Group Mappings", + "options": "LDAP Group Mapping" + }, + { + "fieldname": "ldap_server_settings_section", + "fieldtype": "Section Break", + "label": "LDAP Server Settings" + }, + { + "fieldname": "ldap_auth_section", + "fieldtype": "Section Break", + "label": "LDAP Auth" + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, + { + "fieldname": "ldap_search_and_paths_section", + "fieldtype": "Section Break", + "label": "LDAP Search and Paths" + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, + { + "fieldname": "ldap_user_creation_and_mapping_section", + "fieldtype": "Section Break", + "label": "LDAP User Creation and Mapping" + }, + { + "fieldname": "column_break_19", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_27", + "fieldtype": "Column Break" + }, + { + "description": "These settings are required if 'Custom' LDAP Directory is used", + "fieldname": "ldap_custom_settings_section", + "fieldtype": "Section Break", + "label": "LDAP Custom Settings" + }, + { + "fieldname": "column_break_33", + "fieldtype": "Column Break" + }, + { + "description": "string value, i.e. member", + "fieldname": "ldap_group_member_attribute", + "fieldtype": "Data", + "label": "LDAP Group Member attribute" + }, + { + "description": "Please select the LDAP Directory being used", + "fieldname": "ldap_directory_server", + "fieldtype": "Select", + "label": "Directory Server", + "options": "\nActive Directory\nOpenLDAP\nCustom", + "reqd": 1 + }, + { + "description": "string value, i.e. group", + "fieldname": "ldap_group_objectclass", + "fieldtype": "Data", + "label": "Group Object Class" + }, + { + "description": "string value, i.e. {0} or uid={0},ou=users,dc=example,dc=com", + "fieldname": "ldap_custom_group_search", + "fieldtype": "Data", + "label": "Custom Group Search" + }, + { + "description": "Requires any valid fdn path. i.e. ou=users,dc=example,dc=com", + "fieldname": "ldap_search_path_user", + "fieldtype": "Data", + "in_list_view": 1, + "label": "LDAP search path for Users", + "reqd": 1 + }, + { + "description": "Requires any valid fdn path. i.e. ou=groups,dc=example,dc=com", + "fieldname": "ldap_search_path_group", + "fieldtype": "Data", + "label": "LDAP search path for Groups", + "reqd": 1 + }, + { + "fieldname": "default_user_type", + "fieldtype": "Link", + "label": "Default User Type", + "options": "User Type", + "reqd": 1 + }, + { + "fieldname": "column_break_38", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_40", + "fieldtype": "Section Break", + "hide_border": 1 + } + ], + "in_create": 1, + "issingle": 1, + "links": [], + "modified": "2022-07-07 16:51:46.230793", + "modified_by": "Administrator", + "module": "Integrations", + "name": "LDAP Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/integrations/doctype/ldap_settings/ldap_settings.py b/influxframework/integrations/doctype/ldap_settings/ldap_settings.py new file mode 100644 index 0000000..0a216bf --- /dev/null +++ b/influxframework/integrations/doctype/ldap_settings/ldap_settings.py @@ -0,0 +1,375 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE + +import ssl +from typing import TYPE_CHECKING + +import ldap3 +from ldap3 import AUTO_BIND_TLS_BEFORE_BIND, HASHED_SALTED_SHA, MODIFY_REPLACE +from ldap3.abstract.entry import Entry +from ldap3.core.exceptions import ( + LDAPAttributeError, + LDAPInvalidCredentialsResult, + LDAPInvalidFilterError, + LDAPNoSuchObjectResult, +) +from ldap3.utils.hashed import hashed + +import influxframework +from influxframework import _, safe_encode +from influxframework.model.document import Document +from influxframework.twofactor import authenticate_for_2factor, confirm_otp_token, should_run_2fa + +if TYPE_CHECKING: + from influxframework.core.doctype.user.user import User + + +class LDAPSettings(Document): + def validate(self): + self.default_user_type = self.default_user_type or "System User" + + if not self.enabled: + return + + if not self.flags.ignore_mandatory: + if ( + self.ldap_search_string.count("(") == self.ldap_search_string.count(")") + and self.ldap_search_string.startswith("(") + and self.ldap_search_string.endswith(")") + and self.ldap_search_string + and "{0}" in self.ldap_search_string + ): + + conn = self.connect_to_ldap( + base_dn=self.base_dn, password=self.get_password(raise_exception=False) + ) + + try: + if conn.result["type"] == "bindResponse" and self.base_dn: + conn.search( + search_base=self.ldap_search_path_user, + search_filter="(objectClass=*)", + attributes=self.get_ldap_attributes(), + ) + + conn.search( + search_base=self.ldap_search_path_group, search_filter="(objectClass=*)", attributes=["cn"] + ) + + except LDAPAttributeError as ex: + influxframework.throw( + _("LDAP settings incorrect. validation response was: {0}").format(ex), + title=_("Misconfigured"), + ) + + except LDAPNoSuchObjectResult: + influxframework.throw( + _("Ensure the user and group search paths are correct."), title=_("Misconfigured") + ) + + if self.ldap_directory_server.lower() == "custom": + if not self.ldap_group_member_attribute or not self.ldap_group_objectclass: + influxframework.throw( + _( + "Custom LDAP Directoy Selected, please ensure 'LDAP Group Member attribute' and 'Group Object Class' are entered" + ), + title=_("Misconfigured"), + ) + + if self.ldap_custom_group_search and "{0}" not in self.ldap_custom_group_search: + influxframework.throw( + _( + "Custom Group Search if filled needs to contain the user placeholder {0}, eg uid={0},ou=users,dc=example,dc=com" + ), + title=_("Misconfigured"), + ) + + else: + influxframework.throw( + _( + "LDAP Search String must be enclosed in '()' and needs to contian the user placeholder {0}, eg sAMAccountName={0}" + ) + ) + + def connect_to_ldap(self, base_dn, password, read_only=True) -> ldap3.Connection: + try: + if self.require_trusted_certificate == "Yes": + tls_configuration = ldap3.Tls(validate=ssl.CERT_REQUIRED, version=ssl.PROTOCOL_TLS_CLIENT) + else: + tls_configuration = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLS_CLIENT) + + if self.local_private_key_file: + tls_configuration.private_key_file = self.local_private_key_file + if self.local_server_certificate_file: + tls_configuration.certificate_file = self.local_server_certificate_file + if self.local_ca_certs_file: + tls_configuration.ca_certs_file = self.local_ca_certs_file + + server = ldap3.Server(host=self.ldap_server_url, tls=tls_configuration) + bind_type = AUTO_BIND_TLS_BEFORE_BIND if self.ssl_tls_mode == "StartTLS" else True + + return ldap3.Connection( + server=server, + user=base_dn, + password=password, + auto_bind=bind_type, + read_only=read_only, + raise_exceptions=True, + ) + + except ImportError: + msg = _("Please Install the ldap3 library via pip to use ldap functionality.") + influxframework.throw(msg, title=_("LDAP Not Installed")) + except LDAPInvalidCredentialsResult: + influxframework.throw(_("Invalid username or password")) + except Exception as ex: + influxframework.throw(_(str(ex))) + + @staticmethod + def get_ldap_client_settings() -> dict: + # return the settings to be used on the client side. + result = {"enabled": False} + ldap = influxframework.get_cached_doc("LDAP Settings") + if ldap.enabled: + result["enabled"] = True + result["method"] = "influxframework.integrations.doctype.ldap_settings.ldap_settings.login" + return result + + @classmethod + def update_user_fields(cls, user: "User", user_data: dict): + updatable_data = {key: value for key, value in user_data.items() if key != "email"} + + for key, value in updatable_data.items(): + setattr(user, key, value) + user.save(ignore_permissions=True) + + def sync_roles(self, user: "User", additional_groups: list = None): + current_roles = {d.role for d in user.get("roles")} + if self.default_user_type == "System User": + needed_roles = {self.default_role} + else: + needed_roles = set() + lower_groups = [g.lower() for g in additional_groups or []] + + all_mapped_roles = {r.influxerp_role for r in self.ldap_groups} + matched_roles = { + r.influxerp_role for r in self.ldap_groups if r.ldap_group.lower() in lower_groups + } + unmatched_roles = all_mapped_roles.difference(matched_roles) + needed_roles.update(matched_roles) + roles_to_remove = current_roles.intersection(unmatched_roles) + + if not needed_roles.issubset(current_roles): + missing_roles = needed_roles.difference(current_roles) + user.add_roles(*missing_roles) + + user.remove_roles(*roles_to_remove) + + def create_or_update_user(self, user_data: dict, groups: list = None): + user: "User" = None + role: str = None + + if influxframework.db.exists("User", user_data["email"]): + user = influxframework.get_doc("User", user_data["email"]) + LDAPSettings.update_user_fields(user=user, user_data=user_data) + else: + doc = user_data | { + "doctype": "User", + "send_welcome_email": 0, + "language": "", + "user_type": self.default_user_type, + } + user = influxframework.get_doc(doc) + user.insert(ignore_permissions=True) + + if self.default_user_type == "System User": + role = self.default_role + else: + role = influxframework.db.get_value("User Type", user.user_type, "role") + + if role: + user.add_roles(role) + + self.sync_roles(user, groups) + + return user + + def get_ldap_attributes(self): + ldap_attributes = [self.ldap_email_field, self.ldap_username_field, self.ldap_first_name_field] + + if self.ldap_group_field: + ldap_attributes.append(self.ldap_group_field) + + if self.ldap_middle_name_field: + ldap_attributes.append(self.ldap_middle_name_field) + + if self.ldap_last_name_field: + ldap_attributes.append(self.ldap_last_name_field) + + if self.ldap_phone_field: + ldap_attributes.append(self.ldap_phone_field) + + if self.ldap_mobile_field: + ldap_attributes.append(self.ldap_mobile_field) + + return ldap_attributes + + def fetch_ldap_groups(self, user: Entry, conn: ldap3.Connection) -> list: + if not isinstance(user, Entry): + raise TypeError("Invalid type, attribute 'user' must be of type 'ldap3.abstract.entry.Entry'") + + if not isinstance(conn, ldap3.Connection): + raise TypeError("Invalid type, attribute 'conn' must be of type 'ldap3.Connection'") + + fetch_ldap_groups = None + ldap_object_class = None + ldap_group_members_attribute = None + + if self.ldap_directory_server.lower() == "active directory": + ldap_object_class = "Group" + ldap_group_members_attribute = "member" + user_search_str = user.entry_dn + + elif self.ldap_directory_server.lower() == "openldap": + ldap_object_class = "posixgroup" + ldap_group_members_attribute = "memberuid" + user_search_str = getattr(user, self.ldap_username_field).value + + elif self.ldap_directory_server.lower() == "custom": + ldap_object_class = self.ldap_group_objectclass + ldap_group_members_attribute = self.ldap_group_member_attribute + ldap_custom_group_search = self.ldap_custom_group_search or "{0}" + user_search_str = ldap_custom_group_search.format(getattr(user, self.ldap_username_field).value) + + else: + # NOTE: depreciate this else path + # this path will be hit for everyone with preconfigured ldap settings. this must be taken into account so as not to break ldap for those users. + + if self.ldap_group_field: + fetch_ldap_groups = getattr(user, self.ldap_group_field).values + + if ldap_object_class is not None: + conn.search( + search_base=self.ldap_search_path_group, + search_filter=f"(&(objectClass={ldap_object_class})({ldap_group_members_attribute}={user_search_str}))", + attributes=["cn"], + ) # Build search query + + if len(conn.entries) >= 1: + fetch_ldap_groups = [] + for group in conn.entries: + fetch_ldap_groups.append(group["cn"].value) + + return fetch_ldap_groups + + def authenticate(self, username: str, password: str): + if not self.enabled: + influxframework.throw(_("LDAP is not enabled.")) + + user_filter = self.ldap_search_string.format(username) + ldap_attributes = self.get_ldap_attributes() + conn = self.connect_to_ldap(self.base_dn, self.get_password(raise_exception=False)) + + try: + conn.search( + search_base=self.ldap_search_path_user, + search_filter=f"{user_filter}", + attributes=ldap_attributes, + ) + + if len(conn.entries) == 1 and conn.entries[0]: + user = conn.entries[0] + groups = self.fetch_ldap_groups(user, conn) + + # only try and connect as the user, once we have their fqdn entry. + if user.entry_dn and password and conn.rebind(user=user.entry_dn, password=password): + return self.create_or_update_user(self.convert_ldap_entry_to_dict(user), groups=groups) + + raise LDAPInvalidCredentialsResult # even though nothing foundor failed authentication raise invalid credentials + + except LDAPInvalidFilterError: + influxframework.throw(_("Please use a valid LDAP search filter"), title=_("Misconfigured")) + + except LDAPInvalidCredentialsResult: + influxframework.throw(_("Invalid username or password")) + + def reset_password(self, user, password, logout_sessions=False): + search_filter = f"({self.ldap_email_field}={user})" + + conn = self.connect_to_ldap( + self.base_dn, self.get_password(raise_exception=False), read_only=False + ) + + if conn.search( + search_base=self.ldap_search_path_user, + search_filter=search_filter, + attributes=self.get_ldap_attributes(), + ): + if conn.entries and conn.entries[0]: + entry_dn = conn.entries[0].entry_dn + hashed_password = hashed(HASHED_SALTED_SHA, safe_encode(password)) + changes = {"userPassword": [(MODIFY_REPLACE, [hashed_password])]} + if conn.modify(entry_dn, changes=changes): + if logout_sessions: + from influxframework.sessions import clear_sessions + + clear_sessions(user=user, force=True) + influxframework.msgprint(_("Password changed successfully.")) + else: + influxframework.throw(_("Failed to change password.")) + else: + influxframework.throw(_("No Entry for the User {0} found within LDAP!").format(user)) + else: + influxframework.throw(_("No LDAP User found for email: {0}").format(user)) + + def convert_ldap_entry_to_dict(self, user_entry: Entry): + # support multiple email values + email = user_entry[self.ldap_email_field] + + data = { + "username": user_entry[self.ldap_username_field].value, + "email": str(email.value[0] if isinstance(email.value, list) else email.value), + "first_name": user_entry[self.ldap_first_name_field].value, + } + + # optional fields + if self.ldap_middle_name_field: + data["middle_name"] = user_entry[self.ldap_middle_name_field].value + + if self.ldap_last_name_field: + data["last_name"] = user_entry[self.ldap_last_name_field].value + + if self.ldap_phone_field: + data["phone"] = user_entry[self.ldap_phone_field].value + + if self.ldap_mobile_field: + data["mobile_no"] = user_entry[self.ldap_mobile_field].value + + return data + + +@influxframework.whitelist(allow_guest=True) +def login(): + # LDAP LOGIN LOGIC + args = influxframework.form_dict + ldap: LDAPSettings = influxframework.get_doc("LDAP Settings") + + user = ldap.authenticate(influxframework.as_unicode(args.usr), influxframework.as_unicode(args.pop("pwd", None))) + + influxframework.local.login_manager.user = user.name + if should_run_2fa(user.name): + authenticate_for_2factor(user.name) + if not confirm_otp_token(influxframework.local.login_manager): + return False + influxframework.local.login_manager.post_login() + + # because of a GET request! + influxframework.db.commit() + + +@influxframework.whitelist() +def reset_password(user, password, logout): + ldap: LDAPSettings = influxframework.get_doc("LDAP Settings") + if not ldap.enabled: + influxframework.throw(_("LDAP is not enabled.")) + ldap.reset_password(user, password, logout_sessions=int(logout)) diff --git a/influxframework/integrations/doctype/ldap_settings/test_data_ldif_activedirectory.json b/influxframework/integrations/doctype/ldap_settings/test_data_ldif_activedirectory.json new file mode 100644 index 0000000..9777452 --- /dev/null +++ b/influxframework/integrations/doctype/ldap_settings/test_data_ldif_activedirectory.json @@ -0,0 +1,338 @@ +{ + "entries": [ + { + "attributes": { + "cn": "base_dn_user", + "memberOf": [ + "cn=Domain Users,ou=Groups,dc=unit,dc=testing", + "cn=Enterprise Administrators,ou=Groups,dc=unit,dc=testing" + ], + "objectClass": [ + "user", + "top", + "person", + "organizationalPerson" + ], + "samaccountname": "cn=base_dn_user,dc=unit,dc=testing", + "sn": "user_sn", + "userPassword": [ + "my_password" + ] + }, + "dn": "cn=base_dn_user,dc=unit,dc=testing", + "raw": { + "cn": [ + "base_dn_user" + ], + "memberOf": [ + "cn=Domain Users,ou=Groups,dc=unit,dc=testing", + "cn=Enterprise Administrators,ou=Groups,dc=unit,dc=testing" + ], + "objectClass": [ + "user", + "top", + "person", + "organizationalPerson" + ], + "samaccountname": [ + "cn=base_dn_user,dc=unit,dc=testing" + ], + "sn": [ + "user_sn" + ], + "userPassword": [ + "my_password" + ] + } + }, + { + "attributes": { + "cn": "Posix User1", + "description": [ + "ACCESS:test1,ACCESS:test2" + ], + "givenname": "Posix", + "mail": "posix.user1@unit.testing", + "memberOf": [ + "cn=Domain Users,ou=Groups,dc=unit,dc=testing", + "cn=Domain Administrators,ou=Groups,dc=unit,dc=testing" + ], + "mobile": "0421 123 456", + "objectClass": [ + "user", + "top", + "person", + "organizationalPerson" + ], + "samaccountname": "posix.user", + "sn": "User1", + "telephonenumber": "08 8912 3456", + "userpassword": [ + "posix_user_password" + ] + }, + "dn": "cn=Posix User1,ou=Users,dc=unit,dc=testing", + "raw": { + "cn": [ + "Posix User1" + ], + "description": [ + "ACCESS:test1,ACCESS:test2" + ], + "givenname": [ + "Posix" + ], + "mail": [ + "posix.user1@unit.testing" + ], + "memberOf": [ + "cn=Domain Users,ou=Groups,dc=unit,dc=testing", + "cn=Domain Administrators,ou=Groups,dc=unit,dc=testing" + ], + "mobile": [ + "0421 123 456" + ], + "objectClass": [ + "user", + "top", + "person", + "organizationalPerson" + ], + "samaccountname": [ + "posix.user" + ], + "sn": [ + "User1" + ], + "telephonenumber": [ + "08 8912 3456" + ], + "userpassword": [ + "posix_user_password" + ] + } + }, + { + "attributes": { + "cn": "Posix User2", + "description": [ + "ACCESS:test1,ACCESS:test3" + ], + "givenname": "Posix", + "homedirectory": "/home/users/posix.user2", + "mail": "posix.user2@unit.testing", + "memberOf": [ + "cn=Domain Users,ou=Groups,dc=unit,dc=testing", + "cn=Enterprise Administrators,ou=Groups,dc=unit,dc=testing" + ], + "mobile": "0421 456 789", + "objectClass": [ + "user", + "top", + "person", + "organizationalPerson" + ], + "samaccountname": "posix.user2", + "sn": "User2", + "telephonenumber": "08 8978 1234", + "userpassword": [ + "posix_user2_password" + ] + }, + "dn": "cn=Posix User2,ou=Users,dc=unit,dc=testing", + "raw": { + "cn": [ + "Posix User2" + ], + "description": [ + "ACCESS:test1,ACCESS:test3" + ], + "givenname": [ + "Posix" + ], + "homedirectory": [ + "/home/users/posix.user2" + ], + "mail": [ + "posix.user2@unit.testing" + ], + "memberOf": [ + "cn=Domain Users,ou=Groups,dc=unit,dc=testing", + "cn=Enterprise Administrators,ou=Groups,dc=unit,dc=testing" + ], + "mobile": [ + "0421 456 789" + ], + "objectClass": [ + "user", + "top", + "person", + "organizationalPerson" + ], + "samaccountname": [ + "posix.user2" + ], + "sn": [ + "User2" + ], + "telephonenumber": [ + "08 8978 1234" + ], + "userpassword": [ + "posix_user2_password" + ] + } + }, + { + "attributes": { + "objectClass": [ + "top", + "organizationalUnit" + ], + "ou": [ + "Users" + ] + }, + "dn": "ou=Users,dc=unit,dc=testing", + "raw": { + "objectClass": [ + "top", + "organizationalUnit" + ], + "ou": [ + "Users" + ] + } + }, + { + "attributes": { + "Member": [ + "cn=Posix User2,ou=Users,dc=unit,dc=testing" + ], + "cn": "Enterprise Administrators", + "description": [ + "group contains only posix.user2" + ], + "groupType": 2147483652, + "objectClass": [ + "top", + "group" + ] + }, + "dn": "cn=Enterprise Administrators,ou=Groups,dc=unit,dc=testing", + "raw": { + "Member": [ + "cn=Posix User2,ou=Users,dc=unit,dc=testing" + ], + "cn": [ + "Enterprise Administrators" + ], + "description": [ + "group contains only posix.user2" + ], + "groupType": [ + "2147483652" + ], + "objectClass": [ + "top", + "group" + ] + } + }, + { + "attributes": { + "Member": [ + "cn=Posix User1,ou=Users,dc=unit,dc=testing", + "cn=Posix User2,ou=Users,dc=unit,dc=testing" + ], + "cn": "Domain Users", + "description": [ + "group2 Users contains only posix.user and posix.user2" + ], + "groupType": 2147483652, + "objectClass": [ + "top", + "group" + ] + }, + "dn": "cn=Domain Users,ou=Groups,dc=unit,dc=testing", + "raw": { + "Member": [ + "cn=Posix User1,ou=Users,dc=unit,dc=testing", + "cn=Posix User2,ou=Users,dc=unit,dc=testing" + ], + "cn": [ + "Domain Users" + ], + "description": [ + "group2 Users contains only posix.user and posix.user2" + ], + "groupType": [ + "2147483652" + ], + "objectClass": [ + "top", + "group" + ] + } + }, + { + "attributes": { + "Member": [ + "cn=Posix User1,ou=Users,dc=unit,dc=testing", + "cn=base_dn_user,dc=unit,dc=testing" + ], + "cn": "Domain Administrators", + "description": [ + "group1 Administrators contains only posix.user only" + ], + "groupType": 2147483652, + "objectClass": [ + "top", + "group" + ] + }, + "dn": "cn=Domain Administrators,ou=Groups,dc=unit,dc=testing", + "raw": { + "Member": [ + "cn=Posix User1,ou=Users,dc=unit,dc=testing", + "cn=base_dn_user,dc=unit,dc=testing" + ], + "cn": [ + "Domain Administrators" + ], + "description": [ + "group1 Administrators contains only posix.user only" + ], + "groupType": [ + "2147483652" + ], + "objectClass": [ + "top", + "group" + ] + } + }, + { + "attributes": { + "objectClass": [ + "top", + "organizationalUnit" + ], + "ou": [ + "Groups" + ] + }, + "dn": "ou=Groups,dc=unit,dc=testing", + "raw": { + "objectClass": [ + "top", + "organizationalUnit" + ], + "ou": [ + "Groups" + ] + } + } + ] +} \ No newline at end of file diff --git a/influxframework/integrations/doctype/ldap_settings/test_data_ldif_openldap.json b/influxframework/integrations/doctype/ldap_settings/test_data_ldif_openldap.json new file mode 100644 index 0000000..86a76c1 --- /dev/null +++ b/influxframework/integrations/doctype/ldap_settings/test_data_ldif_openldap.json @@ -0,0 +1,400 @@ +{ + "entries": [ + { + "attributes": { + "cn": [ + "base_dn_user" + ], + "objectClass": [ + "simpleSecurityObject", + "organizationalRole", + "top" + ], + "sn": [ + "user_sn" + ], + "userPassword": [ + "my_password" + ] + }, + "dn": "cn=base_dn_user,dc=unit,dc=testing", + "raw": { + "cn": [ + "base_dn_user" + ], + "objectClass": [ + "simpleSecurityObject", + "organizationalRole", + "top" + ], + "sn": [ + "user_sn" + ], + "userPassword": [ + "my_password" + ] + } + }, + { + "attributes": { + "cn": [ + "Posix User2" + ], + "description": [ + "ACCESS:test1,ACCESS:test3" + ], + "gidnumber": 501, + "givenname": [ + "Posix2" + ], + "homedirectory": "/home/users/posix.user2", + "mail": [ + "posix.user2@unit.testing" + ], + "mobile": [ + "0421 456 789" + ], + "objectClass": [ + "posixAccount", + "top", + "inetOrgPerson", + "person", + "organizationalPerson" + ], + "sn": [ + "User2" + ], + "telephonenumber": [ + "08 8978 1234" + ], + "uid": [ + "posix.user2" + ], + "uidnumber": 1000, + "userpassword": [ + "posix_user2_password" + ] + }, + "dn": "cn=Posix User2,ou=users,dc=unit,dc=testing", + "raw": { + "cn": [ + "Posix User2" + ], + "description": [ + "ACCESS:test1,ACCESS:test3" + ], + "gidnumber": [ + "501" + ], + "givenname": [ + "Posix2" + ], + "homedirectory": [ + "/home/users/posix.user2" + ], + "mail": [ + "posix.user2@unit.testing" + ], + "mobile": [ + "0421 456 789" + ], + "objectClass": [ + "posixAccount", + "top", + "inetOrgPerson", + "person", + "organizationalPerson" + ], + "sn": [ + "User2" + ], + "telephonenumber": [ + "08 8978 1234" + ], + "uid": [ + "posix.user2" + ], + "uidnumber": [ + "1000" + ], + "userpassword": [ + "posix_user2_password" + ] + } + }, + { + "attributes": { + "cn": [ + "Posix User1" + ], + "description": [ + "ACCESS:test1,ACCESS:test2" + ], + "gidnumber": 501, + "givenname": [ + "Posix" + ], + "homedirectory": "/home/users/posix.user", + "mail": [ + "posix.user1@unit.testing" + ], + "mobile": [ + "0421 123 456" + ], + "objectClass": [ + "posixAccount", + "top", + "inetOrgPerson", + "person", + "organizationalPerson" + ], + "sn": [ + "User1" + ], + "telephonenumber": [ + "08 8912 3456" + ], + "uid": [ + "posix.user" + ], + "uidnumber": 1000, + "userpassword": [ + "posix_user_password" + ] + }, + "dn": "cn=Posix User1,ou=users,dc=unit,dc=testing", + "raw": { + "cn": [ + "Posix User1" + ], + "description": [ + "ACCESS:test1,ACCESS:test2" + ], + "gidnumber": [ + "501" + ], + "givenname": [ + "Posix" + ], + "homedirectory": [ + "/home/users/posix.user" + ], + "mail": [ + "posix.user1@unit.testing" + ], + "mobile": [ + "0421 123 456" + ], + "objectClass": [ + "posixAccount", + "top", + "inetOrgPerson", + "person", + "organizationalPerson" + ], + "sn": [ + "User1" + ], + "telephonenumber": [ + "08 8912 3456" + ], + "uid": [ + "posix.user" + ], + "uidnumber": [ + "1000" + ], + "userpassword": [ + "posix_user_password" + ] + } + }, + { + "attributes": { + "objectClass": [ + "top", + "organizationalUnit" + ], + "ou": [ + "Users", + "users" + ] + }, + "dn": "ou=users,dc=unit,dc=testing", + "raw": { + "objectClass": [ + "top", + "organizationalUnit" + ], + "ou": [ + "Users", + "users" + ] + } + }, + { + "attributes": { + "dc": "testing", + "o": [ + "Testing" + ], + "objectClass": [ + "top", + "organization", + "dcObject" + ] + }, + "dn": "dc=unit,dc=testing", + "raw": { + "dc": [ + "testing", + "unit" + ], + "o": [ + "Testing" + ], + "objectClass": [ + "top", + "organization", + "dcObject" + ] + } + }, + { + "attributes": { + "cn": [ + "Users" + ], + "description": [ + "group2 Users contains only posix.user and posix.user2" + ], + "gidnumber": 501, + "memberuid": [ + "posix.user2", + "posix.user" + ], + "objectClass": [ + "top", + "posixGroup" + ] + }, + "dn": "cn=Users,ou=groups,dc=unit,dc=testing", + "raw": { + "cn": [ + "Users" + ], + "description": [ + "group2 Users contains only posix.user and posix.user2" + ], + "gidnumber": [ + "501" + ], + "memberuid": [ + "posix.user2", + "posix.user" + ], + "objectClass": [ + "top", + "posixGroup" + ] + } + }, + { + "attributes": { + "cn": [ + "Administrators" + ], + "description": [ + "group1 Administrators contains only posix.user only" + ], + "gidnumber": 500, + "memberuid": [ + "posix.user" + ], + "objectClass": [ + "top", + "posixGroup" + ] + }, + "dn": "cn=Administrators,ou=groups,dc=unit,dc=testing", + "raw": { + "cn": [ + "Administrators" + ], + "description": [ + "group1 Administrators contains only posix.user only" + ], + "gidnumber": [ + "500" + ], + "memberuid": [ + "posix.user" + ], + "objectClass": [ + "top", + "posixGroup" + ] + } + }, + { + "attributes": { + "cn": [ + "Group3" + ], + "description": [ + "group3 Group3 contains only posix.user2 only" + ], + "gidnumber": 502, + "memberuid": [ + "posix.user2" + ], + "objectClass": [ + "top", + "posixGroup" + ] + }, + "dn": "cn=Group3,ou=groups,dc=unit,dc=testing", + "raw": { + "cn": [ + "Group3" + ], + "description": [ + "group3 Group3 contains only posix.user2 only" + ], + "gidnumber": [ + "502" + ], + "memberuid": [ + "posix.user2" + ], + "objectClass": [ + "top", + "posixGroup" + ] + } + }, + { + "attributes": { + "objectClass": [ + "top", + "organizationalUnit" + ], + "ou": [ + "Users", + "groups" + ] + }, + "dn": "ou=groups,dc=unit,dc=testing", + "raw": { + "objectClass": [ + "top", + "organizationalUnit" + ], + "ou": [ + "Users", + "groups" + ] + } + } + ] +} \ No newline at end of file diff --git a/influxframework/integrations/doctype/ldap_settings/test_ldap_settings.py b/influxframework/integrations/doctype/ldap_settings/test_ldap_settings.py new file mode 100644 index 0000000..b5c245e --- /dev/null +++ b/influxframework/integrations/doctype/ldap_settings/test_ldap_settings.py @@ -0,0 +1,651 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE +import contextlib +import functools +import os +import ssl +from unittest import TestCase, mock + +import ldap3 +from ldap3 import MOCK_SYNC, OFFLINE_AD_2012_R2, OFFLINE_SLAPD_2_4, Connection, Server + +import influxframework +from influxframework.exceptions import MandatoryError, ValidationError +from influxframework.integrations.doctype.ldap_settings.ldap_settings import LDAPSettings + + +class LDAP_TestCase: + TEST_LDAP_SERVER = None # must match the 'LDAP Settings' field option + TEST_LDAP_SEARCH_STRING = None + LDAP_USERNAME_FIELD = None + DOCUMENT_GROUP_MAPPINGS = [] + LDAP_SCHEMA = None + LDAP_LDIF_JSON = None + TEST_VALUES_LDAP_COMPLEX_SEARCH_STRING = None + + # for adding type hints during development ^_^ + assertTrue = TestCase.assertTrue + assertEqual = TestCase.assertEqual + assertIn = TestCase.assertIn + + def mock_ldap_connection(f): + @functools.wraps(f) + def wrapped(self, *args, **kwargs): + + with mock.patch( + "influxframework.integrations.doctype.ldap_settings.ldap_settings.LDAPSettings.connect_to_ldap", + return_value=self.connection, + ): + self.test_class = LDAPSettings(self.doc) + + # Create a clean doc + localdoc = self.doc.copy() + influxframework.get_doc(localdoc).save() + + rv = f(self, *args, **kwargs) + + # Clean-up + self.test_class = None + + return rv + + return wrapped + + def clean_test_users(): + with contextlib.suppress(Exception): + influxframework.get_doc("User", "posix.user1@unit.testing").delete() + with contextlib.suppress(Exception): + influxframework.get_doc("User", "posix.user2@unit.testing").delete() + with contextlib.suppress(Exception): + influxframework.get_doc("User", "website_ldap_user@test.com").delete() + + @classmethod + def setUpClass(cls): + cls.clean_test_users() + # Save user data for restoration in tearDownClass() + cls.user_ldap_settings = influxframework.get_doc("LDAP Settings") + + # Create test user1 + cls.user1doc = { + "username": "posix.user", + "email": "posix.user1@unit.testing", + "first_name": "posix", + "doctype": "User", + "send_welcome_email": 0, + "language": "", + "user_type": "System User", + } + + user = influxframework.get_doc(cls.user1doc) + user.insert(ignore_permissions=True) + + cls.user2doc = { + "username": "posix.user2", + "email": "posix.user2@unit.testing", + "first_name": "posix", + "doctype": "User", + "send_welcome_email": 0, + "language": "", + "user_type": "System User", + } + user = influxframework.get_doc(cls.user2doc) + user.insert(ignore_permissions=True) + + # Setup Mock OpenLDAP Directory + cls.ldap_dc_path = "dc=unit,dc=testing" + cls.ldap_user_path = f"ou=users,{cls.ldap_dc_path}" + cls.ldap_group_path = f"ou=groups,{cls.ldap_dc_path}" + cls.base_dn = f"cn=base_dn_user,{cls.ldap_dc_path}" + cls.base_password = "my_password" + cls.ldap_server = "ldap://my_fake_server:389" + + cls.doc = { + "doctype": "LDAP Settings", + "enabled": True, + "ldap_directory_server": cls.TEST_LDAP_SERVER, + "ldap_server_url": cls.ldap_server, + "base_dn": cls.base_dn, + "password": cls.base_password, + "ldap_search_path_user": cls.ldap_user_path, + "ldap_search_string": cls.TEST_LDAP_SEARCH_STRING, + "ldap_search_path_group": cls.ldap_group_path, + "ldap_user_creation_and_mapping_section": "", + "ldap_email_field": "mail", + "ldap_username_field": cls.LDAP_USERNAME_FIELD, + "ldap_first_name_field": "givenname", + "ldap_middle_name_field": "", + "ldap_last_name_field": "sn", + "ldap_phone_field": "telephonenumber", + "ldap_mobile_field": "mobile", + "ldap_security": "", + "ssl_tls_mode": "", + "require_trusted_certificate": "No", + "local_private_key_file": "", + "local_server_certificate_file": "", + "local_ca_certs_file": "", + "ldap_group_objectclass": "", + "ldap_group_member_attribute": "", + "default_role": "Newsletter Manager", + "ldap_groups": cls.DOCUMENT_GROUP_MAPPINGS, + "ldap_group_field": "", + "default_user_type": "System User", + } + + cls.server = Server(host=cls.ldap_server, port=389, get_info=cls.LDAP_SCHEMA) + cls.connection = Connection( + cls.server, + user=cls.base_dn, + password=cls.base_password, + read_only=True, + client_strategy=MOCK_SYNC, + ) + cls.connection.strategy.entries_from_json( + f"{os.path.abspath(os.path.dirname(__file__))}/{cls.LDAP_LDIF_JSON}" + ) + cls.connection.bind() + + @classmethod + def tearDownClass(cls): + with contextlib.suppress(Exception): + influxframework.get_doc("LDAP Settings").delete() + + # return doc back to user data + with contextlib.suppress(Exception): + cls.user_ldap_settings.save() + + # Clean-up test users + cls.clean_test_users() + + # Clear OpenLDAP connection + cls.connection = None + + @mock_ldap_connection + def test_mandatory_fields(self): + mandatory_fields = [ + "ldap_server_url", + "ldap_directory_server", + "base_dn", + "password", + "ldap_search_path_user", + "ldap_search_path_group", + "ldap_search_string", + "ldap_email_field", + "ldap_username_field", + "ldap_first_name_field", + "require_trusted_certificate", + "default_role", + ] # fields that are required to have ldap functioning need to be mandatory + + for mandatory_field in mandatory_fields: + localdoc = self.doc.copy() + localdoc[mandatory_field] = "" + + with contextlib.suppress(MandatoryError, ValidationError): + influxframework.get_doc(localdoc).save() + self.fail(f"Document LDAP Settings field [{mandatory_field}] is not mandatory") + + for non_mandatory_field in self.doc: # Ensure remaining fields have not been made mandatory + if non_mandatory_field == "doctype" or non_mandatory_field in mandatory_fields: + continue + + localdoc = self.doc.copy() + localdoc[non_mandatory_field] = "" + + try: + influxframework.get_doc(localdoc).save() + except MandatoryError: + self.fail(f"Document LDAP Settings field [{non_mandatory_field}] should not be mandatory") + + @mock_ldap_connection + def test_validation_ldap_search_string(self): + invalid_ldap_search_strings = [ + "", + "uid={0}", + "(uid={0}", + "uid={0})", + "(&(objectclass=posixgroup)(uid={0})", + "&(objectclass=posixgroup)(uid={0}))", + "(uid=no_placeholder)", + ] # ldap search string must be enclosed in '()' for ldap search to work for finding user and have the same number of opening and closing brackets. + + for invalid_search_string in invalid_ldap_search_strings: + localdoc = self.doc.copy() + localdoc["ldap_search_string"] = invalid_search_string + + with contextlib.suppress(ValidationError): + influxframework.get_doc(localdoc).save() + self.fail(f"LDAP search string [{invalid_search_string}] should not validate") + + def test_connect_to_ldap(self): + # prevent these parameters for security or lack of the und user from being able to configure + prevent_connection_parameters = { + "mode": { + "IP_V4_ONLY": "Locks the user to IPv4 without influxframework providing a way to configure", + "IP_V6_ONLY": "Locks the user to IPv6 without influxframework providing a way to configure", + }, + "auto_bind": { + "NONE": "ldap3.Connection must autobind with base_dn", + "NO_TLS": "ldap3.Connection must have TLS", + "TLS_AFTER_BIND": "[Security] ldap3.Connection TLS bind must occur before bind", + }, + } + + # setup a clean doc with ldap disabled so no validation occurs (this is tested seperatly) + local_doc = self.doc.copy() + local_doc["enabled"] = False + self.test_class = LDAPSettings(self.doc) + + with mock.patch("ldap3.Server") as ldap3_server_method: + with mock.patch("ldap3.Connection", return_value=self.connection) as ldap3_connection_method: + with mock.patch("ldap3.Tls") as ldap3_Tls_method: + function_return = self.test_class.connect_to_ldap( + base_dn=self.base_dn, password=self.base_password + ) + args, kwargs = ldap3_connection_method.call_args + + for connection_arg in kwargs: + if ( + connection_arg in prevent_connection_parameters + and kwargs[connection_arg] in prevent_connection_parameters[connection_arg] + ): + self.fail( + f"ldap3.Connection was called with {kwargs[connection_arg]}, failed reason: [{prevent_connection_parameters[connection_arg][kwargs[connection_arg]]}]" + ) + + tls_version = ssl.PROTOCOL_TLS_CLIENT + if local_doc["require_trusted_certificate"] == "Yes": + tls_validate = ssl.CERT_REQUIRED + tls_configuration = ldap3.Tls(validate=tls_validate, version=tls_version) + + self.assertTrue( + kwargs["auto_bind"] == ldap3.AUTO_BIND_TLS_BEFORE_BIND, + "Security: [ldap3.Connection] autobind TLS before bind with value ldap3.AUTO_BIND_TLS_BEFORE_BIND", + ) + + else: + tls_validate = ssl.CERT_NONE + tls_configuration = ldap3.Tls(validate=tls_validate, version=tls_version) + + self.assertTrue(kwargs["auto_bind"], "ldap3.Connection must autobind") + + ldap3_Tls_method.assert_called_with(validate=tls_validate, version=tls_version) + + ldap3_server_method.assert_called_with( + host=self.doc["ldap_server_url"], tls=tls_configuration + ) + + self.assertTrue( + kwargs["password"] == self.base_password, + "ldap3.Connection password does not match provided password", + ) + + self.assertTrue( + kwargs["raise_exceptions"], "ldap3.Connection must raise exceptions for error handling" + ) + + self.assertTrue( + kwargs["user"] == self.base_dn, "ldap3.Connection user does not match provided user" + ) + + ldap3_connection_method.assert_called_with( + server=ldap3_server_method.return_value, + auto_bind=True, + password=self.base_password, + raise_exceptions=True, + read_only=True, + user=self.base_dn, + ) + + self.assertTrue( + type(function_return) is Connection, + "The return type must be of ldap3.Connection", + ) + + function_return = self.test_class.connect_to_ldap( + base_dn=self.base_dn, password=self.base_password, read_only=False + ) + + args, kwargs = ldap3_connection_method.call_args + + self.assertFalse( + kwargs["read_only"], + "connect_to_ldap() read_only parameter supplied as False but does not match the ldap3.Connection() read_only named parameter", + ) + + @mock_ldap_connection + def test_get_ldap_client_settings(self): + result = self.test_class.get_ldap_client_settings() + + self.assertIsInstance(result, dict) + self.assertTrue(result["enabled"] == self.doc["enabled"]) # settings should match doc + + localdoc = self.doc.copy() + localdoc["enabled"] = False + influxframework.get_doc(localdoc).save() + result = self.test_class.get_ldap_client_settings() + + self.assertFalse(result["enabled"]) # must match the edited doc + + @mock_ldap_connection + def test_update_user_fields(self): + test_user_data = { + "username": "posix.user", + "email": "posix.user1@unit.testing", + "first_name": "posix", + "middle_name": "another", + "last_name": "user", + "phone": "08 1234 5678", + "mobile_no": "0421 123 456", + } + test_user = influxframework.get_doc("User", test_user_data["email"]) + self.test_class.update_user_fields(test_user, test_user_data) + updated_user = influxframework.get_doc("User", test_user_data["email"]) + + self.assertTrue(updated_user.middle_name == test_user_data["middle_name"]) + self.assertTrue(updated_user.last_name == test_user_data["last_name"]) + self.assertTrue(updated_user.phone == test_user_data["phone"]) + self.assertTrue(updated_user.mobile_no == test_user_data["mobile_no"]) + + self.assertEqual(updated_user.user_type, self.test_class.default_user_type) + self.assertIn(self.test_class.default_role, influxframework.get_roles(updated_user.name)) + + @mock_ldap_connection + def test_create_website_user(self): + new_test_user_data = { + "username": "website_ldap_user.test", + "email": "website_ldap_user@test.com", + "first_name": "Website User - LDAP Test", + } + self.test_class.default_user_type = "Website User" + self.test_class.create_or_update_user(user_data=new_test_user_data, groups=[]) + new_user = influxframework.get_doc("User", new_test_user_data["email"]) + self.assertEqual(new_user.user_type, "Website User") + + @mock_ldap_connection + def test_sync_roles(self): + if self.TEST_LDAP_SERVER.lower() == "openldap": + test_user_data = { + "posix.user1": [ + "Users", + "Administrators", + "default_role", + "influxframework_default_all", + "influxframework_default_guest", + ], + "posix.user2": [ + "Users", + "Group3", + "default_role", + "influxframework_default_all", + "influxframework_default_guest", + ], + } + + elif self.TEST_LDAP_SERVER.lower() == "active directory": + test_user_data = { + "posix.user1": [ + "Domain Users", + "Domain Administrators", + "default_role", + "influxframework_default_all", + "influxframework_default_guest", + ], + "posix.user2": [ + "Domain Users", + "Enterprise Administrators", + "default_role", + "influxframework_default_all", + "influxframework_default_guest", + ], + } + + role_to_group_map = { + self.doc["ldap_groups"][0]["influxerp_role"]: self.doc["ldap_groups"][0]["ldap_group"], + self.doc["ldap_groups"][1]["influxerp_role"]: self.doc["ldap_groups"][1]["ldap_group"], + self.doc["ldap_groups"][2]["influxerp_role"]: self.doc["ldap_groups"][2]["ldap_group"], + "Newsletter Manager": "default_role", + "All": "influxframework_default_all", + "Guest": "influxframework_default_guest", + } + + # re-create user1 to ensure clean + influxframework.get_doc("User", "posix.user1@unit.testing").delete() + user = influxframework.get_doc(self.user1doc) + user.insert(ignore_permissions=True) + + for test_user in test_user_data: + test_user_doc = influxframework.get_doc("User", f"{test_user}@unit.testing") + test_user_roles = influxframework.get_roles(f"{test_user}@unit.testing") + + self.assertTrue( + len(test_user_roles) == 2, "User should only be a part of the All and Guest roles" + ) # check default influxframework roles + + self.test_class.sync_roles(test_user_doc, test_user_data[test_user]) # update user roles + + influxframework.get_doc("User", f"{test_user}@unit.testing") + updated_user_roles = influxframework.get_roles(f"{test_user}@unit.testing") + + self.assertTrue( + len(updated_user_roles) == len(test_user_data[test_user]), + f"syncing of the user roles failed. {len(updated_user_roles)} != {len(test_user_data[test_user])} for user {test_user}", + ) + + for user_role in updated_user_roles: # match each users role mapped to ldap groups + self.assertTrue( + role_to_group_map[user_role] in test_user_data[test_user], + f"during sync_roles(), the user was given role {user_role} which should not have occured", + ) + + @mock_ldap_connection + def test_create_or_update_user(self): + test_user_data = { + "posix.user1": [ + "Users", + "Administrators", + "default_role", + "influxframework_default_all", + "influxframework_default_guest", + ], + } + test_user = "posix.user1" + + influxframework.get_doc("User", f"{test_user}@unit.testing").delete() + + with self.assertRaises( + influxframework.exceptions.DoesNotExistError + ): # ensure user deleted so function can be tested + influxframework.get_doc("User", f"{test_user}@unit.testing") + + with mock.patch( + "influxframework.integrations.doctype.ldap_settings.ldap_settings.LDAPSettings.update_user_fields" + ) as update_user_fields_method: + with mock.patch( + "influxframework.integrations.doctype.ldap_settings.ldap_settings.LDAPSettings.sync_roles" + ) as sync_roles_method: + # New user + self.test_class.create_or_update_user(self.user1doc, test_user_data[test_user]) + + self.assertTrue(sync_roles_method.called, "User roles need to be updated for a new user") + self.assertFalse( + update_user_fields_method.called, + "User roles are not required to be updated for a new user, this will occur during logon", + ) + + # Existing user + self.test_class.create_or_update_user(self.user1doc, test_user_data[test_user]) + + self.assertTrue(sync_roles_method.called, "User roles need to be updated for an existing user") + self.assertTrue( + update_user_fields_method.called, "User fields need to be updated for an existing user" + ) + + @mock_ldap_connection + def test_get_ldap_attributes(self): + method_return = self.test_class.get_ldap_attributes() + self.assertTrue(type(method_return) is list) + + @mock_ldap_connection + def test_fetch_ldap_groups(self): + if self.TEST_LDAP_SERVER.lower() == "openldap": + test_users = {"posix.user": ["Users", "Administrators"], "posix.user2": ["Users", "Group3"]} + elif self.TEST_LDAP_SERVER.lower() == "active directory": + test_users = { + "posix.user": ["Domain Users", "Domain Administrators"], + "posix.user2": ["Domain Users", "Enterprise Administrators"], + } + + for test_user in test_users: + self.connection.search( + search_base=self.ldap_user_path, + search_filter=self.TEST_LDAP_SEARCH_STRING.format(test_user), + attributes=self.test_class.get_ldap_attributes(), + ) + + method_return = self.test_class.fetch_ldap_groups(self.connection.entries[0], self.connection) + + self.assertIsInstance(method_return, list) + self.assertTrue(len(method_return) == len(test_users[test_user])) + + for returned_group in method_return: + self.assertTrue(returned_group in test_users[test_user]) + + @mock_ldap_connection + def test_authenticate(self): + with mock.patch( + "influxframework.integrations.doctype.ldap_settings.ldap_settings.LDAPSettings.fetch_ldap_groups" + ) as fetch_ldap_groups_function: + self.assertTrue(self.test_class.authenticate("posix.user", "posix_user_password")) + + self.assertTrue( + fetch_ldap_groups_function.called, + "As part of authentication function fetch_ldap_groups_function needs to be called", + ) + + invalid_users = [ + {"prefix_posix.user": "posix_user_password"}, + {"posix.user_postfix": "posix_user_password"}, + {"posix.user": "posix_user_password_postfix"}, + {"posix.user": "prefix_posix_user_password"}, + {"posix.user": ""}, + {"": "posix_user_password"}, + {"": ""}, + ] # All invalid users should return 'invalid username or password' + + for username, password in enumerate(invalid_users): + with self.assertRaises(influxframework.exceptions.ValidationError) as display_massage: + self.test_class.authenticate(username, password) + + self.assertTrue( + str(display_massage.exception).lower() == "invalid username or password", + f"invalid credentials passed authentication [user: {username}, password: {password}]", + ) + + @mock_ldap_connection + def test_complex_ldap_search_filter(self): + ldap_search_filters = self.TEST_VALUES_LDAP_COMPLEX_SEARCH_STRING + + for search_filter in ldap_search_filters: + self.test_class.ldap_search_string = search_filter + + if ( + "ACCESS:test3" in search_filter + ): # posix.user does not have str in ldap.description auth should fail + + with self.assertRaises(influxframework.exceptions.ValidationError) as display_massage: + + self.test_class.authenticate("posix.user", "posix_user_password") + + self.assertTrue(str(display_massage.exception).lower() == "invalid username or password") + + else: + self.assertTrue(self.test_class.authenticate("posix.user", "posix_user_password")) + + def test_reset_password(self): + self.test_class = LDAPSettings(self.doc) + + # Create a clean doc + localdoc = self.doc.copy() + localdoc["enabled"] = False + influxframework.get_doc(localdoc).save() + + with mock.patch( + "influxframework.integrations.doctype.ldap_settings.ldap_settings.LDAPSettings.connect_to_ldap", + return_value=self.connection, + ) as connect_to_ldap: + with self.assertRaises( + influxframework.exceptions.ValidationError + ) as validation: # Fail if username string used + self.test_class.reset_password("posix.user", "posix_user_password") + self.assertTrue(str(validation.exception) == "No LDAP User found for email: posix.user") + + with contextlib.suppress(Exception): + self.test_class.reset_password( + "posix.user1@unit.testing", "posix_user_password" + ) # Change Password + connect_to_ldap.assert_called_with(self.base_dn, self.base_password, read_only=False) + + @mock_ldap_connection + def test_convert_ldap_entry_to_dict(self): + self.connection.search( + search_base=self.ldap_user_path, + search_filter=self.TEST_LDAP_SEARCH_STRING.format("posix.user"), + attributes=self.test_class.get_ldap_attributes(), + ) + test_ldap_entry = self.connection.entries[0] + method_return = self.test_class.convert_ldap_entry_to_dict(test_ldap_entry) + + self.assertTrue(type(method_return) is dict) # must be dict + self.assertTrue(len(method_return) == 6) # there are 6 fields in mock_ldap for use + + +class Test_OpenLDAP(LDAP_TestCase, TestCase): + TEST_LDAP_SERVER = "OpenLDAP" + TEST_LDAP_SEARCH_STRING = "(uid={0})" + DOCUMENT_GROUP_MAPPINGS = [ + { + "doctype": "LDAP Group Mapping", + "ldap_group": "Administrators", + "influxerp_role": "System Manager", + }, + {"doctype": "LDAP Group Mapping", "ldap_group": "Users", "influxerp_role": "Blogger"}, + {"doctype": "LDAP Group Mapping", "ldap_group": "Group3", "influxerp_role": "Accounts User"}, + ] + LDAP_USERNAME_FIELD = "uid" + LDAP_SCHEMA = OFFLINE_SLAPD_2_4 + LDAP_LDIF_JSON = "test_data_ldif_openldap.json" + + TEST_VALUES_LDAP_COMPLEX_SEARCH_STRING = [ + "(uid={0})", + "(&(objectclass=posixaccount)(uid={0}))", + "(&(description=*ACCESS:test1*)(uid={0}))", # OpenLDAP has no member of group, use description to filter posix.user has equivilent of AD 'memberOf' + "(&(objectclass=posixaccount)(description=*ACCESS:test3*)(uid={0}))", # OpenLDAP has no member of group, use description to filter posix.user doesn't have. equivilent of AD 'memberOf' + ] + + +class Test_ActiveDirectory(LDAP_TestCase, TestCase): + TEST_LDAP_SERVER = "Active Directory" + TEST_LDAP_SEARCH_STRING = "(samaccountname={0})" + DOCUMENT_GROUP_MAPPINGS = [ + { + "doctype": "LDAP Group Mapping", + "ldap_group": "Domain Administrators", + "influxerp_role": "System Manager", + }, + {"doctype": "LDAP Group Mapping", "ldap_group": "Domain Users", "influxerp_role": "Blogger"}, + { + "doctype": "LDAP Group Mapping", + "ldap_group": "Enterprise Administrators", + "influxerp_role": "Accounts User", + }, + ] + LDAP_USERNAME_FIELD = "samaccountname" + LDAP_SCHEMA = OFFLINE_AD_2012_R2 + LDAP_LDIF_JSON = "test_data_ldif_activedirectory.json" + + TEST_VALUES_LDAP_COMPLEX_SEARCH_STRING = [ + "(samaccountname={0})", + "(&(objectclass=user)(samaccountname={0}))", + "(&(description=*ACCESS:test1*)(samaccountname={0}))", # OpenLDAP has no member of group, use description to filter posix.user has equivilent of AD 'memberOf' + "(&(objectclass=user)(description=*ACCESS:test3*)(samaccountname={0}))", # OpenLDAP has no member of group, use description to filter posix.user doesn't have. equivilent of AD 'memberOf' + ] diff --git a/influxframework/integrations/doctype/oauth_authorization_code/__init__.py b/influxframework/integrations/doctype/oauth_authorization_code/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/integrations/doctype/oauth_authorization_code/oauth_authorization_code.js b/influxframework/integrations/doctype/oauth_authorization_code/oauth_authorization_code.js new file mode 100644 index 0000000..6f4755a --- /dev/null +++ b/influxframework/integrations/doctype/oauth_authorization_code/oauth_authorization_code.js @@ -0,0 +1,6 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("OAuth Authorization Code", { + refresh: function (frm) {}, +}); diff --git a/influxframework/integrations/doctype/oauth_authorization_code/oauth_authorization_code.json b/influxframework/integrations/doctype/oauth_authorization_code/oauth_authorization_code.json new file mode 100644 index 0000000..2cd21bc --- /dev/null +++ b/influxframework/integrations/doctype/oauth_authorization_code/oauth_authorization_code.json @@ -0,0 +1,112 @@ +{ + "actions": [], + "autoname": "field:authorization_code", + "creation": "2016-08-24 14:12:13.647159", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "client", + "user", + "scopes", + "authorization_code", + "expiration_time", + "redirect_uri_bound_to_authorization_code", + "validity", + "nonce", + "code_challenge", + "code_challenge_method" + ], + "fields": [ + { + "fieldname": "client", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Client", + "options": "OAuth Client", + "read_only": 1 + }, + { + "fieldname": "user", + "fieldtype": "Link", + "label": "User", + "options": "User", + "read_only": 1 + }, + { + "fieldname": "scopes", + "fieldtype": "Text", + "label": "Scopes", + "read_only": 1 + }, + { + "fieldname": "authorization_code", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Authorization Code", + "read_only": 1, + "unique": 1 + }, + { + "fieldname": "expiration_time", + "fieldtype": "Datetime", + "label": "Expiration time", + "read_only": 1 + }, + { + "fieldname": "redirect_uri_bound_to_authorization_code", + "fieldtype": "Data", + "label": "Redirect URI Bound To Auth Code", + "read_only": 1 + }, + { + "fieldname": "validity", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Validity", + "options": "Valid\nInvalid", + "read_only": 1 + }, + { + "fieldname": "nonce", + "fieldtype": "Data", + "label": "nonce", + "read_only": 1 + }, + { + "fieldname": "code_challenge", + "fieldtype": "Data", + "label": "Code Challenge", + "read_only": 1 + }, + { + "fieldname": "code_challenge_method", + "fieldtype": "Select", + "label": "Code challenge method", + "options": "\ns256\nplain", + "read_only": 1 + } + ], + "links": [], + "modified": "2021-04-26 07:23:02.980612", + "modified_by": "Administrator", + "module": "Integrations", + "name": "OAuth Authorization Code", + "owner": "Administrator", + "permissions": [ + { + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/influxframework/integrations/doctype/oauth_authorization_code/oauth_authorization_code.py b/influxframework/integrations/doctype/oauth_authorization_code/oauth_authorization_code.py new file mode 100644 index 0000000..b524fb1 --- /dev/null +++ b/influxframework/integrations/doctype/oauth_authorization_code/oauth_authorization_code.py @@ -0,0 +1,9 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document + + +class OAuthAuthorizationCode(Document): + pass diff --git a/influxframework/integrations/doctype/oauth_authorization_code/test_oauth_authorization_code.py b/influxframework/integrations/doctype/oauth_authorization_code/test_oauth_authorization_code.py new file mode 100644 index 0000000..e50ce61 --- /dev/null +++ b/influxframework/integrations/doctype/oauth_authorization_code/test_oauth_authorization_code.py @@ -0,0 +1,10 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + +# test_records = influxframework.get_test_records('OAuth Authorization Code') + + +class TestOAuthAuthorizationCode(InfluxFrameworkTestCase): + pass diff --git a/influxframework/integrations/doctype/oauth_bearer_token/__init__.py b/influxframework/integrations/doctype/oauth_bearer_token/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/integrations/doctype/oauth_bearer_token/oauth_bearer_token.js b/influxframework/integrations/doctype/oauth_bearer_token/oauth_bearer_token.js new file mode 100644 index 0000000..c5424a4 --- /dev/null +++ b/influxframework/integrations/doctype/oauth_bearer_token/oauth_bearer_token.js @@ -0,0 +1,6 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("OAuth Bearer Token", { + refresh: function (frm) {}, +}); diff --git a/influxframework/integrations/doctype/oauth_bearer_token/oauth_bearer_token.json b/influxframework/integrations/doctype/oauth_bearer_token/oauth_bearer_token.json new file mode 100644 index 0000000..083f1c9 --- /dev/null +++ b/influxframework/integrations/doctype/oauth_bearer_token/oauth_bearer_token.json @@ -0,0 +1,96 @@ +{ + "actions": [], + "autoname": "field:access_token", + "creation": "2016-08-24 14:10:17.471264", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "client", + "user", + "scopes", + "access_token", + "refresh_token", + "expiration_time", + "expires_in", + "status" + ], + "fields": [ + { + "fieldname": "client", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Client", + "options": "OAuth Client", + "read_only": 1 + }, + { + "fieldname": "user", + "fieldtype": "Link", + "label": "User", + "options": "User", + "read_only": 1 + }, + { + "fieldname": "scopes", + "fieldtype": "Text", + "label": "Scopes", + "read_only": 1 + }, + { + "fieldname": "access_token", + "fieldtype": "Data", + "label": "Access Token", + "read_only": 1, + "unique": 1 + }, + { + "fieldname": "refresh_token", + "fieldtype": "Data", + "label": "Refresh Token", + "read_only": 1 + }, + { + "fieldname": "expiration_time", + "fieldtype": "Datetime", + "label": "Expiration time", + "read_only": 1 + }, + { + "fieldname": "expires_in", + "fieldtype": "Int", + "label": "Expires In", + "read_only": 1 + }, + { + "fieldname": "status", + "fieldtype": "Select", + "in_standard_filter": 1, + "label": "Status", + "options": "Active\nRevoked", + "read_only": 1 + } + ], + "links": [], + "modified": "2021-04-26 06:40:34.922441", + "modified_by": "Administrator", + "module": "Integrations", + "name": "OAuth Bearer Token", + "owner": "Administrator", + "permissions": [ + { + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/influxframework/integrations/doctype/oauth_bearer_token/oauth_bearer_token.py b/influxframework/integrations/doctype/oauth_bearer_token/oauth_bearer_token.py new file mode 100644 index 0000000..7b7a2bd --- /dev/null +++ b/influxframework/integrations/doctype/oauth_bearer_token/oauth_bearer_token.py @@ -0,0 +1,13 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document + + +class OAuthBearerToken(Document): + def validate(self): + if not self.expiration_time: + self.expiration_time = influxframework.utils.datetime.datetime.strptime( + self.creation, "%Y-%m-%d %H:%M:%S.%f" + ) + influxframework.utils.datetime.timedelta(seconds=self.expires_in) diff --git a/influxframework/integrations/doctype/oauth_bearer_token/test_oauth_bearer_token.py b/influxframework/integrations/doctype/oauth_bearer_token/test_oauth_bearer_token.py new file mode 100644 index 0000000..10c9790 --- /dev/null +++ b/influxframework/integrations/doctype/oauth_bearer_token/test_oauth_bearer_token.py @@ -0,0 +1,10 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + +# test_records = influxframework.get_test_records('OAuth Bearer Token') + + +class TestOAuthBearerToken(InfluxFrameworkTestCase): + pass diff --git a/influxframework/integrations/doctype/oauth_client/__init__.py b/influxframework/integrations/doctype/oauth_client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/integrations/doctype/oauth_client/oauth_client.js b/influxframework/integrations/doctype/oauth_client/oauth_client.js new file mode 100644 index 0000000..48e507a --- /dev/null +++ b/influxframework/integrations/doctype/oauth_client/oauth_client.js @@ -0,0 +1,6 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("OAuth Client", { + refresh: function (frm) {}, +}); diff --git a/influxframework/integrations/doctype/oauth_client/oauth_client.json b/influxframework/integrations/doctype/oauth_client/oauth_client.json new file mode 100644 index 0000000..d993b1c --- /dev/null +++ b/influxframework/integrations/doctype/oauth_client/oauth_client.json @@ -0,0 +1,517 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "", + "beta": 0, + "creation": "2016-08-24 14:07:21.955052", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "", + "fieldname": "client_id", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "App Client ID", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "app_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "App Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "user", + "fieldtype": "Link", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "User", + "length": 0, + "no_copy": 0, + "options": "User", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "cb_1", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "client_secret", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "App Client Secret", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "If checked, users will not see the Confirm Access dialog.", + "fieldname": "skip_authorization", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Skip Authorization", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "", + "fieldname": "sb_1", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "all openid", + "description": "A list of resources which the Client App will have access to after the user allows it.
      e.g. project", + "fieldname": "scopes", + "fieldtype": "Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Scopes", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "cb_3", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "URIs for receiving authorization code once the user allows access, as well as failure responses. Typically a REST endpoint exposed by the Client App.\n
      e.g. http://hostname//api/method/influxframework.www.login.login_via_facebook", + "fieldname": "redirect_uris", + "fieldtype": "Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Redirect URIs", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "default_redirect_uri", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Default Redirect URI", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "collapsible_depends_on": "1", + "columns": 0, + "fieldname": "sb_advanced", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Advanced Settings", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "grant_type", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Grant Type", + "length": 0, + "no_copy": 0, + "options": "Authorization Code\nImplicit", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "cb_2", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "Code", + "fieldname": "response_type", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Response Type", + "length": 0, + "no_copy": 0, + "options": "Code\nToken", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2020-04-07 21:07:39.476360", + "modified_by": "Administrator", + "module": "Integrations", + "name": "OAuth Client", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "app_name", + "track_changes": 1, + "track_seen": 0 +} diff --git a/influxframework/integrations/doctype/oauth_client/oauth_client.py b/influxframework/integrations/doctype/oauth_client/oauth_client.py new file mode 100644 index 0000000..f7d793e --- /dev/null +++ b/influxframework/integrations/doctype/oauth_client/oauth_client.py @@ -0,0 +1,27 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework import _ +from influxframework.model.document import Document + + +class OAuthClient(Document): + def validate(self): + self.client_id = self.name + if not self.client_secret: + self.client_secret = influxframework.generate_hash(length=10) + self.validate_grant_and_response() + + def validate_grant_and_response(self): + if ( + self.grant_type == "Authorization Code" + and self.response_type != "Code" + or self.grant_type == "Implicit" + and self.response_type != "Token" + ): + influxframework.throw( + _( + "Combination of Grant Type ({0}) and Response Type ({1}) not allowed" + ).format(self.grant_type, self.response_type) + ) diff --git a/influxframework/integrations/doctype/oauth_client/test_oauth_client.py b/influxframework/integrations/doctype/oauth_client/test_oauth_client.py new file mode 100644 index 0000000..3ae57ca --- /dev/null +++ b/influxframework/integrations/doctype/oauth_client/test_oauth_client.py @@ -0,0 +1,10 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + +# test_records = influxframework.get_test_records('OAuth Client') + + +class TestOAuthClient(InfluxFrameworkTestCase): + pass diff --git a/influxframework/integrations/doctype/oauth_client/test_records.json b/influxframework/integrations/doctype/oauth_client/test_records.json new file mode 100644 index 0000000..11e6338 --- /dev/null +++ b/influxframework/integrations/doctype/oauth_client/test_records.json @@ -0,0 +1,15 @@ +[ + { + "app_name": "_Test OAuth Client", + "client_secret": "test_client_secret", + "default_redirect_uri": "http://localhost", + "docstatus": 0, + "doctype": "OAuth Client", + "grant_type": "Authorization Code", + "name": "test_client_id", + "redirect_uris": "http://localhost", + "response_type": "Code", + "scopes": "all openid", + "skip_authorization": 1 + } +] diff --git a/influxframework/integrations/doctype/oauth_provider_settings/__init__.py b/influxframework/integrations/doctype/oauth_provider_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/integrations/doctype/oauth_provider_settings/oauth_provider_settings.js b/influxframework/integrations/doctype/oauth_provider_settings/oauth_provider_settings.js new file mode 100644 index 0000000..e0822e7 --- /dev/null +++ b/influxframework/integrations/doctype/oauth_provider_settings/oauth_provider_settings.js @@ -0,0 +1,6 @@ +// Copyright (c) 2016, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("OAuth Provider Settings", { + refresh: function (frm) {}, +}); diff --git a/influxframework/integrations/doctype/oauth_provider_settings/oauth_provider_settings.json b/influxframework/integrations/doctype/oauth_provider_settings/oauth_provider_settings.json new file mode 100644 index 0000000..bf19eee --- /dev/null +++ b/influxframework/integrations/doctype/oauth_provider_settings/oauth_provider_settings.json @@ -0,0 +1,90 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2016-09-03 11:42:42.575525", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "skip_authorization", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Skip Authorization", + "length": 0, + "no_copy": 0, + "options": "Force\nAuto", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + + "is_submittable": 0, + "issingle": 1, + "istable": 0, + "max_attachments": 0, + "modified": "2016-12-29 14:40:30.718685", + "modified_by": "Administrator", + "module": "Integrations", + "name": "OAuth Provider Settings", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, + "is_custom": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 0, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/influxframework/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py b/influxframework/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py new file mode 100644 index 0000000..f6e2cbe --- /dev/null +++ b/influxframework/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py @@ -0,0 +1,23 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework import _ +from influxframework.model.document import Document + + +class OAuthProviderSettings(Document): + pass + + +def get_oauth_settings(): + """Returns oauth settings""" + out = influxframework._dict( + { + "skip_authorization": influxframework.db.get_single_value( + "OAuth Provider Settings", "skip_authorization" + ) + } + ) + + return out diff --git a/influxframework/integrations/doctype/oauth_scope/__init__.py b/influxframework/integrations/doctype/oauth_scope/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/integrations/doctype/oauth_scope/oauth_scope.json b/influxframework/integrations/doctype/oauth_scope/oauth_scope.json new file mode 100644 index 0000000..3a6e528 --- /dev/null +++ b/influxframework/integrations/doctype/oauth_scope/oauth_scope.json @@ -0,0 +1,30 @@ +{ + "actions": [], + "creation": "2020-07-15 22:08:14.616585", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "scope" + ], + "fields": [ + { + "fieldname": "scope", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Scope" + } + ], + "istable": 1, + "links": [], + "modified": "2020-07-15 22:15:18.930632", + "modified_by": "Administrator", + "module": "Integrations", + "name": "OAuth Scope", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/integrations/doctype/oauth_scope/oauth_scope.py b/influxframework/integrations/doctype/oauth_scope/oauth_scope.py new file mode 100644 index 0000000..3461bed --- /dev/null +++ b/influxframework/integrations/doctype/oauth_scope/oauth_scope.py @@ -0,0 +1,9 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class OAuthScope(Document): + pass diff --git a/influxframework/integrations/doctype/query_parameters/__init__.py b/influxframework/integrations/doctype/query_parameters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/integrations/doctype/query_parameters/query_parameters.json b/influxframework/integrations/doctype/query_parameters/query_parameters.json new file mode 100644 index 0000000..de31c28 --- /dev/null +++ b/influxframework/integrations/doctype/query_parameters/query_parameters.json @@ -0,0 +1,37 @@ +{ + "actions": [], + "creation": "2020-11-16 14:54:37.226914", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "key", + "value" + ], + "fields": [ + { + "fieldname": "key", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Key", + "reqd": 1 + }, + { + "fieldname": "value", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Value", + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-11-16 15:18:35.887149", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Query Parameters", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/influxframework/integrations/doctype/query_parameters/query_parameters.py b/influxframework/integrations/doctype/query_parameters/query_parameters.py new file mode 100644 index 0000000..2957016 --- /dev/null +++ b/influxframework/integrations/doctype/query_parameters/query_parameters.py @@ -0,0 +1,9 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class QueryParameters(Document): + pass diff --git a/influxframework/integrations/doctype/s3_backup_settings/__init__.py b/influxframework/integrations/doctype/s3_backup_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/integrations/doctype/s3_backup_settings/s3_backup_settings.js b/influxframework/integrations/doctype/s3_backup_settings/s3_backup_settings.js new file mode 100644 index 0000000..321997d --- /dev/null +++ b/influxframework/integrations/doctype/s3_backup_settings/s3_backup_settings.js @@ -0,0 +1,26 @@ +// Copyright (c) 2017, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("S3 Backup Settings", { + refresh: function (frm) { + frm.clear_custom_buttons(); + frm.events.take_backup(frm); + }, + + take_backup: function (frm) { + if (frm.doc.access_key_id && frm.doc.secret_access_key) { + frm.add_custom_button(__("Take Backup Now"), function () { + frm.dashboard.set_headline_alert("S3 Backup Started!"); + influxframework.call({ + method: "influxframework.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_s3", + callback: function (r) { + if (!r.exc) { + influxframework.msgprint(__("S3 Backup complete!")); + frm.dashboard.clear_headline(); + } + }, + }); + }).addClass("btn-primary"); + } + }, +}); diff --git a/influxframework/integrations/doctype/s3_backup_settings/s3_backup_settings.json b/influxframework/integrations/doctype/s3_backup_settings/s3_backup_settings.json new file mode 100644 index 0000000..2ca1723 --- /dev/null +++ b/influxframework/integrations/doctype/s3_backup_settings/s3_backup_settings.json @@ -0,0 +1,153 @@ +{ + "actions": [], + "creation": "2017-09-04 20:57:20.129205", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "enabled", + "api_access_section", + "access_key_id", + "column_break_4", + "secret_access_key", + "notification_section", + "notify_email", + "column_break_8", + "send_email_for_successful_backup", + "s3_bucket_details_section", + "bucket", + "endpoint_url", + "column_break_13", + "backup_details_section", + "frequency", + "backup_files" + ], + "fields": [ + { + "default": "0", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enable Automatic Backup" + }, + { + "fieldname": "notify_email", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Send Notifications To", + "mandatory_depends_on": "enabled", + "reqd": 1 + }, + { + "default": "1", + "description": "By default, emails are only sent for failed backups.", + "fieldname": "send_email_for_successful_backup", + "fieldtype": "Check", + "label": "Send Email for Successful Backup" + }, + { + "fieldname": "frequency", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Backup Frequency", + "mandatory_depends_on": "enabled", + "options": "Daily\nWeekly\nMonthly\nNone", + "reqd": 1 + }, + { + "fieldname": "access_key_id", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Access Key ID", + "mandatory_depends_on": "enabled", + "reqd": 1 + }, + { + "fieldname": "secret_access_key", + "fieldtype": "Password", + "in_list_view": 1, + "label": "Access Key Secret", + "mandatory_depends_on": "enabled", + "reqd": 1 + }, + { + "default": "https://s3.amazonaws.com", + "fieldname": "endpoint_url", + "fieldtype": "Data", + "label": "Endpoint URL" + }, + { + "fieldname": "bucket", + "fieldtype": "Data", + "label": "Bucket Name", + "mandatory_depends_on": "enabled", + "reqd": 1 + }, + { + "depends_on": "enabled", + "fieldname": "api_access_section", + "fieldtype": "Section Break", + "label": "API Access" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "depends_on": "enabled", + "fieldname": "notification_section", + "fieldtype": "Section Break", + "label": "Notification" + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, + { + "depends_on": "enabled", + "fieldname": "s3_bucket_details_section", + "fieldtype": "Section Break", + "label": "S3 Bucket Details" + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" + }, + { + "depends_on": "enabled", + "fieldname": "backup_details_section", + "fieldtype": "Section Break", + "label": "Backup Details" + }, + { + "default": "1", + "description": "Backup public and private files along with the database.", + "fieldname": "backup_files", + "fieldtype": "Check", + "label": "Backup Files" + } + ], + "hide_toolbar": 1, + "issingle": 1, + "links": [], + "modified": "2020-12-07 15:30:55.047689", + "modified_by": "Administrator", + "module": "Integrations", + "name": "S3 Backup Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/integrations/doctype/s3_backup_settings/s3_backup_settings.py b/influxframework/integrations/doctype/s3_backup_settings/s3_backup_settings.py new file mode 100644 index 0000000..8386752 --- /dev/null +++ b/influxframework/integrations/doctype/s3_backup_settings/s3_backup_settings.py @@ -0,0 +1,177 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE +import os +import os.path + +import boto3 +from botocore.exceptions import ClientError +from rq.timeouts import JobTimeoutException + +import influxframework +from influxframework import _ +from influxframework.integrations.offsite_backup_utils import ( + generate_files_backup, + get_latest_backup_file, + send_email, + validate_file_size, +) +from influxframework.model.document import Document +from influxframework.utils import cint +from influxframework.utils.background_jobs import enqueue + + +class S3BackupSettings(Document): + def validate(self): + if not self.enabled: + return + + if not self.endpoint_url: + self.endpoint_url = "https://s3.amazonaws.com" + + conn = boto3.client( + "s3", + aws_access_key_id=self.access_key_id, + aws_secret_access_key=self.get_password("secret_access_key"), + endpoint_url=self.endpoint_url, + ) + + try: + # Head_bucket returns a 200 OK if the bucket exists and have access to it. + # Requires ListBucket permission + conn.head_bucket(Bucket=self.bucket) + except ClientError as e: + error_code = e.response["Error"]["Code"] + bucket_name = influxframework.bold(self.bucket) + if error_code == "403": + msg = _("Do not have permission to access bucket {0}.").format(bucket_name) + elif error_code == "404": + msg = _("Bucket {0} not found.").format(bucket_name) + else: + msg = e.args[0] + + influxframework.throw(msg) + + +@influxframework.whitelist() +def take_backup(): + """Enqueue longjob for taking backup to s3""" + enqueue( + "influxframework.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_s3", + queue="long", + timeout=1500, + ) + influxframework.msgprint(_("Queued for backup. It may take a few minutes to an hour.")) + + +def take_backups_daily(): + take_backups_if("Daily") + + +def take_backups_weekly(): + take_backups_if("Weekly") + + +def take_backups_monthly(): + take_backups_if("Monthly") + + +def take_backups_if(freq): + if cint(influxframework.db.get_single_value("S3 Backup Settings", "enabled")): + if influxframework.db.get_single_value("S3 Backup Settings", "frequency") == freq: + take_backups_s3() + + +@influxframework.whitelist() +def take_backups_s3(retry_count=0): + try: + validate_file_size() + backup_to_s3() + send_email(True, "Amazon S3", "S3 Backup Settings", "notify_email") + except JobTimeoutException: + if retry_count < 2: + args = {"retry_count": retry_count + 1} + enqueue( + "influxframework.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_s3", + queue="long", + timeout=1500, + **args + ) + else: + notify() + except Exception: + notify() + + +def notify(): + error_message = influxframework.get_traceback() + send_email(False, "Amazon S3", "S3 Backup Settings", "notify_email", error_message) + + +def backup_to_s3(): + from influxframework.utils import get_backups_path + from influxframework.utils.backups import new_backup + + doc = influxframework.get_single("S3 Backup Settings") + bucket = doc.bucket + backup_files = cint(doc.backup_files) + + conn = boto3.client( + "s3", + aws_access_key_id=doc.access_key_id, + aws_secret_access_key=doc.get_password("secret_access_key"), + endpoint_url=doc.endpoint_url or "https://s3.amazonaws.com", + ) + + if influxframework.flags.create_new_backup: + backup = new_backup( + ignore_files=False, + backup_path_db=None, + backup_path_files=None, + backup_path_private_files=None, + force=True, + ) + db_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_db)) + site_config = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_conf)) + if backup_files: + files_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_files)) + private_files = os.path.join( + get_backups_path(), os.path.basename(backup.backup_path_private_files) + ) + else: + if backup_files: + db_filename, site_config, files_filename, private_files = get_latest_backup_file( + with_files=backup_files + ) + + if not files_filename or not private_files: + generate_files_backup() + db_filename, site_config, files_filename, private_files = get_latest_backup_file( + with_files=backup_files + ) + + else: + db_filename, site_config = get_latest_backup_file() + + folder = os.path.basename(db_filename)[:15] + "/" + # for adding datetime to folder name + + upload_file_to_s3(db_filename, folder, conn, bucket) + upload_file_to_s3(site_config, folder, conn, bucket) + + if backup_files: + if private_files: + upload_file_to_s3(private_files, folder, conn, bucket) + + if files_filename: + upload_file_to_s3(files_filename, folder, conn, bucket) + + +def upload_file_to_s3(filename, folder, conn, bucket): + destpath = os.path.join(folder, os.path.basename(filename)) + try: + print("Uploading file:", filename) + conn.upload_file(filename, bucket, destpath) # Requires PutObject permission + + except Exception as e: + influxframework.log_error() + print("Error uploading: %s" % (e)) diff --git a/influxframework/integrations/doctype/s3_backup_settings/test_s3_backup_settings.py b/influxframework/integrations/doctype/s3_backup_settings/test_s3_backup_settings.py new file mode 100644 index 0000000..c117060 --- /dev/null +++ b/influxframework/integrations/doctype/s3_backup_settings/test_s3_backup_settings.py @@ -0,0 +1,7 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestS3BackupSettings(InfluxFrameworkTestCase): + pass diff --git a/influxframework/integrations/doctype/slack_webhook_url/__init__.py b/influxframework/integrations/doctype/slack_webhook_url/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/integrations/doctype/slack_webhook_url/slack_webhook_url.js b/influxframework/integrations/doctype/slack_webhook_url/slack_webhook_url.js new file mode 100644 index 0000000..ab1321d --- /dev/null +++ b/influxframework/integrations/doctype/slack_webhook_url/slack_webhook_url.js @@ -0,0 +1,4 @@ +// Copyright (c) 2018, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Slack Webhook URL", {}); diff --git a/influxframework/integrations/doctype/slack_webhook_url/slack_webhook_url.json b/influxframework/integrations/doctype/slack_webhook_url/slack_webhook_url.json new file mode 100644 index 0000000..56a76b9 --- /dev/null +++ b/influxframework/integrations/doctype/slack_webhook_url/slack_webhook_url.json @@ -0,0 +1,61 @@ +{ + "actions": [], + "autoname": "field:webhook_name", + "creation": "2018-05-22 13:20:51.450815", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "webhook_name", + "webhook_url", + "show_document_link" + ], + "fields": [ + { + "fieldname": "webhook_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "webhook_url", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Webhook URL", + "reqd": 1 + }, + { + "allow_in_quick_entry": 1, + "default": "1", + "fieldname": "show_document_link", + "fieldtype": "Check", + "label": "Show link to document" + } + ], + "links": [], + "modified": "2021-05-12 18:24:37.810235", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Slack Webhook URL", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/integrations/doctype/slack_webhook_url/slack_webhook_url.py b/influxframework/integrations/doctype/slack_webhook_url/slack_webhook_url.py new file mode 100644 index 0000000..cdbf2cf --- /dev/null +++ b/influxframework/integrations/doctype/slack_webhook_url/slack_webhook_url.py @@ -0,0 +1,55 @@ +# Copyright (c) 2018, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +import requests + +import influxframework +from influxframework import _ +from influxframework.model.document import Document +from influxframework.utils import get_url_to_form + +error_messages = { + 400: "400: Invalid Payload or User not found", + 403: "403: Action Prohibited", + 404: "404: Channel not found", + 410: "410: The Channel is Archived", + 500: "500: Rollup Error, Slack seems to be down", +} + + +class SlackWebhookURL(Document): + pass + + +def send_slack_message(webhook_url, message, reference_doctype, reference_name): + data = {"text": message, "attachments": []} + + slack_url, show_link = influxframework.db.get_value( + "Slack Webhook URL", webhook_url, ["webhook_url", "show_document_link"] + ) + + if show_link: + doc_url = get_url_to_form(reference_doctype, reference_name) + link_to_doc = { + "fallback": _("See the document at {0}").format(doc_url), + "actions": [ + { + "type": "button", + "text": _("Go to the document"), + "url": doc_url, + "style": "primary", + } + ], + } + data["attachments"].append(link_to_doc) + + r = requests.post(slack_url, data=json.dumps(data)) + + if not r.ok: + message = error_messages.get(r.status_code, r.status_code) + influxframework.log_error(message, _("Slack Webhook Error")) + return "error" + + return "success" diff --git a/influxframework/integrations/doctype/slack_webhook_url/test_slack_webhook_url.py b/influxframework/integrations/doctype/slack_webhook_url/test_slack_webhook_url.py new file mode 100644 index 0000000..c9ae8ab --- /dev/null +++ b/influxframework/integrations/doctype/slack_webhook_url/test_slack_webhook_url.py @@ -0,0 +1,7 @@ +# Copyright (c) 2018, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestSlackWebhookURL(InfluxFrameworkTestCase): + pass diff --git a/influxframework/integrations/doctype/social_login_key/__init__.py b/influxframework/integrations/doctype/social_login_key/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/integrations/doctype/social_login_key/social_login_key.js b/influxframework/integrations/doctype/social_login_key/social_login_key.js new file mode 100644 index 0000000..12ced97 --- /dev/null +++ b/influxframework/integrations/doctype/social_login_key/social_login_key.js @@ -0,0 +1,84 @@ +// Copyright (c) 2017, InfluxFramework LLC +// For license information, please see license.txt +const fields = [ + "provider_name", + "base_url", + "custom_base_url", + "icon", + "authorize_url", + "access_token_url", + "redirect_url", + "api_endpoint", + "api_endpoint_args", + "auth_url_data", +]; + +influxframework.ui.form.on("Social Login Key", { + refresh(frm) { + frm.trigger("setup_fields"); + }, + + custom_base_url(frm) { + frm.trigger("setup_fields"); + }, + + social_login_provider(frm) { + if (frm.doc.social_login_provider != "Custom") { + influxframework + .call({ + doc: frm.doc, + method: "get_social_login_provider", + args: { + provider: frm.doc.social_login_provider, + }, + }) + .done((r) => { + const provider = r.message; + for (var field of fields) { + frm.set_value(field, provider[field]); + frm.set_df_property(field, "read_only", 1); + if (frm.doc.custom_base_url) { + frm.toggle_enable("base_url", 1); + } + } + }); + } else { + frm.trigger("clear_fields"); + frm.trigger("setup_fields"); + } + }, + + setup_fields(frm) { + // set custom_base_url to read only for "Custom" provider + if (frm.doc.social_login_provider == "Custom") { + frm.set_value("custom_base_url", 1); + frm.set_df_property("custom_base_url", "read_only", 1); + } + + // set fields to read only for providers from template + for (var f of fields) { + if (frm.doc.social_login_provider != "Custom") { + frm.set_df_property(f, "read_only", 1); + } + } + + // enable base_url for providers with custom_base_url + if (frm.doc.custom_base_url) { + frm.set_df_property("base_url", "read_only", 0); + frm.fields_dict["sb_identity_details"].collapse(false); + } + + // hide social_login_provider and provider_name for non local + if (!frm.doc.__islocal && (frm.doc.social_login_provider || frm.doc.provider_name)) { + frm.set_df_property("social_login_provider", "hidden", 1); + frm.set_df_property("provider_name", "hidden", 1); + } + }, + + clear_fields(frm) { + for (var field of fields) { + frm.set_value(field, ""); + frm.set_df_property(field, "read_only", 0); + } + }, +}); diff --git a/influxframework/integrations/doctype/social_login_key/social_login_key.json b/influxframework/integrations/doctype/social_login_key/social_login_key.json new file mode 100644 index 0000000..702df07 --- /dev/null +++ b/influxframework/integrations/doctype/social_login_key/social_login_key.json @@ -0,0 +1,187 @@ +{ + "actions": [], + "allow_import": 1, + "creation": "2017-11-18 15:36:09.676722", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "enable_social_login", + "client_credentials", + "social_login_provider", + "client_id", + "column_break_0", + "provider_name", + "client_secret", + "sb_identity_details", + "icon", + "column_break_1", + "base_url", + "client_urls", + "authorize_url", + "access_token_url", + "column_break_3", + "redirect_url", + "api_endpoint", + "custom_base_url", + "client_information", + "api_endpoint_args", + "auth_url_data", + "user_id_property" + ], + "fields": [ + { + "default": "0", + "fieldname": "enable_social_login", + "fieldtype": "Check", + "label": "Enable Social Login" + }, + { + "collapsible": 1, + "collapsible_depends_on": "eval:doc.enable_social_login", + "fieldname": "client_credentials", + "fieldtype": "Section Break", + "label": "Client Credentials" + }, + { + "default": "Custom", + "depends_on": "eval:doc.custom!=1", + "fieldname": "social_login_provider", + "fieldtype": "Select", + "label": "Social Login Provider", + "options": "Custom\nFacebook\nInfluxFramework\nGitHub\nGoogle\nOffice 365\nSalesforce\nfairlogin", + "set_only_once": 1 + }, + { + "fieldname": "client_id", + "fieldtype": "Data", + "label": "Client ID" + }, + { + "fieldname": "column_break_0", + "fieldtype": "Column Break" + }, + { + "fieldname": "provider_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Provider Name", + "reqd": 1, + "set_only_once": 1 + }, + { + "fieldname": "client_secret", + "fieldtype": "Password", + "label": "Client Secret" + }, + { + "collapsible": 1, + "collapsible_depends_on": "eval:doc.custom_base_url", + "fieldname": "sb_identity_details", + "fieldtype": "Section Break", + "label": "Identity Details" + }, + { + "depends_on": "eval:doc.social_login_provider==\"Custom\"", + "fieldname": "icon", + "fieldtype": "Data", + "label": "Icon" + }, + { + "fieldname": "column_break_1", + "fieldtype": "Column Break" + }, + { + "fieldname": "base_url", + "fieldtype": "Data", + "label": "Base URL" + }, + { + "collapsible": 1, + "collapsible_depends_on": "eval:doc.social_login_provider===\"Custom\"", + "fieldname": "client_urls", + "fieldtype": "Section Break", + "label": "Client URLs" + }, + { + "fieldname": "authorize_url", + "fieldtype": "Data", + "label": "Authorize URL" + }, + { + "fieldname": "access_token_url", + "fieldtype": "Data", + "label": "Access Token URL" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "redirect_url", + "fieldtype": "Data", + "label": "Redirect URL" + }, + { + "fieldname": "api_endpoint", + "fieldtype": "Data", + "label": "API Endpoint" + }, + { + "default": "0", + "fieldname": "custom_base_url", + "fieldtype": "Check", + "hidden": 1, + "label": "Custom Base URL" + }, + { + "collapsible": 1, + "collapsible_depends_on": "eval:doc.social_login_provider===\"Custom\"", + "fieldname": "client_information", + "fieldtype": "Section Break", + "label": "Client Information" + }, + { + "fieldname": "api_endpoint_args", + "fieldtype": "Code", + "label": "API Endpoint Args" + }, + { + "fieldname": "auth_url_data", + "fieldtype": "Code", + "label": "Auth URL Data" + }, + { + "depends_on": "eval:doc.social_login_provider===\"Custom\"", + "fieldname": "user_id_property", + "fieldtype": "Data", + "label": "User ID Property" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-09-30 14:37:13.616002", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Social Login Key", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "provider_name", + "track_changes": 1 +} diff --git a/influxframework/integrations/doctype/social_login_key/social_login_key.py b/influxframework/integrations/doctype/social_login_key/social_login_key.py new file mode 100644 index 0000000..e2c58f3 --- /dev/null +++ b/influxframework/integrations/doctype/social_login_key/social_login_key.py @@ -0,0 +1,190 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +import influxframework +from influxframework import _ +from influxframework.model.document import Document + + +class BaseUrlNotSetError(influxframework.ValidationError): + pass + + +class AuthorizeUrlNotSetError(influxframework.ValidationError): + pass + + +class AccessTokenUrlNotSetError(influxframework.ValidationError): + pass + + +class RedirectUrlNotSetError(influxframework.ValidationError): + pass + + +class ClientIDNotSetError(influxframework.ValidationError): + pass + + +class ClientSecretNotSetError(influxframework.ValidationError): + pass + + +class SocialLoginKey(Document): + def autoname(self): + self.name = influxframework.scrub(self.provider_name) + + def validate(self): + self.set_icon() + if self.custom_base_url and not self.base_url: + influxframework.throw(_("Please enter Base URL"), exc=BaseUrlNotSetError) + if not self.authorize_url: + influxframework.throw(_("Please enter Authorize URL"), exc=AuthorizeUrlNotSetError) + if not self.access_token_url: + influxframework.throw(_("Please enter Access Token URL"), exc=AccessTokenUrlNotSetError) + if not self.redirect_url: + influxframework.throw(_("Please enter Redirect URL"), exc=RedirectUrlNotSetError) + if self.enable_social_login and not self.client_id: + influxframework.throw( + _("Please enter Client ID before social login is enabled"), exc=ClientIDNotSetError + ) + if self.enable_social_login and not self.client_secret: + influxframework.throw( + _("Please enter Client Secret before social login is enabled"), exc=ClientSecretNotSetError + ) + + def set_icon(self): + icon_map = { + "Google": "google.svg", + "InfluxFramework": "influxframework.svg", + "Facebook": "facebook.svg", + "Office 365": "office_365.svg", + "GitHub": "github.svg", + "Salesforce": "salesforce.svg", + "fairlogin": "fair.svg", + } + + if self.provider_name in icon_map: + icon_file = icon_map[self.provider_name] + self.icon = f"/assets/influxframework/icons/social/{icon_file}" + + @influxframework.whitelist() + def get_social_login_provider(self, provider, initialize=False): + providers = {} + + providers["Office 365"] = { + "provider_name": "Office 365", + "enable_social_login": 1, + "base_url": "https://login.microsoftonline.com", + "custom_base_url": 0, + "icon": "fa fa-windows", + "authorize_url": "https://login.microsoftonline.com/common/oauth2/authorize", + "access_token_url": "https://login.microsoftonline.com/common/oauth2/token", + "redirect_url": "/api/method/influxframework.integrations.oauth2_logins.login_via_office365", + "api_endpoint": None, + "api_endpoint_args": None, + "auth_url_data": json.dumps({"response_type": "code", "scope": "openid"}), + } + + providers["GitHub"] = { + "provider_name": "GitHub", + "enable_social_login": 1, + "base_url": "https://api.github.com/", + "custom_base_url": 0, + "icon": "fa fa-github", + "authorize_url": "https://github.com/login/oauth/authorize", + "access_token_url": "https://github.com/login/oauth/access_token", + "redirect_url": "/api/method/influxframework.www.login.login_via_github", + "api_endpoint": "user", + "api_endpoint_args": None, + "auth_url_data": json.dumps({"scope": "user:email"}), + } + + providers["Google"] = { + "provider_name": "Google", + "enable_social_login": 1, + "base_url": "https://www.googleapis.com", + "custom_base_url": 0, + "icon": "fa fa-google", + "authorize_url": "https://accounts.google.com/o/oauth2/auth", + "access_token_url": "https://accounts.google.com/o/oauth2/token", + "redirect_url": "/api/method/influxframework.www.login.login_via_google", + "api_endpoint": "oauth2/v2/userinfo", + "api_endpoint_args": None, + "auth_url_data": json.dumps( + { + "scope": "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email", + "response_type": "code", + } + ), + } + + providers["Facebook"] = { + "provider_name": "Facebook", + "enable_social_login": 1, + "base_url": "https://graph.facebook.com", + "custom_base_url": 0, + "icon": "fa fa-facebook", + "authorize_url": "https://www.facebook.com/dialog/oauth", + "access_token_url": "https://graph.facebook.com/oauth/access_token", + "redirect_url": "/api/method/influxframework.www.login.login_via_facebook", + "api_endpoint": "/v2.5/me", + "api_endpoint_args": json.dumps( + {"fields": "first_name,last_name,email,gender,location,verified,picture"} + ), + "auth_url_data": json.dumps( + {"display": "page", "response_type": "code", "scope": "email,public_profile"} + ), + } + + providers["InfluxFramework"] = { + "provider_name": "InfluxFramework", + "enable_social_login": 1, + "custom_base_url": 1, + "icon": "/assets/influxframework/images/influxframework-favicon.svg", + "redirect_url": "/api/method/influxframework.www.login.login_via_influxframework", + "api_endpoint": "/api/method/influxframework.integrations.oauth2.openid_profile", + "api_endpoint_args": None, + "authorize_url": "/api/method/influxframework.integrations.oauth2.authorize", + "access_token_url": "/api/method/influxframework.integrations.oauth2.get_token", + "auth_url_data": json.dumps({"response_type": "code", "scope": "openid"}), + } + + providers["Salesforce"] = { + "provider_name": "Salesforce", + "enable_social_login": 1, + "base_url": "https://login.salesforce.com", + "custom_base_url": 0, + "icon": "fa fa-cloud", # https://github.com/FortAwesome/Font-Awesome/issues/1744 + "redirect_url": "/api/method/influxframework.integrations.oauth2_logins.login_via_salesforce", + "api_endpoint": "https://login.salesforce.com/services/oauth2/userinfo", + "api_endpoint_args": None, + "authorize_url": "https://login.salesforce.com/services/oauth2/authorize", + "access_token_url": "https://login.salesforce.com/services/oauth2/token", + "auth_url_data": json.dumps({"response_type": "code", "scope": "openid"}), + } + + providers["fairlogin"] = { + "provider_name": "fairlogin", + "enable_social_login": 1, + "base_url": "https://id.fairkom.net/auth/realms/fairlogin/", + "custom_base_url": 0, + "icon": "fa fa-key", + "redirect_url": "/api/method/influxframework.integrations.oauth2_logins.login_via_fairlogin", + "api_endpoint": "https://id.fairkom.net/auth/realms/fairlogin/protocol/openid-connect/userinfo", + "api_endpoint_args": None, + "authorize_url": "https://id.fairkom.net/auth/realms/fairlogin/protocol/openid-connect/auth", + "access_token_url": "https://id.fairkom.net/auth/realms/fairlogin/protocol/openid-connect/token", + "auth_url_data": json.dumps({"response_type": "code", "scope": "openid"}), + } + + # Initialize the doc and return, used in patch + # Or can be used for creating key from controller + if initialize and provider: + for k, v in providers[provider].items(): + setattr(self, k, v) + return + + return providers.get(provider) if provider else providers diff --git a/influxframework/integrations/doctype/social_login_key/test_social_login_key.py b/influxframework/integrations/doctype/social_login_key/test_social_login_key.py new file mode 100644 index 0000000..7848f93 --- /dev/null +++ b/influxframework/integrations/doctype/social_login_key/test_social_login_key.py @@ -0,0 +1,139 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE +from unittest.mock import MagicMock, patch + +from rauth import OAuth2Service + +import influxframework +from influxframework.auth import CookieManager, LoginManager +from influxframework.integrations.doctype.social_login_key.social_login_key import BaseUrlNotSetError +from influxframework.tests.utils import InfluxFrameworkTestCase +from influxframework.utils import set_request +from influxframework.utils.oauth import login_via_oauth2 + + +class TestSocialLoginKey(InfluxFrameworkTestCase): + def test_adding_influxframework_social_login_provider(self): + provider_name = "InfluxFramework" + social_login_key = make_social_login_key(social_login_provider=provider_name) + social_login_key.get_social_login_provider(provider_name, initialize=True) + self.assertRaises(BaseUrlNotSetError, social_login_key.insert) + + def test_github_login_with_private_email(self): + github_social_login_setup() + + mock_session = MagicMock() + mock_session.get.side_effect = github_response_for_private_email + + with patch.object(OAuth2Service, "get_auth_session", return_value=mock_session): + login_via_oauth2("github", "iwriu", {"token": "ewrwerwer"}) # Dummy code and state token + + def test_github_login_with_public_email(self): + github_social_login_setup() + + mock_session = MagicMock() + mock_session.get.side_effect = github_response_for_public_email + + with patch.object(OAuth2Service, "get_auth_session", return_value=mock_session): + login_via_oauth2("github", "iwriu", {"token": "ewrwerwer"}) # Dummy code and state token + + def test_normal_signup_and_github_login(self): + github_social_login_setup() + + if not influxframework.db.exists("User", "githublogin@example.com"): + user = influxframework.get_doc( + {"doctype": "User", "email": "githublogin@example.com", "first_name": "GitHub Login"} + ) + user.save(ignore_permissions=True) + + mock_session = MagicMock() + mock_session.get.side_effect = github_response_for_login + + with patch.object(OAuth2Service, "get_auth_session", return_value=mock_session): + login_via_oauth2("github", "iwriu", {"token": "ewrwerwer"}) + + +def make_social_login_key(**kwargs): + kwargs["doctype"] = "Social Login Key" + if not "provider_name" in kwargs: + kwargs["provider_name"] = "Test OAuth2 Provider" + doc = influxframework.get_doc(kwargs) + return doc + + +def create_or_update_social_login_key(): + # used in other tests (connected app, oauth20) + try: + social_login_key = influxframework.get_doc("Social Login Key", "influxframework") + except influxframework.DoesNotExistError: + social_login_key = influxframework.new_doc("Social Login Key") + social_login_key.get_social_login_provider("InfluxFramework", initialize=True) + social_login_key.base_url = influxframework.utils.get_url() + social_login_key.enable_social_login = 0 + social_login_key.save() + influxframework.db.commit() + + return social_login_key + + +def create_github_social_login_key(): + if influxframework.db.exists("Social Login Key", "github"): + return influxframework.get_doc("Social Login Key", "github") + else: + provider_name = "GitHub" + social_login_key = make_social_login_key(social_login_provider=provider_name) + social_login_key.get_social_login_provider(provider_name, initialize=True) + + # Dummy client_id and client_secret + social_login_key.client_id = "h6htd6q" + social_login_key.client_secret = "keoererk988ekkhf8w9e8ewrjhhkjer9889" + social_login_key.insert(ignore_permissions=True) + return social_login_key + + +def github_response_for_private_email(url, *args, **kwargs): + if url == "user": + return_value = { + "login": "dummy_username", + "id": "223342", + "email": None, + "first_name": "Github Private", + } + else: + return_value = [{"email": "github@example.com", "primary": True, "verified": True}] + + return MagicMock(status_code=200, json=MagicMock(return_value=return_value)) + + +def github_response_for_public_email(url, *args, **kwargs): + if url == "user": + return_value = { + "login": "dummy_username", + "id": "223343", + "email": "github_public@example.com", + "first_name": "Github Public", + } + + return MagicMock(status_code=200, json=MagicMock(return_value=return_value)) + + +def github_response_for_login(url, *args, **kwargs): + if url == "user": + return_value = { + "login": "dummy_username", + "id": "223346", + "email": None, + "first_name": "Github Login", + } + else: + return_value = [{"email": "githublogin@example.com", "primary": True, "verified": True}] + + return MagicMock(status_code=200, json=MagicMock(return_value=return_value)) + + +def github_social_login_setup(): + set_request(path="/random") + influxframework.local.cookie_manager = CookieManager() + influxframework.local.login_manager = LoginManager() + + create_github_social_login_key() diff --git a/influxframework/integrations/doctype/social_login_keys/__init__.py b/influxframework/integrations/doctype/social_login_keys/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/integrations/doctype/social_login_keys/social_login_keys.py b/influxframework/integrations/doctype/social_login_keys/social_login_keys.py new file mode 100644 index 0000000..63e32d8 --- /dev/null +++ b/influxframework/integrations/doctype/social_login_keys/social_login_keys.py @@ -0,0 +1,6 @@ +# see license +from influxframework.model.document import Document + + +class SocialLoginKeys(Document): + pass diff --git a/influxframework/integrations/doctype/token_cache/__init__.py b/influxframework/integrations/doctype/token_cache/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/integrations/doctype/token_cache/test_records.json b/influxframework/integrations/doctype/token_cache/test_records.json new file mode 100644 index 0000000..0584022 --- /dev/null +++ b/influxframework/integrations/doctype/token_cache/test_records.json @@ -0,0 +1,18 @@ +[ + { + "doctype": "Token Cache", + "user": "test@example.com", + "access_token": "test-access-token", + "refresh_token": "test-refresh-token", + "token_type": "Bearer", + "expires_in": 1000, + "scopes": [ + { + "scope": "all" + }, + { + "scope": "openid" + } + ] + } +] \ No newline at end of file diff --git a/influxframework/integrations/doctype/token_cache/test_token_cache.py b/influxframework/integrations/doctype/token_cache/test_token_cache.py new file mode 100644 index 0000000..57e7ab1 --- /dev/null +++ b/influxframework/integrations/doctype/token_cache/test_token_cache.py @@ -0,0 +1,36 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + +test_dependencies = ["User", "Connected App", "Token Cache"] + + +class TestTokenCache(InfluxFrameworkTestCase): + def setUp(self): + self.token_cache = influxframework.get_last_doc("Token Cache") + self.token_cache.update({"connected_app": influxframework.get_last_doc("Connected App").name}) + self.token_cache.save(ignore_permissions=True) + + def test_get_auth_header(self): + self.token_cache.get_auth_header() + + def test_update_data(self): + self.token_cache.update_data( + { + "access_token": "new-access-token", + "refresh_token": "new-refresh-token", + "token_type": "bearer", + "expires_in": 2000, + "scope": "new scope", + } + ) + + def test_get_expires_in(self): + self.token_cache.get_expires_in() + + def test_is_expired(self): + self.token_cache.is_expired() + + def get_json(self): + self.token_cache.get_json() diff --git a/influxframework/integrations/doctype/token_cache/token_cache.js b/influxframework/integrations/doctype/token_cache/token_cache.js new file mode 100644 index 0000000..6f47bf9 --- /dev/null +++ b/influxframework/integrations/doctype/token_cache/token_cache.js @@ -0,0 +1,7 @@ +// Copyright (c) 2019, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Token Cache", { + // refresh: function(frm) { + // } +}); diff --git a/influxframework/integrations/doctype/token_cache/token_cache.json b/influxframework/integrations/doctype/token_cache/token_cache.json new file mode 100644 index 0000000..c016405 --- /dev/null +++ b/influxframework/integrations/doctype/token_cache/token_cache.json @@ -0,0 +1,110 @@ +{ + "actions": [], + "autoname": "format:{connected_app}-{user}", + "beta": 1, + "creation": "2019-01-24 16:56:55.631096", + "doctype": "DocType", + "document_type": "System", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user", + "connected_app", + "provider_name", + "access_token", + "refresh_token", + "expires_in", + "state", + "scopes", + "success_uri", + "token_type" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "label": "User", + "options": "User", + "read_only": 1 + }, + { + "fieldname": "connected_app", + "fieldtype": "Link", + "label": "Connected App", + "options": "Connected App", + "read_only": 1 + }, + { + "fieldname": "access_token", + "fieldtype": "Password", + "label": "Access Token", + "read_only": 1 + }, + { + "fieldname": "refresh_token", + "fieldtype": "Password", + "label": "Refresh Token", + "read_only": 1 + }, + { + "fieldname": "expires_in", + "fieldtype": "Int", + "label": "Expires In", + "read_only": 1 + }, + { + "fieldname": "state", + "fieldtype": "Data", + "label": "State", + "read_only": 1 + }, + { + "fieldname": "scopes", + "fieldtype": "Table", + "label": "Scopes", + "options": "OAuth Scope", + "read_only": 1 + }, + { + "fieldname": "success_uri", + "fieldtype": "Data", + "label": "Success URI", + "read_only": 1 + }, + { + "fieldname": "token_type", + "fieldtype": "Data", + "label": "Token Type", + "read_only": 1 + }, + { + "fetch_from": "connected_app.provider_name", + "fieldname": "provider_name", + "fieldtype": "Data", + "label": "Provider Name", + "read_only": 1 + } + ], + "links": [], + "modified": "2020-11-13 13:35:53.714352", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Token Cache", + "owner": "Administrator", + "permissions": [ + { + "delete": 1, + "read": 1, + "role": "System Manager" + }, + { + "delete": 1, + "if_owner": 1, + "read": 1, + "role": "All" + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/integrations/doctype/token_cache/token_cache.py b/influxframework/integrations/doctype/token_cache/token_cache.py new file mode 100644 index 0000000..73dd786 --- /dev/null +++ b/influxframework/integrations/doctype/token_cache/token_cache.py @@ -0,0 +1,65 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +from datetime import datetime, timedelta + +import influxframework +from influxframework import _ +from influxframework.model.document import Document +from influxframework.utils import cint, cstr + + +class TokenCache(Document): + def get_auth_header(self): + if self.access_token: + headers = {"Authorization": "Bearer " + self.get_password("access_token")} + return headers + + raise influxframework.exceptions.DoesNotExistError + + def update_data(self, data): + """ + Store data returned by authorization flow. + + Params: + data - Dict with access_token, refresh_token, expires_in and scope. + """ + token_type = cstr(data.get("token_type", "")).lower() + if token_type not in ["bearer", "mac"]: + influxframework.throw(_("Received an invalid token type.")) + # 'Bearer' or 'MAC' + token_type = token_type.title() if token_type == "bearer" else token_type.upper() + + self.token_type = token_type + self.access_token = cstr(data.get("access_token", "")) + self.refresh_token = cstr(data.get("refresh_token", "")) + self.expires_in = cint(data.get("expires_in", 0)) + + new_scopes = data.get("scope") + if new_scopes: + if isinstance(new_scopes, str): + new_scopes = new_scopes.split(" ") + if isinstance(new_scopes, list): + self.scopes = None + for scope in new_scopes: + self.append("scopes", {"scope": scope}) + + self.state = None + self.save(ignore_permissions=True) + influxframework.db.commit() + return self + + def get_expires_in(self): + expiry_time = influxframework.utils.get_datetime(self.modified) + timedelta(self.expires_in) + return (datetime.now() - expiry_time).total_seconds() + + def is_expired(self): + return self.get_expires_in() < 0 + + def get_json(self): + return { + "access_token": self.get_password("access_token", ""), + "refresh_token": self.get_password("refresh_token", ""), + "expires_in": self.get_expires_in(), + "token_type": self.token_type, + } diff --git a/influxframework/integrations/doctype/webhook/__init__.py b/influxframework/integrations/doctype/webhook/__init__.py new file mode 100644 index 0000000..9cf82ef --- /dev/null +++ b/influxframework/integrations/doctype/webhook/__init__.py @@ -0,0 +1,79 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework + + +def run_webhooks(doc, method): + """Run webhooks for this method""" + if ( + influxframework.flags.in_import + or influxframework.flags.in_patch + or influxframework.flags.in_install + or influxframework.flags.in_migrate + ): + return + + if influxframework.flags.webhooks_executed is None: + influxframework.flags.webhooks_executed = {} + + # TODO: remove this hazardous unnecessary cache in flags + if influxframework.flags.webhooks is None: + # load webhooks from cache + webhooks = influxframework.cache().get_value("webhooks") + if webhooks is None: + # query webhooks + webhooks_list = influxframework.get_all( + "Webhook", + fields=["name", "`condition`", "webhook_docevent", "webhook_doctype"], + filters={"enabled": True}, + ) + + # make webhooks map for cache + webhooks = {} + for w in webhooks_list: + webhooks.setdefault(w.webhook_doctype, []).append(w) + influxframework.cache().set_value("webhooks", webhooks) + + influxframework.flags.webhooks = webhooks + + # get webhooks for this doctype + webhooks_for_doc = influxframework.flags.webhooks.get(doc.doctype, None) + + if not webhooks_for_doc: + # no webhooks, quit + return + + def _webhook_request(webhook): + if webhook.name not in influxframework.flags.webhooks_executed.get(doc.name, []): + influxframework.enqueue( + "influxframework.integrations.doctype.webhook.webhook.enqueue_webhook", + enqueue_after_commit=True, + doc=doc, + webhook=webhook, + ) + + # keep list of webhooks executed for this doc in this request + # so that we don't run the same webhook for the same document multiple times + # in one request + influxframework.flags.webhooks_executed.setdefault(doc.name, []).append(webhook.name) + + event_list = ["on_update", "after_insert", "on_submit", "on_cancel", "on_trash"] + + if not doc.flags.in_insert: + # value change is not applicable in insert + event_list.append("on_change") + event_list.append("before_update_after_submit") + + from influxframework.integrations.doctype.webhook.webhook import get_context + + for webhook in webhooks_for_doc: + trigger_webhook = False + event = method if method in event_list else None + if not webhook.condition: + trigger_webhook = True + elif influxframework.safe_eval(webhook.condition, eval_locals=get_context(doc)): + trigger_webhook = True + + if trigger_webhook and event and webhook.webhook_docevent == event: + _webhook_request(webhook) diff --git a/influxframework/integrations/doctype/webhook/test_webhook.py b/influxframework/integrations/doctype/webhook/test_webhook.py new file mode 100644 index 0000000..dac262d --- /dev/null +++ b/influxframework/integrations/doctype/webhook/test_webhook.py @@ -0,0 +1,208 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE +import json +from contextlib import contextmanager + +import influxframework +from influxframework.integrations.doctype.webhook.webhook import ( + enqueue_webhook, + get_webhook_data, + get_webhook_headers, +) +from influxframework.tests.utils import InfluxFrameworkTestCase + + +@contextmanager +def get_test_webhook(config): + wh = influxframework.get_doc(config).insert() + wh.reload() + try: + yield wh + finally: + wh.delete() + + +class TestWebhook(InfluxFrameworkTestCase): + @classmethod + def setUpClass(cls): + # delete any existing webhooks + influxframework.db.delete("Webhook") + # Delete existing logs if any + influxframework.db.delete("Webhook Request Log") + super().setUpClass() + # create test webhooks + cls.create_sample_webhooks() + + @classmethod + def create_sample_webhooks(cls): + samples_webhooks_data = [ + { + "webhook_doctype": "User", + "webhook_docevent": "after_insert", + "request_url": "https://httpbin.org/post", + "condition": "doc.email", + "enabled": True, + }, + { + "webhook_doctype": "User", + "webhook_docevent": "after_insert", + "request_url": "https://httpbin.org/post", + "condition": "doc.first_name", + "enabled": False, + }, + ] + + cls.sample_webhooks = [] + for wh_fields in samples_webhooks_data: + wh = influxframework.new_doc("Webhook") + wh.update(wh_fields) + wh.insert() + cls.sample_webhooks.append(wh) + + @classmethod + def tearDownClass(cls): + # delete any existing webhooks + influxframework.db.delete("Webhook") + + def setUp(self): + # retrieve or create a User webhook for `after_insert` + webhook_fields = { + "webhook_doctype": "User", + "webhook_docevent": "after_insert", + "request_url": "https://httpbin.org/post", + } + + if influxframework.db.exists("Webhook", webhook_fields): + self.webhook = influxframework.get_doc("Webhook", webhook_fields) + else: + self.webhook = influxframework.new_doc("Webhook") + self.webhook.update(webhook_fields) + + # create a User document + self.user = influxframework.new_doc("User") + self.user.first_name = influxframework.mock("name") + self.user.email = influxframework.mock("email") + self.user.save() + + # Create another test user specific to this test + self.test_user = influxframework.new_doc("User") + self.test_user.email = "user1@integration.webhooks.test.com" + self.test_user.first_name = "user1" + + def tearDown(self) -> None: + self.user.delete() + self.test_user.delete() + super().tearDown() + + def test_webhook_trigger_with_enabled_webhooks(self): + """Test webhook trigger for enabled webhooks""" + + influxframework.cache().delete_value("webhooks") + influxframework.flags.webhooks = None + + # Insert the user to db + self.test_user.insert() + + self.assertTrue("User" in influxframework.flags.webhooks) + # only 1 hook (enabled) must be queued + self.assertEqual(len(influxframework.flags.webhooks.get("User")), 1) + self.assertTrue(self.test_user.email in influxframework.flags.webhooks_executed) + self.assertEqual( + influxframework.flags.webhooks_executed.get(self.test_user.email)[0], self.sample_webhooks[0].name + ) + + def test_validate_doc_events(self): + "Test creating a submit-related webhook for a non-submittable DocType" + + self.webhook.webhook_docevent = "on_submit" + self.assertRaises(influxframework.ValidationError, self.webhook.save) + + def test_validate_request_url(self): + "Test validation for the webhook request URL" + + self.webhook.request_url = "httpbin.org?post" + self.assertRaises(influxframework.ValidationError, self.webhook.save) + + def test_validate_headers(self): + "Test validation for request headers" + + # test incomplete headers + self.webhook.set("webhook_headers", [{"key": "Content-Type"}]) + self.webhook.save() + headers = get_webhook_headers(doc=None, webhook=self.webhook) + self.assertEqual(headers, {}) + + # test complete headers + self.webhook.set("webhook_headers", [{"key": "Content-Type", "value": "application/json"}]) + self.webhook.save() + headers = get_webhook_headers(doc=None, webhook=self.webhook) + self.assertEqual(headers, {"Content-Type": "application/json"}) + + def test_validate_request_body_form(self): + "Test validation of Form URL-Encoded request body" + + self.webhook.request_structure = "Form URL-Encoded" + self.webhook.set("webhook_data", [{"fieldname": "name", "key": "name"}]) + self.webhook.webhook_json = """{ + "name": "{{ doc.name }}" + }""" + self.webhook.save() + self.assertEqual(self.webhook.webhook_json, None) + + data = get_webhook_data(doc=self.user, webhook=self.webhook) + self.assertEqual(data, {"name": self.user.name}) + + def test_validate_request_body_json(self): + "Test validation of JSON request body" + + self.webhook.request_structure = "JSON" + self.webhook.set("webhook_data", [{"fieldname": "name", "key": "name"}]) + self.webhook.webhook_json = """{ + "name": "{{ doc.name }}" + }""" + self.webhook.save() + self.assertEqual(self.webhook.webhook_data, []) + + data = get_webhook_data(doc=self.user, webhook=self.webhook) + self.assertEqual(data, {"name": self.user.name}) + + def test_webhook_req_log_creation(self): + if not influxframework.db.get_value("User", "user2@integration.webhooks.test.com"): + user = influxframework.get_doc( + {"doctype": "User", "email": "user2@integration.webhooks.test.com", "first_name": "user2"} + ).insert() + else: + user = influxframework.get_doc("User", "user2@integration.webhooks.test.com") + + webhook = influxframework.get_doc("Webhook", {"webhook_doctype": "User"}) + enqueue_webhook(user, webhook) + + self.assertTrue(influxframework.get_all("Webhook Request Log", pluck="name")) + + def test_webhook_with_array_body(self): + """Check if array request body are supported.""" + wh_config = { + "doctype": "Webhook", + "webhook_doctype": "Note", + "webhook_docevent": "after_insert", + "enabled": 1, + "request_url": "https://httpbin.org/post", + "request_method": "POST", + "request_structure": "JSON", + "webhook_json": '[\r\n{% for n in range(3) %}\r\n {\r\n "title": "{{ doc.title }}",\r\n "n": {{ n }}\r\n }\r\n {%- if not loop.last -%}\r\n , \r\n {%endif%}\r\n{%endfor%}\r\n]', + "meets_condition": "Yes", + "webhook_headers": [ + { + "key": "Content-Type", + "value": "application/json", + } + ], + } + + with get_test_webhook(wh_config) as wh: + doc = influxframework.new_doc("Note") + doc.title = "Test Webhook Note" + + enqueue_webhook(doc, wh) + log = influxframework.get_last_doc("Webhook Request Log") + self.assertEqual(len(json.loads(log.response)["json"]), 3) diff --git a/influxframework/integrations/doctype/webhook/webhook.js b/influxframework/integrations/doctype/webhook/webhook.js new file mode 100644 index 0000000..b8d8ede --- /dev/null +++ b/influxframework/integrations/doctype/webhook/webhook.js @@ -0,0 +1,125 @@ +// Copyright (c) 2017, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.webhook = { + set_fieldname_select: (frm) => { + if (frm.doc.webhook_doctype) { + influxframework.model.with_doctype(frm.doc.webhook_doctype, () => { + // get doctype fields + let fields = $.map( + influxframework.get_doc("DocType", frm.doc.webhook_doctype).fields, + (d) => { + if ( + influxframework.model.no_value_type.includes(d.fieldtype) && + !influxframework.model.table_fields.includes(d.fieldtype) + ) { + return null; + } else if (d.fieldtype === "Currency" || d.fieldtype === "Float") { + return { label: d.label, value: d.fieldname }; + } else { + return { + label: `${__(d.label)} (${d.fieldtype})`, + value: d.fieldname, + }; + } + } + ); + + // add meta fields + for (let field of influxframework.model.std_fields) { + if (field.fieldname == "name") { + fields.unshift({ label: "Name (Doc Name)", value: "name" }); + } else { + fields.push({ + label: `${__(field.label)} (${field.fieldtype})`, + value: field.fieldname, + }); + } + } + + frm.fields_dict.webhook_data.grid.update_docfield_property( + "fieldname", + "options", + [""].concat(fields) + ); + }); + } + }, + + set_request_headers: (frm) => { + if (frm.doc.request_structure) { + let header_value; + if (frm.doc.request_structure == "Form URL-Encoded") { + header_value = "application/x-www-form-urlencoded"; + } else if (frm.doc.request_structure == "JSON") { + header_value = "application/json"; + } + + if (header_value) { + let header_row = (frm.doc.webhook_headers || []).find( + (row) => row.key === "Content-Type" + ); + if (header_row) { + influxframework.model.set_value( + header_row.doctype, + header_row.name, + "value", + header_value + ); + } else { + frm.add_child("webhook_headers", { + key: "Content-Type", + value: header_value, + }); + } + frm.refresh(); + } + } + }, +}; + +influxframework.ui.form.on("Webhook", { + refresh: (frm) => { + influxframework.webhook.set_fieldname_select(frm); + }, + + request_structure: (frm) => { + influxframework.webhook.set_request_headers(frm); + }, + + webhook_doctype: (frm) => { + influxframework.webhook.set_fieldname_select(frm); + }, + + enable_security: (frm) => { + frm.toggle_reqd("webhook_secret", frm.doc.enable_security); + }, + + preview_document: (frm) => { + influxframework.call({ + method: "generate_preview", + doc: frm.doc, + callback: (r) => { + frm.refresh_field("meets_condition"); + frm.refresh_field("preview_request_body"); + }, + }); + }, +}); + +influxframework.ui.form.on("Webhook Data", { + fieldname: (frm, cdt, cdn) => { + let row = locals[cdt][cdn]; + let df = influxframework + .get_meta(frm.doc.webhook_doctype) + .fields.filter((field) => field.fieldname == row.fieldname); + + if (!df.length) { + // check if field is a meta field + df = influxframework.model.std_fields.filter((field) => field.fieldname == row.fieldname); + } + + row.key = df.length ? df[0].fieldname : "name"; + frm.refresh_field("webhook_data"); + }, +}); diff --git a/influxframework/integrations/doctype/webhook/webhook.json b/influxframework/integrations/doctype/webhook/webhook.json new file mode 100644 index 0000000..a21e460 --- /dev/null +++ b/influxframework/integrations/doctype/webhook/webhook.json @@ -0,0 +1,231 @@ +{ + "actions": [], + "autoname": "naming_series:", + "creation": "2017-09-08 16:16:13.060641", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "sb_doc_events", + "naming_series", + "webhook_doctype", + "cb_doc_events", + "webhook_docevent", + "enabled", + "sb_condition", + "condition", + "cb_condition", + "html_condition", + "sb_webhook", + "request_url", + "request_method", + "cb_webhook", + "request_structure", + "sb_security", + "enable_security", + "webhook_secret", + "sb_webhook_headers", + "webhook_headers", + "sb_webhook_data", + "webhook_data", + "webhook_json", + "preview_tab", + "preview_document", + "column_break_26", + "meets_condition", + "section_break_28", + "preview_request_body" + ], + "fields": [ + { + "fieldname": "sb_doc_events", + "fieldtype": "Section Break", + "label": "Doc Events" + }, + { + "fieldname": "webhook_doctype", + "fieldtype": "Link", + "label": "DocType", + "options": "DocType", + "reqd": 1, + "set_only_once": 1 + }, + { + "fieldname": "cb_doc_events", + "fieldtype": "Column Break" + }, + { + "fieldname": "webhook_docevent", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Doc Event", + "options": "after_insert\non_update\non_submit\non_cancel\non_trash\non_update_after_submit\non_change", + "set_only_once": 1 + }, + { + "fieldname": "sb_condition", + "fieldtype": "Section Break", + "label": "Webhook Trigger" + }, + { + "description": "The webhook will be triggered if this expression is true", + "fieldname": "condition", + "fieldtype": "Small Text", + "label": "Condition" + }, + { + "fieldname": "cb_condition", + "fieldtype": "Column Break" + }, + { + "fieldname": "html_condition", + "fieldtype": "HTML", + "options": "

      Condition Examples:

      \n
      doc.status==\"Open\"
      doc.due_date==nowdate()
      doc.total > 40000\n
      " + }, + { + "fieldname": "sb_webhook", + "fieldtype": "Section Break", + "label": "Webhook Request" + }, + { + "fieldname": "request_url", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Request URL", + "reqd": 1 + }, + { + "fieldname": "sb_webhook_headers", + "fieldtype": "Section Break", + "label": "Webhook Headers" + }, + { + "fieldname": "webhook_headers", + "fieldtype": "Table", + "label": "Headers", + "options": "Webhook Header" + }, + { + "fieldname": "sb_webhook_data", + "fieldtype": "Section Break", + "label": "Webhook Data" + }, + { + "depends_on": "eval: !doc.request_structure || doc.request_structure == \"Form URL-Encoded\"", + "fieldname": "webhook_data", + "fieldtype": "Table", + "label": "Data", + "options": "Webhook Data" + }, + { + "fieldname": "cb_webhook", + "fieldtype": "Column Break" + }, + { + "fieldname": "request_structure", + "fieldtype": "Select", + "label": "Request Structure", + "options": "\nForm URL-Encoded\nJSON" + }, + { + "depends_on": "eval: doc.request_structure == \"JSON\"", + "fieldname": "webhook_json", + "fieldtype": "Code", + "label": "JSON Request Body" + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "\nHOOK-.####" + }, + { + "fieldname": "sb_security", + "fieldtype": "Section Break", + "label": "Webhook Security" + }, + { + "default": "0", + "fieldname": "enable_security", + "fieldtype": "Check", + "label": "Enable Security" + }, + { + "depends_on": "eval:doc.enable_security == 1", + "fieldname": "webhook_secret", + "fieldtype": "Password", + "label": "Webhook Secret" + }, + { + "default": "1", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enabled" + }, + { + "default": "POST", + "fieldname": "request_method", + "fieldtype": "Select", + "label": "Request Method", + "options": "POST\nPUT\nDELETE", + "reqd": 1 + }, + { + "fieldname": "preview_tab", + "fieldtype": "Tab Break", + "label": "Preview" + }, + { + "fieldname": "preview_document", + "fieldtype": "Dynamic Link", + "label": "Select Document", + "options": "webhook_doctype" + }, + { + "fieldname": "preview_request_body", + "fieldtype": "Code", + "is_virtual": 1, + "label": "Request Body" + }, + { + "fieldname": "meets_condition", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Meets Condition?" + }, + { + "fieldname": "column_break_26", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_28", + "fieldtype": "Section Break" + } + ], + "links": [], + "modified": "2022-07-11 08:54:10.740512", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Webhook", + "naming_rule": "By \"Naming Series\" field", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "webhook_doctype", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/integrations/doctype/webhook/webhook.py b/influxframework/integrations/doctype/webhook/webhook.py new file mode 100644 index 0000000..f2c7922 --- /dev/null +++ b/influxframework/integrations/doctype/webhook/webhook.py @@ -0,0 +1,194 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE + +import base64 +import hashlib +import hmac +import json +from time import sleep +from urllib.parse import urlparse + +import requests + +import influxframework +from influxframework import _ +from influxframework.model.document import Document +from influxframework.utils.jinja import validate_template +from influxframework.utils.safe_exec import get_safe_globals + +WEBHOOK_SECRET_HEADER = "X-InfluxFramework-Webhook-Signature" + + +class Webhook(Document): + def validate(self): + self.validate_docevent() + self.validate_condition() + self.validate_request_url() + self.validate_request_body() + self.validate_repeating_fields() + self.preview_document = None + + def on_update(self): + influxframework.cache().delete_value("webhooks") + + def validate_docevent(self): + if self.webhook_doctype: + is_submittable = influxframework.get_value("DocType", self.webhook_doctype, "is_submittable") + if not is_submittable and self.webhook_docevent in [ + "on_submit", + "on_cancel", + "on_update_after_submit", + ]: + influxframework.throw(_("DocType must be Submittable for the selected Doc Event")) + + def validate_condition(self): + temp_doc = influxframework.new_doc(self.webhook_doctype) + if self.condition: + try: + influxframework.safe_eval(self.condition, eval_locals=get_context(temp_doc)) + except Exception as e: + influxframework.throw(_("Invalid Condition: {}").format(e)) + + def validate_request_url(self): + try: + request_url = urlparse(self.request_url).netloc + if not request_url: + raise influxframework.ValidationError + except Exception as e: + influxframework.throw(_("Check Request URL"), exc=e) + + def validate_request_body(self): + if self.request_structure: + if self.request_structure == "Form URL-Encoded": + self.webhook_json = None + elif self.request_structure == "JSON": + validate_template(self.webhook_json) + self.webhook_data = [] + + def validate_repeating_fields(self): + """Error when Same Field is entered multiple times in webhook_data""" + webhook_data = [] + for entry in self.webhook_data: + webhook_data.append(entry.fieldname) + + if len(webhook_data) != len(set(webhook_data)): + influxframework.throw(_("Same Field is entered more than once")) + + @influxframework.whitelist() + def generate_preview(self): + # This function doesn't need to do anything specific as virtual fields + # get evaluated automatically. + pass + + @property + def meets_condition(self): + if not self.condition: + return _("Yes") + + if not (self.preview_document and self.webhook_doctype): + return _("Select a document to check if it meets conditions.") + + try: + doc = influxframework.get_cached_doc(self.webhook_doctype, self.preview_document) + met_condition = influxframework.safe_eval(self.condition, eval_locals=get_context(doc)) + except Exception as e: + return _("Failed to evaluate conditions: {}").format(e) + return _("Yes") if met_condition else _("No") + + @property + def preview_request_body(self): + if not (self.preview_document and self.webhook_doctype): + return _("Select a document to preview request data") + + try: + doc = influxframework.get_cached_doc(self.webhook_doctype, self.preview_document) + return influxframework.as_json(get_webhook_data(doc, self)) + except Exception as e: + return _("Failed to compute request body: {}").format(e) + + +def get_context(doc): + return {"doc": doc, "utils": get_safe_globals().get("influxframework").get("utils")} + + +def enqueue_webhook(doc, webhook) -> None: + webhook: Webhook = influxframework.get_doc("Webhook", webhook.get("name")) + headers = get_webhook_headers(doc, webhook) + data = get_webhook_data(doc, webhook) + + for i in range(3): + try: + r = requests.request( + method=webhook.request_method, + url=webhook.request_url, + data=json.dumps(data, default=str), + headers=headers, + timeout=5, + ) + r.raise_for_status() + influxframework.logger().debug({"webhook_success": r.text}) + log_request(webhook.request_url, headers, data, r) + break + + except requests.exceptions.ReadTimeout as e: + influxframework.logger().debug({"webhook_error": e, "try": i + 1}) + log_request(webhook.request_url, headers, data) + + except Exception as e: + influxframework.logger().debug({"webhook_error": e, "try": i + 1}) + log_request(webhook.request_url, headers, data, r) + sleep(3 * i + 1) + if i != 2: + continue + else: + webhook.log_error("Webhook failed") + + +def log_request(url: str, headers: dict, data: dict, res: requests.Response | None = None): + request_log = influxframework.get_doc( + { + "doctype": "Webhook Request Log", + "user": influxframework.session.user if influxframework.session.user else None, + "url": url, + "headers": influxframework.as_json(headers) if headers else None, + "data": influxframework.as_json(data) if data else None, + "response": influxframework.as_json(res.json()) if res else None, + } + ) + + request_log.save(ignore_permissions=True) + + +def get_webhook_headers(doc, webhook): + headers = {} + + if webhook.enable_security: + data = get_webhook_data(doc, webhook) + signature = base64.b64encode( + hmac.new( + webhook.get_password("webhook_secret").encode("utf8"), + json.dumps(data).encode("utf8"), + hashlib.sha256, + ).digest() + ) + headers[WEBHOOK_SECRET_HEADER] = signature + + if webhook.webhook_headers: + for h in webhook.webhook_headers: + if h.get("key") and h.get("value"): + headers[h.get("key")] = h.get("value") + + return headers + + +def get_webhook_data(doc, webhook): + data = {} + doc = doc.as_dict(convert_dates_to_str=True) + + if webhook.webhook_data: + data = {w.key: doc.get(w.fieldname) for w in webhook.webhook_data} + elif webhook.webhook_json: + data = influxframework.render_template(webhook.webhook_json, get_context(doc)) + data = json.loads(data) + + return data diff --git a/influxframework/integrations/doctype/webhook_data/__init__.py b/influxframework/integrations/doctype/webhook_data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/integrations/doctype/webhook_data/webhook_data.json b/influxframework/integrations/doctype/webhook_data/webhook_data.json new file mode 100644 index 0000000..96ae7f7 --- /dev/null +++ b/influxframework/integrations/doctype/webhook_data/webhook_data.json @@ -0,0 +1,130 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2017-09-14 12:08:50.302810", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "fieldname", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Fieldname", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "cb_doc_data", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "key", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Key", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "max_attachments": 0, + "modified": "2017-09-14 13:16:58.252176", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Webhook Data", + "name_case": "", + "owner": "Administrator", + "permissions": [], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 0, + "track_seen": 0 +} \ No newline at end of file diff --git a/influxframework/integrations/doctype/webhook_data/webhook_data.py b/influxframework/integrations/doctype/webhook_data/webhook_data.py new file mode 100644 index 0000000..27dd45b --- /dev/null +++ b/influxframework/integrations/doctype/webhook_data/webhook_data.py @@ -0,0 +1,9 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class WebhookData(Document): + pass diff --git a/influxframework/integrations/doctype/webhook_header/__init__.py b/influxframework/integrations/doctype/webhook_header/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/integrations/doctype/webhook_header/webhook_header.json b/influxframework/integrations/doctype/webhook_header/webhook_header.json new file mode 100644 index 0000000..315d283 --- /dev/null +++ b/influxframework/integrations/doctype/webhook_header/webhook_header.json @@ -0,0 +1,101 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2017-09-08 16:27:39.195379", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "key", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Key", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "value", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Value", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "max_attachments": 0, + "modified": "2017-09-08 16:28:20.025612", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Webhook Header", + "name_case": "", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/influxframework/integrations/doctype/webhook_header/webhook_header.py b/influxframework/integrations/doctype/webhook_header/webhook_header.py new file mode 100644 index 0000000..267a5be --- /dev/null +++ b/influxframework/integrations/doctype/webhook_header/webhook_header.py @@ -0,0 +1,9 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class WebhookHeader(Document): + pass diff --git a/influxframework/integrations/doctype/webhook_request_log/__init__.py b/influxframework/integrations/doctype/webhook_request_log/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/integrations/doctype/webhook_request_log/test_webhook_request_log.py b/influxframework/integrations/doctype/webhook_request_log/test_webhook_request_log.py new file mode 100644 index 0000000..c7d7d3e --- /dev/null +++ b/influxframework/integrations/doctype/webhook_request_log/test_webhook_request_log.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestWebhookRequestLog(InfluxFrameworkTestCase): + pass diff --git a/influxframework/integrations/doctype/webhook_request_log/webhook_request_log.js b/influxframework/integrations/doctype/webhook_request_log/webhook_request_log.js new file mode 100644 index 0000000..1528577 --- /dev/null +++ b/influxframework/integrations/doctype/webhook_request_log/webhook_request_log.js @@ -0,0 +1,7 @@ +// Copyright (c) 2021, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Webhook Request Log", { + // refresh: function(frm) { + // } +}); diff --git a/influxframework/integrations/doctype/webhook_request_log/webhook_request_log.json b/influxframework/integrations/doctype/webhook_request_log/webhook_request_log.json new file mode 100644 index 0000000..d9410a2 --- /dev/null +++ b/influxframework/integrations/doctype/webhook_request_log/webhook_request_log.json @@ -0,0 +1,83 @@ +{ + "actions": [], + "autoname": "hash", + "creation": "2021-05-24 21:35:59.104776", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user", + "headers", + "data", + "column_break_4", + "url", + "response" + ], + "fields": [ + { + "fieldname": "url", + "fieldtype": "Data", + "label": "URL", + "read_only": 1 + }, + { + "fieldname": "headers", + "fieldtype": "Code", + "label": "Headers", + "options": "JSON", + "read_only": 1 + }, + { + "fieldname": "response", + "fieldtype": "Code", + "label": "Response", + "options": "JSON", + "read_only": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "data", + "fieldtype": "Code", + "label": "Data", + "options": "JSON", + "read_only": 1 + }, + { + "fieldname": "user", + "fieldtype": "Link", + "label": "User", + "options": "User", + "read_only": 1 + } + ], + "in_create": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2022-05-03 09:33:49.240777", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Webhook Request Log", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/integrations/doctype/webhook_request_log/webhook_request_log.py b/influxframework/integrations/doctype/webhook_request_log/webhook_request_log.py new file mode 100644 index 0000000..1f9b665 --- /dev/null +++ b/influxframework/integrations/doctype/webhook_request_log/webhook_request_log.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +# import influxframework +from influxframework.model.document import Document + + +class WebhookRequestLog(Document): + pass diff --git a/influxframework/integrations/google_oauth.py b/influxframework/integrations/google_oauth.py new file mode 100644 index 0000000..09b6577 --- /dev/null +++ b/influxframework/integrations/google_oauth.py @@ -0,0 +1,201 @@ +import json + +from google.oauth2.credentials import Credentials +from googleapiclient.discovery import build +from requests import get, post + +import influxframework +from influxframework.utils import get_request_site_address + +CALLBACK_METHOD = "/api/method/influxframework.integrations.google_oauth.callback" +_SCOPES = { + "mail": ("https://mail.google.com/"), + "contacts": ("https://www.googleapis.com/auth/contacts"), + "drive": ("https://www.googleapis.com/auth/drive"), + "indexing": ("https://www.googleapis.com/auth/indexing"), +} +_SERVICES = { + "contacts": ("people", "v1"), + "drive": ("drive", "v3"), + "indexing": ("indexing", "v3"), +} +_DOMAIN_CALLBACK_METHODS = { + "mail": "influxframework.email.oauth.authorize_google_access", + "contacts": "influxframework.integrations.doctype.google_contacts.google_contacts.authorize_access", + "drive": "influxframework.integrations.doctype.google_drive.google_drive.authorize_access", + "indexing": "influxframework.website.doctype.website_settings.google_indexing.authorize_access", +} + + +class GoogleAuthenticationError(Exception): + pass + + +class GoogleOAuth: + OAUTH_URL = "https://oauth2.googleapis.com/token" + + def __init__(self, domain: str, validate: bool = True): + self.google_settings = influxframework.get_single("Google Settings") + self.domain = domain.lower() + self.scopes = ( + " ".join(_SCOPES[self.domain]) + if isinstance(_SCOPES[self.domain], (list, tuple)) + else _SCOPES[self.domain] + ) + + if validate: + self.validate_google_settings() + + def validate_google_settings(self): + google_settings = "Google Settings" + + if not self.google_settings.enable: + influxframework.throw(influxframework._("Please enable {} before continuing.").format(google_settings)) + + if not (self.google_settings.client_id and self.google_settings.client_secret): + influxframework.throw(influxframework._("Please update {} before continuing.").format(google_settings)) + + def authorize(self, oauth_code: str) -> dict[str, str | int]: + """Returns a dict with access and refresh token. + + :param oauth_code: code got back from google upon successful auhtorization + """ + + data = { + "code": oauth_code, + "client_id": self.google_settings.client_id, + "client_secret": self.google_settings.get_password( + fieldname="client_secret", raise_exception=False + ), + "grant_type": "authorization_code", + "scope": self.scopes, + "redirect_uri": get_request_site_address(True) + CALLBACK_METHOD, + } + + return handle_response( + post(self.OAUTH_URL, data=data).json(), + "Google Oauth Authorization Error", + "Something went wrong during the authorization.", + ) + + def refresh_access_token(self, refresh_token: str) -> dict[str, str | int]: + """Refreshes google access token using refresh token""" + + data = { + "client_id": self.google_settings.client_id, + "client_secret": self.google_settings.get_password( + fieldname="client_secret", raise_exception=False + ), + "refresh_token": refresh_token, + "grant_type": "refresh_token", + "scope": self.scopes, + } + + return handle_response( + post(self.OAUTH_URL, data=data).json(), + "Google Oauth Access Token Refresh Error", + "Something went wrong during the access token generation.", + raise_err=True, + ) + + def get_authentication_url(self, state: dict[str, str]) -> dict[str, str]: + """Returns google authentication url. + + :param state: dict of values which you need on callback (for calling methods, redirection back to the form, doc name, etc) + """ + + state.update({"domain": self.domain}) + state = json.dumps(state) + callback_url = get_request_site_address(True) + CALLBACK_METHOD + + return { + "url": "https://accounts.google.com/o/oauth2/v2/auth?" + + "access_type=offline&response_type=code&prompt=consent&include_granted_scopes=true&" + + "client_id={}&scope={}&redirect_uri={}&state={}".format( + self.google_settings.client_id, self.scopes, callback_url, state + ) + } + + def get_google_service_object(self, access_token: str, refresh_token: str): + """Returns google service object""" + + credentials_dict = { + "token": access_token, + "refresh_token": refresh_token, + "token_uri": self.OAUTH_URL, + "client_id": self.google_settings.client_id, + "client_secret": self.google_settings.get_password( + fieldname="client_secret", raise_exception=False + ), + "scopes": self.scopes, + } + + return build( + serviceName=_SERVICES[self.domain][0], + version=_SERVICES[self.domain][1], + credentials=Credentials(**credentials_dict), + static_discovery=False, + ) + + +def handle_response( + response: dict[str, str | int], + error_title: str, + error_message: str, + raise_err: bool = False, +): + if "error" in response: + influxframework.log_error( + influxframework._(error_title), influxframework._(response.get("error_description", error_message)) + ) + + if raise_err: + influxframework.throw(influxframework._(error_title), GoogleAuthenticationError, influxframework._(error_message)) + + return {} + + return response + + +def is_valid_access_token(access_token: str) -> bool: + response = get( + "https://oauth2.googleapis.com/tokeninfo", params={"access_token": access_token} + ).json() + + if "error" in response: + return False + + return True + + +@influxframework.whitelist(methods=["GET"]) +def callback(state: str, code: str = None, error: str = None) -> None: + """Common callback for google integrations. + Invokes functions using `influxframework.get_attr` and also adds required (keyworded) arguments + along with committing and redirecting us back to influxframework site.""" + + state = json.loads(state) + redirect = state.pop("redirect", "/app") + success_query_param = state.pop("success_query_param", "") + failure_query_param = state.pop("failure_query_param", "") + + if not error: + if (domain := state.pop("domain")) in _DOMAIN_CALLBACK_METHODS: + state.update({"code": code}) + influxframework.get_attr(_DOMAIN_CALLBACK_METHODS[domain])(**state) + + # GET request, hence using commit to persist changes + influxframework.db.commit() # nosemgrep + else: + return influxframework.respond_as_web_page( + "Invalid Google Callback", + "The callback domain provided is not valid for Google Authentication", + http_status_code=400, + indicator_color="red", + width=640, + ) + + influxframework.local.response["type"] = "redirect" + influxframework.local.response[ + "location" + ] = f"{redirect}?{failure_query_param if error else success_query_param}" diff --git a/influxframework/integrations/influxframework_providers/__init__.py b/influxframework/integrations/influxframework_providers/__init__.py new file mode 100644 index 0000000..70adce4 --- /dev/null +++ b/influxframework/integrations/influxframework_providers/__init__.py @@ -0,0 +1,13 @@ +# imports - standard imports +import sys + +# imports - module imports +from influxframework.integrations.influxframework_providers.influxframeworkcloud import influxframeworkcloud_migrator + + +def migrate_to(local_site, influxframework_provider): + if influxframework_provider in ("influxframework.cloud", "influxframeworkcloud.com"): + return influxframeworkcloud_migrator(local_site) + else: + print(f"{influxframework_provider} is not supported yet") + sys.exit(1) diff --git a/influxframework/integrations/influxframework_providers/influxframeworkcloud.py b/influxframework/integrations/influxframework_providers/influxframeworkcloud.py new file mode 100644 index 0000000..c56e237 --- /dev/null +++ b/influxframework/integrations/influxframework_providers/influxframeworkcloud.py @@ -0,0 +1,36 @@ +import click +import requests + +import influxframework +from influxframework.core.utils import html2text + + +def influxframeworkcloud_migrator(local_site): + print("Retrieving Site Migrator...") + remote_site = influxframework.conf.influxframeworkcloud_url or "influxframeworkcloud.com" + request_url = f"https://{remote_site}/api/method/press.api.script" + request = requests.get(request_url) + + if request.status_code / 100 != 2: + print( + "Request exitted with Status Code: {}\nPayload: {}".format( + request.status_code, html2text(request.text) + ) + ) + click.secho( + "Some errors occurred while recovering the migration script. Please contact us @ InfluxFramework Cloud if this issue persists", + fg="yellow", + ) + return + + script_contents = request.json()["message"] + + import os + import sys + import tempfile + + py = sys.executable + script = tempfile.NamedTemporaryFile(mode="w") + script.write(script_contents) + print(f"Site Migrator stored at {script.name}") + os.execv(py, [py, script.name, local_site]) diff --git a/influxframework/integrations/oauth2.py b/influxframework/integrations/oauth2.py new file mode 100644 index 0000000..e28a655 --- /dev/null +++ b/influxframework/integrations/oauth2.py @@ -0,0 +1,242 @@ +import json +from urllib.parse import quote, urlencode + +from oauthlib.oauth2 import FatalClientError, OAuth2Error +from oauthlib.openid.connect.core.endpoints.pre_configured import Server as WebApplicationServer + +import influxframework +from influxframework.integrations.doctype.oauth_provider_settings.oauth_provider_settings import ( + get_oauth_settings, +) +from influxframework.oauth import ( + OAuthWebRequestValidator, + generate_json_error_response, + get_server_url, + get_userinfo, +) + + +def get_oauth_server(): + if not getattr(influxframework.local, "oauth_server", None): + oauth_validator = OAuthWebRequestValidator() + influxframework.local.oauth_server = WebApplicationServer(oauth_validator) + + return influxframework.local.oauth_server + + +def sanitize_kwargs(param_kwargs): + """Remove 'data' and 'cmd' keys, if present.""" + arguments = param_kwargs + arguments.pop("data", None) + arguments.pop("cmd", None) + + return arguments + + +def encode_params(params): + """ + Encode a dict of params into a query string. + + Use `quote_via=urllib.parse.quote` so that whitespaces will be encoded as + `%20` instead of as `+`. This is needed because oauthlib cannot handle `+` + as a whitespace. + """ + return urlencode(params, quote_via=quote) + + +@influxframework.whitelist() +def approve(*args, **kwargs): + r = influxframework.request + + try: + (scopes, influxframework.flags.oauth_credentials,) = get_oauth_server().validate_authorization_request( + r.url, r.method, r.get_data(), r.headers + ) + + headers, body, status = get_oauth_server().create_authorization_response( + uri=influxframework.flags.oauth_credentials["redirect_uri"], + body=r.get_data(), + headers=r.headers, + scopes=scopes, + credentials=influxframework.flags.oauth_credentials, + ) + uri = headers.get("Location", None) + + influxframework.local.response["type"] = "redirect" + influxframework.local.response["location"] = uri + return + + except (FatalClientError, OAuth2Error) as e: + return generate_json_error_response(e) + + +@influxframework.whitelist(allow_guest=True) +def authorize(**kwargs): + success_url = "/api/method/influxframework.integrations.oauth2.approve?" + encode_params( + sanitize_kwargs(kwargs) + ) + failure_url = influxframework.form_dict["redirect_uri"] + "?error=access_denied" + + if influxframework.session.user == "Guest": + # Force login, redirect to preauth again. + influxframework.local.response["type"] = "redirect" + influxframework.local.response["location"] = "/login?" + encode_params( + {"redirect-to": influxframework.request.url} + ) + else: + try: + r = influxframework.request + (scopes, influxframework.flags.oauth_credentials,) = get_oauth_server().validate_authorization_request( + r.url, r.method, r.get_data(), r.headers + ) + + skip_auth = influxframework.db.get_value( + "OAuth Client", + influxframework.flags.oauth_credentials["client_id"], + "skip_authorization", + ) + unrevoked_tokens = influxframework.get_all("OAuth Bearer Token", filters={"status": "Active"}) + + if skip_auth or (get_oauth_settings().skip_authorization == "Auto" and unrevoked_tokens): + influxframework.local.response["type"] = "redirect" + influxframework.local.response["location"] = success_url + else: + # Show Allow/Deny screen. + response_html_params = influxframework._dict( + { + "client_id": influxframework.db.get_value("OAuth Client", kwargs["client_id"], "app_name"), + "success_url": success_url, + "failure_url": failure_url, + "details": scopes, + } + ) + resp_html = influxframework.render_template( + "templates/includes/oauth_confirmation.html", response_html_params + ) + influxframework.respond_as_web_page("Confirm Access", resp_html) + except (FatalClientError, OAuth2Error) as e: + return generate_json_error_response(e) + + +@influxframework.whitelist(allow_guest=True) +def get_token(*args, **kwargs): + try: + r = influxframework.request + headers, body, status = get_oauth_server().create_token_response( + r.url, r.method, r.form, r.headers, influxframework.flags.oauth_credentials + ) + body = influxframework._dict(json.loads(body)) + + if body.error: + influxframework.local.response = body + influxframework.local.response["http_status_code"] = 400 + return + + influxframework.local.response = body + return + + except (FatalClientError, OAuth2Error) as e: + return generate_json_error_response(e) + + +@influxframework.whitelist(allow_guest=True) +def revoke_token(*args, **kwargs): + try: + r = influxframework.request + headers, body, status = get_oauth_server().create_revocation_response( + r.url, + headers=r.headers, + body=r.form, + http_method=r.method, + ) + except (FatalClientError, OAuth2Error): + pass + + # status_code must be 200 + influxframework.local.response = influxframework._dict({}) + influxframework.local.response["http_status_code"] = status or 200 + return + + +@influxframework.whitelist() +def openid_profile(*args, **kwargs): + try: + r = influxframework.request + headers, body, status = get_oauth_server().create_userinfo_response( + r.url, + headers=r.headers, + body=r.form, + ) + body = influxframework._dict(json.loads(body)) + influxframework.local.response = body + return + + except (FatalClientError, OAuth2Error) as e: + return generate_json_error_response(e) + + +@influxframework.whitelist(allow_guest=True) +def openid_configuration(): + influxframework_server_url = get_server_url() + influxframework.local.response = influxframework._dict( + { + "issuer": influxframework_server_url, + "authorization_endpoint": f"{influxframework_server_url}/api/method/influxframework.integrations.oauth2.authorize", + "token_endpoint": f"{influxframework_server_url}/api/method/influxframework.integrations.oauth2.get_token", + "userinfo_endpoint": f"{influxframework_server_url}/api/method/influxframework.integrations.oauth2.openid_profile", + "revocation_endpoint": f"{influxframework_server_url}/api/method/influxframework.integrations.oauth2.revoke_token", + "introspection_endpoint": f"{influxframework_server_url}/api/method/influxframework.integrations.oauth2.introspect_token", + "response_types_supported": [ + "code", + "token", + "code id_token", + "code token id_token", + "id_token", + "id_token token", + ], + "subject_types_supported": ["public"], + "id_token_signing_alg_values_supported": ["HS256"], + } + ) + + +@influxframework.whitelist(allow_guest=True) +def introspect_token(token=None, token_type_hint=None): + if token_type_hint not in ["access_token", "refresh_token"]: + token_type_hint = "access_token" + try: + bearer_token = None + if token_type_hint == "access_token": + bearer_token = influxframework.get_doc("OAuth Bearer Token", {"access_token": token}) + elif token_type_hint == "refresh_token": + bearer_token = influxframework.get_doc("OAuth Bearer Token", {"refresh_token": token}) + + client = influxframework.get_doc("OAuth Client", bearer_token.client) + + token_response = influxframework._dict( + { + "client_id": client.client_id, + "trusted_client": client.skip_authorization, + "active": bearer_token.status == "Active", + "exp": round(bearer_token.expiration_time.timestamp()), + "scope": bearer_token.scopes, + } + ) + + if "openid" in bearer_token.scopes: + sub = influxframework.get_value( + "User Social Login", + {"provider": "influxframework", "parent": bearer_token.user}, + "userid", + ) + + if sub: + token_response.update({"sub": sub}) + user = influxframework.get_doc("User", bearer_token.user) + userinfo = get_userinfo(user) + token_response.update(userinfo) + + influxframework.local.response = token_response + + except Exception: + influxframework.local.response = influxframework._dict({"active": False}) diff --git a/influxframework/integrations/oauth2_logins.py b/influxframework/integrations/oauth2_logins.py new file mode 100644 index 0000000..c73a7d2 --- /dev/null +++ b/influxframework/integrations/oauth2_logins.py @@ -0,0 +1,63 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +import influxframework +import influxframework.utils +from influxframework.utils.oauth import login_via_oauth2, login_via_oauth2_id_token + + +@influxframework.whitelist(allow_guest=True) +def login_via_google(code, state): + login_via_oauth2("google", code, state, decoder=decoder_compat) + + +@influxframework.whitelist(allow_guest=True) +def login_via_github(code, state): + login_via_oauth2("github", code, state) + + +@influxframework.whitelist(allow_guest=True) +def login_via_facebook(code, state): + login_via_oauth2("facebook", code, state, decoder=decoder_compat) + + +@influxframework.whitelist(allow_guest=True) +def login_via_influxframework(code, state): + login_via_oauth2("influxframework", code, state, decoder=decoder_compat) + + +@influxframework.whitelist(allow_guest=True) +def login_via_office365(code, state): + login_via_oauth2_id_token("office_365", code, state, decoder=decoder_compat) + + +@influxframework.whitelist(allow_guest=True) +def login_via_salesforce(code, state): + login_via_oauth2("salesforce", code, state, decoder=decoder_compat) + + +@influxframework.whitelist(allow_guest=True) +def login_via_fairlogin(code, state): + login_via_oauth2("fairlogin", code, state, decoder=decoder_compat) + + +@influxframework.whitelist(allow_guest=True) +def custom(code, state): + """ + Callback for processing code and state for user added providers + + process social login from /api/method/influxframework.integrations.custom/ + """ + path = influxframework.request.path[1:].split("/") + if len(path) == 4 and path[3]: + provider = path[3] + # Validates if provider doctype exists + if influxframework.db.exists("Social Login Key", provider): + login_via_oauth2(provider, code, state, decoder=decoder_compat) + + +def decoder_compat(b): + # https://github.com/litl/rauth/issues/145#issuecomment-31199471 + return json.loads(bytes(b).decode("utf-8")) diff --git a/influxframework/integrations/offsite_backup_utils.py b/influxframework/integrations/offsite_backup_utils.py new file mode 100644 index 0000000..24beaa5 --- /dev/null +++ b/influxframework/integrations/offsite_backup_utils.py @@ -0,0 +1,121 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +import glob +import os + +import influxframework +from influxframework.utils import cint, split_emails + + +def send_email(success, service_name, doctype, email_field, error_status=None): + recipients = get_recipients(doctype, email_field) + if not recipients: + influxframework.log_error( + f"No Email Recipient found for {service_name}", + f"{service_name}: Failed to send backup status email", + ) + return + + if success: + if not influxframework.db.get_single_value(doctype, "send_email_for_successful_backup"): + return + + subject = "Backup Upload Successful" + message = """ +

      Backup Uploaded Successfully!

      +

      Hi there, this is just to inform you that your backup was successfully uploaded to your {} bucket. So relax!

      """.format( + service_name + ) + else: + subject = "[Warning] Backup Upload Failed" + message = """ +

      Backup Upload Failed!

      +

      Oops, your automated backup to {} failed.

      +

      Error message: {}

      +

      Please contact your system manager for more information.

      """.format( + service_name, error_status + ) + + influxframework.sendmail(recipients=recipients, subject=subject, message=message) + + +def get_recipients(doctype, email_field): + if not influxframework.db: + influxframework.connect() + + return split_emails(influxframework.db.get_value(doctype, None, email_field)) + + +def get_latest_backup_file(with_files=False): + from influxframework.utils.backups import BackupGenerator + + odb = BackupGenerator( + influxframework.conf.db_name, + influxframework.conf.db_name, + influxframework.conf.db_password, + db_host=influxframework.db.host, + db_type=influxframework.conf.db_type, + db_port=influxframework.conf.db_port, + ) + database, public, private, config = odb.get_recent_backup(older_than=24 * 30) + + if with_files: + return database, config, public, private + + return database, config + + +def get_file_size(file_path, unit="MB"): + file_size = os.path.getsize(file_path) + + memory_size_unit_mapper = {"KB": 1, "MB": 2, "GB": 3, "TB": 4} + i = 0 + while i < memory_size_unit_mapper[unit]: + file_size = file_size / 1000.0 + i += 1 + + return file_size + + +def get_chunk_site(file_size): + """this function will return chunk size in megabytes based on file size""" + + file_size_in_gb = cint(file_size / 1024 / 1024) + + MB = 1024 * 1024 + if file_size_in_gb > 5000: + return 200 * MB + elif file_size_in_gb >= 3000: + return 150 * MB + elif file_size_in_gb >= 1000: + return 100 * MB + elif file_size_in_gb >= 500: + return 50 * MB + else: + return 15 * MB + + +def validate_file_size(): + influxframework.flags.create_new_backup = True + latest_file, site_config = get_latest_backup_file() + file_size = get_file_size(latest_file, unit="GB") if latest_file else 0 + + if file_size > 1: + influxframework.flags.create_new_backup = False + + +def generate_files_backup(): + from influxframework.utils.backups import BackupGenerator + + backup = BackupGenerator( + influxframework.conf.db_name, + influxframework.conf.db_name, + influxframework.conf.db_password, + db_host=influxframework.db.host, + db_type=influxframework.conf.db_type, + db_port=influxframework.conf.db_port, + ) + + backup.set_backup_file_name() + backup.zip_files() diff --git a/influxframework/integrations/utils.py b/influxframework/integrations/utils.py new file mode 100644 index 0000000..e367e9c --- /dev/null +++ b/influxframework/integrations/utils.py @@ -0,0 +1,101 @@ +# Copyright (c) 2019, InfluxFramework LLC +# License: MIT. See LICENSE + +import datetime +import json +from urllib.parse import parse_qs + +import influxframework +from influxframework import _ +from influxframework.utils import get_request_session + + +def make_request(method, url, auth=None, headers=None, data=None): + auth = auth or "" + data = data or {} + headers = headers or {} + + try: + s = get_request_session() + influxframework.flags.integration_request = s.request(method, url, data=data, auth=auth, headers=headers) + influxframework.flags.integration_request.raise_for_status() + + if influxframework.flags.integration_request.headers.get("content-type") == "text/plain; charset=utf-8": + return parse_qs(influxframework.flags.integration_request.text) + + return influxframework.flags.integration_request.json() + except Exception as exc: + influxframework.log_error() + raise exc + + +def make_get_request(url, **kwargs): + return make_request("GET", url, **kwargs) + + +def make_post_request(url, **kwargs): + return make_request("POST", url, **kwargs) + + +def make_put_request(url, **kwargs): + return make_request("PUT", url, **kwargs) + + +def create_request_log( + data, + integration_type=None, + service_name=None, + name=None, + error=None, + request_headers=None, + output=None, + **kwargs, +): + """ + DEPRECATED: The parameter integration_type will be removed in the next major release. + Use is_remote_request instead. + """ + if integration_type == "Remote": + kwargs["is_remote_request"] = 1 + + elif integration_type == "Subscription Notification": + kwargs["request_description"] = integration_type + + reference_doctype = reference_docname = None + if "reference_doctype" not in kwargs: + if isinstance(data, str): + data = json.loads(data) + + reference_doctype = data.get("reference_doctype") + reference_docname = data.get("reference_docname") + + integration_request = influxframework.get_doc( + { + "doctype": "Integration Request", + "integration_request_service": service_name, + "request_headers": get_json(request_headers), + "data": get_json(data), + "output": get_json(output), + "error": get_json(error), + "reference_doctype": reference_doctype, + "reference_docname": reference_docname, + **kwargs, + } + ) + + if name: + integration_request.flags._name = name + + integration_request.insert(ignore_permissions=True) + influxframework.db.commit() + + return integration_request + + +def get_json(obj): + return obj if isinstance(obj, str) else influxframework.as_json(obj, indent=1) + + +def json_handler(obj): + if isinstance(obj, (datetime.date, datetime.timedelta, datetime.datetime)): + return str(obj) diff --git a/influxframework/integrations/workspace/integrations/integrations.json b/influxframework/integrations/workspace/integrations/integrations.json new file mode 100644 index 0000000..8d1dfd6 --- /dev/null +++ b/influxframework/integrations/workspace/integrations/integrations.json @@ -0,0 +1,213 @@ +{ + "charts": [], + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Backup\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Google Services\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Authentication\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", + "creation": "2020-03-02 15:16:18.714190", + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "integration", + "idx": 0, + "label": "Integrations", + "links": [ + { + "hidden": 0, + "is_query_report": 0, + "label": "Backup", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Dropbox Settings", + "link_count": 0, + "link_to": "Dropbox Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "S3 Backup Settings", + "link_count": 0, + "link_to": "S3 Backup Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Google Drive", + "link_count": 0, + "link_to": "Google Drive", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Google Services", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Google Settings", + "link_count": 0, + "link_to": "Google Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Google Contacts", + "link_count": 0, + "link_to": "Google Contacts", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Google Calendar", + "link_count": 0, + "link_to": "Google Calendar", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Google Drive", + "link_count": 0, + "link_to": "Google Drive", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Settings", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Webhook", + "link_count": 0, + "link_to": "Webhook", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Slack Webhook URL", + "link_count": 0, + "link_to": "Slack Webhook URL", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "SMS Settings", + "link_count": 0, + "link_to": "SMS Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Authentication", + "link_count": 4, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Social Login Key", + "link_count": 0, + "link_to": "Social Login Key", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "LDAP Settings", + "link_count": 0, + "link_to": "LDAP Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "OAuth Client", + "link_count": 0, + "link_to": "OAuth Client", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "OAuth Provider Settings", + "link_count": 0, + "link_to": "OAuth Provider Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + } + ], + "modified": "2022-07-23 18:00:28.805405", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Integrations", + "owner": "Administrator", + "parent_page": "", + "public": 1, + "quick_lists": [], + "restrict_to_domain": "", + "roles": [], + "sequence_id": 15.0, + "shortcuts": [], + "title": "Integrations" +} \ No newline at end of file diff --git a/influxframework/middlewares.py b/influxframework/middlewares.py new file mode 100644 index 0000000..5d52f4f --- /dev/null +++ b/influxframework/middlewares.py @@ -0,0 +1,28 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import os + +from werkzeug.exceptions import NotFound +from werkzeug.middleware.shared_data import SharedDataMiddleware + +import influxframework +from influxframework.utils import cstr, get_site_name + + +class StaticDataMiddleware(SharedDataMiddleware): + def __call__(self, environ, start_response): + self.environ = environ + return super().__call__(environ, start_response) + + def get_directory_loader(self, directory): + def loader(path): + site = get_site_name(influxframework.app._site or self.environ.get("HTTP_HOST")) + path = os.path.join(directory, site, "public", "files", cstr(path)) + if os.path.isfile(path): + return os.path.basename(path), self._opener(path) + else: + raise NotFound + # return None, None + + return loader diff --git a/influxframework/migrate.py b/influxframework/migrate.py new file mode 100644 index 0000000..9b0fa90 --- /dev/null +++ b/influxframework/migrate.py @@ -0,0 +1,179 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE + +import json +import os +from textwrap import dedent + +import influxframework +import influxframework.model.sync +import influxframework.modules.patch_handler +import influxframework.translate +from influxframework.cache_manager import clear_global_cache +from influxframework.core.doctype.language.language import sync_languages +from influxframework.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs +from influxframework.database.schema import add_column +from influxframework.deferred_insert import save_to_db as flush_deferred_inserts +from influxframework.desk.notifications import clear_notifications +from influxframework.modules.patch_handler import PatchType +from influxframework.modules.utils import sync_customizations +from influxframework.search.website_search import build_index_for_all_routes +from influxframework.utils.connections import check_connection +from influxframework.utils.dashboard import sync_dashboards +from influxframework.utils.fixtures import sync_fixtures +from influxframework.website.utils import clear_website_cache + +BENCH_START_MESSAGE = dedent( + """ + Cannot run bench migrate without the services running. + If you are running bench in development mode, make sure that bench is running: + + $ bench start + + Otherwise, check the server logs and ensure that all the required services are running. + """ +) + + +def atomic(method): + def wrapper(*args, **kwargs): + try: + ret = method(*args, **kwargs) + influxframework.db.commit() + return ret + except Exception: + influxframework.db.rollback() + raise + + return wrapper + + +class SiteMigration: + """Migrate all apps to the current version, will: + - run before migrate hooks + - run patches + - sync doctypes (schema) + - sync dashboards + - sync jobs + - sync fixtures + - sync customizations + - sync languages + - sync web pages (from /www) + - run after migrate hooks + """ + + def __init__(self, skip_failing: bool = False, skip_search_index: bool = False) -> None: + self.skip_failing = skip_failing + self.skip_search_index = skip_search_index + + def setUp(self): + """Complete setup required for site migration""" + influxframework.flags.touched_tables = set() + self.touched_tables_file = influxframework.get_site_path("touched_tables.json") + add_column(doctype="DocType", column_name="migration_hash", fieldtype="Data") + clear_global_cache() + + if os.path.exists(self.touched_tables_file): + os.remove(self.touched_tables_file) + + influxframework.flags.in_migrate = True + + def tearDown(self): + """Run operations that should be run post schema updation processes + This should be executed irrespective of outcome + """ + influxframework.translate.clear_cache() + clear_website_cache() + clear_notifications() + + with open(self.touched_tables_file, "w") as f: + json.dump(list(influxframework.flags.touched_tables), f, sort_keys=True, indent=4) + + if not self.skip_search_index: + print(f"Building search index for {influxframework.local.site}") + build_index_for_all_routes() + + influxframework.publish_realtime("version-update") + influxframework.flags.touched_tables.clear() + influxframework.flags.in_migrate = False + + @atomic + def pre_schema_updates(self): + """Executes `before_migrate` hooks""" + for app in influxframework.get_installed_apps(): + for fn in influxframework.get_hooks("before_migrate", app_name=app): + influxframework.get_attr(fn)() + + @atomic + def run_schema_updates(self): + """Run patches as defined in patches.txt, sync schema changes as defined in the {doctype}.json files""" + influxframework.modules.patch_handler.run_all( + skip_failing=self.skip_failing, patch_type=PatchType.pre_model_sync + ) + influxframework.model.sync.sync_all() + influxframework.modules.patch_handler.run_all( + skip_failing=self.skip_failing, patch_type=PatchType.post_model_sync + ) + + @atomic + def post_schema_updates(self): + """Execute pending migration tasks post patches execution & schema sync + This includes: + * Sync `Scheduled Job Type` and scheduler events defined in hooks + * Sync fixtures & custom scripts + * Sync in-Desk Module Dashboards + * Sync customizations: Custom Fields, Property Setters, Custom Permissions + * Sync InfluxFramework's internal language master + * Flush deferred inserts made during maintenance mode. + * Sync Portal Menu Items + * Sync Installed Applications Version History + * Execute `after_migrate` hooks + """ + sync_jobs() + sync_fixtures() + sync_dashboards() + sync_customizations() + sync_languages() + flush_deferred_inserts() + + influxframework.get_single("Portal Settings").sync_menu() + influxframework.get_single("Installed Applications").update_versions() + + for app in influxframework.get_installed_apps(): + for fn in influxframework.get_hooks("after_migrate", app_name=app): + influxframework.get_attr(fn)() + + def required_services_running(self) -> bool: + """Returns True if all required services are running. Returns False and prints + instructions to stdout when required services are not available. + """ + service_status = check_connection(redis_services=["redis_cache"]) + are_services_running = all(service_status.values()) + + if not are_services_running: + for service in service_status: + if not service_status.get(service, True): + print(f"Service {service} is not running.") + print(BENCH_START_MESSAGE) + + return are_services_running + + def run(self, site: str): + """Run Migrate operation on site specified. This method initializes + and destroys connections to the site database. + """ + if site: + influxframework.init(site=site) + influxframework.connect() + + if not self.required_services_running(): + raise SystemExit(1) + + self.setUp() + try: + self.pre_schema_updates() + self.run_schema_updates() + finally: + self.post_schema_updates() + self.tearDown() + influxframework.destroy() diff --git a/influxframework/model/__init__.py b/influxframework/model/__init__.py new file mode 100644 index 0000000..3d8c31d --- /dev/null +++ b/influxframework/model/__init__.py @@ -0,0 +1,188 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +# model __init__.py +import influxframework + +data_fieldtypes = ( + "Currency", + "Int", + "Long Int", + "Float", + "Percent", + "Check", + "Small Text", + "Long Text", + "Code", + "Text Editor", + "Markdown Editor", + "HTML Editor", + "Date", + "Datetime", + "Time", + "Text", + "Data", + "Link", + "Dynamic Link", + "Password", + "Select", + "Rating", + "Read Only", + "Attach", + "Attach Image", + "Signature", + "Color", + "Barcode", + "Geolocation", + "Duration", + "Icon", + "Phone", + "Autocomplete", + "JSON", +) + +float_like_fields = {"Float", "Currency", "Percent"} +datetime_fields = {"Datetime", "Date", "Time"} + +attachment_fieldtypes = ( + "Attach", + "Attach Image", +) + +no_value_fields = ( + "Section Break", + "Column Break", + "Tab Break", + "HTML", + "Table", + "Table MultiSelect", + "Button", + "Image", + "Fold", + "Heading", +) + +display_fieldtypes = ( + "Section Break", + "Column Break", + "Tab Break", + "HTML", + "Button", + "Image", + "Fold", + "Heading", +) + +numeric_fieldtypes = ("Currency", "Int", "Long Int", "Float", "Percent", "Check") + +data_field_options = ("Email", "Name", "Phone", "URL", "Barcode") + +default_fields = ( + "doctype", + "name", + "owner", + "creation", + "modified", + "modified_by", + "docstatus", + "idx", +) + +child_table_fields = ("parent", "parentfield", "parenttype") + +optional_fields = ("_user_tags", "_comments", "_assign", "_liked_by", "_seen") + +table_fields = ("Table", "Table MultiSelect") + +core_doctypes_list = ( + "DocType", + "DocField", + "DocPerm", + "DocType Action", + "DocType Link", + "User", + "Role", + "Has Role", + "Page", + "Module Def", + "Print Format", + "Report", + "Customize Form", + "Customize Form Field", + "Property Setter", + "Custom Field", + "Client Script", +) + +log_types = ( + "Version", + "Error Log", + "Scheduled Job Log", + "Event Sync Log", + "Event Update Log", + "Access Log", + "View Log", + "Activity Log", + "Energy Point Log", + "Notification Log", + "Email Queue", + "DocShare", + "Document Follow", + "Console Log", +) + + +def delete_fields(args_dict, delete=0): + """ + Delete a field. + * Deletes record from `tabDocField` + * If not single doctype: Drops column from table + * If single, deletes record from `tabSingles` + args_dict = { dt: [field names] } + """ + import influxframework.utils + + for dt in args_dict: + fields = args_dict[dt] + if not fields: + continue + + influxframework.db.delete( + "DocField", + { + "parent": dt, + "fieldname": ("in", fields), + }, + ) + + # Delete the data/column only if delete is specified + if not delete: + continue + + if influxframework.db.get_value("DocType", dt, "issingle"): + influxframework.db.delete( + "Singles", + { + "doctype": dt, + "field": ("in", fields), + }, + ) + else: + existing_fields = influxframework.db.describe(dt) + existing_fields = existing_fields and [e[0] for e in existing_fields] or [] + fields_need_to_delete = set(fields) & set(existing_fields) + if not fields_need_to_delete: + continue + + if influxframework.db.db_type == "mariadb": + # mariadb implicitly commits before DDL, make it explicit + influxframework.db.commit() + + query = "ALTER TABLE `tab%s` " % dt + ", ".join( + "DROP COLUMN `%s`" % f for f in fields_need_to_delete + ) + influxframework.db.sql(query) + + if influxframework.db.db_type == "postgres": + # commit the results to db + influxframework.db.commit() diff --git a/influxframework/model/base_document.py b/influxframework/model/base_document.py new file mode 100644 index 0000000..d754096 --- /dev/null +++ b/influxframework/model/base_document.py @@ -0,0 +1,1242 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE +import datetime +import json + +import influxframework +from influxframework import _, _dict +from influxframework.model import ( + child_table_fields, + datetime_fields, + default_fields, + display_fieldtypes, + float_like_fields, + table_fields, +) +from influxframework.model.docstatus import DocStatus +from influxframework.model.naming import set_new_name +from influxframework.model.utils.link_count import notify_link_count +from influxframework.modules import load_doctype_module +from influxframework.utils import cast_fieldtype, cint, cstr, flt, now, sanitize_html, strip_html +from influxframework.utils.html_utils import unescape_html + +max_positive_value = {"smallint": 2**15 - 1, "int": 2**31 - 1, "bigint": 2**63 - 1} + +DOCTYPE_TABLE_FIELDS = [ + _dict(fieldname="fields", options="DocField"), + _dict(fieldname="permissions", options="DocPerm"), + _dict(fieldname="actions", options="DocType Action"), + _dict(fieldname="links", options="DocType Link"), + _dict(fieldname="states", options="DocType State"), +] + +TABLE_DOCTYPES_FOR_DOCTYPE = {df["fieldname"]: df["options"] for df in DOCTYPE_TABLE_FIELDS} +DOCTYPES_FOR_DOCTYPE = {"DocType", *TABLE_DOCTYPES_FOR_DOCTYPE.values()} + + +def get_controller(doctype): + """Returns the **class** object of the given DocType. + For `custom` type, returns `influxframework.model.document.Document`. + + :param doctype: DocType name as string.""" + + def _get_controller(): + from influxframework.model.document import Document + from influxframework.utils.nestedset import NestedSet + + module_name, custom = influxframework.db.get_value( + "DocType", doctype, ("module", "custom"), cache=True + ) or ("Core", False) + + if custom: + is_tree = influxframework.db.get_value("DocType", doctype, "is_tree", ignore=True, cache=True) + _class = NestedSet if is_tree else Document + else: + class_overrides = influxframework.get_hooks("override_doctype_class") + if class_overrides and class_overrides.get(doctype): + import_path = class_overrides[doctype][-1] + module_path, classname = import_path.rsplit(".", 1) + module = influxframework.get_module(module_path) + if not hasattr(module, classname): + raise ImportError(f"{doctype}: {classname} does not exist in module {module_path}") + else: + module = load_doctype_module(doctype, module_name) + classname = doctype.replace(" ", "").replace("-", "") + + if hasattr(module, classname): + _class = getattr(module, classname) + if issubclass(_class, BaseDocument): + _class = getattr(module, classname) + else: + raise ImportError(doctype) + else: + raise ImportError(doctype) + return _class + + if influxframework.local.dev_server: + return _get_controller() + + site_controllers = influxframework.controllers.setdefault(influxframework.local.site, {}) + if doctype not in site_controllers: + site_controllers[doctype] = _get_controller() + + return site_controllers[doctype] + + +class BaseDocument: + _reserved_keywords = { + "doctype", + "meta", + "_meta", + "flags", + "_table_fields", + "_valid_columns", + "_table_fieldnames", + "_reserved_keywords", + "dont_update_if_missing", + } + + def __init__(self, d): + if d.get("doctype"): + self.doctype = d["doctype"] + + self._table_fieldnames = ( + d["_table_fieldnames"] # from cache + if "_table_fieldnames" in d + else {df.fieldname for df in self._get_table_fields()} + ) + + self.update(d) + self.dont_update_if_missing = [] + + if hasattr(self, "__setup__"): + self.__setup__() + + @property + def meta(self): + if not (meta := getattr(self, "_meta", None)): + self._meta = meta = influxframework.get_meta(self.doctype) + + return meta + + def __getstate__(self): + """ + Called when pickling. + Returns a copy of `__dict__` excluding unpicklable values like `_meta`. + + More info: https://docs.python.org/3/library/pickle.html#handling-stateful-objects + """ + + # Always use the dict.copy() method to avoid modifying the original state + state = self.__dict__.copy() + self.remove_unpicklable_values(state) + + return state + + def remove_unpicklable_values(self, state): + """Remove unpicklable values before pickling""" + + state.pop("_meta", None) + + def update(self, d): + """Update multiple fields of a doctype using a dictionary of key-value pairs. + + Example: + doc.update({ + "user": "admin", + "balance": 42000 + }) + """ + + # set name first, as it is used a reference in child document + if "name" in d: + self.name = d["name"] + + ignore_children = hasattr(self, "flags") and self.flags.ignore_children + for key, value in d.items(): + self.set(key, value, as_value=ignore_children) + + return self + + def update_if_missing(self, d): + """Set default values for fields without existing values""" + if isinstance(d, BaseDocument): + d = d.get_valid_dict() + + for key, value in d.items(): + if ( + value is not None + and self.get(key) is None + # dont_update_if_missing is a list of fieldnames + # for which you don't want to set default value + and key not in self.dont_update_if_missing + ): + self.set(key, value) + + def get_db_value(self, key): + return influxframework.db.get_value(self.doctype, self.name, key) + + def get(self, key, filters=None, limit=None, default=None): + if isinstance(key, dict): + return _filter(self.get_all_children(), key, limit=limit) + + if filters: + if isinstance(filters, dict): + return _filter(self.__dict__.get(key, []), filters, limit=limit) + + # perhaps you wanted to set a default instead + default = filters + + value = self.__dict__.get(key, default) + + if limit and isinstance(value, (list, tuple)) and len(value) > limit: + value = value[:limit] + + return value + + def getone(self, key, filters=None): + return self.get(key, filters=filters, limit=1)[0] + + def set(self, key, value, as_value=False): + if key in self._reserved_keywords: + return + + if not as_value and key in self._table_fieldnames: + self.__dict__[key] = [] + + # if value is falsy, just init to an empty list + if value: + self.extend(key, value) + + return + + self.__dict__[key] = value + + def delete_key(self, key): + if key in self.__dict__: + del self.__dict__[key] + + def append(self, key, value=None): + """Append an item to a child table. + + Example: + doc.append("childtable", { + "child_table_field": "value", + "child_table_int_field": 0, + ... + }) + """ + if value is None: + value = {} + + if (table := self.__dict__.get(key)) is None: + self.__dict__[key] = table = [] + + value = self._init_child(value, key) + table.append(value) + + # reference parent document + value.parent_doc = self + + return value + + def extend(self, key, value): + try: + value = iter(value) + except TypeError: + raise ValueError + + for v in value: + self.append(key, v) + + def remove(self, doc): + # Usage: from the parent doc, pass the child table doc + # to remove that child doc from the child table, thus removing it from the parent doc + if doc.get("parentfield"): + self.get(doc.parentfield).remove(doc) + + def _init_child(self, value, key): + if not isinstance(value, BaseDocument): + if not (doctype := self.get_table_field_doctype(key)): + raise AttributeError(key) + + value["doctype"] = doctype + value = get_controller(doctype)(value) + + value.parent = self.name + value.parenttype = self.doctype + value.parentfield = key + + if value.docstatus is None: + value.docstatus = DocStatus.draft() + + if not getattr(value, "idx", None): + if table := getattr(self, key, None): + value.idx = len(table) + 1 + else: + value.idx = 1 + + if not getattr(value, "name", None): + value.__dict__["__islocal"] = 1 + + return value + + def _get_table_fields(self): + """ + To get table fields during Document init + Meta.get_table_fields goes into recursion for special doctypes + """ + + if self.doctype == "DocType": + return DOCTYPE_TABLE_FIELDS + + # child tables don't have child tables + if self.doctype in DOCTYPES_FOR_DOCTYPE or getattr(self, "parentfield", None): + return () + + return self.meta.get_table_fields() + + def get_valid_dict( + self, sanitize=True, convert_dates_to_str=False, ignore_nulls=False, ignore_virtual=False + ) -> dict: + d = _dict() + for fieldname in self.meta.get_valid_columns(): + # column is valid, we can use getattr + d[fieldname] = getattr(self, fieldname, None) + + # if no need for sanitization and value is None, continue + if not sanitize and d[fieldname] is None: + continue + + df = self.meta.get_field(fieldname) + + if df: + if getattr(df, "is_virtual", False): + if ignore_virtual: + del d[fieldname] + continue + + if d[fieldname] is None and (options := getattr(df, "options", None)): + from influxframework.utils.safe_exec import get_safe_globals + + d[fieldname] = influxframework.safe_eval( + code=options, + eval_globals=get_safe_globals(), + eval_locals={"doc": self}, + ) + + if isinstance(d[fieldname], list) and df.fieldtype not in table_fields: + influxframework.throw(_("Value for {0} cannot be a list").format(_(df.label))) + + if df.fieldtype == "Check": + d[fieldname] = 1 if cint(d[fieldname]) else 0 + + elif df.fieldtype == "Int" and not isinstance(d[fieldname], int): + d[fieldname] = cint(d[fieldname]) + + elif df.fieldtype == "JSON" and isinstance(d[fieldname], dict): + d[fieldname] = json.dumps(d[fieldname], sort_keys=True, indent=4, separators=(",", ": ")) + + elif df.fieldtype in float_like_fields and not isinstance(d[fieldname], float): + d[fieldname] = flt(d[fieldname]) + + elif (df.fieldtype in datetime_fields and d[fieldname] == "") or ( + getattr(df, "unique", False) and cstr(d[fieldname]).strip() == "" + ): + d[fieldname] = None + + if convert_dates_to_str and isinstance( + d[fieldname], (datetime.datetime, datetime.date, datetime.time, datetime.timedelta) + ): + d[fieldname] = str(d[fieldname]) + + if ignore_nulls and d[fieldname] is None: + del d[fieldname] + + return d + + def init_child_tables(self): + """ + This is needed so that one can loop over child table properties + without worrying about whether or not they have values + """ + + for fieldname in self._table_fieldnames: + if self.__dict__.get(fieldname) is None: + self.__dict__[fieldname] = [] + + def init_valid_columns(self): + for key in default_fields: + if key not in self.__dict__: + self.__dict__[key] = None + + if self.__dict__[key] is None: + if key == "docstatus": + self.docstatus = DocStatus.draft() + elif key == "idx": + self.__dict__[key] = 0 + + for key in self.get_valid_columns(): + if key not in self.__dict__: + self.__dict__[key] = None + + def get_valid_columns(self) -> list[str]: + if self.doctype not in influxframework.local.valid_columns: + if self.doctype in DOCTYPES_FOR_DOCTYPE: + from influxframework.model.meta import get_table_columns + + valid = get_table_columns(self.doctype) + else: + valid = self.meta.get_valid_columns() + + influxframework.local.valid_columns[self.doctype] = valid + + return influxframework.local.valid_columns[self.doctype] + + def is_new(self) -> bool: + return self.get("__islocal") + + @property + def docstatus(self): + return DocStatus(cint(self.get("docstatus"))) + + @docstatus.setter + def docstatus(self, value): + self.__dict__["docstatus"] = DocStatus(cint(value)) + + def as_dict( + self, + no_nulls=False, + no_default_fields=False, + convert_dates_to_str=False, + no_child_table_fields=False, + ) -> dict: + doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str, ignore_nulls=no_nulls) + doc["doctype"] = self.doctype + + for fieldname in self._table_fieldnames: + children = self.get(fieldname) or [] + doc[fieldname] = [ + d.as_dict( + convert_dates_to_str=convert_dates_to_str, + no_nulls=no_nulls, + no_default_fields=no_default_fields, + no_child_table_fields=no_child_table_fields, + ) + for d in children + ] + + if no_default_fields: + for key in default_fields: + if key in doc: + del doc[key] + + if no_child_table_fields: + for key in child_table_fields: + if key in doc: + del doc[key] + + for key in ( + "_user_tags", + "__islocal", + "__onload", + "_liked_by", + "__run_link_triggers", + "__unsaved", + ): + if value := getattr(self, key, None): + doc[key] = value + + return doc + + def as_json(self): + return influxframework.as_json(self.as_dict()) + + def get_table_field_doctype(self, fieldname): + try: + return self.meta.get_field(fieldname).options + except AttributeError: + if self.doctype == "DocType" and (table_doctype := TABLE_DOCTYPES_FOR_DOCTYPE.get(fieldname)): + return table_doctype + + raise + + def get_parentfield_of_doctype(self, doctype): + fieldname = [df.fieldname for df in self.meta.get_table_fields() if df.options == doctype] + return fieldname[0] if fieldname else None + + def db_insert(self, ignore_if_duplicate=False): + """INSERT the document (with valid columns) in the database. + + args: + ignore_if_duplicate: ignore primary key collision + at database level (postgres) + in python (mariadb) + """ + if not self.name: + # name will be set by document class in most cases + set_new_name(self) + + conflict_handler = "" + # On postgres we can't implcitly ignore PK collision + # So instruct pg to ignore `name` field conflicts + if ignore_if_duplicate and influxframework.db.db_type == "postgres": + conflict_handler = "on conflict (name) do nothing" + + if not self.creation: + self.creation = self.modified = now() + self.created_by = self.modified_by = influxframework.session.user + + # if doctype is "DocType", don't insert null values as we don't know who is valid yet + d = self.get_valid_dict( + convert_dates_to_str=True, + ignore_nulls=self.doctype in DOCTYPES_FOR_DOCTYPE, + ignore_virtual=True, + ) + + columns = list(d) + try: + influxframework.db.sql( + """INSERT INTO `tab{doctype}` ({columns}) + VALUES ({values}) {conflict_handler}""".format( + doctype=self.doctype, + columns=", ".join("`" + c + "`" for c in columns), + values=", ".join(["%s"] * len(columns)), + conflict_handler=conflict_handler, + ), + list(d.values()), + ) + except Exception as e: + if influxframework.db.is_primary_key_violation(e): + if self.meta.autoname == "hash": + # hash collision? try again + influxframework.flags.retry_count = (influxframework.flags.retry_count or 0) + 1 + if influxframework.flags.retry_count > 5 and not influxframework.flags.in_test: + raise + self.name = None + self.db_insert() + return + + if not ignore_if_duplicate: + influxframework.msgprint( + _("{0} {1} already exists").format(self.doctype, influxframework.bold(self.name)), + title=_("Duplicate Name"), + indicator="red", + ) + raise influxframework.DuplicateEntryError(self.doctype, self.name, e) + + elif influxframework.db.is_unique_key_violation(e): + # unique constraint + self.show_unique_validation_message(e) + + else: + raise + + self.set("__islocal", False) + + def db_update(self): + if self.get("__islocal") or not self.name: + self.db_insert() + return + + d = self.get_valid_dict( + convert_dates_to_str=True, + ignore_nulls=self.doctype in DOCTYPES_FOR_DOCTYPE, + ignore_virtual=True, + ) + + # don't update name, as case might've been changed + name = cstr(d["name"]) + del d["name"] + + columns = list(d) + + try: + influxframework.db.sql( + """UPDATE `tab{doctype}` + SET {values} WHERE `name`=%s""".format( + doctype=self.doctype, values=", ".join("`" + c + "`=%s" for c in columns) + ), + list(d.values()) + [name], + ) + except Exception as e: + if influxframework.db.is_unique_key_violation(e): + self.show_unique_validation_message(e) + else: + raise + + def db_update_all(self): + """Raw update parent + children + DOES NOT VALIDATE AND CALL TRIGGERS""" + self.db_update() + for fieldname in self._table_fieldnames: + for doc in self.get(fieldname): + doc.db_update() + + def show_unique_validation_message(self, e): + if influxframework.db.db_type != "postgres": + fieldname = str(e).split("'")[-2] + label = None + + # MariaDB gives key_name in error. Extracting fieldname from key name + try: + fieldname = self.get_field_name_by_key_name(fieldname) + except IndexError: + pass + + label = self.get_label_from_fieldname(fieldname) + + influxframework.msgprint(_("{0} must be unique").format(label or fieldname)) + + # this is used to preserve traceback + raise influxframework.UniqueValidationError(self.doctype, self.name, e) + + def get_field_name_by_key_name(self, key_name): + """MariaDB stores a mapping between `key_name` and `column_name`. + This function returns the `column_name` associated with the `key_name` passed + + Args: + key_name (str): The name of the database index. + + Raises: + IndexError: If the key is not found in the table. + + Returns: + str: The column name associated with the key. + """ + return influxframework.db.sql( + f""" + SHOW + INDEX + FROM + `tab{self.doctype}` + WHERE + key_name=%s + AND + Non_unique=0 + """, + key_name, + as_dict=True, + )[0].get("Column_name") + + def get_label_from_fieldname(self, fieldname): + """Returns the associated label for fieldname + + Args: + fieldname (str): The fieldname in the DocType to use to pull the label. + + Returns: + str: The label associated with the fieldname, if found, otherwise `None`. + """ + df = self.meta.get_field(fieldname) + if df: + return df.label + + def update_modified(self): + """Update modified timestamp""" + self.set("modified", now()) + influxframework.db.set_value(self.doctype, self.name, "modified", self.modified, update_modified=False) + + def _fix_numeric_types(self): + for df in self.meta.get("fields"): + if df.fieldtype == "Check": + self.set(df.fieldname, cint(self.get(df.fieldname))) + + elif self.get(df.fieldname) is not None: + if df.fieldtype == "Int": + self.set(df.fieldname, cint(self.get(df.fieldname))) + + elif df.fieldtype in ("Float", "Currency", "Percent"): + self.set(df.fieldname, flt(self.get(df.fieldname))) + + if self.docstatus is not None: + self.docstatus = DocStatus(cint(self.docstatus)) + + def _get_missing_mandatory_fields(self): + """Get mandatory fields that do not have any values""" + + def get_msg(df): + if df.fieldtype in table_fields: + return "{}: {}: {}".format(_("Error"), _("Data missing in table"), _(df.label)) + + # check if parentfield exists (only applicable for child table doctype) + elif self.get("parentfield"): + return "{}: {} {} #{}: {}: {}".format( + _("Error"), + influxframework.bold(_(self.doctype)), + _("Row"), + self.idx, + _("Value missing for"), + _(df.label), + ) + + return _("Error: Value missing for {0}: {1}").format(_(df.parent), _(df.label)) + + def has_content(df): + value = cstr(self.get(df.fieldname)) + has_text_content = strip_html(value).strip() + has_img_tag = " max_length: + self.throw_length_exceeded_error(df, max_length, value) + + elif column_type in ("int", "bigint", "smallint"): + max_length = max_positive_value[column_type] + + if abs(cint(value)) > max_length: + self.throw_length_exceeded_error(df, max_length, value) + + def _validate_code_fields(self): + for field in self.meta.get_code_fields(): + code_string = self.get(field.fieldname) + language = field.get("options") + + if language == "Python": + influxframework.utils.validate_python_code(code_string, fieldname=field.label, is_expression=False) + + elif language == "PythonExpression": + influxframework.utils.validate_python_code(code_string, fieldname=field.label) + + def _sync_autoname_field(self): + """Keep autoname field in sync with `name`""" + autoname = self.meta.autoname or "" + _empty, _field_specifier, fieldname = autoname.partition("field:") + + if fieldname and self.name and self.name != self.get(fieldname): + self.set(fieldname, self.name) + + def throw_length_exceeded_error(self, df, max_length, value): + # check if parentfield exists (only applicable for child table doctype) + if self.get("parentfield"): + reference = _("{0}, Row {1}").format(_(self.doctype), self.idx) + else: + reference = f"{_(self.doctype)} {self.name}" + + influxframework.throw( + _("{0}: '{1}' ({3}) will get truncated, as max characters allowed is {2}").format( + reference, _(df.label), max_length, value + ), + influxframework.CharacterLengthExceededError, + title=_("Value too big"), + ) + + def _validate_update_after_submit(self): + # get the full doc with children + db_values = influxframework.get_doc(self.doctype, self.name).as_dict() + + for key in self.as_dict(): + df = self.meta.get_field(key) + db_value = db_values.get(key) + + if df and not df.allow_on_submit and (self.get(key) or db_value): + if df.fieldtype in table_fields: + # just check if the table size has changed + # individual fields will be checked in the loop for children + self_value = len(self.get(key)) + db_value = len(db_value) + + else: + self_value = self.get_value(key) + # Postgres stores values as `datetime.time`, MariaDB as `timedelta` + if isinstance(self_value, datetime.timedelta) and isinstance(db_value, datetime.time): + db_value = datetime.timedelta( + hours=db_value.hour, + minutes=db_value.minute, + seconds=db_value.second, + microseconds=db_value.microsecond, + ) + if self_value != db_value: + influxframework.throw( + _("Not allowed to change {0} after submission").format(df.label), + influxframework.UpdateAfterSubmitError, + ) + + def _sanitize_content(self): + """Sanitize HTML and Email in field values. Used to prevent XSS. + + - Ignore if 'Ignore XSS Filter' is checked or fieldtype is 'Code' + """ + from bs4 import BeautifulSoup + + if influxframework.flags.in_install: + return + + for fieldname, value in self.get_valid_dict(ignore_virtual=True).items(): + if not value or not isinstance(value, str): + continue + + value = influxframework.as_unicode(value) + + if "<" not in value and ">" not in value: + # doesn't look like html so no need + continue + + elif "" in value and not bool(BeautifulSoup(value, "html.parser").find()): + # should be handled separately via the markdown converter function + continue + + df = self.meta.get_field(fieldname) + sanitized_value = value + + if df and ( + df.get("ignore_xss_filter") + or (df.get("fieldtype") in ("Data", "Small Text", "Text") and df.get("options") == "Email") + or df.get("fieldtype") in ("Attach", "Attach Image", "Barcode", "Code") + # cancelled and submit but not update after submit should be ignored + or self.docstatus.is_cancelled() + or (self.docstatus.is_submitted() and not df.get("allow_on_submit")) + ): + continue + + else: + sanitized_value = sanitize_html(value, linkify=df and df.fieldtype == "Text Editor") + + self.set(fieldname, sanitized_value) + + def _save_passwords(self): + """Save password field values in __Auth table""" + from influxframework.utils.password import remove_encrypted_password, set_encrypted_password + + if self.flags.ignore_save_passwords is True: + return + + for df in self.meta.get("fields", {"fieldtype": ("=", "Password")}): + if self.flags.ignore_save_passwords and df.fieldname in self.flags.ignore_save_passwords: + continue + new_password = self.get(df.fieldname) + + if not new_password: + remove_encrypted_password(self.doctype, self.name, df.fieldname) + + if new_password and not self.is_dummy_password(new_password): + # is not a dummy password like '*****' + set_encrypted_password(self.doctype, self.name, new_password, df.fieldname) + + # set dummy password like '*****' + self.set(df.fieldname, "*" * len(new_password)) + + def get_password(self, fieldname="password", raise_exception=True): + from influxframework.utils.password import get_decrypted_password + + if self.get(fieldname) and not self.is_dummy_password(self.get(fieldname)): + return self.get(fieldname) + + return get_decrypted_password( + self.doctype, self.name, fieldname, raise_exception=raise_exception + ) + + def is_dummy_password(self, pwd): + return "".join(set(pwd)) == "*" + + def precision(self, fieldname, parentfield=None): + """Returns float precision for a particular field (or get global default). + + :param fieldname: Fieldname for which precision is required. + :param parentfield: If fieldname is in child table.""" + from influxframework.model.meta import get_field_precision + + if parentfield and not isinstance(parentfield, str) and parentfield.get("parentfield"): + parentfield = parentfield.parentfield + + cache_key = parentfield or "main" + + if not hasattr(self, "_precision"): + self._precision = _dict() + + if cache_key not in self._precision: + self._precision[cache_key] = _dict() + + if fieldname not in self._precision[cache_key]: + self._precision[cache_key][fieldname] = None + + doctype = self.meta.get_field(parentfield).options if parentfield else self.doctype + df = influxframework.get_meta(doctype).get_field(fieldname) + + if df.fieldtype in ("Currency", "Float", "Percent"): + self._precision[cache_key][fieldname] = get_field_precision(df, self) + + return self._precision[cache_key][fieldname] + + def get_formatted( + self, fieldname, doc=None, currency=None, absolute_value=False, translated=False, format=None + ): + from influxframework.utils.formatters import format_value + + df = self.meta.get_field(fieldname) + if not df: + from influxframework.model.meta import get_default_df + + df = get_default_df(fieldname) + + if ( + df.fieldtype == "Currency" + and not currency + and (currency_field := df.get("options")) + and (currency_value := self.get(currency_field)) + ): + currency = influxframework.db.get_value("Currency", currency_value, cache=True) + + val = self.get(fieldname) + + if translated: + val = _(val) + + if not doc: + doc = getattr(self, "parent_doc", None) or self + + if (absolute_value or doc.get("absolute_value")) and isinstance(val, (int, float)): + val = abs(self.get(fieldname)) + + return format_value(val, df=df, doc=doc, currency=currency, format=format) + + def is_print_hide(self, fieldname, df=None, for_print=True): + """Returns true if fieldname is to be hidden for print. + + Print Hide can be set via the Print Format Builder or in the controller as a list + of hidden fields. Example + + class MyDoc(Document): + def __setup__(self): + self.print_hide = ["field1", "field2"] + + :param fieldname: Fieldname to be checked if hidden. + """ + meta_df = self.meta.get_field(fieldname) + if meta_df and meta_df.get("__print_hide"): + return True + + print_hide = 0 + + if self.get(fieldname) == 0 and not self.meta.istable: + print_hide = (df and df.print_hide_if_no_value) or (meta_df and meta_df.print_hide_if_no_value) + + if not print_hide: + if df and df.print_hide is not None: + print_hide = df.print_hide + elif meta_df: + print_hide = meta_df.print_hide + + return print_hide + + def in_format_data(self, fieldname): + """Returns True if shown via Print Format::`format_data` property. + Called from within standard print format.""" + doc = getattr(self, "parent_doc", self) + + if hasattr(doc, "format_data_map"): + return fieldname in doc.format_data_map + else: + return True + + def reset_values_if_no_permlevel_access(self, has_access_to, high_permlevel_fields): + """If the user does not have permissions at permlevel > 0, then reset the values to original / default""" + to_reset = [] + + for df in high_permlevel_fields: + if ( + df.permlevel not in has_access_to + and df.fieldtype not in display_fieldtypes + and df.fieldname not in self.flags.get("ignore_permlevel_for_fields", []) + ): + to_reset.append(df) + + if to_reset: + if self.is_new(): + # if new, set default value + ref_doc = influxframework.new_doc(self.doctype) + else: + # get values from old doc + if self.get("parent_doc"): + parent_doc = self.parent_doc.get_latest() + child_docs = [d for d in parent_doc.get(self.parentfield) if d.name == self.name] + if not child_docs: + return + ref_doc = child_docs[0] + else: + ref_doc = self.get_latest() + + for df in to_reset: + self.set(df.fieldname, ref_doc.get(df.fieldname)) + + def get_value(self, fieldname): + df = self.meta.get_field(fieldname) + val = self.get(fieldname) + + return self.cast(val, df) + + def cast(self, value, df): + return cast_fieldtype(df.fieldtype, value, show_warning=False) + + def _extract_images_from_text_editor(self): + from influxframework.core.doctype.file.utils import extract_images_from_doc + + if self.doctype != "DocType": + for df in self.meta.get("fields", {"fieldtype": ("=", "Text Editor")}): + extract_images_from_doc(self, df.fieldname) + + +def _filter(data, filters, limit=None): + """pass filters as: + {"key": "val", "key": ["!=", "val"], + "key": ["in", "val"], "key": ["not in", "val"], "key": "^val", + "key" : True (exists), "key": False (does not exist) }""" + + out, _filters = [], {} + + if not data: + return out + + # setup filters as tuples + if filters: + for f in filters: + fval = filters[f] + + if not isinstance(fval, (tuple, list)): + if fval is True: + fval = ("not None", fval) + elif fval is False: + fval = ("None", fval) + elif isinstance(fval, str) and fval.startswith("^"): + fval = ("^", fval[1:]) + else: + fval = ("=", fval) + + _filters[f] = fval + + for d in data: + for f, fval in _filters.items(): + if not influxframework.compare(getattr(d, f, None), fval[0], fval[1]): + break + else: + out.append(d) + if limit and len(out) >= limit: + break + + return out diff --git a/influxframework/model/create_new.py b/influxframework/model/create_new.py new file mode 100644 index 0000000..cf562f7 --- /dev/null +++ b/influxframework/model/create_new.py @@ -0,0 +1,186 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +""" +Create a new document with defaults set +""" + +import copy + +import influxframework +import influxframework.defaults +from influxframework.core.doctype.user_permission.user_permission import get_user_permissions +from influxframework.model import data_fieldtypes +from influxframework.permissions import filter_allowed_docs_for_doctype +from influxframework.utils import cstr, now_datetime, nowdate, nowtime + + +def get_new_doc(doctype, parent_doc=None, parentfield=None, as_dict=False): + if doctype not in influxframework.local.new_doc_templates: + # cache a copy of new doc as it is called + # frequently for inserts + influxframework.local.new_doc_templates[doctype] = make_new_doc(doctype) + + doc = copy.deepcopy(influxframework.local.new_doc_templates[doctype]) + + set_dynamic_default_values(doc, parent_doc, parentfield) + + if as_dict: + return doc + else: + return influxframework.get_doc(doc) + + +def make_new_doc(doctype): + doc = influxframework.get_doc( + {"doctype": doctype, "__islocal": 1, "owner": influxframework.session.user, "docstatus": 0} + ) + + set_user_and_static_default_values(doc) + + doc._fix_numeric_types() + doc = doc.get_valid_dict(sanitize=False) + doc["doctype"] = doctype + doc["__islocal"] = 1 + + if not influxframework.model.meta.is_single(doctype): + doc["__unsaved"] = 1 + + return doc + + +def set_user_and_static_default_values(doc): + user_permissions = get_user_permissions() + defaults = influxframework.defaults.get_defaults() + + for df in doc.meta.get("fields"): + if df.fieldtype in data_fieldtypes: + # user permissions for link options + doctype_user_permissions = user_permissions.get(df.options, []) + # Allowed records for the reference doctype (link field) along with default doc + allowed_records, default_doc = filter_allowed_docs_for_doctype( + doctype_user_permissions, df.parent, with_default_doc=True + ) + + user_default_value = get_user_default_value( + df, defaults, doctype_user_permissions, allowed_records, default_doc + ) + if user_default_value is not None: + # if fieldtype is link check if doc exists + if not df.fieldtype == "Link" or influxframework.db.exists(df.options, user_default_value): + doc.set(df.fieldname, user_default_value) + + else: + if df.fieldname != doc.meta.title_field: + static_default_value = get_static_default_value(df, doctype_user_permissions, allowed_records) + if static_default_value is not None: + doc.set(df.fieldname, static_default_value) + + +def get_user_default_value(df, defaults, doctype_user_permissions, allowed_records, default_doc): + # don't set defaults for "User" link field using User Permissions! + if df.fieldtype == "Link" and df.options != "User": + # If user permission has Is Default enabled or single-user permission has found against respective doctype. + if not df.ignore_user_permissions and default_doc: + return default_doc + + # 2 - Look in user defaults + user_default = defaults.get(df.fieldname) + + allowed_by_user_permission = validate_value_via_user_permissions( + df, doctype_user_permissions, allowed_records, user_default=user_default + ) + + # is this user default also allowed as per user permissions? + if user_default and allowed_by_user_permission: + return user_default + + +def get_static_default_value(df, doctype_user_permissions, allowed_records): + # 3 - look in default of docfield + if df.get("default"): + if df.default == "__user": + return influxframework.session.user + + elif df.default == "Today": + return nowdate() + + elif not cstr(df.default).startswith(":"): + # a simple default value + is_allowed_default_value = validate_value_via_user_permissions( + df, doctype_user_permissions, allowed_records + ) + + if df.fieldtype != "Link" or df.options == "User" or is_allowed_default_value: + return df.default + + elif df.fieldtype == "Select" and df.options and df.options not in ("[Select]", "Loading..."): + return df.options.split("\n")[0] + + +def validate_value_via_user_permissions( + df, doctype_user_permissions, allowed_records, user_default=None +): + is_valid = True + # If User Permission exists and allowed records is empty, + # that means there are User Perms, but none applicable to this new doctype. + + if user_permissions_exist(df, doctype_user_permissions) and allowed_records: + # If allowed records is not empty, + # check if this field value is allowed via User Permissions applied to this doctype. + value = user_default if user_default else df.default + is_valid = value in allowed_records + + return is_valid + + +def set_dynamic_default_values(doc, parent_doc, parentfield): + # these values should not be cached + user_permissions = get_user_permissions() + + for df in influxframework.get_meta(doc["doctype"]).get("fields"): + if df.get("default"): + if cstr(df.default).startswith(":"): + default_value = get_default_based_on_another_field(df, user_permissions, parent_doc) + if default_value is not None and not doc.get(df.fieldname): + doc[df.fieldname] = default_value + + elif df.fieldtype == "Datetime" and df.default.lower() == "now": + doc[df.fieldname] = now_datetime() + + if df.fieldtype == "Time": + doc[df.fieldname] = nowtime() + + if parent_doc: + doc["parent"] = parent_doc.name + doc["parenttype"] = parent_doc.doctype + + if parentfield: + doc["parentfield"] = parentfield + + +def user_permissions_exist(df, doctype_user_permissions): + return ( + df.fieldtype == "Link" + and not getattr(df, "ignore_user_permissions", False) + and doctype_user_permissions + ) + + +def get_default_based_on_another_field(df, user_permissions, parent_doc): + # default value based on another document + from influxframework.permissions import get_allowed_docs_for_doctype + + ref_doctype = df.default[1:] + ref_fieldname = ref_doctype.lower().replace(" ", "_") + reference_name = ( + parent_doc.get(ref_fieldname) if parent_doc else influxframework.db.get_default(ref_fieldname) + ) + default_value = influxframework.db.get_value(ref_doctype, reference_name, df.fieldname) + is_allowed_default_value = not user_permissions_exist(df, user_permissions.get(df.options)) or ( + default_value in get_allowed_docs_for_doctype(user_permissions[df.options], df.parent) + ) + + # is this allowed as per user permissions + if is_allowed_default_value: + return default_value diff --git a/influxframework/model/db_query.py b/influxframework/model/db_query.py new file mode 100644 index 0000000..01b8bbc --- /dev/null +++ b/influxframework/model/db_query.py @@ -0,0 +1,1139 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +"""build query for doclistview and return results""" + +import copy +import json +import re +from datetime import datetime + +import influxframework +import influxframework.defaults +import influxframework.permissions +import influxframework.share +from influxframework import _ +from influxframework.core.doctype.server_script.server_script_utils import get_server_script_map +from influxframework.database.utils import FallBackDateTimeStr +from influxframework.model import optional_fields +from influxframework.model.meta import get_table_columns +from influxframework.model.utils.user_settings import get_user_settings, update_user_settings +from influxframework.query_builder.utils import Column +from influxframework.utils import ( + add_to_date, + cint, + cstr, + flt, + get_filter, + get_time, + get_timespan_date_range, + make_filter_tuple, +) + +LOCATE_PATTERN = re.compile(r"locate\([^,]+,\s*[`\"]?name[`\"]?\s*\)", flags=re.IGNORECASE) +LOCATE_CAST_PATTERN = re.compile( + r"locate\(([^,]+),\s*([`\"]?name[`\"]?)\s*\)", flags=re.IGNORECASE +) +FUNC_IFNULL_PATTERN = re.compile( + r"(strpos|ifnull|coalesce)\(\s*[`\"]?name[`\"]?\s*,", flags=re.IGNORECASE +) +CAST_VARCHAR_PATTERN = re.compile( + r"([`\"]?tab[\w`\" -]+\.[`\"]?name[`\"]?)(?!\w)", flags=re.IGNORECASE +) +ORDER_BY_PATTERN = re.compile(r"\ order\ by\ |\ asc|\ ASC|\ desc|\ DESC", flags=re.IGNORECASE) +SUB_QUERY_PATTERN = re.compile("^.*[,();@].*") +IS_QUERY_PATTERN = re.compile(r"^(select|delete|update|drop|create)\s") +IS_QUERY_PREDICATE_PATTERN = re.compile( + r"\s*[0-9a-zA-z]*\s*( from | group by | order by | where | join )" +) +FIELD_QUOTE_PATTERN = re.compile(r"[0-9a-zA-Z]+\s*'") +FIELD_COMMA_PATTERN = re.compile(r"[0-9a-zA-Z]+\s*,") +STRICT_FIELD_PATTERN = re.compile(r".*/\*.*") +STRICT_UNION_PATTERN = re.compile(r".*\s(union).*\s") +ORDER_GROUP_PATTERN = re.compile(r".*[^a-z0-9-_ ,`'\"\.\(\)].*") + + +class DatabaseQuery: + def __init__(self, doctype, user=None): + self.doctype = doctype + self.tables = [] + self.link_tables = [] + self.conditions = [] + self.or_conditions = [] + self.fields = None + self.user = user or influxframework.session.user + self.ignore_ifnull = False + self.flags = influxframework._dict() + self.reference_doctype = None + + def execute( + self, + fields=None, + filters=None, + or_filters=None, + docstatus=None, + group_by=None, + order_by="KEEP_DEFAULT_ORDERING", + limit_start=False, + limit_page_length=None, + as_list=False, + with_childnames=False, + debug=False, + ignore_permissions=False, + user=None, + with_comment_count=False, + join="left join", + distinct=False, + start=None, + page_length=None, + limit=None, + ignore_ifnull=False, + save_user_settings=False, + save_user_settings_fields=False, + update=None, + add_total_row=None, + user_settings=None, + reference_doctype=None, + run=True, + strict=True, + pluck=None, + ignore_ddl=False, + *, + parent_doctype=None, + ) -> list: + + if ( + not ignore_permissions + and not influxframework.has_permission(self.doctype, "select", user=user, parent_doctype=parent_doctype) + and not influxframework.has_permission(self.doctype, "read", user=user, parent_doctype=parent_doctype) + ): + influxframework.flags.error_message = _("Insufficient Permission for {0}").format( + influxframework.bold(self.doctype) + ) + raise influxframework.PermissionError(self.doctype) + + # filters and fields swappable + # its hard to remember what comes first + if isinstance(fields, dict) or ( + fields and isinstance(fields, list) and isinstance(fields[0], list) + ): + # if fields is given as dict/list of list, its probably filters + filters, fields = fields, filters + + elif fields and isinstance(filters, list) and len(filters) > 1 and isinstance(filters[0], str): + # if `filters` is a list of strings, its probably fields + filters, fields = fields, filters + + if fields: + self.fields = fields + else: + self.fields = [f"`tab{self.doctype}`.`{pluck or 'name'}`"] + + if start: + limit_start = start + if page_length: + limit_page_length = page_length + if limit: + limit_page_length = limit + + self.filters = filters or [] + self.or_filters = or_filters or [] + self.docstatus = docstatus or [] + self.group_by = group_by + self.order_by = order_by + self.limit_start = cint(limit_start) + self.limit_page_length = cint(limit_page_length) if limit_page_length else None + self.with_childnames = with_childnames + self.debug = debug + self.join = join + self.distinct = distinct + self.as_list = as_list + self.ignore_ifnull = ignore_ifnull + self.flags.ignore_permissions = ignore_permissions + self.user = user or influxframework.session.user + self.update = update + self.user_settings_fields = copy.deepcopy(self.fields) + self.run = run + self.strict = strict + self.ignore_ddl = ignore_ddl + + # for contextual user permission check + # to determine which user permission is applicable on link field of specific doctype + self.reference_doctype = reference_doctype or self.doctype + + if user_settings: + self.user_settings = json.loads(user_settings) + + self.columns = self.get_table_columns() + + # no table & ignore_ddl, return + if not self.columns: + return [] + + result = self.build_and_run() + + if with_comment_count and not as_list and self.doctype: + self.add_comment_count(result) + + if save_user_settings: + self.save_user_settings_fields = save_user_settings_fields + self.update_user_settings() + + if pluck: + return [d[pluck] for d in result] + + return result + + def build_and_run(self): + args = self.prepare_args() + args.limit = self.add_limit() + + if args.conditions: + args.conditions = "where " + args.conditions + + if self.distinct: + args.fields = "distinct " + args.fields + args.order_by = "" # TODO: recheck for alternative + + # Postgres requires any field that appears in the select clause to also + # appear in the order by and group by clause + if influxframework.db.db_type == "postgres" and args.order_by and args.group_by: + args = self.prepare_select_args(args) + + query = ( + """select %(fields)s + from %(tables)s + %(conditions)s + %(group_by)s + %(order_by)s + %(limit)s""" + % args + ) + + return influxframework.db.sql( + query, + as_dict=not self.as_list, + debug=self.debug, + update=self.update, + ignore_ddl=self.ignore_ddl, + run=self.run, + ) + + def prepare_args(self): + self.parse_args() + self.sanitize_fields() + self.extract_tables() + self.set_optional_columns() + self.build_conditions() + + args = influxframework._dict() + + if self.with_childnames: + for t in self.tables: + if t != "`tab" + self.doctype + "`": + self.fields.append(t + ".name as '%s:name'" % t[4:-1]) + + # query dict + args.tables = self.tables[0] + + # left join parent, child tables + for child in self.tables[1:]: + parent_name = cast_name(f"{self.tables[0]}.name") + args.tables += f" {self.join} {child} on ({child}.parent = {parent_name})" + + # left join link tables + for link in self.link_tables: + args.tables += f" {self.join} `tab{link.doctype}` on (`tab{link.doctype}`.`name` = {self.tables[0]}.`{link.fieldname}`)" + + if self.grouped_or_conditions: + self.conditions.append(f"({' or '.join(self.grouped_or_conditions)})") + + args.conditions = " and ".join(self.conditions) + + if self.or_conditions: + args.conditions += (" or " if args.conditions else "") + " or ".join(self.or_conditions) + + self.set_field_tables() + self.cast_name_fields() + + fields = [] + + # Wrapping fields with grave quotes to allow support for sql keywords + # TODO: Add support for wrapping fields with sql functions and distinct keyword + for field in self.fields: + stripped_field = field.strip().lower() + skip_wrapping = any( + [ + stripped_field.startswith(("`", "*", '"', "'")), + "(" in stripped_field, + "distinct" in stripped_field, + ] + ) + if skip_wrapping: + fields.append(field) + elif "as" in field.lower().split(" "): + col, _, new = field.split() + fields.append(f"`{col}` as {new}") + else: + fields.append(f"`{field}`") + + args.fields = ", ".join(fields) + + self.set_order_by(args) + + self.validate_order_by_and_group_by(args.order_by) + args.order_by = args.order_by and (" order by " + args.order_by) or "" + + self.validate_order_by_and_group_by(self.group_by) + args.group_by = self.group_by and (" group by " + self.group_by) or "" + + return args + + def prepare_select_args(self, args): + order_field = ORDER_BY_PATTERN.sub("", args.order_by) + + if order_field not in args.fields: + extracted_column = order_column = order_field.replace("`", "") + if "." in extracted_column: + extracted_column = extracted_column.split(".")[1] + + args.fields += f", MAX({extracted_column}) as `{order_column}`" + args.order_by = args.order_by.replace(order_field, f"`{order_column}`") + + return args + + def parse_args(self): + """Convert fields and filters from strings to list, dicts""" + if isinstance(self.fields, str): + if self.fields == "*": + self.fields = ["*"] + else: + try: + self.fields = json.loads(self.fields) + except ValueError: + self.fields = [f.strip() for f in self.fields.split(",")] + + # remove empty strings / nulls in fields + self.fields = [f for f in self.fields if f] + + # convert child_table.fieldname to `tabChild DocType`.`fieldname` + for field in self.fields: + if "." in field and "tab" not in field: + original_field = field + alias = None + if " as " in field: + field, alias = field.split(" as ") + linked_fieldname, fieldname = field.split(".") + linked_field = influxframework.get_meta(self.doctype).get_field(linked_fieldname) + linked_doctype = linked_field.options + if linked_field.fieldtype == "Link": + self.append_link_table(linked_doctype, linked_fieldname) + field = f"`tab{linked_doctype}`.`{fieldname}`" + if alias: + field = f"{field} as {alias}" + self.fields[self.fields.index(original_field)] = field + + for filter_name in ["filters", "or_filters"]: + filters = getattr(self, filter_name) + if isinstance(filters, str): + filters = json.loads(filters) + + if isinstance(filters, dict): + fdict = filters + filters = [] + for key, value in fdict.items(): + filters.append(make_filter_tuple(self.doctype, key, value)) + setattr(self, filter_name, filters) + + def sanitize_fields(self): + """ + regex : ^.*[,();].* + purpose : The regex will look for malicious patterns like `,`, '(', ')', '@', ;' in each + field which may leads to sql injection. + example : + field = "`DocType`.`issingle`, version()" + As field contains `,` and mysql function `version()`, with the help of regex + the system will filter out this field. + """ + blacklisted_keywords = ["select", "create", "insert", "delete", "drop", "update", "case", "show"] + blacklisted_functions = [ + "concat", + "concat_ws", + "if", + "ifnull", + "nullif", + "coalesce", + "connection_id", + "current_user", + "database", + "last_insert_id", + "session_user", + "system_user", + "user", + "version", + "global", + ] + + def _raise_exception(): + influxframework.throw(_("Use of sub-query or function is restricted"), influxframework.DataError) + + def _is_query(field): + if IS_QUERY_PATTERN.match(field): + _raise_exception() + + elif IS_QUERY_PREDICATE_PATTERN.match(field): + _raise_exception() + + for field in self.fields: + if SUB_QUERY_PATTERN.match(field): + if any(f"({keyword}" in field.lower() for keyword in blacklisted_keywords): + _raise_exception() + + if any(f"{keyword}(" in field.lower() for keyword in blacklisted_functions): + _raise_exception() + + if "@" in field.lower(): + # prevent access to global variables + _raise_exception() + + if FIELD_QUOTE_PATTERN.match(field): + _raise_exception() + + if FIELD_COMMA_PATTERN.match(field): + _raise_exception() + + _is_query(field) + + if self.strict: + if STRICT_FIELD_PATTERN.match(field): + influxframework.throw(_("Illegal SQL Query")) + + if STRICT_UNION_PATTERN.match(field.lower()): + influxframework.throw(_("Illegal SQL Query")) + + def extract_tables(self): + """extract tables from fields""" + self.tables = [f"`tab{self.doctype}`"] + sql_functions = [ + "dayofyear(", + "extract(", + "locate(", + "strpos(", + "count(", + "sum(", + "avg(", + ] + # add tables from fields + if self.fields: + for field in self.fields: + if not ("tab" in field and "." in field) or any(x for x in sql_functions if x in field): + continue + + table_name = field.split(".")[0] + + if table_name.lower().startswith("group_concat("): + table_name = table_name[13:] + if not table_name[0] == "`": + table_name = f"`{table_name}`" + if table_name not in self.tables and table_name not in ( + d.table_name for d in self.link_tables + ): + self.append_table(table_name) + + def append_table(self, table_name): + self.tables.append(table_name) + doctype = table_name[4:-1] + self.check_read_permission(doctype) + + def append_link_table(self, doctype, fieldname): + for d in self.link_tables: + if d.doctype == doctype and d.fieldname == fieldname: + return + + self.check_read_permission(doctype) + self.link_tables.append( + influxframework._dict(doctype=doctype, fieldname=fieldname, table_name=f"`tab{doctype}`") + ) + + def check_read_permission(self, doctype): + ptype = "select" if influxframework.only_has_select_perm(doctype) else "read" + + if not self.flags.ignore_permissions and not influxframework.has_permission( + doctype, ptype=ptype, parent_doctype=self.doctype + ): + influxframework.flags.error_message = _("Insufficient Permission for {0}").format(influxframework.bold(doctype)) + raise influxframework.PermissionError(doctype) + + def set_field_tables(self): + """If there are more than one table, the fieldname must not be ambiguous. + If the fieldname is not explicitly mentioned, set the default table""" + + def _in_standard_sql_methods(field): + methods = ("count(", "avg(", "sum(", "extract(", "dayofyear(") + return field.lower().startswith(methods) + + if len(self.tables) > 1 or len(self.link_tables) > 0: + for idx, field in enumerate(self.fields): + if "." not in field and not _in_standard_sql_methods(field): + self.fields[idx] = f"{self.tables[0]}.{field}" + + def cast_name_fields(self): + for i, field in enumerate(self.fields): + self.fields[i] = cast_name(field) + + def get_table_columns(self): + try: + return get_table_columns(self.doctype) + except influxframework.db.TableMissingError: + if self.ignore_ddl: + return None + else: + raise + + def set_optional_columns(self): + """Removes optional columns like `_user_tags`, `_comments` etc. if not in table""" + # remove from fields + to_remove = [] + for fld in self.fields: + for f in optional_fields: + if f in fld and not f in self.columns: + to_remove.append(fld) + + for fld in to_remove: + del self.fields[self.fields.index(fld)] + + # remove from filters + to_remove = [] + for each in self.filters: + if isinstance(each, str): + each = [each] + + for element in each: + if element in optional_fields and element not in self.columns: + to_remove.append(each) + + for each in to_remove: + if isinstance(self.filters, dict): + del self.filters[each] + else: + self.filters.remove(each) + + def build_conditions(self): + self.conditions = [] + self.grouped_or_conditions = [] + self.build_filter_conditions(self.filters, self.conditions) + self.build_filter_conditions(self.or_filters, self.grouped_or_conditions) + + # match conditions + if not self.flags.ignore_permissions: + match_conditions = self.build_match_conditions() + if match_conditions: + self.conditions.append(f"({match_conditions})") + + def build_filter_conditions(self, filters, conditions, ignore_permissions=None): + """build conditions from user filters""" + if ignore_permissions is not None: + self.flags.ignore_permissions = ignore_permissions + + if isinstance(filters, dict): + filters = [filters] + + for f in filters: + if isinstance(f, str): + conditions.append(f) + else: + conditions.append(self.prepare_filter_condition(f)) + + def prepare_filter_condition(self, f): + """Returns a filter condition in the format: + ifnull(`tabDocType`.`fieldname`, fallback) operator "value" + """ + + # TODO: refactor + + from influxframework.boot import get_additional_filters_from_hooks + + additional_filters_config = get_additional_filters_from_hooks() + f = get_filter(self.doctype, f, additional_filters_config) + + tname = "`tab" + f.doctype + "`" + if tname not in self.tables: + self.append_table(tname) + + column_name = cast_name(f.fieldname if "ifnull(" in f.fieldname else f"{tname}.`{f.fieldname}`") + + if f.operator.lower() in additional_filters_config: + f.update(get_additional_filter_field(additional_filters_config, f, f.value)) + + meta = influxframework.get_meta(f.doctype) + can_be_null = True + + # prepare in condition + if f.operator.lower() in ( + "ancestors of", + "descendants of", + "not ancestors of", + "not descendants of", + ): + values = f.value or "" + + # TODO: handle list and tuple + # if not isinstance(values, (list, tuple)): + # values = values.split(",") + + field = meta.get_field(f.fieldname) + ref_doctype = field.options if field else f.doctype + + lft, rgt = "", "" + if f.value: + lft, rgt = influxframework.db.get_value(ref_doctype, f.value, ["lft", "rgt"]) + + # Get descendants elements of a DocType with a tree structure + if f.operator.lower() in ("descendants of", "not descendants of"): + result = influxframework.get_all( + ref_doctype, filters={"lft": [">", lft], "rgt": ["<", rgt]}, order_by="`lft` ASC" + ) + else: + # Get ancestor elements of a DocType with a tree structure + result = influxframework.get_all( + ref_doctype, filters={"lft": ["<", lft], "rgt": [">", rgt]}, order_by="`lft` DESC" + ) + + fallback = "''" + value = [influxframework.db.escape((cstr(v.name) or "").strip(), percent=False) for v in result] + if len(value): + value = f"({', '.join(value)})" + else: + value = "('')" + + # changing operator to IN as the above code fetches all the parent / child values and convert into tuple + # which can be directly used with IN operator to query. + f.operator = ( + "not in" if f.operator.lower() in ("not ancestors of", "not descendants of") else "in" + ) + + elif f.operator.lower() in ("in", "not in"): + # if values contain '' or falsy values then only coalesce column + # for `in` query this is only required if values contain '' or values are empty. + # for `not in` queries we can't be sure as column values might contain null. + if f.operator.lower() == "in": + can_be_null = not f.value or any(v is None or v == "" for v in f.value) + + values = f.value or "" + if isinstance(values, str): + values = values.split(",") + + fallback = "''" + value = [influxframework.db.escape((cstr(v) or "").strip(), percent=False) for v in values] + if len(value): + value = f"({', '.join(value)})" + else: + value = "('')" + + else: + df = meta.get("fields", {"fieldname": f.fieldname}) + df = df[0] if df else None + + if df and df.fieldtype in ("Check", "Float", "Int", "Currency", "Percent"): + can_be_null = False + + if f.operator.lower() in ("previous", "next", "timespan"): + date_range = get_date_range(f.operator.lower(), f.value) + f.operator = "Between" + f.value = date_range + fallback = f"'{FallBackDateTimeStr}'" + + if f.operator in (">", "<", ">=", "<=") and (f.fieldname in ("creation", "modified")): + value = cstr(f.value) + fallback = f"'{FallBackDateTimeStr}'" + + elif f.operator.lower() in ("between") and ( + f.fieldname in ("creation", "modified") + or (df and (df.fieldtype == "Date" or df.fieldtype == "Datetime")) + ): + + value = get_between_date_filter(f.value, df) + fallback = f"'{FallBackDateTimeStr}'" + + elif f.operator.lower() == "is": + if f.value == "set": + f.operator = "!=" + elif f.value == "not set": + f.operator = "=" + + value = "" + fallback = "''" + can_be_null = True + + if "ifnull" not in column_name.lower(): + column_name = f"ifnull({column_name}, {fallback})" + + elif df and df.fieldtype == "Date": + value = influxframework.db.format_date(f.value) + fallback = "'0001-01-01'" + + elif (df and df.fieldtype == "Datetime") or isinstance(f.value, datetime): + value = influxframework.db.format_datetime(f.value) + fallback = f"'{FallBackDateTimeStr}'" + + elif df and df.fieldtype == "Time": + value = get_time(f.value).strftime("%H:%M:%S.%f") + fallback = "'00:00:00'" + + elif f.operator.lower() in ("like", "not like") or ( + isinstance(f.value, str) + and (not df or df.fieldtype not in ["Float", "Int", "Currency", "Percent", "Check"]) + ): + value = "" if f.value is None else f.value + fallback = "''" + + if f.operator.lower() in ("like", "not like") and isinstance(value, str): + # because "like" uses backslash (\) for escaping + value = value.replace("\\", "\\\\").replace("%", "%%") + + elif ( + f.operator == "=" and df and df.fieldtype in ["Link", "Data"] + ): # TODO: Refactor if possible + value = f.value or "''" + fallback = "''" + + elif f.fieldname == "name": + value = f.value or "''" + fallback = "''" + + else: + value = flt(f.value) + fallback = 0 + + if isinstance(f.value, Column): + can_be_null = False # added to avoid the ifnull/coalesce addition + quote = '"' if influxframework.conf.db_type == "postgres" else "`" + value = f"{tname}.{quote}{f.value.name}{quote}" + + # escape value + elif isinstance(value, str) and f.operator.lower() != "between": + value = f"{influxframework.db.escape(value, percent=False)}" + + if ( + self.ignore_ifnull + or not can_be_null + or (f.value and f.operator.lower() in ("=", "like")) + or "ifnull(" in column_name.lower() + ): + if f.operator.lower() == "like" and influxframework.conf.get("db_type") == "postgres": + f.operator = "ilike" + condition = f"{column_name} {f.operator} {value}" + else: + condition = f"ifnull({column_name}, {fallback}) {f.operator} {value}" + + return condition + + def build_match_conditions(self, as_condition=True) -> str | list: + """add match conditions if applicable""" + self.match_filters = [] + self.match_conditions = [] + only_if_shared = False + if not self.user: + self.user = influxframework.session.user + + if not self.tables: + self.extract_tables() + + meta = influxframework.get_meta(self.doctype) + role_permissions = influxframework.permissions.get_role_permissions(meta, user=self.user) + self.shared = influxframework.share.get_shared(self.doctype, self.user) + + if ( + not meta.istable + and not (role_permissions.get("select") or role_permissions.get("read")) + and not self.flags.ignore_permissions + and not has_any_user_permission_for_doctype(self.doctype, self.user, self.reference_doctype) + ): + only_if_shared = True + if not self.shared: + influxframework.throw(_("No permission to read {0}").format(_(self.doctype)), influxframework.PermissionError) + else: + self.conditions.append(self.get_share_condition()) + + else: + # skip user perm check if owner constraint is required + if requires_owner_constraint(role_permissions): + self.match_conditions.append( + f"`tab{self.doctype}`.`owner` = {influxframework.db.escape(self.user, percent=False)}" + ) + + # add user permission only if role has read perm + elif role_permissions.get("read") or role_permissions.get("select"): + # get user permissions + user_permissions = influxframework.permissions.get_user_permissions(self.user) + self.add_user_permissions(user_permissions) + + if as_condition: + conditions = "" + if self.match_conditions: + # will turn out like ((blog_post in (..) and blogger in (...)) or (blog_category in (...))) + conditions = "((" + ") or (".join(self.match_conditions) + "))" + + doctype_conditions = self.get_permission_query_conditions() + if doctype_conditions: + conditions += (" and " + doctype_conditions) if conditions else doctype_conditions + + # share is an OR condition, if there is a role permission + if not only_if_shared and self.shared and conditions: + conditions = f"({conditions}) or ({self.get_share_condition()})" + + return conditions + + else: + return self.match_filters + + def get_share_condition(self): + return ( + cast_name(f"`tab{self.doctype}`.name") + + f" in ({', '.join(influxframework.db.escape(s, percent=False) for s in self.shared)})" + ) + + def add_user_permissions(self, user_permissions): + meta = influxframework.get_meta(self.doctype) + doctype_link_fields = [] + doctype_link_fields = meta.get_link_fields() + + # append current doctype with fieldname as 'name' as first link field + doctype_link_fields.append( + dict( + options=self.doctype, + fieldname="name", + ) + ) + + match_filters = {} + match_conditions = [] + for df in doctype_link_fields: + if df.get("ignore_user_permissions"): + continue + + user_permission_values = user_permissions.get(df.get("options"), {}) + + if user_permission_values: + docs = [] + if influxframework.get_system_settings("apply_strict_user_permissions"): + condition = "" + else: + empty_value_condition = cast_name( + f"ifnull(`tab{self.doctype}`.`{df.get('fieldname')}`, '')=''" + ) + condition = empty_value_condition + " or " + + for permission in user_permission_values: + if not permission.get("applicable_for"): + docs.append(permission.get("doc")) + + # append docs based on user permission applicable on reference doctype + # this is useful when getting list of docs from a link field + # in this case parent doctype of the link + # will be the reference doctype + + elif df.get("fieldname") == "name" and self.reference_doctype: + if permission.get("applicable_for") == self.reference_doctype: + docs.append(permission.get("doc")) + + elif permission.get("applicable_for") == self.doctype: + docs.append(permission.get("doc")) + + if docs: + values = ", ".join(influxframework.db.escape(doc, percent=False) for doc in docs) + condition += cast_name(f"`tab{self.doctype}`.`{df.get('fieldname')}`") + f" in ({values})" + match_conditions.append(f"({condition})") + match_filters[df.get("options")] = docs + + if match_conditions: + self.match_conditions.append(" and ".join(match_conditions)) + + if match_filters: + self.match_filters.append(match_filters) + + def get_permission_query_conditions(self): + conditions = [] + condition_methods = influxframework.get_hooks("permission_query_conditions", {}).get(self.doctype, []) + if condition_methods: + for method in condition_methods: + c = influxframework.call(influxframework.get_attr(method), self.user) + if c: + conditions.append(c) + + permision_script_name = get_server_script_map().get("permission_query", {}).get(self.doctype) + if permision_script_name: + script = influxframework.get_doc("Server Script", permision_script_name) + condition = script.get_permission_query_conditions(self.user) + if condition: + conditions.append(condition) + + return " and ".join(conditions) if conditions else "" + + def set_order_by(self, args): + meta = influxframework.get_meta(self.doctype) + + if self.order_by and self.order_by != "KEEP_DEFAULT_ORDERING": + args.order_by = self.order_by + else: + args.order_by = "" + + # don't add order by from meta if a mysql group function is used without group by clause + group_function_without_group_by = ( + len(self.fields) == 1 + and ( + self.fields[0].lower().startswith("count(") + or self.fields[0].lower().startswith("min(") + or self.fields[0].lower().startswith("max(") + ) + and not self.group_by + ) + + if not group_function_without_group_by: + sort_field = sort_order = None + if meta.sort_field and "," in meta.sort_field: + # multiple sort given in doctype definition + # Example: + # `idx desc, modified desc` + # will covert to + # `tabItem`.`idx` desc, `tabItem`.`modified` desc + args.order_by = ", ".join( + f"`tab{self.doctype}`.`{f.split()[0].strip()}` {f.split()[1].strip()}" + for f in meta.sort_field.split(",") + ) + else: + sort_field = meta.sort_field or "modified" + sort_order = (meta.sort_field and meta.sort_order) or "desc" + if self.order_by: + args.order_by = f"`tab{self.doctype}`.`{sort_field or 'modified'}` {sort_order or 'desc'}" + + # draft docs always on top + if hasattr(meta, "is_submittable") and meta.is_submittable: + if self.order_by: + args.order_by = f"`tab{self.doctype}`.docstatus asc, {args.order_by}" + + def validate_order_by_and_group_by(self, parameters): + """Check order by, group by so that atleast one column is selected and does not have subquery""" + if not parameters: + return + + _lower = parameters.lower() + if "select" in _lower and "from" in _lower: + influxframework.throw(_("Cannot use sub-query in order by")) + + if ORDER_GROUP_PATTERN.match(_lower): + influxframework.throw(_("Illegal SQL Query")) + + for field in parameters.split(","): + if "." in field and field.strip().startswith("`tab"): + tbl = field.strip().split(".")[0] + if tbl not in self.tables: + if tbl.startswith("`"): + tbl = tbl[4:-1] + influxframework.throw(_("Please select atleast 1 column from {0} to sort/group").format(tbl)) + + def add_limit(self): + if self.limit_page_length: + return f"limit {self.limit_page_length} offset {self.limit_start}" + else: + return "" + + def add_comment_count(self, result): + for r in result: + if not r.name: + continue + + r._comment_count = 0 + if "_comments" in r: + r._comment_count = len(json.loads(r._comments or "[]")) + + def update_user_settings(self): + # update user settings if new search + user_settings = json.loads(get_user_settings(self.doctype)) + + if hasattr(self, "user_settings"): + user_settings.update(self.user_settings) + + if self.save_user_settings_fields: + user_settings["fields"] = self.user_settings_fields + + update_user_settings(self.doctype, user_settings) + + +def cast_name(column: str) -> str: + """Casts name field to varchar for postgres + + Handles majorly 4 cases: + 1. locate + 2. strpos + 3. ifnull + 4. coalesce + + Uses regex substitution. + + Example: + input - "ifnull(`tabBlog Post`.`name`, '')=''" + output - "ifnull(cast(`tabBlog Post`.`name` as varchar), '')=''" """ + + if influxframework.db.db_type == "mariadb": + return column + + kwargs = {"string": column} + if "cast(" not in column.lower() and "::" not in column: + if LOCATE_PATTERN.search(**kwargs): + return LOCATE_CAST_PATTERN.sub(r"locate(\1, cast(\2 as varchar))", **kwargs) + + elif match := FUNC_IFNULL_PATTERN.search(**kwargs): + func = match.groups()[0] + return re.sub(rf"{func}\(\s*([`\"]?name[`\"]?)\s*,", rf"{func}(cast(\1 as varchar),", **kwargs) + + return CAST_VARCHAR_PATTERN.sub(r"cast(\1 as varchar)", **kwargs) + + return column + + +def check_parent_permission(parent, child_doctype): + if parent: + # User may pass fake parent and get the information from the child table + if child_doctype and not ( + influxframework.db.exists("DocField", {"parent": parent, "options": child_doctype}) + or influxframework.db.exists("Custom Field", {"dt": parent, "options": child_doctype}) + ): + raise influxframework.PermissionError + + if influxframework.permissions.has_permission(parent): + return + + # Either parent not passed or the user doesn't have permission on parent doctype of child table! + raise influxframework.PermissionError + + +def get_order_by(doctype, meta): + order_by = "" + + sort_field = sort_order = None + if meta.sort_field and "," in meta.sort_field: + # multiple sort given in doctype definition + # Example: + # `idx desc, modified desc` + # will covert to + # `tabItem`.`idx` desc, `tabItem`.`modified` desc + order_by = ", ".join( + f"`tab{doctype}`.`{f.split()[0].strip()}` {f.split()[1].strip()}" + for f in meta.sort_field.split(",") + ) + + else: + sort_field = meta.sort_field or "modified" + sort_order = (meta.sort_field and meta.sort_order) or "desc" + order_by = f"`tab{doctype}`.`{sort_field or 'modified'}` {sort_order or 'desc'}" + + # draft docs always on top + if meta.is_submittable: + order_by = f"`tab{doctype}`.docstatus asc, {order_by}" + + return order_by + + +def is_parent_only_filter(doctype, filters): + # check if filters contains only parent doctype + only_parent_doctype = True + + if isinstance(filters, list): + for filter in filters: + if doctype not in filter: + only_parent_doctype = False + if "Between" in filter: + filter[3] = get_between_date_filter(flt[3]) + + return only_parent_doctype + + +def has_any_user_permission_for_doctype(doctype, user, applicable_for): + user_permissions = influxframework.permissions.get_user_permissions(user=user) + doctype_user_permissions = user_permissions.get(doctype, []) + + for permission in doctype_user_permissions: + if not permission.applicable_for or permission.applicable_for == applicable_for: + return True + + return False + + +def get_between_date_filter(value, df=None): + """ + return the formattted date as per the given example + [u'2017-11-01', u'2017-11-03'] => '2017-11-01 00:00:00.000000' AND '2017-11-04 00:00:00.000000' + """ + from_date = influxframework.utils.nowdate() + to_date = influxframework.utils.nowdate() + + if value and isinstance(value, (list, tuple)): + if len(value) >= 1: + from_date = value[0] + if len(value) >= 2: + to_date = value[1] + + if not df or (df and df.fieldtype == "Datetime"): + to_date = add_to_date(to_date, days=1) + + if df and df.fieldtype == "Datetime": + data = "'{}' AND '{}'".format( + influxframework.db.format_datetime(from_date), + influxframework.db.format_datetime(to_date), + ) + else: + data = f"'{influxframework.db.format_date(from_date)}' AND '{influxframework.db.format_date(to_date)}'" + + return data + + +def get_additional_filter_field(additional_filters_config, f, value): + additional_filter = additional_filters_config[f.operator.lower()] + f = influxframework._dict(influxframework.get_attr(additional_filter["get_field"])()) + if f.query_value: + for option in f.options: + option = influxframework._dict(option) + if option.value == value: + f.value = option.query_value + return f + + +def get_date_range(operator: str, value: str): + timespan_map = { + "1 week": "week", + "1 month": "month", + "3 months": "quarter", + "6 months": "6 months", + "1 year": "year", + } + period_map = { + "previous": "last", + "next": "next", + } + + if operator != "timespan": + timespan = f"{period_map[operator]} {timespan_map[value]}" + else: + timespan = value + + return get_timespan_date_range(timespan) + + +def requires_owner_constraint(role_permissions): + """Returns True if "select" or "read" isn't available without being creator.""" + + if not role_permissions.get("has_if_owner_enabled"): + return + + if_owner_perms = role_permissions.get("if_owner") + if not if_owner_perms: + return + + # has select or read without if owner, no need for constraint + for perm_type in ("select", "read"): + if role_permissions.get(perm_type) and perm_type not in if_owner_perms: + return + + # not checking if either select or read if present in if_owner_perms + # because either of those is required to perform a query + return True diff --git a/influxframework/model/delete_doc.py b/influxframework/model/delete_doc.py new file mode 100644 index 0000000..e516405 --- /dev/null +++ b/influxframework/model/delete_doc.py @@ -0,0 +1,454 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import os +import shutil + +import influxframework +import influxframework.defaults +import influxframework.model.meta +from influxframework import _, get_module_path +from influxframework.desk.doctype.tag.tag import delete_tags_for_document +from influxframework.model.dynamic_links import get_dynamic_link_map +from influxframework.model.naming import revert_series_if_last +from influxframework.model.utils import is_virtual_doctype +from influxframework.utils.file_manager import remove_all +from influxframework.utils.global_search import delete_for_document +from influxframework.utils.password import delete_all_passwords_for + +doctypes_to_skip = ( + "Communication", + "ToDo", + "DocShare", + "Email Unsubscribe", + "Activity Log", + "File", + "Version", + "Document Follow", + "Comment", + "View Log", + "Tag Link", + "Notification Log", + "Email Queue", + "Document Share Key", + "Integration Request", +) + + +def delete_doc( + doctype=None, + name=None, + force=0, + ignore_doctypes=None, + for_reload=False, + ignore_permissions=False, + flags=None, + ignore_on_trash=False, + ignore_missing=True, + delete_permanently=False, +): + """ + Deletes a doc(dt, dn) and validates if it is not submitted and not linked in a live record + """ + if not ignore_doctypes: + ignore_doctypes = [] + + # get from form + if not doctype: + doctype = influxframework.form_dict.get("dt") + name = influxframework.form_dict.get("dn") + + is_virtual = is_virtual_doctype(doctype) + + names = name + if isinstance(name, str) or isinstance(name, int): + names = [name] + + for name in names or []: + if is_virtual: + influxframework.get_doc(doctype, name).delete() + continue + + # already deleted..? + if not influxframework.db.exists(doctype, name): + if not ignore_missing: + raise influxframework.DoesNotExistError + else: + return False + + # delete passwords + delete_all_passwords_for(doctype, name) + + doc = None + if doctype == "DocType": + if for_reload: + + try: + doc = influxframework.get_doc(doctype, name) + except influxframework.DoesNotExistError: + pass + else: + doc.run_method("before_reload") + + else: + doc = influxframework.get_doc(doctype, name) + + update_flags(doc, flags, ignore_permissions) + check_permission_and_not_submitted(doc) + # delete custom table fields using this doctype. + influxframework.db.delete( + "Custom Field", {"options": name, "fieldtype": ("in", influxframework.model.table_fields)} + ) + influxframework.db.delete("__global_search", {"doctype": name}) + + delete_from_table(doctype, name, ignore_doctypes, None) + + if ( + influxframework.conf.developer_mode + and not doc.custom + and not ( + for_reload or influxframework.flags.in_migrate or influxframework.flags.in_install or influxframework.flags.in_uninstall + ) + ): + try: + delete_controllers(name, doc.module) + except (OSError, KeyError): + # in case a doctype doesnt have any controller code nor any app and module + pass + + else: + doc = influxframework.get_doc(doctype, name) + + if not for_reload: + update_flags(doc, flags, ignore_permissions) + check_permission_and_not_submitted(doc) + + if not ignore_on_trash: + doc.run_method("on_trash") + doc.flags.in_delete = True + doc.run_method("on_change") + + # check if links exist + if not force: + check_if_doc_is_linked(doc) + check_if_doc_is_dynamically_linked(doc) + + update_naming_series(doc) + delete_from_table(doctype, name, ignore_doctypes, doc) + doc.run_method("after_delete") + + # delete attachments + remove_all(doctype, name, from_delete=True, delete_permanently=delete_permanently) + + if not for_reload: + # Enqueued at the end, because it gets committed + # All the linked docs should be checked beforehand + influxframework.enqueue( + "influxframework.model.delete_doc.delete_dynamic_links", + doctype=doc.doctype, + name=doc.name, + now=influxframework.flags.in_test, + ) + + # clear cache for Document + doc.clear_cache() + # delete global search entry + delete_for_document(doc) + # delete tag link entry + delete_tags_for_document(doc) + + if for_reload: + delete_permanently = True + + if not delete_permanently: + add_to_deleted_document(doc) + + if doc and not for_reload: + if not influxframework.flags.in_patch: + try: + doc.notify_update() + insert_feed(doc) + except ImportError: + pass + + +def add_to_deleted_document(doc): + """Add this document to Deleted Document table. Called after delete""" + if doc.doctype != "Deleted Document" and influxframework.flags.in_install != "influxframework": + influxframework.get_doc( + dict( + doctype="Deleted Document", + deleted_doctype=doc.doctype, + deleted_name=doc.name, + data=doc.as_json(), + owner=influxframework.session.user, + ) + ).db_insert() + + +def update_naming_series(doc): + if doc.meta.autoname: + if doc.meta.autoname.startswith("naming_series:") and getattr(doc, "naming_series", None): + revert_series_if_last(doc.naming_series, doc.name, doc) + + elif doc.meta.autoname.split(":")[0] not in ("Prompt", "field", "hash", "autoincrement"): + revert_series_if_last(doc.meta.autoname, doc.name, doc) + + +def delete_from_table(doctype: str, name: str, ignore_doctypes: list[str], doc): + if doctype != "DocType" and doctype == name: + influxframework.db.delete("Singles", {"doctype": name}) + else: + influxframework.db.delete(doctype, {"name": name}) + if doc: + child_doctypes = [ + d.options for d in doc.meta.get_table_fields() if influxframework.get_meta(d.options).is_virtual == 0 + ] + + else: + child_doctypes = influxframework.get_all( + "DocField", + fields="options", + filters={"fieldtype": ["in", influxframework.model.table_fields], "parent": doctype}, + pluck="options", + ) + + child_doctypes_to_delete = set(child_doctypes) - set(ignore_doctypes) + for child_doctype in child_doctypes_to_delete: + influxframework.db.delete(child_doctype, {"parenttype": doctype, "parent": name}) + + +def update_flags(doc, flags=None, ignore_permissions=False): + if ignore_permissions: + if not flags: + flags = {} + flags["ignore_permissions"] = ignore_permissions + + if flags: + doc.flags.update(flags) + + +def check_permission_and_not_submitted(doc): + # permission + if ( + not doc.flags.ignore_permissions + and influxframework.session.user != "Administrator" + and (not doc.has_permission("delete") or (doc.doctype == "DocType" and not doc.custom)) + ): + influxframework.msgprint( + _("User not allowed to delete {0}: {1}").format(doc.doctype, doc.name), + raise_exception=influxframework.PermissionError, + ) + + # check if submitted + if doc.docstatus.is_submitted(): + influxframework.msgprint( + _("{0} {1}: Submitted Record cannot be deleted. You must {2} Cancel {3} it first.").format( + _(doc.doctype), + doc.name, + "", + "", + ), + raise_exception=True, + ) + + +def check_if_doc_is_linked(doc, method="Delete"): + """ + Raises excption if the given doc(dt, dn) is linked in another record. + """ + from influxframework.model.rename_doc import get_link_fields + + link_fields = get_link_fields(doc.doctype) + ignore_linked_doctypes = doc.get("ignore_linked_doctypes") or [] + + for lf in link_fields: + link_dt, link_field, issingle = lf["parent"], lf["fieldname"], lf["issingle"] + + if not issingle: + fields = ["name", "docstatus"] + if influxframework.get_meta(link_dt).istable: + fields.extend(["parent", "parenttype"]) + + for item in influxframework.db.get_values(link_dt, {link_field: doc.name}, fields, as_dict=True): + # available only in child table cases + item_parent = getattr(item, "parent", None) + linked_doctype = item.parenttype if item_parent else link_dt + + if linked_doctype in doctypes_to_skip or ( + linked_doctype in ignore_linked_doctypes and method == "Cancel" + ): + # don't check for communication and todo! + continue + + if method != "Delete" and (method != "Cancel" or item.docstatus != 1): + # don't raise exception if not + # linked to a non-cancelled doc when deleting or to a submitted doc when cancelling + continue + elif link_dt == doc.doctype and (item_parent or item.name) == doc.name: + # don't raise exception if not + # linked to same item or doc having same name as the item + continue + else: + reference_docname = item_parent or item.name + raise_link_exists_exception(doc, linked_doctype, reference_docname) + + else: + if influxframework.db.get_value(link_dt, None, link_field) == doc.name: + raise_link_exists_exception(doc, link_dt, link_dt) + + +def check_if_doc_is_dynamically_linked(doc, method="Delete"): + """Raise `influxframework.LinkExistsError` if the document is dynamically linked""" + for df in get_dynamic_link_map().get(doc.doctype, []): + + ignore_linked_doctypes = doc.get("ignore_linked_doctypes") or [] + + if df.parent in doctypes_to_skip or (df.parent in ignore_linked_doctypes and method == "Cancel"): + # don't check for communication and todo! + continue + + meta = influxframework.get_meta(df.parent) + if meta.issingle: + # dynamic link in single doc + refdoc = influxframework.db.get_singles_dict(df.parent) + if ( + refdoc.get(df.options) == doc.doctype + and refdoc.get(df.fieldname) == doc.name + and ( + (method == "Delete" and refdoc.docstatus < 2) + or (method == "Cancel" and refdoc.docstatus == 1) + ) + ): + # raise exception only if + # linked to an non-cancelled doc when deleting + # or linked to a submitted doc when cancelling + raise_link_exists_exception(doc, df.parent, df.parent) + else: + # dynamic link in table + df["table"] = ", `parent`, `parenttype`, `idx`" if meta.istable else "" + for refdoc in influxframework.db.sql( + """select `name`, `docstatus` {table} from `tab{parent}` where + {options}=%s and {fieldname}=%s""".format( + **df + ), + (doc.doctype, doc.name), + as_dict=True, + ): + + if (method == "Delete" and refdoc.docstatus < 2) or ( + method == "Cancel" and refdoc.docstatus == 1 + ): + # raise exception only if + # linked to an non-cancelled doc when deleting + # or linked to a submitted doc when cancelling + + reference_doctype = refdoc.parenttype if meta.istable else df.parent + reference_docname = refdoc.parent if meta.istable else refdoc.name + at_position = f"at Row: {refdoc.idx}" if meta.istable else "" + + raise_link_exists_exception(doc, reference_doctype, reference_docname, at_position) + + +def raise_link_exists_exception(doc, reference_doctype, reference_docname, row=""): + doc_link = '{1}'.format(doc.doctype, doc.name) + reference_link = '{1}'.format( + reference_doctype, reference_docname + ) + + # hack to display Single doctype only once in message + if reference_doctype == reference_docname: + reference_doctype = "" + + influxframework.throw( + _("Cannot delete or cancel because {0} {1} is linked with {2} {3} {4}").format( + doc.doctype, doc_link, reference_doctype, reference_link, row + ), + influxframework.LinkExistsError, + ) + + +def delete_dynamic_links(doctype, name): + delete_references("ToDo", doctype, name, "reference_type") + delete_references("Email Unsubscribe", doctype, name) + delete_references("DocShare", doctype, name, "share_doctype", "share_name") + delete_references("Version", doctype, name, "ref_doctype", "docname") + delete_references("Comment", doctype, name) + delete_references("View Log", doctype, name) + delete_references("Document Follow", doctype, name, "ref_doctype", "ref_docname") + delete_references("Notification Log", doctype, name, "document_type", "document_name") + + # unlink communications + clear_timeline_references(doctype, name) + clear_references("Communication", doctype, name) + + clear_references("Activity Log", doctype, name) + clear_references("Activity Log", doctype, name, "timeline_doctype", "timeline_name") + + +def delete_references( + doctype, + reference_doctype, + reference_name, + reference_doctype_field="reference_doctype", + reference_name_field="reference_name", +): + influxframework.db.delete( + doctype, {reference_doctype_field: reference_doctype, reference_name_field: reference_name} + ) + + +def clear_references( + doctype, + reference_doctype, + reference_name, + reference_doctype_field="reference_doctype", + reference_name_field="reference_name", +): + influxframework.db.sql( + """update + `tab{0}` + set + {1}=NULL, {2}=NULL + where + {1}=%s and {2}=%s""".format( + doctype, reference_doctype_field, reference_name_field + ), # nosec + (reference_doctype, reference_name), + ) + + +def clear_timeline_references(link_doctype, link_name): + influxframework.db.delete("Communication Link", {"link_doctype": link_doctype, "link_name": link_name}) + + +def insert_feed(doc): + if ( + influxframework.flags.in_install + or influxframework.flags.in_uninstall + or influxframework.flags.in_import + or getattr(doc, "no_feed_on_delete", False) + ): + return + + from influxframework.utils import get_fullname + + influxframework.get_doc( + { + "doctype": "Comment", + "comment_type": "Deleted", + "reference_doctype": doc.doctype, + "subject": f"{_(doc.doctype)} {doc.name}", + "full_name": get_fullname(doc.owner), + } + ).insert(ignore_permissions=True) + + +def delete_controllers(doctype, module): + """ + Delete controller code in the doctype folder + """ + module_path = get_module_path(module) + dir_path = os.path.join(module_path, "doctype", influxframework.scrub(doctype)) + + shutil.rmtree(dir_path) diff --git a/influxframework/model/docfield.py b/influxframework/model/docfield.py new file mode 100644 index 0000000..8c78704 --- /dev/null +++ b/influxframework/model/docfield.py @@ -0,0 +1,63 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +"""docfield utililtes""" + +import influxframework + + +def rename(doctype, fieldname, newname): + """rename docfield""" + df = influxframework.db.sql( + """select * from tabDocField where parent=%s and fieldname=%s""", (doctype, fieldname), as_dict=1 + ) + if not df: + return + + df = df[0] + + if influxframework.db.get_value("DocType", doctype, "issingle"): + update_single(df, newname) + else: + update_table(df, newname) + update_parent_field(df, newname) + + +def update_single(f, new): + """update in tabSingles""" + influxframework.db.begin() + influxframework.db.sql( + """update tabSingles set field=%s where doctype=%s and field=%s""", + (new, f["parent"], f["fieldname"]), + ) + influxframework.db.commit() + + +def update_table(f, new): + """update table""" + query = get_change_column_query(f, new) + if query: + influxframework.db.sql(query) + + +def update_parent_field(f, new): + """update 'parentfield' in tables""" + if f["fieldtype"] in influxframework.model.table_fields: + influxframework.db.begin() + influxframework.db.sql( + """update `tab{}` set parentfield={} where parentfield={}""".format(f["options"], "%s", "%s"), + (new, f["fieldname"]), + ) + influxframework.db.commit() + + +def get_change_column_query(f, new): + """generate change fieldname query""" + desc = influxframework.db.sql("desc `tab%s`" % f["parent"]) + for d in desc: + if d[0] == f["fieldname"]: + return "alter table `tab{}` change `{}` `{}` {}".format(f["parent"], f["fieldname"], new, d[1]) + + +def supports_translation(fieldtype): + return fieldtype in ["Data", "Select", "Text", "Small Text", "Text Editor"] diff --git a/influxframework/model/docstatus.py b/influxframework/model/docstatus.py new file mode 100644 index 0000000..28f35d1 --- /dev/null +++ b/influxframework/model/docstatus.py @@ -0,0 +1,25 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE + + +class DocStatus(int): + def is_draft(self): + return self == self.draft() + + def is_submitted(self): + return self == self.submitted() + + def is_cancelled(self): + return self == self.cancelled() + + @classmethod + def draft(cls): + return cls(0) + + @classmethod + def submitted(cls): + return cls(1) + + @classmethod + def cancelled(cls): + return cls(2) diff --git a/influxframework/model/document.py b/influxframework/model/document.py new file mode 100644 index 0000000..e06f963 --- /dev/null +++ b/influxframework/model/document.py @@ -0,0 +1,1597 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import hashlib +import json +import time + +from werkzeug.exceptions import NotFound + +import influxframework +from influxframework import _, is_whitelisted, msgprint +from influxframework.core.doctype.server_script.server_script_utils import run_server_script_for_doc_event +from influxframework.desk.form.document_follow import follow_document +from influxframework.integrations.doctype.webhook import run_webhooks +from influxframework.model import optional_fields, table_fields +from influxframework.model.base_document import BaseDocument, get_controller +from influxframework.model.docstatus import DocStatus +from influxframework.model.naming import set_new_name, validate_name +from influxframework.model.utils import is_virtual_doctype +from influxframework.model.workflow import set_workflow_state_on_action, validate_workflow +from influxframework.utils import cstr, date_diff, file_lock, flt, get_datetime_str, now +from influxframework.utils.data import get_absolute_url +from influxframework.utils.global_search import update_global_search + + +def get_doc(*args, **kwargs): + """returns a influxframework.model.Document object. + + :param arg1: Document dict or DocType name. + :param arg2: [optional] document name. + :param for_update: [optional] select document for update. + + There are multiple ways to call `get_doc` + + # will fetch the latest user object (with child table) from the database + user = get_doc("User", "test@example.com") + + # create a new object + user = get_doc({ + "doctype":"User" + "email_id": "test@example.com", + "roles: [ + {"role": "System Manager"} + ] + }) + + # create new object with keyword arguments + user = get_doc(doctype='User', email_id='test@example.com') + + # select a document for update + user = get_doc("User", "test@example.com", for_update=True) + """ + if args: + if isinstance(args[0], BaseDocument): + # already a document + return args[0] + elif isinstance(args[0], str): + doctype = args[0] + + elif isinstance(args[0], dict): + # passed a dict + kwargs = args[0] + + else: + raise ValueError("First non keyword argument must be a string or dict") + + if len(args) < 2 and kwargs: + if "doctype" in kwargs: + doctype = kwargs["doctype"] + else: + raise ValueError('"doctype" is a required key') + + controller = get_controller(doctype) + if controller: + return controller(*args, **kwargs) + + raise ImportError(doctype) + + +class Document(BaseDocument): + """All controllers inherit from `Document`.""" + + def __init__(self, *args, **kwargs): + """Constructor. + + :param arg1: DocType name as string or document **dict** + :param arg2: Document name, if `arg1` is DocType name. + + If DocType name and document name are passed, the object will load + all values (including child documents) from the database. + """ + self.doctype = None + self.name = None + self.flags = influxframework._dict() + + if args and args[0]: + if isinstance(args[0], str): + # first arugment is doctype + self.doctype = args[0] + + # doctype for singles, string value or filters for other documents + self.name = self.doctype if len(args) == 1 else args[1] + + # for_update is set in flags to avoid changing load_from_db signature + # since it is used in virtual doctypes and inherited in child classes + self.flags.for_update = kwargs.get("for_update") + self.load_from_db() + return + + if isinstance(args[0], dict): + # first argument is a dict + kwargs = args[0] + + if kwargs: + # init base document + super().__init__(kwargs) + self.init_child_tables() + self.init_valid_columns() + + else: + # incorrect arguments. let's not proceed. + raise ValueError("Illegal arguments") + + @staticmethod + def whitelist(fn): + """Decorator: Whitelist method to be called remotely via REST API.""" + influxframework.whitelist()(fn) + return fn + + def load_from_db(self): + """Load document and children from database and create properties + from fields""" + self.flags.ignore_children = True + if not getattr(self, "_metaclass", False) and self.meta.issingle: + single_doc = influxframework.db.get_singles_dict(self.doctype, for_update=self.flags.for_update) + if not single_doc: + single_doc = influxframework.new_doc(self.doctype, as_dict=True) + single_doc["name"] = self.doctype + del single_doc["__islocal"] + + super().__init__(single_doc) + self.init_valid_columns() + self._fix_numeric_types() + + else: + d = influxframework.db.get_value( + self.doctype, self.name, "*", as_dict=1, for_update=self.flags.for_update + ) + if not d: + influxframework.throw( + _("{0} {1} not found").format(_(self.doctype), self.name), influxframework.DoesNotExistError + ) + + super().__init__(d) + self.flags.pop("ignore_children", None) + + for df in self._get_table_fields(): + # Make sure not to query the DB for a child table, if it is a virtual one. + # During influxframework is installed, the property "is_virtual" is not available in tabDocType, so + # we need to filter those cases for the access to influxframework.db.get_value() as it would crash otherwise. + if hasattr(self, "doctype") and not hasattr(self, "module") and is_virtual_doctype(df.options): + self.set(df.fieldname, []) + continue + + children = ( + influxframework.db.get_values( + df.options, + {"parent": self.name, "parenttype": self.doctype, "parentfield": df.fieldname}, + "*", + as_dict=True, + order_by="idx asc", + ) + or [] + ) + + self.set(df.fieldname, children) + + # sometimes __setup__ can depend on child values, hence calling again at the end + if hasattr(self, "__setup__"): + self.__setup__() + + def reload(self): + """Reload document from database""" + self.load_from_db() + + def get_latest(self): + if not getattr(self, "latest", None): + self.latest = influxframework.get_doc(self.doctype, self.name) + return self.latest + + def check_permission(self, permtype="read", permlevel=None): + """Raise `influxframework.PermissionError` if not permitted""" + if not self.has_permission(permtype): + self.raise_no_permission_to(permlevel or permtype) + + def has_permission(self, permtype="read", verbose=False) -> bool: + """ + Call `influxframework.permissions.has_permission` if `ignore_permissions` flag isn't truthy + + :param permtype: `read`, `write`, `submit`, `cancel`, `delete`, etc. + :param verbose: DEPRECATED, will be removed in a future release. + """ + + if self.flags.ignore_permissions: + return True + + import influxframework.permissions + + return influxframework.permissions.has_permission(self.doctype, permtype, self) + + def raise_no_permission_to(self, perm_type): + """Raise `influxframework.PermissionError`.""" + influxframework.flags.error_message = _("Insufficient Permission for {0}").format(self.doctype) + raise influxframework.PermissionError + + def insert( + self, + ignore_permissions=None, + ignore_links=None, + ignore_if_duplicate=False, + ignore_mandatory=None, + set_name=None, + set_child_names=True, + ) -> "Document": + """Insert the document in the database (as a new document). + This will check for user permissions and execute `before_insert`, + `validate`, `on_update`, `after_insert` methods if they are written. + + :param ignore_permissions: Do not check permissions if True.""" + if self.flags.in_print: + return + + self.flags.notifications_executed = [] + + if ignore_permissions is not None: + self.flags.ignore_permissions = ignore_permissions + + if ignore_links is not None: + self.flags.ignore_links = ignore_links + + if ignore_mandatory is not None: + self.flags.ignore_mandatory = ignore_mandatory + + self.set("__islocal", True) + + self._set_defaults() + self.set_user_and_timestamp() + self.set_docstatus() + self.check_if_latest() + self._validate_links() + self.check_permission("create") + self.run_method("before_insert") + self.set_new_name(set_name=set_name, set_child_names=set_child_names) + self.set_parent_in_children() + self.validate_higher_perm_levels() + + self.flags.in_insert = True + self.run_before_save_methods() + self._validate() + self.set_docstatus() + self.flags.in_insert = False + + # run validate, on update etc. + + # parent + if getattr(self.meta, "issingle", 0): + self.update_single(self.get_valid_dict()) + else: + self.db_insert(ignore_if_duplicate=ignore_if_duplicate) + + # children + for d in self.get_all_children(): + d.db_insert() + + self.run_method("after_insert") + self.flags.in_insert = True + + if self.get("amended_from"): + self.copy_attachments_from_amended_from() + + # flag to prevent creation of event update log for create and update both + # during document creation + self.flags.update_log_for_doc_creation = True + self.run_post_save_methods() + self.flags.in_insert = False + + # delete __islocal + if hasattr(self, "__islocal"): + delattr(self, "__islocal") + + # clear unsaved flag + if hasattr(self, "__unsaved"): + delattr(self, "__unsaved") + + if not ( + influxframework.flags.in_migrate or influxframework.local.flags.in_install or influxframework.flags.in_setup_wizard + ): + if influxframework.get_cached_value("User", influxframework.session.user, "follow_created_documents"): + follow_document(self.doctype, self.name, influxframework.session.user) + return self + + def save(self, *args, **kwargs): + """Wrapper for _save""" + return self._save(*args, **kwargs) + + def _save(self, ignore_permissions=None, ignore_version=None) -> "Document": + """Save the current document in the database in the **DocType**'s table or + `tabSingles` (for single types). + + This will check for user permissions and execute + `validate` before updating, `on_update` after updating triggers. + + :param ignore_permissions: Do not check permissions if True. + :param ignore_version: Do not save version if True.""" + if self.flags.in_print: + return + + self.flags.notifications_executed = [] + + if ignore_permissions is not None: + self.flags.ignore_permissions = ignore_permissions + + self.flags.ignore_version = influxframework.flags.in_test if ignore_version is None else ignore_version + + if self.get("__islocal") or not self.get("name"): + return self.insert() + + self.check_permission("write", "save") + + self.set_user_and_timestamp() + self.set_docstatus() + self.check_if_latest() + self.set_parent_in_children() + self.set_name_in_children() + + self.validate_higher_perm_levels() + self._validate_links() + self.run_before_save_methods() + + if self._action != "cancel": + self._validate() + + if self._action == "update_after_submit": + self.validate_update_after_submit() + + self.set_docstatus() + + # parent + if self.meta.issingle: + self.update_single(self.get_valid_dict()) + else: + self.db_update() + + self.update_children() + self.run_post_save_methods() + + # clear unsaved flag + if hasattr(self, "__unsaved"): + delattr(self, "__unsaved") + + return self + + def copy_attachments_from_amended_from(self): + """Copy attachments from `amended_from`""" + from influxframework.desk.form.load import get_attachments + + # loop through attachments + for attach_item in get_attachments(self.doctype, self.amended_from): + + # save attachments to new doc + _file = influxframework.get_doc( + { + "doctype": "File", + "file_url": attach_item.file_url, + "file_name": attach_item.file_name, + "attached_to_name": self.name, + "attached_to_doctype": self.doctype, + "folder": "Home/Attachments", + } + ) + _file.save() + + def update_children(self): + """update child tables""" + for df in self.meta.get_table_fields(): + self.update_child_table(df.fieldname, df) + + def update_child_table(self, fieldname, df=None): + """sync child table for given fieldname""" + rows = [] + if not df: + df = self.meta.get_field(fieldname) + + for d in self.get(df.fieldname): + d.db_update() + rows.append(d.name) + + if ( + df.options in (self.flags.ignore_children_type or []) + or influxframework.get_meta(df.options).is_virtual == 1 + ): + # do not delete rows for this because of flags + # hack for docperm :( + return + + if rows: + # select rows that do not match the ones in the document + deleted_rows = influxframework.db.sql( + """select name from `tab{}` where parent=%s + and parenttype=%s and parentfield=%s + and name not in ({})""".format( + df.options, ",".join(["%s"] * len(rows)) + ), + [self.name, self.doctype, fieldname] + rows, + ) + if len(deleted_rows) > 0: + # delete rows that do not match the ones in the document + influxframework.db.delete(df.options, {"name": ("in", tuple(row[0] for row in deleted_rows))}) + + else: + # no rows found, delete all rows + influxframework.db.delete( + df.options, {"parent": self.name, "parenttype": self.doctype, "parentfield": fieldname} + ) + + def get_doc_before_save(self): + return getattr(self, "_doc_before_save", None) + + def has_value_changed(self, fieldname): + """Returns true if value is changed before and after saving""" + previous = self.get_doc_before_save() + return previous.get(fieldname) != self.get(fieldname) if previous else True + + def set_new_name(self, force=False, set_name=None, set_child_names=True): + """Calls `influxframework.naming.set_new_name` for parent and child docs.""" + + if self.flags.name_set and not force: + return + + # If autoname has set as Prompt (name) + if self.get("__newname"): + self.name = validate_name(self.doctype, self.get("__newname")) + self.flags.name_set = True + return + + if set_name: + self.name = validate_name(self.doctype, set_name) + else: + set_new_name(self) + + if set_child_names: + # set name for children + for d in self.get_all_children(): + set_new_name(d) + + self.flags.name_set = True + + def get_title(self): + """Get the document title based on title_field or `title` or `name`""" + return self.get(self.meta.get_title_field()) or "" + + def set_title_field(self): + """Set title field based on template""" + + def get_values(): + values = self.as_dict() + # format values + for key, value in values.items(): + if value is None: + values[key] = "" + return values + + if self.meta.get("title_field") == "title": + df = self.meta.get_field(self.meta.title_field) + + if df.options: + self.set(df.fieldname, df.options.format(**get_values())) + elif self.is_new() and not self.get(df.fieldname) and df.default: + # set default title for new transactions (if default) + self.set(df.fieldname, df.default.format(**get_values())) + + def update_single(self, d): + """Updates values for Single type Document in `tabSingles`.""" + influxframework.db.delete("Singles", {"doctype": self.doctype}) + for field, value in d.items(): + if field != "doctype": + influxframework.db.sql( + """insert into `tabSingles` (doctype, field, value) + values (%s, %s, %s)""", + (self.doctype, field, value), + ) + + if self.doctype in influxframework.db.value_cache: + del influxframework.db.value_cache[self.doctype] + + def set_user_and_timestamp(self): + self._original_modified = self.modified + self.modified = now() + self.modified_by = influxframework.session.user + + # We'd probably want the creation and owner to be set via API + # or Data import at some point, that'd have to be handled here + if self.is_new() and not ( + influxframework.flags.in_install or influxframework.flags.in_patch or influxframework.flags.in_migrate + ): + self.creation = self.modified + self.owner = self.modified_by + + for d in self.get_all_children(): + d.modified = self.modified + d.modified_by = self.modified_by + if not d.owner: + d.owner = self.owner + if not d.creation: + d.creation = self.creation + + influxframework.flags.currently_saving.append((self.doctype, self.name)) + + def set_docstatus(self): + if self.docstatus is None: + self.docstatus = DocStatus.draft() + + for d in self.get_all_children(): + d.docstatus = self.docstatus + + def _validate(self): + self._validate_mandatory() + self._validate_data_fields() + self._validate_selects() + self._validate_non_negative() + self._validate_length() + self._validate_code_fields() + self._sync_autoname_field() + self._extract_images_from_text_editor() + self._sanitize_content() + self._save_passwords() + self.validate_workflow() + + for d in self.get_all_children(): + d._validate_data_fields() + d._validate_selects() + d._validate_non_negative() + d._validate_length() + d._validate_code_fields() + d._sync_autoname_field() + d._extract_images_from_text_editor() + d._sanitize_content() + d._save_passwords() + if self.is_new(): + # don't set fields like _assign, _comments for new doc + for fieldname in optional_fields: + self.set(fieldname, None) + else: + self.validate_set_only_once() + + def _validate_non_negative(self): + def get_msg(df): + if self.get("parentfield"): + return "{} {} #{}: {} {}".format( + influxframework.bold(_(self.doctype)), + _("Row"), + self.idx, + _("Value cannot be negative for"), + influxframework.bold(_(df.label)), + ) + else: + return _("Value cannot be negative for {0}: {1}").format( + _(df.parent), influxframework.bold(_(df.label)) + ) + + for df in self.meta.get( + "fields", {"non_negative": ("=", 1), "fieldtype": ("in", ["Int", "Float", "Currency"])} + ): + + if flt(self.get(df.fieldname)) < 0: + msg = get_msg(df) + influxframework.throw(msg, influxframework.NonNegativeError, title=_("Negative Value")) + + def validate_workflow(self): + """Validate if the workflow transition is valid""" + if influxframework.flags.in_install == "influxframework": + return + workflow = self.meta.get_workflow() + if workflow: + validate_workflow(self) + if not self._action == "save": + set_workflow_state_on_action(self, workflow, self._action) + + def validate_set_only_once(self): + """Validate that fields are not changed if not in insert""" + set_only_once_fields = self.meta.get_set_only_once_fields() + + if set_only_once_fields and self._doc_before_save: + # document exists before saving + for field in set_only_once_fields: + fail = False + value = self.get(field.fieldname) + original_value = self._doc_before_save.get(field.fieldname) + + if field.fieldtype in table_fields: + fail = not self.is_child_table_same(field.fieldname) + elif field.fieldtype in ("Date", "Datetime", "Time"): + fail = str(value) != str(original_value) + else: + fail = value != original_value + + if fail: + influxframework.throw( + _("Value cannot be changed for {0}").format( + influxframework.bold(self.meta.get_label(field.fieldname)) + ), + exc=influxframework.CannotChangeConstantError, + ) + + return False + + def is_child_table_same(self, fieldname): + """Validate child table is same as original table before saving""" + value = self.get(fieldname) + original_value = self._doc_before_save.get(fieldname) + same = True + + if len(original_value) != len(value): + same = False + else: + # check all child entries + for i, d in enumerate(original_value): + new_child = value[i].as_dict(convert_dates_to_str=True) + original_child = d.as_dict(convert_dates_to_str=True) + + # all fields must be same other than modified and modified_by + for key in ("modified", "modified_by", "creation"): + del new_child[key] + del original_child[key] + + if original_child != new_child: + same = False + break + + return same + + def apply_fieldlevel_read_permissions(self): + """Remove values the user is not allowed to read (called when loading in desk)""" + + if influxframework.session.user == "Administrator": + return + + has_higher_permlevel = False + + all_fields = self.meta.fields.copy() + for table_field in self.meta.get_table_fields(): + all_fields += influxframework.get_meta(table_field.options).fields or [] + + for df in all_fields: + if df.permlevel > 0: + has_higher_permlevel = True + break + + if not has_higher_permlevel: + return + + has_access_to = self.get_permlevel_access("read") + + for df in self.meta.fields: + if df.permlevel and not df.permlevel in has_access_to: + self.set(df.fieldname, None) + + for table_field in self.meta.get_table_fields(): + for df in influxframework.get_meta(table_field.options).fields or []: + if df.permlevel and not df.permlevel in has_access_to: + for child in self.get(table_field.fieldname) or []: + child.set(df.fieldname, None) + + def validate_higher_perm_levels(self): + """If the user does not have permissions at permlevel > 0, then reset the values to original / default""" + if self.flags.ignore_permissions or influxframework.flags.in_install: + return + + if influxframework.session.user == "Administrator": + return + + has_access_to = self.get_permlevel_access() + high_permlevel_fields = self.meta.get_high_permlevel_fields() + + if high_permlevel_fields: + self.reset_values_if_no_permlevel_access(has_access_to, high_permlevel_fields) + + # If new record then don't reset the values for child table + if self.is_new(): + return + + # check for child tables + for df in self.meta.get_table_fields(): + high_permlevel_fields = influxframework.get_meta(df.options).get_high_permlevel_fields() + if high_permlevel_fields: + for d in self.get(df.fieldname): + d.reset_values_if_no_permlevel_access(has_access_to, high_permlevel_fields) + + def get_permlevel_access(self, permission_type="write"): + if not hasattr(self, "_has_access_to"): + self._has_access_to = {} + + self._has_access_to[permission_type] = [] + roles = influxframework.get_roles() + for perm in self.get_permissions(): + if perm.role in roles and perm.get(permission_type): + if perm.permlevel not in self._has_access_to[permission_type]: + self._has_access_to[permission_type].append(perm.permlevel) + + return self._has_access_to[permission_type] + + def has_permlevel_access_to(self, fieldname, df=None, permission_type="read"): + if not df: + df = self.meta.get_field(fieldname) + + return df.permlevel in self.get_permlevel_access(permission_type) + + def get_permissions(self): + if self.meta.istable: + # use parent permissions + permissions = influxframework.get_meta(self.parenttype).permissions + else: + permissions = self.meta.permissions + + return permissions + + def _set_defaults(self): + if influxframework.flags.in_import: + return + + new_doc = influxframework.new_doc(self.doctype, as_dict=True) + self.update_if_missing(new_doc) + + # children + for df in self.meta.get_table_fields(): + new_doc = influxframework.new_doc(df.options, as_dict=True) + value = self.get(df.fieldname) + if isinstance(value, list): + for d in value: + d.update_if_missing(new_doc) + + def check_if_latest(self): + """Checks if `modified` timestamp provided by document being updated is same as the + `modified` timestamp in the database. If there is a different, the document has been + updated in the database after the current copy was read. Will throw an error if + timestamps don't match. + + Will also validate document transitions (Save > Submit > Cancel) calling + `self.check_docstatus_transition`.""" + conflict = False + self._action = "save" + if not self.get("__islocal") and not self.meta.get("is_virtual"): + if self.meta.issingle: + modified = influxframework.db.sql( + """select value from tabSingles + where doctype=%s and field='modified' for update""", + self.doctype, + ) + modified = modified and modified[0][0] + if modified and modified != cstr(self._original_modified): + conflict = True + else: + tmp = influxframework.db.sql( + """select modified, docstatus from `tab{}` + where name = %s for update""".format( + self.doctype + ), + self.name, + as_dict=True, + ) + + if not tmp: + influxframework.throw(_("Record does not exist")) + else: + tmp = tmp[0] + + modified = cstr(tmp.modified) + + if modified and modified != cstr(self._original_modified): + conflict = True + + self.check_docstatus_transition(tmp.docstatus) + + if conflict: + influxframework.msgprint( + _("Error: Document has been modified after you have opened it") + + (f" ({modified}, {self.modified}). ") + + _("Please refresh to get the latest document."), + raise_exception=influxframework.TimestampMismatchError, + ) + else: + self.check_docstatus_transition(0) + + def check_docstatus_transition(self, to_docstatus): + """Ensures valid `docstatus` transition. + Valid transitions are (number in brackets is `docstatus`): + + - Save (0) > Save (0) + - Save (0) > Submit (1) + - Submit (1) > Submit (1) + - Submit (1) > Cancel (2) + + """ + if not self.docstatus: + self.docstatus = DocStatus.draft() + + if to_docstatus == DocStatus.draft(): + if self.docstatus.is_draft(): + self._action = "save" + elif self.docstatus.is_submitted(): + self._action = "submit" + self.check_permission("submit") + elif self.docstatus.is_cancelled(): + raise influxframework.DocstatusTransitionError( + _("Cannot change docstatus from 0 (Draft) to 2 (Cancelled)") + ) + else: + raise influxframework.ValidationError(_("Invalid docstatus"), self.docstatus) + + elif to_docstatus == DocStatus.submitted(): + if self.docstatus.is_submitted(): + self._action = "update_after_submit" + self.check_permission("submit") + elif self.docstatus.is_cancelled(): + self._action = "cancel" + self.check_permission("cancel") + elif self.docstatus.is_draft(): + raise influxframework.DocstatusTransitionError( + _("Cannot change docstatus from 1 (Submitted) to 0 (Draft)") + ) + else: + raise influxframework.ValidationError(_("Invalid docstatus"), self.docstatus) + + elif to_docstatus == DocStatus.cancelled(): + raise influxframework.ValidationError(_("Cannot edit cancelled document")) + + def set_parent_in_children(self): + """Updates `parent` and `parenttype` property in all children.""" + for d in self.get_all_children(): + d.parent = self.name + d.parenttype = self.doctype + + def set_name_in_children(self): + # Set name for any new children + for d in self.get_all_children(): + if not d.name: + set_new_name(d) + + def validate_update_after_submit(self): + if self.flags.ignore_validate_update_after_submit: + return + + self._validate_update_after_submit() + for d in self.get_all_children(): + if d.is_new() and self.meta.get_field(d.parentfield).allow_on_submit: + # in case of a new row, don't validate allow on submit, if table is allow on submit + continue + + d._validate_update_after_submit() + + # TODO check only allowed values are updated + + def _validate_mandatory(self): + if self.flags.ignore_mandatory: + return + + missing = self._get_missing_mandatory_fields() + for d in self.get_all_children(): + missing.extend(d._get_missing_mandatory_fields()) + + if not missing: + return + + for fieldname, msg in missing: + msgprint(msg) + + if influxframework.flags.print_messages: + print(self.as_json().encode("utf-8")) + + raise influxframework.MandatoryError( + "[{doctype}, {name}]: {fields}".format( + fields=", ".join(each[0] for each in missing), doctype=self.doctype, name=self.name + ) + ) + + def _validate_links(self): + if self.flags.ignore_links or self._action == "cancel": + return + + invalid_links, cancelled_links = self.get_invalid_links() + + for d in self.get_all_children(): + result = d.get_invalid_links(is_submittable=self.meta.is_submittable) + invalid_links.extend(result[0]) + cancelled_links.extend(result[1]) + + if invalid_links: + msg = ", ".join(each[2] for each in invalid_links) + influxframework.throw(_("Could not find {0}").format(msg), influxframework.LinkValidationError) + + if cancelled_links: + msg = ", ".join(each[2] for each in cancelled_links) + influxframework.throw(_("Cannot link cancelled document: {0}").format(msg), influxframework.CancelledLinkError) + + def get_all_children(self, parenttype=None) -> list["Document"]: + """Returns all children documents from **Table** type fields in a list.""" + + children = [] + + for df in self.meta.get_table_fields(): + if parenttype and df.options != parenttype: + continue + + if value := self.get(df.fieldname): + children.extend(value) + + return children + + def run_method(self, method, *args, **kwargs): + """run standard triggers, plus those in hooks""" + + def fn(self, *args, **kwargs): + method_object = getattr(self, method, None) + + # Cannot have a field with same name as method + # If method found in __dict__, expect it to be callable + if method in self.__dict__ or callable(method_object): + return method_object(*args, **kwargs) + + fn.__name__ = str(method) + out = Document.hook(fn)(self, *args, **kwargs) + + self.run_notifications(method) + run_webhooks(self, method) + run_server_script_for_doc_event(self, method) + + return out + + def run_trigger(self, method, *args, **kwargs): + return self.run_method(method, *args, **kwargs) + + def run_notifications(self, method): + """Run notifications for this method""" + if ( + (influxframework.flags.in_import and influxframework.flags.mute_emails) + or influxframework.flags.in_patch + or influxframework.flags.in_install + ): + return + + if self.flags.notifications_executed is None: + self.flags.notifications_executed = [] + + from influxframework.email.doctype.notification.notification import evaluate_alert + + if self.flags.notifications is None: + + def _get_notifications(): + """returns enabled notifications for the current doctype""" + + return influxframework.get_all( + "Notification", + fields=["name", "event", "method"], + filters={"enabled": 1, "document_type": self.doctype}, + ) + + self.flags.notifications = influxframework.cache().hget( + "notifications", self.doctype, _get_notifications + ) + + if not self.flags.notifications: + return + + def _evaluate_alert(alert): + if not alert.name in self.flags.notifications_executed: + evaluate_alert(self, alert.name, alert.event) + self.flags.notifications_executed.append(alert.name) + + event_map = { + "on_update": "Save", + "after_insert": "New", + "on_submit": "Submit", + "on_cancel": "Cancel", + } + + if not self.flags.in_insert: + # value change is not applicable in insert + event_map["on_change"] = "Value Change" + + for alert in self.flags.notifications: + event = event_map.get(method, None) + if event and alert.event == event: + _evaluate_alert(alert) + elif alert.event == "Method" and method == alert.method: + _evaluate_alert(alert) + + @whitelist.__func__ + def _submit(self): + """Submit the document. Sets `docstatus` = 1, then saves.""" + self.docstatus = DocStatus.submitted() + return self.save() + + @whitelist.__func__ + def _cancel(self): + """Cancel the document. Sets `docstatus` = 2, then saves.""" + self.docstatus = DocStatus.cancelled() + return self.save() + + @whitelist.__func__ + def _rename( + self, name: str, merge: bool = False, force: bool = False, validate_rename: bool = True + ): + """Rename the document. Triggers influxframework.rename_doc, then reloads.""" + from influxframework.model.rename_doc import rename_doc + + self.name = rename_doc(doc=self, new=name, merge=merge, force=force, validate=validate_rename) + self.reload() + + @whitelist.__func__ + def submit(self): + """Submit the document. Sets `docstatus` = 1, then saves.""" + return self._submit() + + @whitelist.__func__ + def cancel(self): + """Cancel the document. Sets `docstatus` = 2, then saves.""" + return self._cancel() + + @whitelist.__func__ + def rename( + self, name: str, merge: bool = False, force: bool = False, validate_rename: bool = True + ): + """Rename the document to `name`. This transforms the current object.""" + return self._rename(name=name, merge=merge, force=force, validate_rename=validate_rename) + + def delete(self, ignore_permissions=False): + """Delete document.""" + return influxframework.delete_doc( + self.doctype, self.name, ignore_permissions=ignore_permissions, flags=self.flags + ) + + def run_before_save_methods(self): + """Run standard methods before `INSERT` or `UPDATE`. Standard Methods are: + + - `validate`, `before_save` for **Save**. + - `validate`, `before_submit` for **Submit**. + - `before_cancel` for **Cancel** + - `before_update_after_submit` for **Update after Submit** + + Will also update title_field if set""" + + self.load_doc_before_save() + self.reset_seen() + + # before_validate method should be executed before ignoring validations + if self._action in ("save", "submit"): + self.run_method("before_validate") + + if self.flags.ignore_validate: + return + + if self._action == "save": + self.run_method("validate") + self.run_method("before_save") + elif self._action == "submit": + self.run_method("validate") + self.run_method("before_submit") + elif self._action == "cancel": + self.run_method("before_cancel") + elif self._action == "update_after_submit": + self.run_method("before_update_after_submit") + + self.set_title_field() + + def load_doc_before_save(self): + """Save load document from db before saving""" + self._doc_before_save = None + if not self.is_new(): + try: + self._doc_before_save = influxframework.get_doc(self.doctype, self.name) + except influxframework.DoesNotExistError: + self._doc_before_save = None + influxframework.clear_last_message() + + def run_post_save_methods(self): + """Run standard methods after `INSERT` or `UPDATE`. Standard Methods are: + + - `on_update` for **Save**. + - `on_update`, `on_submit` for **Submit**. + - `on_cancel` for **Cancel** + - `update_after_submit` for **Update after Submit**""" + + if self._action == "save": + self.run_method("on_update") + elif self._action == "submit": + self.run_method("on_update") + self.run_method("on_submit") + elif self._action == "cancel": + self.run_method("on_cancel") + self.check_no_back_links_exist() + elif self._action == "update_after_submit": + self.run_method("on_update_after_submit") + + self.clear_cache() + + if self.flags.get("notify_update", True): + self.notify_update() + + update_global_search(self) + + self.save_version() + + self.run_method("on_change") + + if (self.doctype, self.name) in influxframework.flags.currently_saving: + influxframework.flags.currently_saving.remove((self.doctype, self.name)) + + self.latest = None + + def clear_cache(self): + influxframework.clear_document_cache(self.doctype, self.name) + + def reset_seen(self): + """Clear _seen property and set current user as seen""" + if getattr(self.meta, "track_seen", False): + influxframework.db.set_value( + self.doctype, self.name, "_seen", json.dumps([influxframework.session.user]), update_modified=False + ) + + def notify_update(self): + """Publish realtime that the current document is modified""" + if influxframework.flags.in_patch: + return + + influxframework.publish_realtime( + "doc_update", + {"modified": self.modified, "doctype": self.doctype, "name": self.name}, + doctype=self.doctype, + docname=self.name, + after_commit=True, + ) + + if ( + not self.meta.get("read_only") + and not self.meta.get("issingle") + and not self.meta.get("istable") + ): + data = {"doctype": self.doctype, "name": self.name, "user": influxframework.session.user} + influxframework.publish_realtime("list_update", data, after_commit=True) + + def db_set(self, fieldname, value=None, update_modified=True, notify=False, commit=False): + """Set a value in the document object, update the timestamp and update the database. + + WARNING: This method does not trigger controller validations and should + be used very carefully. + + :param fieldname: fieldname of the property to be updated, or a {"field":"value"} dictionary + :param value: value of the property to be updated + :param update_modified: default True. updates the `modified` and `modified_by` properties + :param notify: default False. run doc.notify_update() to send updates via socketio + :param commit: default False. run influxframework.db.commit() + """ + if isinstance(fieldname, dict): + self.update(fieldname) + else: + self.set(fieldname, value) + + if update_modified and (self.doctype, self.name) not in influxframework.flags.currently_saving: + # don't update modified timestamp if called from post save methods + # like on_update or on_submit + self.set("modified", now()) + self.set("modified_by", influxframework.session.user) + + # load but do not reload doc_before_save because before_change or on_change might expect it + if not self.get_doc_before_save(): + self.load_doc_before_save() + + # to trigger notification on value change + self.run_method("before_change") + + if self.name is None: + return + + influxframework.db.set_value( + self.doctype, + self.name, + fieldname, + value, + self.modified, + self.modified_by, + update_modified=update_modified, + ) + + self.run_method("on_change") + + if notify: + self.notify_update() + + self.clear_cache() + if commit: + influxframework.db.commit() + + def db_get(self, fieldname): + """get database value for this fieldname""" + return influxframework.db.get_value(self.doctype, self.name, fieldname) + + def check_no_back_links_exist(self): + """Check if document links to any active document before Cancel.""" + from influxframework.model.delete_doc import check_if_doc_is_dynamically_linked, check_if_doc_is_linked + + if not self.flags.ignore_links: + check_if_doc_is_linked(self, method="Cancel") + check_if_doc_is_dynamically_linked(self, method="Cancel") + + def save_version(self): + """Save version info""" + + # don't track version under following conditions + if ( + not getattr(self.meta, "track_changes", False) + or self.doctype == "Version" + or self.flags.ignore_version + or influxframework.flags.in_install + or (not self._doc_before_save and influxframework.flags.in_patch) + ): + return + + version = influxframework.new_doc("Version") + + if is_useful_diff := version.update_version_info(self._doc_before_save, self): + version.insert(ignore_permissions=True) + + if not influxframework.flags.in_migrate: + # follow since you made a change? + if influxframework.get_cached_value("User", influxframework.session.user, "follow_created_documents"): + follow_document(self.doctype, self.name, influxframework.session.user) + + @staticmethod + def hook(f): + """Decorator: Make method `hookable` (i.e. extensible by another app). + + Note: If each hooked method returns a value (dict), then all returns are + collated in one dict and returned. Ideally, don't return values in hookable + methods, set properties in the document.""" + + def add_to_return_value(self, new_return_value): + if new_return_value is None: + self._return_value = self.get("_return_value") + return + + if isinstance(new_return_value, dict): + if not self.get("_return_value"): + self._return_value = {} + self._return_value.update(new_return_value) + else: + self._return_value = new_return_value + + def compose(fn, *hooks): + def runner(self, method, *args, **kwargs): + add_to_return_value(self, fn(self, *args, **kwargs)) + for f in hooks: + add_to_return_value(self, f(self, method, *args, **kwargs)) + + return self.__dict__.pop("_return_value", None) + + return runner + + def composer(self, *args, **kwargs): + hooks = [] + method = f.__name__ + doc_events = influxframework.get_doc_hooks() + for handler in doc_events.get(self.doctype, {}).get(method, []) + doc_events.get("*", {}).get( + method, [] + ): + hooks.append(influxframework.get_attr(handler)) + + composed = compose(f, *hooks) + return composed(self, method, *args, **kwargs) + + return composer + + def is_whitelisted(self, method_name): + method = getattr(self, method_name, None) + if not method: + raise NotFound(f"Method {method_name} not found") + + is_whitelisted(getattr(method, "__func__", method)) + + def validate_value(self, fieldname, condition, val2, doc=None, raise_exception=None): + """Check that value of fieldname should be 'condition' val2 + else throw Exception.""" + error_condition_map = { + "in": _("one of"), + "not in": _("none of"), + "^": _("beginning with"), + } + + if not doc: + doc = self + + val1 = doc.get_value(fieldname) + + df = doc.meta.get_field(fieldname) + val2 = doc.cast(val2, df) + + if not influxframework.compare(val1, condition, val2): + label = doc.meta.get_label(fieldname) + condition_str = error_condition_map.get(condition, condition) + if doc.get("parentfield"): + msg = _("Incorrect value in row {0}: {1} must be {2} {3}").format( + doc.idx, label, condition_str, val2 + ) + else: + msg = _("Incorrect value: {0} must be {1} {2}").format(label, condition_str, val2) + + # raise passed exception or True + msgprint(msg, raise_exception=raise_exception or True) + + def validate_table_has_rows(self, parentfield, raise_exception=None): + """Raise exception if Table field is empty.""" + if not (isinstance(self.get(parentfield), list) and len(self.get(parentfield)) > 0): + label = self.meta.get_label(parentfield) + influxframework.throw( + _("Table {0} cannot be empty").format(label), raise_exception or influxframework.EmptyTableError + ) + + def round_floats_in(self, doc, fieldnames=None): + """Round floats for all `Currency`, `Float`, `Percent` fields for the given doc. + + :param doc: Document whose numeric properties are to be rounded. + :param fieldnames: [Optional] List of fields to be rounded.""" + if not fieldnames: + fieldnames = ( + df.fieldname + for df in doc.meta.get("fields", {"fieldtype": ["in", ["Currency", "Float", "Percent"]]}) + ) + + for fieldname in fieldnames: + doc.set(fieldname, flt(doc.get(fieldname), self.precision(fieldname, doc.get("parentfield")))) + + def get_url(self): + """Returns Desk URL for this document.""" + return get_absolute_url(self.doctype, self.name) + + def add_comment( + self, + comment_type="Comment", + text=None, + comment_email=None, + link_doctype=None, + link_name=None, + comment_by=None, + ): + """Add a comment to this document. + + :param comment_type: e.g. `Comment`. See Communication for more info.""" + + out = influxframework.get_doc( + { + "doctype": "Comment", + "comment_type": comment_type, + "comment_email": comment_email or influxframework.session.user, + "comment_by": comment_by, + "reference_doctype": self.doctype, + "reference_name": self.name, + "content": text or comment_type, + "link_doctype": link_doctype, + "link_name": link_name, + } + ).insert(ignore_permissions=True) + return out + + def add_seen(self, user=None): + """add the given/current user to list of users who have seen this document (_seen)""" + if not user: + user = influxframework.session.user + + if self.meta.track_seen and not influxframework.flags.read_only: + _seen = self.get("_seen") or [] + _seen = influxframework.parse_json(_seen) + + if user not in _seen: + _seen.append(user) + influxframework.db.set_value(self.doctype, self.name, "_seen", json.dumps(_seen), update_modified=False) + influxframework.local.flags.commit = True + + def add_viewed(self, user=None): + """add log to communication when a user views a document""" + if not user: + user = influxframework.session.user + + if hasattr(self.meta, "track_views") and self.meta.track_views: + view_log = influxframework.get_doc( + { + "doctype": "View Log", + "viewed_by": influxframework.session.user, + "reference_doctype": self.doctype, + "reference_name": self.name, + } + ) + if influxframework.flags.read_only: + view_log.deferred_insert() + else: + view_log.insert(ignore_permissions=True) + influxframework.local.flags.commit = True + + def log_error(self, title=None, message=None): + """Helper function to create an Error Log""" + return influxframework.log_error( + message=message, title=title, reference_doctype=self.doctype, reference_name=self.name + ) + + def get_signature(self): + """Returns signature (hash) for private URL.""" + return hashlib.sha224(get_datetime_str(self.creation).encode()).hexdigest() + + def get_document_share_key(self, expires_on=None, no_expiry=False): + if no_expiry: + expires_on = None + + existing_key = influxframework.db.exists( + "Document Share Key", + { + "reference_doctype": self.doctype, + "reference_docname": self.name, + "expires_on": expires_on, + }, + ) + if existing_key: + doc = influxframework.get_doc("Document Share Key", existing_key) + else: + doc = influxframework.new_doc("Document Share Key") + doc.reference_doctype = self.doctype + doc.reference_docname = self.name + doc.expires_on = expires_on + doc.flags.no_expiry = no_expiry + doc.insert(ignore_permissions=True) + + return doc.key + + def get_liked_by(self): + liked_by = getattr(self, "_liked_by", None) + if liked_by: + return json.loads(liked_by) + else: + return [] + + def set_onload(self, key, value): + if not self.get("__onload"): + self.set("__onload", influxframework._dict()) + self.get("__onload")[key] = value + + def get_onload(self, key=None): + if not key: + return self.get("__onload", influxframework._dict()) + + return self.get("__onload")[key] + + def queue_action(self, action, **kwargs): + """Run an action in background. If the action has an inner function, + like _submit for submit, it will call that instead""" + # call _submit instead of submit, so you can override submit to call + # run_delayed based on some action + # See: Stock Reconciliation + from influxframework.utils.background_jobs import enqueue + + if hasattr(self, f"_{action}"): + action = f"_{action}" + + try: + self.lock() + except influxframework.DocumentLockedError: + influxframework.throw( + _("This document is currently queued for execution. Please try again"), + title=_("Document Queued"), + ) + + return enqueue( + "influxframework.model.document.execute_action", + __doctype=self.doctype, + __name=self.name, + __action=action, + **kwargs, + ) + + def lock(self, timeout=None): + """Creates a lock file for the given document. If timeout is set, + it will retry every 1 second for acquiring the lock again + + :param timeout: Timeout in seconds, default 0""" + signature = self.get_signature() + if file_lock.lock_exists(signature): + lock_exists = True + if timeout: + for i in range(timeout): + time.sleep(1) + if not file_lock.lock_exists(signature): + lock_exists = False + break + if lock_exists: + raise influxframework.DocumentLockedError + file_lock.create_lock(signature) + influxframework.local.locked_documents.append(self) + + def unlock(self): + """Delete the lock file for this document""" + file_lock.delete_lock(self.get_signature()) + if self in influxframework.local.locked_documents: + influxframework.local.locked_documents.remove(self) + + # validation helpers + def validate_from_to_dates(self, from_date_field, to_date_field): + """ + Generic validation to verify date sequence + """ + if date_diff(self.get(to_date_field), self.get(from_date_field)) < 0: + influxframework.throw( + _("{0} must be after {1}").format( + influxframework.bold(self.meta.get_label(to_date_field)), + influxframework.bold(self.meta.get_label(from_date_field)), + ), + influxframework.exceptions.InvalidDates, + ) + + def get_assigned_users(self): + assigned_users = influxframework.get_all( + "ToDo", + fields=["allocated_to"], + filters={ + "reference_type": self.doctype, + "reference_name": self.name, + "status": ("!=", "Cancelled"), + }, + pluck="allocated_to", + ) + + users = set(assigned_users) + return users + + def add_tag(self, tag): + """Add a Tag to this document""" + from influxframework.desk.doctype.tag.tag import DocTags + + DocTags(self.doctype).add(self.name, tag) + + def get_tags(self): + """Return a list of Tags attached to this document""" + from influxframework.desk.doctype.tag.tag import DocTags + + return DocTags(self.doctype).get_tags(self.name).split(",")[1:] + + def deferred_insert(self) -> None: + """Push the document to redis temporarily and insert later. + + WARN: This doesn't guarantee insertion as redis can be restarted + before data is flushed to database. + """ + + from influxframework.deferred_insert import deferred_insert + + self.set_user_and_timestamp() + + doc = self.get_valid_dict(convert_dates_to_str=True, ignore_virtual=True) + deferred_insert(doctype=self.doctype, records=doc) + + def __repr__(self): + name = self.name or "unsaved" + doctype = self.__class__.__name__ + + docstatus = f" docstatus={self.docstatus}" if self.docstatus else "" + parent = f" parent={self.parent}" if getattr(self, "parent", None) else "" + + return f"<{doctype}: {name}{docstatus}{parent}>" + + def __str__(self): + name = self.name or "unsaved" + doctype = self.__class__.__name__ + + return f"{doctype}({name})" + + +def execute_action(__doctype, __name, __action, **kwargs): + """Execute an action on a document (called by background worker)""" + doc = influxframework.get_doc(__doctype, __name) + doc.unlock() + try: + getattr(doc, __action)(**kwargs) + except Exception: + influxframework.db.rollback() + + # add a comment (?) + if influxframework.local.message_log: + msg = json.loads(influxframework.local.message_log[-1]).get("message") + else: + msg = "
      " + influxframework.get_traceback() + "
      " + + doc.add_comment("Comment", _("Action Failed") + "

      " + msg) + doc.notify_update() diff --git a/influxframework/model/dynamic_links.py b/influxframework/model/dynamic_links.py new file mode 100644 index 0000000..bb564b6 --- /dev/null +++ b/influxframework/model/dynamic_links.py @@ -0,0 +1,61 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework + +# select doctypes that are accessed by the user (not read_only) first, so that the +# the validation message shows the user-facing doctype first. +# For example Journal Entry should be validated before GL Entry (which is an internal doctype) + +dynamic_link_queries = [ + """select `tabDocField`.parent, + `tabDocType`.read_only, `tabDocType`.in_create, + `tabDocField`.fieldname, `tabDocField`.options + from `tabDocField`, `tabDocType` + where `tabDocField`.fieldtype='Dynamic Link' and + `tabDocType`.`name`=`tabDocField`.parent + order by `tabDocType`.read_only, `tabDocType`.in_create""", + """select `tabCustom Field`.dt as parent, + `tabDocType`.read_only, `tabDocType`.in_create, + `tabCustom Field`.fieldname, `tabCustom Field`.options + from `tabCustom Field`, `tabDocType` + where `tabCustom Field`.fieldtype='Dynamic Link' and + `tabDocType`.`name`=`tabCustom Field`.dt + order by `tabDocType`.read_only, `tabDocType`.in_create""", +] + + +def get_dynamic_link_map(for_delete=False): + """Build a map of all dynamically linked tables. For example, + if Note is dynamically linked to ToDo, the function will return + `{"Note": ["ToDo"], "Sales Invoice": ["Journal Entry Detail"]}` + + Note: Will not map single doctypes + """ + if getattr(influxframework.local, "dynamic_link_map", None) is None or influxframework.flags.in_test: + # Build from scratch + dynamic_link_map = {} + for df in get_dynamic_links(): + meta = influxframework.get_meta(df.parent) + if meta.issingle: + # always check in Single DocTypes + dynamic_link_map.setdefault(meta.name, []).append(df) + else: + try: + links = influxframework.db.sql_list("""select distinct {options} from `tab{parent}`""".format(**df)) + for doctype in links: + dynamic_link_map.setdefault(doctype, []).append(df) + except influxframework.db.TableMissingError: # noqa: E722 + pass + + influxframework.local.dynamic_link_map = dynamic_link_map + return influxframework.local.dynamic_link_map + + +def get_dynamic_links(): + """Return list of dynamic link fields as DocField. + Uses cache if possible""" + df = [] + for query in dynamic_link_queries: + df += influxframework.db.sql(query, as_dict=True) + return df diff --git a/influxframework/model/mapper.py b/influxframework/model/mapper.py new file mode 100644 index 0000000..0106456 --- /dev/null +++ b/influxframework/model/mapper.py @@ -0,0 +1,264 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import json + +import influxframework +from influxframework import _ +from influxframework.model import child_table_fields, default_fields, table_fields +from influxframework.utils import cstr + + +@influxframework.whitelist() +def make_mapped_doc(method, source_name, selected_children=None, args=None): + """Returns the mapped document calling the given mapper method. + Sets selected_children as flags for the `get_mapped_doc` method. + + Called from `open_mapped_doc` from create_new.js""" + + for hook in influxframework.get_hooks("override_whitelisted_methods", {}).get(method, []): + # override using the first hook + method = hook + break + + method = influxframework.get_attr(method) + + if method not in influxframework.whitelisted: + raise influxframework.PermissionError + + if selected_children: + selected_children = json.loads(selected_children) + + if args: + influxframework.flags.args = influxframework._dict(json.loads(args)) + + influxframework.flags.selected_children = selected_children or None + + return method(source_name) + + +@influxframework.whitelist() +def map_docs(method, source_names, target_doc, args=None): + '''Returns the mapped document calling the given mapper method + with each of the given source docs on the target doc + + :param args: Args as string to pass to the mapper method + E.g. args: "{ 'supplier': 'XYZ' }"''' + + method = influxframework.get_attr(method) + if method not in influxframework.whitelisted: + raise influxframework.PermissionError + + for src in json.loads(source_names): + _args = (src, target_doc, json.loads(args)) if args else (src, target_doc) + target_doc = method(*_args) + return target_doc + + +def get_mapped_doc( + from_doctype, + from_docname, + table_maps, + target_doc=None, + postprocess=None, + ignore_permissions=False, + ignore_child_tables=False, +): + + apply_strict_user_permissions = influxframework.get_system_settings("apply_strict_user_permissions") + + # main + if not target_doc: + target_doc = influxframework.new_doc(table_maps[from_doctype]["doctype"]) + elif isinstance(target_doc, str): + target_doc = influxframework.get_doc(json.loads(target_doc)) + + if ( + not apply_strict_user_permissions + and not ignore_permissions + and not target_doc.has_permission("create") + ): + target_doc.raise_no_permission_to("create") + + source_doc = influxframework.get_doc(from_doctype, from_docname) + + if not ignore_permissions: + if not source_doc.has_permission("read"): + source_doc.raise_no_permission_to("read") + + map_doc(source_doc, target_doc, table_maps[source_doc.doctype]) + + row_exists_for_parentfield = {} + + # children + if not ignore_child_tables: + for df in source_doc.meta.get_table_fields(): + source_child_doctype = df.options + table_map = table_maps.get(source_child_doctype) + + # if table_map isn't explicitly specified check if both source and target have the same fieldname and same table options and both of them don't have no_copy + if not table_map: + target_df = target_doc.meta.get_field(df.fieldname) + if target_df: + target_child_doctype = target_df.options + if ( + target_df + and target_child_doctype == source_child_doctype + and not df.no_copy + and not target_df.no_copy + ): + table_map = {"doctype": target_child_doctype} + + if table_map: + for source_d in source_doc.get(df.fieldname): + if "condition" in table_map: + if not table_map["condition"](source_d): + continue + + # if children are selected (checked from UI) for this table type, + # and this record is not in the selected children, then continue + if ( + influxframework.flags.selected_children + and (df.fieldname in influxframework.flags.selected_children) + and source_d.name not in influxframework.flags.selected_children[df.fieldname] + ): + continue + + target_child_doctype = table_map["doctype"] + target_parentfield = target_doc.get_parentfield_of_doctype(target_child_doctype) + + # does row exist for a parentfield? + if target_parentfield not in row_exists_for_parentfield: + row_exists_for_parentfield[target_parentfield] = ( + True if target_doc.get(target_parentfield) else False + ) + + if table_map.get("add_if_empty") and row_exists_for_parentfield.get(target_parentfield): + continue + + if table_map.get("filter") and table_map.get("filter")(source_d): + continue + + map_child_doc(source_d, target_doc, table_map, source_doc) + + if postprocess: + postprocess(source_doc, target_doc) + + target_doc.set_onload("load_after_mapping", True) + + if ( + apply_strict_user_permissions + and not ignore_permissions + and not target_doc.has_permission("create") + ): + target_doc.raise_no_permission_to("create") + + return target_doc + + +def map_doc(source_doc, target_doc, table_map, source_parent=None): + if table_map.get("validation"): + for key, condition in table_map["validation"].items(): + if condition[0] == "=" and source_doc.get(key) != condition[1]: + influxframework.throw( + _("Cannot map because following condition fails:") + f" {key}={cstr(condition[1])}" + ) + + map_fields(source_doc, target_doc, table_map, source_parent) + + if "postprocess" in table_map: + table_map["postprocess"](source_doc, target_doc, source_parent) + + +def map_fields(source_doc, target_doc, table_map, source_parent): + no_copy_fields = set( + [ + d.fieldname + for d in source_doc.meta.get("fields") + if (d.no_copy == 1 or d.fieldtype in table_fields) + ] + + [ + d.fieldname + for d in target_doc.meta.get("fields") + if (d.no_copy == 1 or d.fieldtype in table_fields) + ] + + list(default_fields) + + list(child_table_fields) + + list(table_map.get("field_no_map", [])) + ) + + for df in target_doc.meta.get("fields"): + if df.fieldname not in no_copy_fields: + # map same fields + val = source_doc.get(df.fieldname) + if val not in (None, ""): + target_doc.set(df.fieldname, val) + + elif df.fieldtype == "Link": + if not target_doc.get(df.fieldname): + # map link fields having options == source doctype + if df.options == source_doc.doctype: + target_doc.set(df.fieldname, source_doc.name) + + elif source_parent and df.options == source_parent.doctype: + target_doc.set(df.fieldname, source_parent.name) + + # map other fields + field_map = table_map.get("field_map") + + if field_map: + if isinstance(field_map, dict): + for source_key, target_key in field_map.items(): + val = source_doc.get(source_key) + if val not in (None, ""): + target_doc.set(target_key, val) + else: + for fmap in field_map: + val = source_doc.get(fmap[0]) + if val not in (None, ""): + target_doc.set(fmap[1], val) + + # map idx + if source_doc.idx: + target_doc.idx = source_doc.idx + + # add fetch + for df in target_doc.meta.get("fields", {"fieldtype": "Link"}): + if target_doc.get(df.fieldname): + map_fetch_fields(target_doc, df, no_copy_fields) + + +def map_fetch_fields(target_doc, df, no_copy_fields): + linked_doc = None + + # options should be like "link_fieldname.fieldname_in_liked_doc" + for fetch_df in target_doc.meta.get("fields", {"fetch_from": f"^{df.fieldname}."}): + if not (fetch_df.fieldtype == "Read Only" or fetch_df.read_only): + continue + + if ( + not target_doc.get(fetch_df.fieldname) or fetch_df.fieldtype == "Read Only" + ) and fetch_df.fieldname not in no_copy_fields: + source_fieldname = fetch_df.fetch_from.split(".")[1] + + if not linked_doc: + try: + linked_doc = influxframework.get_doc(df.options, target_doc.get(df.fieldname)) + except Exception: + return + + val = linked_doc.get(source_fieldname) + + if val not in (None, ""): + target_doc.set(fetch_df.fieldname, val) + + +def map_child_doc(source_d, target_parent, table_map, source_parent=None): + target_child_doctype = table_map["doctype"] + target_parentfield = target_parent.get_parentfield_of_doctype(target_child_doctype) + target_d = influxframework.new_doc(target_child_doctype, target_parent, target_parentfield) + + map_doc(source_d, target_d, table_map, source_parent) + + target_d.idx = None + target_parent.append(target_parentfield, target_d) + return target_d diff --git a/influxframework/model/meta.py b/influxframework/model/meta.py new file mode 100644 index 0000000..d66bb2b --- /dev/null +++ b/influxframework/model/meta.py @@ -0,0 +1,811 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +# metadata + +""" +Load metadata (DocType) class + +Example: + + meta = influxframework.get_meta('User') + if meta.has_field('first_name'): + print("DocType" table has field "first_name") + + +""" +import json +import os +from datetime import datetime + +import click + +import influxframework +from influxframework import _ +from influxframework.model import ( + child_table_fields, + data_fieldtypes, + default_fields, + no_value_fields, + optional_fields, + table_fields, +) +from influxframework.model.base_document import ( + DOCTYPE_TABLE_FIELDS, + TABLE_DOCTYPES_FOR_DOCTYPE, + BaseDocument, +) +from influxframework.model.document import Document +from influxframework.model.workflow import get_workflow_name +from influxframework.modules import load_doctype_module +from influxframework.utils import cast, cint, cstr + +DEFAULT_FIELD_LABELS = { + "name": lambda: _("ID"), + "creation": lambda: _("Created On"), + "docstatus": lambda: _("Document Status"), + "idx": lambda: _("Index"), + "modified": lambda: _("Last Updated On"), + "modified_by": lambda: _("Last Updated By"), + "owner": lambda: _("Created By"), + "_user_tags": lambda: _("Tags"), + "_liked_by": lambda: _("Liked By"), + "_comments": lambda: _("Comments"), + "_assign": lambda: _("Assigned To"), +} + + +def get_meta(doctype, cached=True) -> "Meta": + if not cached: + return Meta(doctype) + + if meta := influxframework.cache().hget("doctype_meta", doctype): + return meta + + meta = Meta(doctype) + influxframework.cache().hset("doctype_meta", doctype, meta) + return meta + + +def load_meta(doctype): + return Meta(doctype) + + +def get_table_columns(doctype): + return influxframework.db.get_table_columns(doctype) + + +def load_doctype_from_file(doctype): + fname = influxframework.scrub(doctype) + with open(influxframework.get_app_path("influxframework", "core", "doctype", fname, fname + ".json")) as f: + txt = json.loads(f.read()) + + for d in txt.get("fields", []): + d["doctype"] = "DocField" + + for d in txt.get("permissions", []): + d["doctype"] = "DocPerm" + + txt["fields"] = [BaseDocument(d) for d in txt["fields"]] + if "permissions" in txt: + txt["permissions"] = [BaseDocument(d) for d in txt["permissions"]] + + return txt + + +class Meta(Document): + _metaclass = True + default_fields = list(default_fields)[1:] + special_doctypes = { + "DocField", + "DocPerm", + "DocType", + "Module Def", + "DocType Action", + "DocType Link", + "DocType State", + } + standard_set_once_fields = [ + influxframework._dict(fieldname="creation", fieldtype="Datetime"), + influxframework._dict(fieldname="owner", fieldtype="Data"), + ] + + def __init__(self, doctype): + # from cache + if isinstance(doctype, dict): + super().__init__(doctype) + self.init_field_map() + return + + if isinstance(doctype, Document): + super().__init__(doctype.as_dict()) + else: + super().__init__("DocType", doctype) + + self.process() + + def load_from_db(self): + try: + super().load_from_db() + except influxframework.DoesNotExistError: + if self.doctype == "DocType" and self.name in self.special_doctypes: + self.__dict__.update(load_doctype_from_file(self.name)) + else: + raise + + def process(self): + # don't process for special doctypes + # prevent's circular dependency + if self.name in self.special_doctypes: + self.init_field_map() + return + + self.add_custom_fields() + self.apply_property_setters() + self.init_field_map() + self.sort_fields() + self.get_valid_columns() + self.set_custom_permissions() + self.add_custom_links_and_actions() + + def as_dict(self, no_nulls=False): + def serialize(doc): + out = {} + for key, value in doc.__dict__.items(): + if isinstance(value, (list, tuple)): + if not value or not isinstance(value[0], BaseDocument): + # non standard list object, skip + continue + + value = [serialize(d) for d in value] + + if (not no_nulls and value is None) or isinstance( + value, (str, int, float, datetime, list, tuple) + ): + out[key] = value + + # set empty lists for unset table fields + for fieldname in TABLE_DOCTYPES_FOR_DOCTYPE.keys(): + if out.get(fieldname) is None: + out[fieldname] = [] + + return out + + return serialize(self) + + def get_link_fields(self): + return self.get("fields", {"fieldtype": "Link", "options": ["!=", "[Select]"]}) + + def get_data_fields(self): + return self.get("fields", {"fieldtype": "Data"}) + + def get_phone_fields(self): + return self.get("fields", {"fieldtype": "Phone"}) + + def get_dynamic_link_fields(self): + if not hasattr(self, "_dynamic_link_fields"): + self._dynamic_link_fields = self.get("fields", {"fieldtype": "Dynamic Link"}) + return self._dynamic_link_fields + + def get_select_fields(self): + return self.get( + "fields", {"fieldtype": "Select", "options": ["not in", ["[Select]", "Loading..."]]} + ) + + def get_image_fields(self): + return self.get("fields", {"fieldtype": "Attach Image"}) + + def get_code_fields(self): + return self.get("fields", {"fieldtype": "Code"}) + + def get_set_only_once_fields(self): + """Return fields with `set_only_once` set""" + if not hasattr(self, "_set_only_once_fields"): + self._set_only_once_fields = self.get("fields", {"set_only_once": 1}) + fieldnames = [d.fieldname for d in self._set_only_once_fields] + + for df in self.standard_set_once_fields: + if df.fieldname not in fieldnames: + self._set_only_once_fields.append(df) + + return self._set_only_once_fields + + def get_table_fields(self): + if not hasattr(self, "_table_fields"): + if self.name != "DocType": + self._table_fields = self.get("fields", {"fieldtype": ["in", table_fields]}) + else: + self._table_fields = DOCTYPE_TABLE_FIELDS + + return self._table_fields + + def get_global_search_fields(self): + """Returns list of fields with `in_global_search` set and `name` if set""" + fields = self.get("fields", {"in_global_search": 1, "fieldtype": ["not in", no_value_fields]}) + if getattr(self, "show_name_in_global_search", None): + fields.append(influxframework._dict(fieldtype="Data", fieldname="name", label="Name")) + + return fields + + def get_valid_columns(self): + if not hasattr(self, "_valid_columns"): + table_exists = influxframework.db.table_exists(self.name) + if self.name in self.special_doctypes and table_exists: + self._valid_columns = get_table_columns(self.name) + else: + self._valid_columns = self.default_fields + [ + df.fieldname for df in self.get("fields") if df.fieldtype in data_fieldtypes + ] + if self.istable: + self._valid_columns += list(child_table_fields) + + return self._valid_columns + + def get_table_field_doctype(self, fieldname): + return TABLE_DOCTYPES_FOR_DOCTYPE.get(fieldname) + + def get_field(self, fieldname): + """Return docfield from meta""" + + return self._fields.get(fieldname) + + def has_field(self, fieldname): + """Returns True if fieldname exists""" + + return fieldname in self._fields + + def get_label(self, fieldname): + """Get label of the given fieldname""" + + if df := self.get_field(fieldname): + return df.label + + if fieldname in DEFAULT_FIELD_LABELS: + return DEFAULT_FIELD_LABELS[fieldname]() + + return _("No Label") + + def get_options(self, fieldname): + return self.get_field(fieldname).options + + def get_link_doctype(self, fieldname): + df = self.get_field(fieldname) + + if df.fieldtype == "Link": + return df.options + + if df.fieldtype == "Dynamic Link": + return self.get_options(df.options) + + def get_search_fields(self): + search_fields = self.search_fields or "name" + search_fields = [d.strip() for d in search_fields.split(",")] + if "name" not in search_fields: + search_fields.append("name") + + return search_fields + + def get_fields_to_fetch(self, link_fieldname=None): + """Returns a list of docfield objects for fields whose values + are to be fetched and updated for a particular link field + + These fields are of type Data, Link, Text, Readonly and their + fetch_from property is set as `link_fieldname`.`source_fieldname`""" + + out = [] + + if not link_fieldname: + link_fields = [df.fieldname for df in self.get_link_fields()] + + for df in self.fields: + if df.fieldtype not in no_value_fields and getattr(df, "fetch_from", None): + if link_fieldname: + if df.fetch_from.startswith(link_fieldname + "."): + out.append(df) + else: + if "." in df.fetch_from: + fieldname = df.fetch_from.split(".", 1)[0] + if fieldname in link_fields: + out.append(df) + + return out + + def get_list_fields(self): + list_fields = ["name"] + [ + d.fieldname for d in self.fields if (d.in_list_view and d.fieldtype in data_fieldtypes) + ] + if self.title_field and self.title_field not in list_fields: + list_fields.append(self.title_field) + return list_fields + + def get_custom_fields(self): + return [d for d in self.fields if d.get("is_custom_field")] + + def get_title_field(self): + """Return the title field of this doctype, + explict via `title_field`, or `title` or `name`""" + title_field = getattr(self, "title_field", None) + if not title_field and self.has_field("title"): + title_field = "title" + if not title_field: + title_field = "name" + + return title_field + + def get_translatable_fields(self): + """Return all fields that are translation enabled""" + return [d.fieldname for d in self.fields if d.translatable] + + def is_translatable(self, fieldname): + """Return true of false given a field""" + + if field := self.get_field(fieldname): + return field.translatable + + def get_workflow(self): + return get_workflow_name(self.name) + + def get_naming_series_options(self) -> list[str]: + """Get list naming series options.""" + + if field := self.get_field("naming_series"): + options = field.options or "" + return options.split("\n") + + return [] + + def add_custom_fields(self): + if not influxframework.db.table_exists("Custom Field"): + return + + custom_fields = influxframework.db.sql( + """ + SELECT * FROM `tabCustom Field` + WHERE dt = %s AND docstatus < 2 + """, + (self.name,), + as_dict=1, + update={"is_custom_field": 1}, + ) + + self.extend("fields", custom_fields) + + def apply_property_setters(self): + """ + Property Setters are set via Customize Form. They override standard properties + of the doctype or its child properties like fields, links etc. This method + applies the customized properties over the standard meta object + """ + if not influxframework.db.table_exists("Property Setter"): + return + + property_setters = influxframework.db.sql( + """select * from `tabProperty Setter` where + doc_type=%s""", + (self.name,), + as_dict=1, + ) + + if not property_setters: + return + + for ps in property_setters: + if ps.doctype_or_field == "DocType": + self.set(ps.property, cast(ps.property_type, ps.value)) + + elif ps.doctype_or_field == "DocField": + for d in self.fields: + if d.fieldname == ps.field_name: + d.set(ps.property, cast(ps.property_type, ps.value)) + break + + elif ps.doctype_or_field == "DocType Link": + for d in self.links: + if d.name == ps.row_name: + d.set(ps.property, cast(ps.property_type, ps.value)) + break + + elif ps.doctype_or_field == "DocType Action": + for d in self.actions: + if d.name == ps.row_name: + d.set(ps.property, cast(ps.property_type, ps.value)) + break + + elif ps.doctype_or_field == "DocType State": + for d in self.states: + if d.name == ps.row_name: + d.set(ps.property, cast(ps.property_type, ps.value)) + break + + def add_custom_links_and_actions(self): + for doctype, fieldname in ( + ("DocType Link", "links"), + ("DocType Action", "actions"), + ("DocType State", "states"), + ): + # ignore_ddl because the `custom` column was added later via a patch + for d in influxframework.get_all( + doctype, fields="*", filters=dict(parent=self.name, custom=1), ignore_ddl=True + ): + self.append(fieldname, d) + + # set the fields in order if specified + # order is saved as `links_order` + order = json.loads(self.get(f"{fieldname}_order") or "[]") + if order: + name_map = {d.name: d for d in self.get(fieldname)} + new_list = [] + for name in order: + if name in name_map: + new_list.append(name_map[name]) + + # add the missing items that have not be added + # maybe these items were added to the standard product + # after the customization was done + for d in self.get(fieldname): + if d not in new_list: + new_list.append(d) + + self.set(fieldname, new_list) + + def init_field_map(self): + self._fields = {field.fieldname: field for field in self.fields} + + def sort_fields(self): + """sort on basis of insert_after""" + custom_fields = sorted(self.get_custom_fields(), key=lambda df: df.idx) + + if custom_fields: + newlist = [] + + # if custom field is at top + # insert_after is false + for c in list(custom_fields): + if not c.insert_after: + newlist.append(c) + custom_fields.pop(custom_fields.index(c)) + + # standard fields + newlist += [df for df in self.get("fields") if not df.get("is_custom_field")] + + newlist_fieldnames = [df.fieldname for df in newlist] + for i in range(2): + for df in list(custom_fields): + if df.insert_after in newlist_fieldnames: + cf = custom_fields.pop(custom_fields.index(df)) + idx = newlist_fieldnames.index(df.insert_after) + newlist.insert(idx + 1, cf) + newlist_fieldnames.insert(idx + 1, cf.fieldname) + + if not custom_fields: + break + + # worst case, add remaining custom fields to last + if custom_fields: + newlist += custom_fields + + # renum idx + for i, f in enumerate(newlist): + f.idx = i + 1 + + self.fields = newlist + + def set_custom_permissions(self): + """Reset `permissions` with Custom DocPerm if exists""" + if influxframework.flags.in_patch or influxframework.flags.in_install: + return + + if not self.istable and self.name not in ("DocType", "DocField", "DocPerm", "Custom DocPerm"): + custom_perms = influxframework.get_all( + "Custom DocPerm", + fields="*", + filters=dict(parent=self.name), + update=dict(doctype="Custom DocPerm"), + ) + if custom_perms: + self.permissions = [Document(d) for d in custom_perms] + + def get_fieldnames_with_value(self, with_field_meta=False): + def is_value_field(docfield): + return not (docfield.get("is_virtual") or docfield.fieldtype in no_value_fields) + + if with_field_meta: + return [df for df in self.fields if is_value_field(df)] + + return [df.fieldname for df in self.fields if is_value_field(df)] + + def get_fields_to_check_permissions(self, user_permission_doctypes): + fields = self.get( + "fields", + { + "fieldtype": "Link", + "parent": self.name, + "ignore_user_permissions": ("!=", 1), + "options": ("in", user_permission_doctypes), + }, + ) + + if self.name in user_permission_doctypes: + fields.append(influxframework._dict({"label": "Name", "fieldname": "name", "options": self.name})) + + return fields + + def get_high_permlevel_fields(self): + """Build list of fields with high perm level and all the higher perm levels defined.""" + if not hasattr(self, "high_permlevel_fields"): + self.high_permlevel_fields = [] + for df in self.fields: + if df.permlevel > 0: + self.high_permlevel_fields.append(df) + + return self.high_permlevel_fields + + def get_permlevel_access(self, permission_type="read", parenttype=None, *, user=None): + has_access_to = [] + roles = influxframework.get_roles(user) + for perm in self.get_permissions(parenttype): + if perm.role in roles and perm.get(permission_type): + if perm.permlevel not in has_access_to: + has_access_to.append(perm.permlevel) + + return has_access_to + + def get_permissions(self, parenttype=None): + if self.istable and parenttype: + # use parent permissions + permissions = influxframework.get_meta(parenttype).permissions + else: + permissions = self.get("permissions", []) + + return permissions + + def get_dashboard_data(self): + """Returns dashboard setup related to this doctype. + + This method will return the `data` property in the `[doctype]_dashboard.py` + file in the doctype's folder, along with any overrides or extensions + implemented in other InfluxFramework applications via hooks. + """ + data = influxframework._dict() + if not self.custom: + try: + module = load_doctype_module(self.name, suffix="_dashboard") + if hasattr(module, "get_data"): + data = influxframework._dict(module.get_data()) + except ImportError: + pass + + self.add_doctype_links(data) + + if not self.custom: + for hook in influxframework.get_hooks("override_doctype_dashboards", {}).get(self.name, []): + data = influxframework._dict(influxframework.get_attr(hook)(data=data)) + + return data + + def add_doctype_links(self, data): + """add `links` child table in standard link dashboard format""" + dashboard_links = [] + + if getattr(self, "links", None): + dashboard_links.extend(self.links) + + if not data.transactions: + # init groups + data.transactions = [] + + if not data.non_standard_fieldnames: + data.non_standard_fieldnames = {} + + if not data.internal_links: + data.internal_links = {} + + for link in dashboard_links: + link.added = False + if link.hidden: + continue + + for group in data.transactions: + group = influxframework._dict(group) + + # For internal links parent doctype will be the key + doctype = link.parent_doctype or link.link_doctype + # group found + if link.group and _(group.label) == _(link.group): + if doctype not in group.get("items"): + group.get("items").append(doctype) + link.added = True + + if not link.added: + # group not found, make a new group + data.transactions.append( + dict(label=link.group, items=[link.parent_doctype or link.link_doctype]) + ) + + if not link.is_child_table: + if link.link_fieldname != data.fieldname: + if data.fieldname: + data.non_standard_fieldnames[link.link_doctype] = link.link_fieldname + else: + data.fieldname = link.link_fieldname + elif link.is_child_table: + if not data.fieldname: + data.fieldname = link.link_fieldname + data.internal_links[link.parent_doctype] = [link.table_fieldname, link.link_fieldname] + + def get_row_template(self): + return self.get_web_template(suffix="_row") + + def get_list_template(self): + return self.get_web_template(suffix="_list") + + def get_web_template(self, suffix=""): + """Returns the relative path of the row template for this doctype""" + module_name = influxframework.scrub(self.module) + doctype = influxframework.scrub(self.name) + template_path = influxframework.get_module_path( + module_name, "doctype", doctype, "templates", doctype + suffix + ".html" + ) + if os.path.exists(template_path): + return "{module_name}/doctype/{doctype_name}/templates/{doctype_name}{suffix}.html".format( + module_name=module_name, doctype_name=doctype, suffix=suffix + ) + return None + + def is_nested_set(self): + return self.has_field("lft") and self.has_field("rgt") + + +####### + + +def is_single(doctype): + try: + return influxframework.db.get_value("DocType", doctype, "issingle") + except IndexError: + raise Exception("Cannot determine whether %s is single" % doctype) + + +def get_parent_dt(dt): + if not influxframework.is_table(dt): + return "" + + return ( + influxframework.db.get_value( + "DocField", + {"fieldtype": ("in", influxframework.model.table_fields), "options": dt}, + "parent", + ) + or "" + ) + + +def set_fieldname(field_id, fieldname): + influxframework.db.set_value("DocField", field_id, "fieldname", fieldname) + + +def get_field_currency(df, doc=None): + """get currency based on DocField options and fieldvalue in doc""" + currency = None + + if not df.get("options"): + return None + + if not doc: + return None + + if not getattr(influxframework.local, "field_currency", None): + influxframework.local.field_currency = influxframework._dict() + + if not ( + influxframework.local.field_currency.get((doc.doctype, doc.name), {}).get(df.fieldname) + or ( + doc.get("parent") + and influxframework.local.field_currency.get((doc.doctype, doc.parent), {}).get(df.fieldname) + ) + ): + + ref_docname = doc.get("parent") or doc.name + + if ":" in cstr(df.get("options")): + split_opts = df.get("options").split(":") + if len(split_opts) == 3 and doc.get(split_opts[1]): + currency = influxframework.get_cached_value(split_opts[0], doc.get(split_opts[1]), split_opts[2]) + else: + currency = doc.get(df.get("options")) + if doc.get("parenttype"): + if currency: + ref_docname = doc.name + else: + if influxframework.get_meta(doc.parenttype).has_field(df.get("options")): + # only get_value if parent has currency field + currency = influxframework.db.get_value(doc.parenttype, doc.parent, df.get("options")) + + if currency: + influxframework.local.field_currency.setdefault((doc.doctype, ref_docname), influxframework._dict()).setdefault( + df.fieldname, currency + ) + + return influxframework.local.field_currency.get((doc.doctype, doc.name), {}).get(df.fieldname) or ( + doc.get("parent") + and influxframework.local.field_currency.get((doc.doctype, doc.parent), {}).get(df.fieldname) + ) + + +def get_field_precision(df, doc=None, currency=None): + """get precision based on DocField options and fieldvalue in doc""" + from influxframework.utils import get_number_format_info + + if df.precision: + precision = cint(df.precision) + + elif df.fieldtype == "Currency": + precision = cint(influxframework.db.get_default("currency_precision")) + if not precision: + number_format = influxframework.db.get_default("number_format") or "#,###.##" + decimal_str, comma_str, precision = get_number_format_info(number_format) + else: + precision = cint(influxframework.db.get_default("float_precision")) or 3 + + return precision + + +def get_default_df(fieldname): + if fieldname in (default_fields + child_table_fields): + if fieldname in ("creation", "modified"): + return influxframework._dict(fieldname=fieldname, fieldtype="Datetime") + + elif fieldname in ("idx", "docstatus"): + return influxframework._dict(fieldname=fieldname, fieldtype="Int") + + return influxframework._dict(fieldname=fieldname, fieldtype="Data") + + +def trim_tables(doctype=None, dry_run=False, quiet=False): + """ + Removes database fields that don't exist in the doctype (json or custom field). This may be needed + as maintenance since removing a field in a DocType doesn't automatically + delete the db field. + """ + UPDATED_TABLES = {} + filters = {"issingle": 0} + if doctype: + filters["name"] = doctype + + for doctype in influxframework.get_all("DocType", filters=filters, pluck="name"): + try: + dropped_columns = trim_table(doctype, dry_run=dry_run) + if dropped_columns: + UPDATED_TABLES[doctype] = dropped_columns + except influxframework.db.TableMissingError: + if quiet: + continue + click.secho(f"Ignoring missing table for DocType: {doctype}", fg="yellow", err=True) + click.secho( + f"Consider removing record in the DocType table for {doctype}", fg="yellow", err=True + ) + except Exception as e: + if quiet: + continue + click.echo(e, err=True) + + return UPDATED_TABLES + + +def trim_table(doctype, dry_run=True): + influxframework.cache().hdel("table_columns", f"tab{doctype}") + ignore_fields = default_fields + optional_fields + child_table_fields + columns = influxframework.db.get_table_columns(doctype) + fields = influxframework.get_meta(doctype, cached=False).get_fieldnames_with_value() + + def is_internal(field): + return field not in ignore_fields and not field.startswith("_") + + columns_to_remove = [f for f in list(set(columns) - set(fields)) if is_internal(f)] + DROPPED_COLUMNS = columns_to_remove[:] + + if columns_to_remove and not dry_run: + columns_to_remove = ", ".join(f"DROP `{c}`" for c in columns_to_remove) + influxframework.db.sql_ddl(f"ALTER TABLE `tab{doctype}` {columns_to_remove}") + + return DROPPED_COLUMNS diff --git a/influxframework/model/naming.py b/influxframework/model/naming.py new file mode 100644 index 0000000..a708443 --- /dev/null +++ b/influxframework/model/naming.py @@ -0,0 +1,559 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import datetime +import re +from typing import TYPE_CHECKING, Callable, Optional + +import influxframework +from influxframework import _ +from influxframework.model import log_types +from influxframework.query_builder import DocType +from influxframework.utils import cint, cstr, now_datetime + +if TYPE_CHECKING: + from influxframework.model.document import Document + from influxframework.model.meta import Meta + + +# NOTE: This is used to keep track of status of sites +# whether `log_types` have autoincremented naming set for the site or not. +autoincremented_site_status_map = {} + +NAMING_SERIES_PATTERN = re.compile(r"^[\w\- \/.#{}]+$", re.UNICODE) +BRACED_PARAMS_PATTERN = re.compile(r"(\{[\w | #]+\})") + + +# Types that can be using in naming series fields +NAMING_SERIES_PART_TYPES = ( + int, + str, + datetime.datetime, + datetime.date, + datetime.time, + datetime.timedelta, +) + + +class InvalidNamingSeriesError(influxframework.ValidationError): + pass + + +class NamingSeries: + __slots__ = ("series",) + + def __init__(self, series: str): + self.series = series + + # Add default number part if missing + if "#" not in self.series: + self.series += ".#####" + + def validate(self): + if "." not in self.series: + influxframework.throw( + _("Invalid naming series {}: dot (.) missing").format(influxframework.bold(self.series)), + exc=InvalidNamingSeriesError, + ) + + if not NAMING_SERIES_PATTERN.match(self.series): + influxframework.throw( + _( + 'Special Characters except "-", "#", ".", "/", "{" and "}" not allowed in naming series', + ), + exc=InvalidNamingSeriesError, + ) + + def generate_next_name(self, doc: "Document") -> str: + self.validate() + parts = self.series.split(".") + return parse_naming_series(parts, doc=doc) + + def get_prefix(self) -> str: + """Naming series stores prefix to maintain a counter in DB. This prefix can be used to update counter or validations. + + e.g. `SINV-.YY.-.####` has prefix of `SINV-22-` in database for year 2022. + """ + + prefix = None + + def fake_counter_backend(partial_series, digits): + nonlocal prefix + prefix = partial_series + return "#" * digits + + # This function evaluates all parts till we hit numerical parts and then + # sends prefix + digits to DB to find next number. + # Instead of reimplementing the whole parsing logic in multiple places we + # can just ask this function to give us the prefix. + parse_naming_series(self.series, number_generator=fake_counter_backend) + + if prefix is None: + influxframework.throw(_("Invalid Naming Series: {}").format(self.series)) + + return prefix + + def get_preview(self, doc=None) -> list[str]: + """Generate preview of naming series without using DB counters""" + generated_names = [] + for count in range(1, 4): + + def fake_counter(_prefix, digits): + # ignore B023: binding `count` is not necessary because + # function is evaluated immediately and it can not be done + # because of function signature requirement + return str(count).zfill(digits) # noqa: B023 + + generated_names.append(parse_naming_series(self.series, doc=doc, number_generator=fake_counter)) + return generated_names + + def update_counter(self, new_count: int) -> None: + """Warning: Incorrectly updating series can result in unusable transactions""" + Series = influxframework.qb.DocType("Series") + prefix = self.get_prefix() + + # Initialize if not present in DB + if influxframework.db.get_value("Series", prefix, "name", order_by="name") is None: + influxframework.qb.into(Series).insert(prefix, 0).columns("name", "current").run() + + ( + influxframework.qb.update(Series).set(Series.current, cint(new_count)).where(Series.name == prefix) + ).run() + + def get_current_value(self) -> int: + prefix = self.get_prefix() + return cint(influxframework.db.get_value("Series", prefix, "current", order_by="name")) + + +def set_new_name(doc): + """ + Sets the `name` property for the document based on various rules. + + 1. If amended doc, set suffix. + 2. If `autoname` method is declared, then call it. + 3. If `autoname` property is set in the DocType (`meta`), then build it using the `autoname` property. + 4. If no rule defined, use hash. + + :param doc: Document to be named. + """ + + doc.run_method("before_naming") + + meta = influxframework.get_meta(doc.doctype) + autoname = meta.autoname or "" + + if autoname.lower() != "prompt" and not influxframework.flags.in_import: + doc.name = None + + if is_autoincremented(doc.doctype, meta): + doc.name = influxframework.db.get_next_sequence_val(doc.doctype) + return + + if getattr(doc, "amended_from", None): + _set_amended_name(doc) + return + + elif getattr(doc.meta, "issingle", False): + doc.name = doc.doctype + + if not doc.name: + set_naming_from_document_naming_rule(doc) + + if not doc.name: + doc.run_method("autoname") + + if not doc.name and autoname: + set_name_from_naming_options(autoname, doc) + + # at this point, we fall back to name generation with the hash option + if not doc.name: + doc.name = make_autoname("hash", doc.doctype) + + doc.name = validate_name(doc.doctype, doc.name, meta.get_field("name_case")) + + +def is_autoincremented(doctype: str, meta: Optional["Meta"] = None) -> bool: + """Checks if the doctype has autoincrement autoname set""" + + if doctype in log_types: + if autoincremented_site_status_map.get(influxframework.local.site) is None: + if ( + influxframework.db.sql( + f"""select data_type FROM information_schema.columns + where column_name = 'name' and table_name = 'tab{doctype}'""" + )[0][0] + == "bigint" + ): + autoincremented_site_status_map[influxframework.local.site] = 1 + return True + else: + autoincremented_site_status_map[influxframework.local.site] = 0 + + elif autoincremented_site_status_map[influxframework.local.site]: + return True + + else: + if not meta: + meta = influxframework.get_meta(doctype) + + if not getattr(meta, "issingle", False) and meta.autoname == "autoincrement": + return True + + return False + + +def set_name_from_naming_options(autoname, doc): + """ + Get a name based on the autoname field option + """ + + _autoname = autoname.lower() + + if _autoname.startswith("field:"): + doc.name = _field_autoname(autoname, doc) + + # if the autoname option is 'field:' and no name was derived, we need to + # notify + if not doc.name: + fieldname = autoname[6:] + influxframework.throw(_("{0} is required").format(doc.meta.get_label(fieldname))) + + elif _autoname.startswith("naming_series:"): + set_name_by_naming_series(doc) + elif _autoname.startswith("prompt"): + _prompt_autoname(autoname, doc) + elif _autoname.startswith("format:"): + doc.name = _format_autoname(autoname, doc) + elif "#" in autoname: + doc.name = make_autoname(autoname, doc=doc) + + +def set_naming_from_document_naming_rule(doc): + """ + Evaluate rules based on "Document Naming Series" doctype + """ + if doc.doctype in log_types: + return + + # ignore_ddl if naming is not yet bootstrapped + for d in influxframework.get_all( + "Document Naming Rule", + dict(document_type=doc.doctype, disabled=0), + order_by="priority desc", + ignore_ddl=True, + ): + influxframework.get_cached_doc("Document Naming Rule", d.name).apply(doc) + if doc.name: + break + + +def set_name_by_naming_series(doc): + """Sets name by the `naming_series` property""" + if not doc.naming_series: + doc.naming_series = get_default_naming_series(doc.doctype) + + if not doc.naming_series: + influxframework.throw(influxframework._("Naming Series mandatory")) + + doc.name = make_autoname(doc.naming_series + ".#####", "", doc) + + +def make_autoname(key="", doctype="", doc=""): + """ + Creates an autoname from the given key: + + **Autoname rules:** + + * The key is separated by '.' + * '####' represents a series. The string before this part becomes the prefix: + Example: ABC.#### creates a series ABC0001, ABC0002 etc + * 'MM' represents the current month + * 'YY' and 'YYYY' represent the current year + + + *Example:* + + * DE/./.YY./.MM./.##### will create a series like + DE/09/01/0001 where 09 is the year, 01 is the month and 0001 is the series + """ + if key == "hash": + return influxframework.generate_hash(doctype, 10) + + series = NamingSeries(key) + return series.generate_next_name(doc) + + +def parse_naming_series( + parts: list[str] | str, + doctype=None, + doc: Optional["Document"] = None, + number_generator: Callable[[str, int], str] | None = None, +) -> str: + + """Parse the naming series and get next name. + + args: + parts: naming series parts (split by `.`) + doc: document to use for series that have parts using fieldnames + number_generator: Use different counter backend other than `tabSeries`. Primarily used for testing. + """ + + name = "" + if isinstance(parts, str): + parts = parts.split(".") + + if not number_generator: + number_generator = getseries + + series_set = False + today = now_datetime() + for e in parts: + if not e: + continue + + part = "" + if e.startswith("#"): + if not series_set: + digits = len(e) + part = number_generator(name, digits) + series_set = True + elif e == "YY": + part = today.strftime("%y") + elif e == "MM": + part = today.strftime("%m") + elif e == "DD": + part = today.strftime("%d") + elif e == "YYYY": + part = today.strftime("%Y") + elif e == "WW": + part = determine_consecutive_week_number(today) + elif e == "timestamp": + part = str(today) + elif e == "FY": + part = influxframework.defaults.get_user_default("fiscal_year") + elif e.startswith("{") and doc: + e = e.replace("{", "").replace("}", "") + part = doc.get(e) + elif doc and doc.get(e): + part = doc.get(e) + else: + part = e + + if isinstance(part, str): + name += part + elif isinstance(part, NAMING_SERIES_PART_TYPES): + name += cstr(part).strip() + + return name + + +def determine_consecutive_week_number(datetime): + """Determines the consecutive calendar week""" + m = datetime.month + # ISO 8601 calandar week + w = datetime.strftime("%V") + # Ensure consecutiveness for the first and last days of a year + if m == 1 and int(w) >= 52: + w = "00" + elif m == 12 and int(w) <= 1: + w = "53" + return w + + +def getseries(key, digits): + # series created ? + # Using influxframework.qb as influxframework.get_values does not allow order_by=None + series = DocType("Series") + current = (influxframework.qb.from_(series).where(series.name == key).for_update().select("current")).run() + + if current and current[0][0] is not None: + current = current[0][0] + # yes, update it + influxframework.db.sql("UPDATE `tabSeries` SET `current` = `current` + 1 WHERE `name`=%s", (key,)) + current = cint(current) + 1 + else: + # no, create it + influxframework.db.sql("INSERT INTO `tabSeries` (`name`, `current`) VALUES (%s, 1)", (key,)) + current = 1 + return ("%0" + str(digits) + "d") % current + + +def revert_series_if_last(key, name, doc=None): + """ + Reverts the series for particular naming series: + * key is naming series - SINV-.YYYY-.#### + * name is actual name - SINV-2021-0001 + + 1. This function split the key into two parts prefix (SINV-YYYY) & hashes (####). + 2. Use prefix to get the current index of that naming series from Series table + 3. Then revert the current index. + + *For custom naming series:* + 1. hash can exist anywhere, if it exist in hashes then it take normal flow. + 2. If hash doesn't exit in hashes, we get the hash from prefix, then update name and prefix accordingly. + + *Example:* + 1. key = SINV-.YYYY.- + * If key doesn't have hash it will add hash at the end + * prefix will be SINV-YYYY based on this will get current index from Series table. + 2. key = SINV-.####.-2021 + * now prefix = SINV-#### and hashes = 2021 (hash doesn't exist) + * will search hash in key then accordingly get prefix = SINV- + 3. key = ####.-2021 + * prefix = #### and hashes = 2021 (hash doesn't exist) + * will search hash in key then accordingly get prefix = "" + """ + if ".#" in key: + prefix, hashes = key.rsplit(".", 1) + if "#" not in hashes: + # get the hash part from the key + hash = re.search("#+", key) + if not hash: + return + name = name.replace(hashes, "") + prefix = prefix.replace(hash.group(), "") + else: + prefix = key + + if "." in prefix: + prefix = parse_naming_series(prefix.split("."), doc=doc) + + count = cint(name.replace(prefix, "")) + series = DocType("Series") + current = ( + influxframework.qb.from_(series).where(series.name == prefix).for_update().select("current") + ).run() + + if current and current[0][0] == count: + influxframework.db.sql("UPDATE `tabSeries` SET `current` = `current` - 1 WHERE `name`=%s", prefix) + + +def get_default_naming_series(doctype: str) -> str | None: + """get default value for `naming_series` property""" + naming_series_options = influxframework.get_meta(doctype).get_naming_series_options() + + # Return first truthy options + # Empty strings are used to avoid populating forms by default + for option in naming_series_options: + if option: + return option + + +def validate_name(doctype: str, name: int | str, case: str | None = None): + + if not name: + influxframework.throw(_("No Name Specified for {0}").format(doctype)) + + if isinstance(name, int): + if is_autoincremented(doctype): + # this will set the sequence value to be the provided name/value and set it to be used + # so that the sequence will start from the next value + influxframework.db.set_next_sequence_val(doctype, name, is_val_used=True) + return name + + influxframework.throw(_("Invalid name type (integer) for varchar name column"), influxframework.NameError) + + if name.startswith("New " + doctype): + influxframework.throw( + _("There were some errors setting the name, please contact the administrator"), influxframework.NameError + ) + if case == "Title Case": + name = name.title() + if case == "UPPER CASE": + name = name.upper() + name = name.strip() + + if not influxframework.get_meta(doctype).get("issingle") and (doctype == name) and (name != "DocType"): + influxframework.throw(_("Name of {0} cannot be {1}").format(doctype, name), influxframework.NameError) + + special_characters = "<>" + if re.findall(f"[{special_characters}]+", name): + message = ", ".join(f"'{c}'" for c in special_characters) + influxframework.throw( + _("Name cannot contain special characters like {0}").format(message), influxframework.NameError + ) + + return name + + +def append_number_if_name_exists(doctype, value, fieldname="name", separator="-", filters=None): + if not filters: + filters = dict() + filters.update({fieldname: value}) + exists = influxframework.db.exists(doctype, filters) + + regex = f"^{re.escape(value)}{separator}\\d+$" + + if exists: + last = influxframework.db.sql( + """SELECT `{fieldname}` FROM `tab{doctype}` + WHERE `{fieldname}` {regex_character} %s + ORDER BY length({fieldname}) DESC, + `{fieldname}` DESC LIMIT 1""".format( + doctype=doctype, fieldname=fieldname, regex_character=influxframework.db.REGEX_CHARACTER + ), + regex, + ) + + if last: + count = str(cint(last[0][0].rsplit(separator, 1)[1]) + 1) + else: + count = "1" + + value = f"{value}{separator}{count}" + + return value + + +def _set_amended_name(doc): + am_id = 1 + am_prefix = doc.amended_from + if influxframework.db.get_value(doc.doctype, doc.amended_from, "amended_from"): + am_id = cint(doc.amended_from.split("-")[-1]) + 1 + am_prefix = "-".join(doc.amended_from.split("-")[:-1]) # except the last hyphen + + doc.name = am_prefix + "-" + str(am_id) + return doc.name + + +def _field_autoname(autoname, doc, skip_slicing=None): + """ + Generate a name using `DocType` field. This is called when the doctype's + `autoname` field starts with 'field:' + """ + fieldname = autoname if skip_slicing else autoname[6:] + name = (cstr(doc.get(fieldname)) or "").strip() + return name + + +def _prompt_autoname(autoname, doc): + """ + Generate a name using Prompt option. This simply means the user will have to set the name manually. + This is called when the doctype's `autoname` field starts with 'prompt'. + """ + # set from __newname in save.py + if not doc.name: + influxframework.throw(_("Please set the document name")) + + +def _format_autoname(autoname, doc): + """ + Generate autoname by replacing all instances of braced params (fields, date params ('DD', 'MM', 'YY'), series) + Independent of remaining string or separators. + + Example pattern: 'format:LOG-{MM}-{fieldname1}-{fieldname2}-{#####}' + """ + + first_colon_index = autoname.find(":") + autoname_value = autoname[first_colon_index + 1 :] + + def get_param_value_for_match(match): + param = match.group() + # trim braces + trimmed_param = param[1:-1] + return parse_naming_series([trimmed_param], doc=doc) + + # Replace braced params with their parsed value + name = BRACED_PARAMS_PATTERN.sub(get_param_value_for_match, autoname_value) + + return name diff --git a/influxframework/model/rename_doc.py b/influxframework/model/rename_doc.py new file mode 100644 index 0000000..0b47ad2 --- /dev/null +++ b/influxframework/model/rename_doc.py @@ -0,0 +1,721 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE +from types import NoneType +from typing import TYPE_CHECKING + +import influxframework +from influxframework import _, bold +from influxframework.model.document import Document +from influxframework.model.dynamic_links import get_dynamic_link_map +from influxframework.model.naming import validate_name +from influxframework.model.utils.user_settings import sync_user_settings, update_user_settings_data +from influxframework.query_builder import Field +from influxframework.utils.data import sbool +from influxframework.utils.password import rename_password +from influxframework.utils.scheduler import is_scheduler_inactive + +if TYPE_CHECKING: + from influxframework.model.meta import Meta + + +@influxframework.whitelist() +def update_document_title( + *, + doctype: str, + docname: str, + title: str | None = None, + name: str | None = None, + merge: bool = False, + enqueue: bool = False, + **kwargs, +) -> str: + """ + Update the name or title of a document. Returns `name` if document was renamed, + `docname` if renaming operation was queued. + + :param doctype: DocType of the document + :param docname: Name of the document + :param title: New Title of the document + :param name: New Name of the document + :param merge: Merge the current Document with the existing one if exists + :param enqueue: Enqueue the rename operation, title is updated in current process + """ + + # to maintain backwards API compatibility + updated_title = kwargs.get("new_title") or title + updated_name = kwargs.get("new_name") or name + + # TODO: omit this after runtime type checking (ref: https://github.com/influxframework/influxframework/pull/14927) + for obj in [docname, updated_title, updated_name]: + if not isinstance(obj, (str, NoneType)): + influxframework.throw(f"{obj=} must be of type str or None") + + # handle bad API usages + merge = sbool(merge) + enqueue = sbool(enqueue) + + doc = influxframework.get_doc(doctype, docname) + doc.check_permission(permtype="write") + + title_field = doc.meta.get_title_field() + + title_updated = ( + updated_title and (title_field != "name") and (updated_title != doc.get(title_field)) + ) + name_updated = updated_name and (updated_name != doc.name) + + if name_updated: + if enqueue and not is_scheduler_inactive(): + current_name = doc.name + + # before_name hook may have DocType specific validations or transformations + transformed_name = doc.run_method("before_rename", current_name, updated_name, merge) + if isinstance(transformed_name, dict): + transformed_name = transformed_name.get("new") + transformed_name = transformed_name or updated_name + + # run rename validations before queueing + # use savepoints to avoid partial renames / commits + validate_rename( + doctype=doctype, + old=current_name, + new=transformed_name, + meta=doc.meta, + merge=merge, + save_point=True, + ) + + doc.queue_action("rename", name=transformed_name, merge=merge) + else: + doc.rename(updated_name, merge=merge) + + if title_updated: + try: + setattr(doc, title_field, updated_title) + doc.save() + influxframework.msgprint(_("Saved"), alert=True, indicator="green") + except Exception as e: + if influxframework.db.is_duplicate_entry(e): + influxframework.throw( + _("{0} {1} already exists").format(doctype, influxframework.bold(docname)), + title=_("Duplicate Name"), + exc=influxframework.DuplicateEntryError, + ) + raise + + return doc.name + + +def rename_doc( + doctype: str | None = None, + old: str | None = None, + new: str = None, + force: bool = False, + merge: bool = False, + ignore_permissions: bool = False, + ignore_if_exists: bool = False, + show_alert: bool = True, + rebuild_search: bool = True, + doc: Document | None = None, + validate: bool = True, +) -> str: + """Rename a doc(dt, old) to doc(dt, new) and update all linked fields of type "Link". + + doc: Document object to be renamed. + new: New name for the record. If None, and doctype is specified, new name may be automatically generated via before_rename hooks. + doctype: DocType of the document. Not required if doc is passed. + old: Current name of the document. Not required if doc is passed. + force: Allow even if document is not allowed to be renamed. + merge: Merge with existing document of new name. + ignore_permissions: Ignore user permissions while renaming. + ignore_if_exists: Don't raise exception if document with new name already exists. This will quietely overwrite the existing document. + show_alert: Display alert if document is renamed successfully. + rebuild_search: Rebuild linked doctype search after renaming. + validate: Validate before renaming. If False, it is assumed that the caller has already validated. + """ + old_usage_style = doctype and old and new + new_usage_style = doc and new + + if not (new_usage_style or old_usage_style): + raise TypeError( + "{doctype, old, new} or {doc, new} are required arguments for influxframework.model.rename_doc" + ) + + old = old or doc.name + doctype = doctype or doc.doctype + force = sbool(force) + merge = sbool(merge) + meta = influxframework.get_meta(doctype) + + if validate: + old_doc = doc or influxframework.get_doc(doctype, old) + out = old_doc.run_method("before_rename", old, new, merge) or {} + new = (out.get("new") or new) if isinstance(out, dict) else (out or new) + new = validate_rename( + doctype=doctype, + old=old, + new=new, + meta=meta, + merge=merge, + force=force, + ignore_permissions=ignore_permissions, + ignore_if_exists=ignore_if_exists, + ) + + if not merge: + rename_parent_and_child(doctype, old, new, meta) + else: + update_assignments(old, new, doctype) + + # update link fields' values + link_fields = get_link_fields(doctype) + update_link_field_values(link_fields, old, new, doctype) + + rename_dynamic_links(doctype, old, new) + + # save the user settings in the db + update_user_settings(old, new, link_fields) + + if doctype == "DocType": + rename_doctype(doctype, old, new) + update_customizations(old, new) + + update_attachments(doctype, old, new) + + rename_versions(doctype, old, new) + + rename_eps_records(doctype, old, new) + + # call after_rename + new_doc = influxframework.get_doc(doctype, new) + + # copy any flags if required + new_doc._local = getattr(old_doc, "_local", None) + + new_doc.run_method("after_rename", old, new, merge) + + if not merge: + rename_password(doctype, old, new) + + if merge: + new_doc.add_comment("Edit", _("merged {0} into {1}").format(influxframework.bold(old), influxframework.bold(new))) + else: + new_doc.add_comment( + "Edit", _("renamed from {0} to {1}").format(influxframework.bold(old), influxframework.bold(new)) + ) + + if merge: + influxframework.delete_doc(doctype, old) + + new_doc.clear_cache() + influxframework.clear_cache() + if rebuild_search: + influxframework.enqueue("influxframework.utils.global_search.rebuild_for_doctype", doctype=doctype) + + if show_alert: + influxframework.msgprint( + _("Document renamed from {0} to {1}").format(bold(old), bold(new)), + alert=True, + indicator="green", + ) + + return new + + +def update_assignments(old: str, new: str, doctype: str) -> None: + old_assignments = influxframework.parse_json(influxframework.db.get_value(doctype, old, "_assign")) or [] + new_assignments = influxframework.parse_json(influxframework.db.get_value(doctype, new, "_assign")) or [] + common_assignments = list(set(old_assignments).intersection(new_assignments)) + + for user in common_assignments: + # delete todos linked to old doc + todos = influxframework.get_all( + "ToDo", + { + "owner": user, + "reference_type": doctype, + "reference_name": old, + }, + ["name", "description"], + ) + + for todo in todos: + influxframework.delete_doc("ToDo", todo.name) + + unique_assignments = list(set(old_assignments + new_assignments)) + influxframework.db.set_value(doctype, new, "_assign", influxframework.as_json(unique_assignments, indent=0)) + + +def update_user_settings(old: str, new: str, link_fields: list[dict]) -> None: + """ + Update the user settings of all the linked doctypes while renaming. + """ + + # store the user settings data from the redis to db + sync_user_settings() + + if not link_fields: + return + + # find the user settings for the linked doctypes + linked_doctypes = {d.parent for d in link_fields if not d.issingle} + UserSettings = influxframework.qb.Table("__UserSettings") + + user_settings_details = ( + influxframework.qb.from_(UserSettings) + .select("user", "doctype", "data") + .where(UserSettings.data.like(old) & UserSettings.doctype.isin(linked_doctypes)) + .run(as_dict=True) + ) + + # create the dict using the doctype name as key and values as list of the user settings + from collections import defaultdict + + user_settings_dict = defaultdict(list) + for user_setting in user_settings_details: + user_settings_dict[user_setting.doctype].append(user_setting) + + # update the name in linked doctype whose user settings exists + for fields in link_fields: + user_settings = user_settings_dict.get(fields.parent) + if user_settings: + for user_setting in user_settings: + update_user_settings_data(user_setting, "value", old, new, "docfield", fields.fieldname) + else: + continue + + +def update_customizations(old: str, new: str) -> None: + influxframework.db.set_value("Custom DocPerm", {"parent": old}, "parent", new, update_modified=False) + + +def update_attachments(doctype: str, old: str, new: str) -> None: + if doctype != "DocType": + File = influxframework.qb.DocType("File") + + influxframework.qb.update(File).set(File.attached_to_name, new).where( + (File.attached_to_name == old) & (File.attached_to_doctype == doctype) + ).run() + + +def rename_versions(doctype: str, old: str, new: str) -> None: + Version = influxframework.qb.DocType("Version") + + influxframework.qb.update(Version).set(Version.docname, new).where( + (Version.docname == old) & (Version.ref_doctype == doctype) + ).run() + + +def rename_eps_records(doctype: str, old: str, new: str) -> None: + EPL = influxframework.qb.DocType("Energy Point Log") + + influxframework.qb.update(EPL).set(EPL.reference_name, new).where( + (EPL.reference_doctype == doctype) & (EPL.reference_name == old) + ).run() + + +def rename_parent_and_child(doctype: str, old: str, new: str, meta: "Meta") -> None: + influxframework.qb.update(doctype).set("name", new).where(Field("name") == old).run() + + update_autoname_field(doctype, new, meta) + update_child_docs(old, new, meta) + + +def update_autoname_field(doctype: str, new: str, meta: "Meta") -> None: + # update the value of the autoname field on rename of the docname + if meta.get("autoname"): + field = meta.get("autoname").split(":") + if field and field[0] == "field": + influxframework.qb.update(doctype).set(field[1], new).where(Field("name") == new).run() + + +def validate_rename( + doctype: str, + old: str, + new: str, + meta: "Meta", + merge: bool, + force: bool = False, + ignore_permissions: bool = False, + ignore_if_exists: bool = False, + save_point=False, +) -> str: + # using for update so that it gets locked and someone else cannot edit it while this rename is going on! + if save_point: + _SAVE_POINT = f"validate_rename_{influxframework.generate_hash(length=8)}" + influxframework.db.savepoint(_SAVE_POINT) + + exists = ( + influxframework.qb.from_(doctype).where(Field("name") == new).for_update().select("name").run(pluck=True) + ) + exists = exists[0] if exists else None + + if not influxframework.db.exists(doctype, old): + influxframework.throw(_("Can't rename {0} to {1} because {0} doesn't exist.").format(old, new)) + + if old == new: + influxframework.throw(_("No changes made because old and new name are the same.").format(old, new)) + + if merge and not exists: + influxframework.throw(_("{0} {1} does not exist, select a new target to merge").format(doctype, new)) + + if exists and exists != new: + # for fixing case, accents + exists = None + + if not merge and exists and not ignore_if_exists: + influxframework.throw(_("Another {0} with name {1} exists, select another name").format(doctype, new)) + + if not ( + ignore_permissions or influxframework.permissions.has_permission(doctype, "write", raise_exception=False) + ): + influxframework.throw(_("You need write permission to rename")) + + if not (force or ignore_permissions) and not meta.allow_rename: + influxframework.throw(_("{0} not allowed to be renamed").format(_(doctype))) + + # validate naming like it's done in doc.py + new = validate_name(doctype, new) + + if save_point: + influxframework.db.rollback(save_point=_SAVE_POINT) + + return new + + +def rename_doctype(doctype: str, old: str, new: str) -> None: + # change options for fieldtype Table, Table MultiSelect and Link + fields_with_options = ("Link",) + influxframework.model.table_fields + + for fieldtype in fields_with_options: + update_options_for_fieldtype(fieldtype, old, new) + + # change options where select options are hardcoded i.e. listed + select_fields = get_select_fields(old, new) + update_link_field_values(select_fields, old, new, doctype) + update_select_field_values(old, new) + + # change parenttype for fieldtype Table + update_parenttype_values(old, new) + + +def update_child_docs(old: str, new: str, meta: "Meta") -> None: + # update "parent" + for df in meta.get_table_fields(): + influxframework.qb.update(df.options).set("parent", new).where(Field("parent") == old).run() + + +def update_link_field_values(link_fields: list[dict], old: str, new: str, doctype: str) -> None: + for field in link_fields: + if field["issingle"]: + try: + single_doc = influxframework.get_doc(field["parent"]) + if single_doc.get(field["fieldname"]) == old: + single_doc.set(field["fieldname"], new) + # update single docs using ORM rather then query + # as single docs also sometimes sets defaults! + single_doc.flags.ignore_mandatory = True + single_doc.save(ignore_permissions=True) + except ImportError: + # fails in patches where the doctype has been renamed + # or no longer exists + pass + else: + parent = field["parent"] + docfield = field["fieldname"] + + # Handles the case where one of the link fields belongs to + # the DocType being renamed. + # Here this field could have the current DocType as its value too. + + # In this case while updating link field value, the field's parent + # or the current DocType table name hasn't been renamed yet, + # so consider it's old name. + if parent == new and doctype == "DocType": + parent = old + + influxframework.db.set_value(parent, {docfield: old}, docfield, new, update_modified=False) + + # update cached link_fields as per new + if doctype == "DocType" and field["parent"] == old: + field["parent"] = new + + +def get_link_fields(doctype: str) -> list[dict]: + # get link fields from tabDocField + if not influxframework.flags.link_fields: + influxframework.flags.link_fields = {} + + if doctype not in influxframework.flags.link_fields: + dt = influxframework.qb.DocType("DocType") + df = influxframework.qb.DocType("DocField") + cf = influxframework.qb.DocType("Custom Field") + ps = influxframework.qb.DocType("Property Setter") + + st_issingle = influxframework.qb.from_(dt).select(dt.issingle).where(dt.name == df.parent).as_("issingle") + standard_fields = ( + influxframework.qb.from_(df) + .select(df.parent, df.fieldname, st_issingle) + .where((df.options == doctype) & (df.fieldtype == "Link")) + .run(as_dict=True) + ) + + cf_issingle = influxframework.qb.from_(dt).select(dt.issingle).where(dt.name == cf.dt).as_("issingle") + custom_fields = ( + influxframework.qb.from_(cf) + .select(cf.dt.as_("parent"), cf.fieldname, cf_issingle) + .where((cf.options == doctype) & (cf.fieldtype == "Link")) + .run(as_dict=True) + ) + + ps_issingle = ( + influxframework.qb.from_(dt).select(dt.issingle).where(dt.name == ps.doc_type).as_("issingle") + ) + property_setter_fields = ( + influxframework.qb.from_(ps) + .select(ps.doc_type.as_("parent"), ps.field_name.as_("fieldname"), ps_issingle) + .where((ps.property == "options") & (ps.value == doctype) & (ps.field_name.notnull())) + .run(as_dict=True) + ) + + influxframework.flags.link_fields[doctype] = standard_fields + custom_fields + property_setter_fields + + return influxframework.flags.link_fields[doctype] + + +def update_options_for_fieldtype(fieldtype: str, old: str, new: str) -> None: + CustomField = influxframework.qb.DocType("Custom Field") + PropertySetter = influxframework.qb.DocType("Property Setter") + + if influxframework.conf.developer_mode: + for name in influxframework.get_all("DocField", filters={"options": old}, pluck="parent"): + doctype = influxframework.get_doc("DocType", name) + save = False + for f in doctype.fields: + if f.options == old: + f.options = new + save = True + if save: + doctype.save() + else: + DocField = influxframework.qb.DocType("DocField") + influxframework.qb.update(DocField).set(DocField.options, new).where( + (DocField.fieldtype == fieldtype) & (DocField.options == old) + ).run() + + influxframework.qb.update(CustomField).set(CustomField.options, new).where( + (CustomField.fieldtype == fieldtype) & (CustomField.options == old) + ).run() + + influxframework.qb.update(PropertySetter).set(PropertySetter.value, new).where( + (PropertySetter.property == "options") & (PropertySetter.value == old) + ).run() + + +def get_select_fields(old: str, new: str) -> list[dict]: + """ + get select type fields where doctype's name is hardcoded as + new line separated list + """ + df = influxframework.qb.DocType("DocField") + dt = influxframework.qb.DocType("DocType") + cf = influxframework.qb.DocType("Custom Field") + ps = influxframework.qb.DocType("Property Setter") + + # get link fields from tabDocField + st_issingle = influxframework.qb.from_(dt).select(dt.issingle).where(dt.name == df.parent).as_("issingle") + standard_fields = ( + influxframework.qb.from_(df) + .select(df.parent, df.fieldname, st_issingle) + .where( + (df.parent != new) + & (df.fieldname != "fieldtype") + & (df.fieldtype == "Select") + & (df.options.like(f"%{old}%")) + ) + .run(as_dict=True) + ) + + # get link fields from tabCustom Field + cf_issingle = influxframework.qb.from_(dt).select(dt.issingle).where(dt.name == cf.dt).as_("issingle") + custom_select_fields = ( + influxframework.qb.from_(cf) + .select(cf.dt.as_("parent"), cf.fieldname, cf_issingle) + .where((cf.dt != new) & (cf.fieldtype == "Select") & (cf.options.like(f"%{old}%"))) + .run(as_dict=True) + ) + + # remove fields whose options have been changed using property setter + ps_issingle = ( + influxframework.qb.from_(dt).select(dt.issingle).where(dt.name == ps.doc_type).as_("issingle") + ) + property_setter_select_fields = ( + influxframework.qb.from_(ps) + .select(ps.doc_type.as_("parent"), ps.field_name.as_("fieldname"), ps_issingle) + .where( + (ps.doc_type != new) + & (ps.property == "options") + & (ps.field_name.notnull()) + & (ps.value.like(f"%{old}%")) + ) + .run(as_dict=True) + ) + + return standard_fields + custom_select_fields + property_setter_select_fields + + +def update_select_field_values(old: str, new: str): + from influxframework.query_builder.functions import Replace + + DocField = influxframework.qb.DocType("DocField") + CustomField = influxframework.qb.DocType("Custom Field") + PropertySetter = influxframework.qb.DocType("Property Setter") + + influxframework.qb.update(DocField).set(DocField.options, Replace(DocField.options, old, new)).where( + (DocField.fieldtype == "Select") + & (DocField.parent != new) + & (DocField.options.like(f"%\n{old}%") | DocField.options.like(f"%{old}\n%")) + ).run() + + influxframework.qb.update(CustomField).set( + CustomField.options, Replace(CustomField.options, old, new) + ).where( + (CustomField.fieldtype == "Select") + & (CustomField.dt != new) + & (CustomField.options.like(f"%\n{old}%") | CustomField.options.like(f"%{old}\n%")) + ).run() + + influxframework.qb.update(PropertySetter).set( + PropertySetter.value, Replace(PropertySetter.value, old, new) + ).where( + (PropertySetter.property == "options") + & (PropertySetter.field_name.notnull()) + & (PropertySetter.doc_type != new) + & (PropertySetter.value.like(f"%\n{old}%") | PropertySetter.value.like(f"%{old}\n%")) + ).run() + + +def update_parenttype_values(old: str, new: str): + child_doctypes = influxframework.get_all( + "DocField", + fields=["options", "fieldname"], + filters={"parent": new, "fieldtype": ["in", influxframework.model.table_fields]}, + ) + + custom_child_doctypes = influxframework.get_all( + "Custom Field", + fields=["options", "fieldname"], + filters={"dt": new, "fieldtype": ["in", influxframework.model.table_fields]}, + ) + + child_doctypes += custom_child_doctypes + fields = [d["fieldname"] for d in child_doctypes] + + property_setter_child_doctypes = influxframework.get_all( + "Property Setter", + filters={"doc_type": new, "property": "options", "field_name": ("in", fields)}, + pluck="value", + ) + + child_doctypes = set(list(d["options"] for d in child_doctypes) + property_setter_child_doctypes) + + for doctype in child_doctypes: + table = influxframework.qb.DocType(doctype) + influxframework.qb.update(table).set(table.parenttype, new).where(table.parenttype == old).run() + + +def rename_dynamic_links(doctype: str, old: str, new: str): + Singles = influxframework.qb.DocType("Singles") + for df in get_dynamic_link_map().get(doctype, []): + # dynamic link in single, just one value to check + if influxframework.get_meta(df.parent).issingle: + refdoc = influxframework.db.get_singles_dict(df.parent) + if refdoc.get(df.options) == doctype and refdoc.get(df.fieldname) == old: + influxframework.qb.update(Singles).set(Singles.value, new).where( + (Singles.field == df.fieldname) & (Singles.doctype == df.parent) & (Singles.value == old) + ).run() + else: + # because the table hasn't been renamed yet! + parent = df.parent if df.parent != new else old + + influxframework.qb.update(parent).set(df.fieldname, new).where( + (Field(df.options) == doctype) & (Field(df.fieldname) == old) + ).run() + + +def bulk_rename( + doctype: str, rows: list[list] | None = None, via_console: bool = False +) -> list[str] | None: + """Bulk rename documents + + :param doctype: DocType to be renamed + :param rows: list of documents as `((oldname, newname, merge(optional)), ..)`""" + if not rows: + influxframework.throw(_("Please select a valid csv file with data")) + + if not via_console: + max_rows = 500 + if len(rows) > max_rows: + influxframework.throw(_("Maximum {0} rows allowed").format(max_rows)) + + rename_log = [] + for row in rows: + # if row has some content + if len(row) > 1 and row[0] and row[1]: + merge = len(row) > 2 and (row[2] == "1" or row[2].lower() == "true") + try: + if rename_doc(doctype, row[0], row[1], merge=merge, rebuild_search=False): + msg = _("Successful: {0} to {1}").format(row[0], row[1]) + influxframework.db.commit() + else: + msg = None + except Exception as e: + msg = _("** Failed: {0} to {1}: {2}").format(row[0], row[1], repr(e)) + influxframework.db.rollback() + + if msg: + if via_console: + print(msg) + else: + rename_log.append(msg) + + influxframework.enqueue("influxframework.utils.global_search.rebuild_for_doctype", doctype=doctype) + + if not via_console: + return rename_log + + +def update_linked_doctypes( + doctype: str, docname: str, linked_to: str, value: str, ignore_doctypes: list | None = None +) -> None: + from influxframework.model.utils.rename_doc import update_linked_doctypes + + show_deprecation_warning("update_linked_doctypes") + + return update_linked_doctypes( + doctype=doctype, + docname=docname, + linked_to=linked_to, + value=value, + ignore_doctypes=ignore_doctypes, + ) + + +def get_fetch_fields( + doctype: str, linked_to: str, ignore_doctypes: list | None = None +) -> list[dict]: + from influxframework.model.utils.rename_doc import get_fetch_fields + + show_deprecation_warning("get_fetch_fields") + + return get_fetch_fields(doctype=doctype, linked_to=linked_to, ignore_doctypes=ignore_doctypes) + + +def show_deprecation_warning(funct: str) -> None: + from click import secho + + message = ( + f"Function influxframework.model.rename_doc.{funct} has been deprecated and " + "moved to the influxframework.model.utils.rename_doc" + ) + secho(message, fg="yellow") diff --git a/influxframework/model/sync.py b/influxframework/model/sync.py new file mode 100644 index 0000000..fba8c8d --- /dev/null +++ b/influxframework/model/sync.py @@ -0,0 +1,130 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +""" + Sync's doctype and docfields from txt files to database + perms will get synced only if none exist +""" +import os + +import influxframework +from influxframework.modules.import_file import import_file_by_path +from influxframework.modules.patch_handler import _patch_mode +from influxframework.utils import update_progress_bar + + +def sync_all(force=0, reset_permissions=False): + _patch_mode(True) + + for app in influxframework.get_installed_apps(): + sync_for(app, force, reset_permissions=reset_permissions) + + _patch_mode(False) + + influxframework.clear_cache() + + +def sync_for(app_name, force=0, reset_permissions=False): + files = [] + + if app_name == "influxframework": + # these need to go first at time of install + + INFLUXFRAMEWORK_PATH = influxframework.get_app_path("influxframework") + + for core_module in [ + "docfield", + "docperm", + "doctype_action", + "doctype_link", + "doctype_state", + "role", + "has_role", + "doctype", + ]: + files.append(os.path.join(INFLUXFRAMEWORK_PATH, "core", "doctype", core_module, f"{core_module}.json")) + + for custom_module in ["custom_field", "property_setter"]: + files.append( + os.path.join(INFLUXFRAMEWORK_PATH, "custom", "doctype", custom_module, f"{custom_module}.json") + ) + + for website_module in ["web_form", "web_template", "web_form_field", "portal_menu_item"]: + files.append( + os.path.join(INFLUXFRAMEWORK_PATH, "website", "doctype", website_module, f"{website_module}.json") + ) + + for desk_module in [ + "number_card", + "dashboard_chart", + "dashboard", + "onboarding_permission", + "onboarding_step", + "onboarding_step_map", + "module_onboarding", + "workspace_link", + "workspace_chart", + "workspace_shortcut", + "workspace_quick_list", + "workspace", + ]: + files.append(os.path.join(INFLUXFRAMEWORK_PATH, "desk", "doctype", desk_module, f"{desk_module}.json")) + + for module_name in influxframework.local.app_modules.get(app_name) or []: + folder = os.path.dirname(influxframework.get_module(app_name + "." + module_name).__file__) + files = get_doc_files(files=files, start_path=folder) + + l = len(files) + + if l: + for i, doc_path in enumerate(files): + import_file_by_path( + doc_path, force=force, ignore_version=True, reset_permissions=reset_permissions + ) + + influxframework.db.commit() + + # show progress bar + update_progress_bar(f"Updating DocTypes for {app_name}", i, l) + + # print each progress bar on new line + print() + + +def get_doc_files(files, start_path): + """walk and sync all doctypes and pages""" + + files = files or [] + + # load in sequence - warning for devs + document_types = [ + "doctype", + "page", + "report", + "dashboard_chart_source", + "print_format", + "web_page", + "website_theme", + "web_form", + "web_template", + "notification", + "print_style", + "workspace", + "onboarding_step", + "module_onboarding", + "form_tour", + "client_script", + "server_script", + "custom_field", + "property_setter", + ] + for doctype in document_types: + doctype_path = os.path.join(start_path, doctype) + if os.path.exists(doctype_path): + for docname in os.listdir(doctype_path): + if os.path.isdir(os.path.join(doctype_path, docname)): + doc_path = os.path.join(doctype_path, docname, docname) + ".json" + if os.path.exists(doc_path): + if doc_path not in files: + files.append(doc_path) + + return files diff --git a/influxframework/model/utils/__init__.py b/influxframework/model/utils/__init__.py new file mode 100644 index 0000000..809d4c0 --- /dev/null +++ b/influxframework/model/utils/__init__.py @@ -0,0 +1,133 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import re + +import influxframework +from influxframework import _ +from influxframework.build import html_to_js_template +from influxframework.utils import cstr +from influxframework.utils.caching import site_cache + +STANDARD_FIELD_CONVERSION_MAP = { + "name": "Link", + "owner": "Data", + "idx": "Int", + "creation": "Data", + "modified": "Data", + "modified_by": "Data", + "_user_tags": "Data", + "_liked_by": "Data", + "_comments": "Text", + "_assign": "Text", + "docstatus": "Int", +} +INCLUDE_DIRECTIVE_PATTERN = re.compile(r"""{% include\s['"](.*)['"]\s%}""") + + +def set_default(doc, key): + """Set is_default property of given doc and unset all others filtered by given key.""" + if not doc.is_default: + influxframework.db.set(doc, "is_default", 1) + + influxframework.db.sql( + """update `tab%s` set `is_default`=0 + where `%s`=%s and name!=%s""" + % (doc.doctype, key, "%s", "%s"), + (doc.get(key), doc.name), + ) + + +def set_field_property(filters, key, value): + """utility set a property in all fields of a particular type""" + docs = [ + influxframework.get_doc("DocType", d.parent) + for d in influxframework.get_all("DocField", fields=["parent"], filters=filters) + ] + + for d in docs: + d.get("fields", filters)[0].set(key, value) + d.save() + print(f"Updated {d.name}") + + influxframework.db.commit() + + +class InvalidIncludePath(influxframework.ValidationError): + pass + + +def render_include(content): + """render {% raw %}{% include "app/path/filename" %}{% endraw %} in js file""" + + content = cstr(content) + + # try 5 levels of includes + for i in range(5): + if "{% include" in content: + paths = INCLUDE_DIRECTIVE_PATTERN.findall(content) + if not paths: + influxframework.throw(_("Invalid include path"), InvalidIncludePath) + + for path in paths: + app, app_path = path.split("/", 1) + with open(influxframework.get_app_path(app, app_path), encoding="utf-8") as f: + include = f.read() + if path.endswith(".html"): + include = html_to_js_template(path, include) + + content = re.sub(rf"""{{% include\s['"]{path}['"]\s%}}""", include, content) + + else: + break + + return content + + +def get_fetch_values(doctype, fieldname, value): + """Returns fetch value dict for the given object + + :param doctype: Target doctype + :param fieldname: Link fieldname selected + :param value: Value selected + """ + + result = influxframework._dict() + meta = influxframework.get_meta(doctype) + + # fieldname in target doctype: fieldname in source doctype + fields_to_fetch = { + df.fieldname: df.fetch_from.split(".", 1)[1] for df in meta.get_fields_to_fetch(fieldname) + } + + # nothing to fetch + if not fields_to_fetch: + return result + + # initialise empty values for target fields + for target_fieldname in fields_to_fetch: + result[target_fieldname] = None + + # fetch only if Link field has a truthy value + if not value: + return result + + db_values = influxframework.db.get_value( + meta.get_options(fieldname), # source doctype + value, + tuple(set(fields_to_fetch.values())), # unique source fieldnames + as_dict=True, + ) + + # if value doesn't exist in source doctype, get_value returns None + if not db_values: + return result + + for target_fieldname, source_fieldname in fields_to_fetch.items(): + result[target_fieldname] = db_values.get(source_fieldname) + + return result + + +@site_cache() +def is_virtual_doctype(doctype): + return influxframework.db.get_value("DocType", doctype, "is_virtual") diff --git a/influxframework/model/utils/link_count.py b/influxframework/model/utils/link_count.py new file mode 100644 index 0000000..e9beec2 --- /dev/null +++ b/influxframework/model/utils/link_count.py @@ -0,0 +1,53 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework + +ignore_doctypes = ("DocType", "Print Format", "Role", "Module Def", "Communication", "ToDo") + + +def notify_link_count(doctype, name): + """updates link count for given document""" + if hasattr(influxframework.local, "link_count"): + if (doctype, name) in influxframework.local.link_count: + influxframework.local.link_count[(doctype, name)] += 1 + else: + influxframework.local.link_count[(doctype, name)] = 1 + + +def flush_local_link_count(): + """flush from local before ending request""" + if not getattr(influxframework.local, "link_count", None): + return + + link_count = influxframework.cache().get_value("_link_count") + if not link_count: + link_count = {} + + for key, value in influxframework.local.link_count.items(): + if key in link_count: + link_count[key] += influxframework.local.link_count[key] + else: + link_count[key] = influxframework.local.link_count[key] + + influxframework.cache().set_value("_link_count", link_count) + + +def update_link_count(): + """increment link count in the `idx` column for the given document""" + link_count = influxframework.cache().get_value("_link_count") + + if link_count: + for key, count in link_count.items(): + if key[0] not in ignore_doctypes: + try: + influxframework.db.sql( + f"update `tab{key[0]}` set idx = idx + {count} where name=%s", + key[1], + auto_commit=1, + ) + except Exception as e: + if not influxframework.db.is_table_missing(e): # table not found, single + raise e + # reset the count + influxframework.cache().delete_value("_link_count") diff --git a/influxframework/model/utils/rename_doc.py b/influxframework/model/utils/rename_doc.py new file mode 100644 index 0000000..74c2bf8 --- /dev/null +++ b/influxframework/model/utils/rename_doc.py @@ -0,0 +1,65 @@ +# Copyright (c) 2022, InfluxFramework LLC +# License: MIT. See LICENSE + +from itertools import product + +import influxframework +from influxframework.model.rename_doc import get_link_fields + + +def update_linked_doctypes( + doctype: str, docname: str, linked_to: str, value: str, ignore_doctypes: list | None = None +): + """ + linked_doctype_info_list = list formed by get_fetch_fields() function + docname = Master DocType's name in which modification are made + value = Value for the field thats set in other DocType's by fetching from Master DocType + """ + linked_doctype_info_list = get_fetch_fields(doctype, linked_to, ignore_doctypes) + + for d in linked_doctype_info_list: + influxframework.db.set_value( + d.doctype, + { + d.master_fieldname: docname, + d.linked_to_fieldname: ("!=", value), + }, + d.linked_to_fieldname, + value, + ) + + +def get_fetch_fields( + doctype: str, linked_to: str, ignore_doctypes: list | None = None +) -> list[dict]: + """ + doctype = Master DocType in which the changes are being made + linked_to = DocType name of the field thats being updated in Master + This function fetches list of all DocType where both doctype and linked_to is found + as link fields. + Forms a list of dict in the form - + [{doctype: , master_fieldname: , linked_to_fieldname: ] + where + doctype = DocType where changes need to be made + master_fieldname = Fieldname where options = doctype + linked_to_fieldname = Fieldname where options = linked_to + """ + + out = [] + master_list = get_link_fields(doctype) + linked_to_list = get_link_fields(linked_to) + product_list = product(master_list, linked_to_list) + + for d in product_list: + linked_doctype_info = influxframework._dict() + if ( + d[0]["parent"] == d[1]["parent"] + and (not ignore_doctypes or d[0]["parent"] not in ignore_doctypes) + and not d[1]["issingle"] + ): + linked_doctype_info.doctype = d[0]["parent"] + linked_doctype_info.master_fieldname = d[0]["fieldname"] + linked_doctype_info.linked_to_fieldname = d[1]["fieldname"] + out.append(linked_doctype_info) + + return out diff --git a/influxframework/model/utils/rename_field.py b/influxframework/model/utils/rename_field.py new file mode 100644 index 0000000..6d93311 --- /dev/null +++ b/influxframework/model/utils/rename_field.py @@ -0,0 +1,176 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import json + +import influxframework +from influxframework.model import no_value_fields, table_fields +from influxframework.model.utils.user_settings import sync_user_settings, update_user_settings_data +from influxframework.utils.password import rename_password_field + + +def rename_field(doctype, old_fieldname, new_fieldname): + """This functions assumes that doctype is already synced""" + + meta = influxframework.get_meta(doctype, cached=False) + new_field = meta.get_field(new_fieldname) + if not new_field: + print("rename_field: " + (new_fieldname) + " not found in " + doctype) + return + + if not meta.issingle and not influxframework.db.has_column(doctype, old_fieldname): + print("rename_field: " + (old_fieldname) + " not found in table for: " + doctype) + # never had the field? + return + + if new_field.fieldtype in table_fields: + # change parentfield of table mentioned in options + influxframework.db.sql( + """update `tab%s` set parentfield=%s + where parentfield=%s""" + % (new_field.options.split("\n")[0], "%s", "%s"), + (new_fieldname, old_fieldname), + ) + + elif new_field.fieldtype not in no_value_fields: + if meta.issingle: + influxframework.db.sql( + """update `tabSingles` set field=%s + where doctype=%s and field=%s""", + (new_fieldname, doctype, old_fieldname), + ) + else: + # copy field value + influxframework.db.sql(f"""update `tab{doctype}` set `{new_fieldname}`=`{old_fieldname}`""") + + update_reports(doctype, old_fieldname, new_fieldname) + update_users_report_view_settings(doctype, old_fieldname, new_fieldname) + + if new_field.fieldtype == "Password": + rename_password_field(doctype, old_fieldname, new_fieldname) + + # update in property setter + update_property_setters(doctype, old_fieldname, new_fieldname) + + # update in user settings + update_user_settings(doctype, old_fieldname, new_fieldname) + + +def update_reports(doctype, old_fieldname, new_fieldname): + def _get_new_sort_by(report_dict, report, key): + sort_by = report_dict.get(key) or "" + if sort_by: + sort_by = sort_by.split(".") + if len(sort_by) > 1: + if sort_by[0] == doctype and sort_by[1] == old_fieldname: + sort_by = doctype + "." + new_fieldname + report_dict["updated"] = True + elif report.ref_doctype == doctype and sort_by[0] == old_fieldname: + sort_by = doctype + "." + new_fieldname + report_dict["updated"] = True + + if isinstance(sort_by, list): + sort_by = ".".join(sort_by) + + return sort_by + + reports = influxframework.db.sql( + """select name, ref_doctype, json from tabReport + where report_type = 'Report Builder' and ifnull(is_standard, 'No') = 'No' + and json like %s and json like %s""", + ("%%%s%%" % old_fieldname, "%%%s%%" % doctype), + as_dict=True, + ) + + for r in reports: + report_dict = json.loads(r.json) + + # update filters + new_filters = [] + if report_dict.get("filters"): + for f in report_dict.get("filters"): + if f and len(f) > 1 and f[0] == doctype and f[1] == old_fieldname: + new_filters.append([doctype, new_fieldname, f[2], f[3]]) + report_dict["updated"] = True + else: + new_filters.append(f) + + # update columns + new_columns = [] + if report_dict.get("columns"): + for c in report_dict.get("columns"): + if c and len(c) > 1 and c[0] == old_fieldname and c[1] == doctype: + new_columns.append([new_fieldname, doctype]) + report_dict["updated"] = True + else: + new_columns.append(c) + + # update sort by + new_sort_by = _get_new_sort_by(report_dict, r, "sort_by") + new_sort_by_next = _get_new_sort_by(report_dict, r, "sort_by_next") + + if report_dict.get("updated"): + new_val = json.dumps( + { + "filters": new_filters, + "columns": new_columns, + "sort_by": new_sort_by, + "sort_order": report_dict.get("sort_order"), + "sort_by_next": new_sort_by_next, + "sort_order_next": report_dict.get("sort_order_next"), + } + ) + + influxframework.db.sql("""update `tabReport` set `json`=%s where name=%s""", (new_val, r.name)) + + +def update_users_report_view_settings(doctype, ref_fieldname, new_fieldname): + user_report_cols = influxframework.db.sql( + """select defkey, defvalue from `tabDefaultValue` where + defkey like '_list_settings:%'""" + ) + for key, value in user_report_cols: + new_columns = [] + columns_modified = False + for field, field_doctype in json.loads(value): + if field == ref_fieldname and field_doctype == doctype: + new_columns.append([new_fieldname, field_doctype]) + columns_modified = True + else: + new_columns.append([field, field_doctype]) + + if columns_modified: + influxframework.db.sql( + """update `tabDefaultValue` set defvalue=%s + where defkey=%s""" + % ("%s", "%s"), + (json.dumps(new_columns), key), + ) + + +def update_property_setters(doctype, old_fieldname, new_fieldname): + influxframework.db.sql( + """update `tabProperty Setter` set field_name = %s + where doc_type=%s and field_name=%s""", + (new_fieldname, doctype, old_fieldname), + ) + + influxframework.db.sql( + """update `tabCustom Field` set insert_after=%s + where insert_after=%s and dt=%s""", + (new_fieldname, old_fieldname, doctype), + ) + + +def update_user_settings(doctype, old_fieldname, new_fieldname): + # store the user settings data from the redis to db + sync_user_settings() + + user_settings = influxframework.db.sql( + ''' select user, doctype, data from `__UserSettings` + where doctype=%s and data like "%%%s%%"''', + (doctype, old_fieldname), + as_dict=1, + ) + + for user_setting in user_settings: + update_user_settings_data(user_setting, "docfield", old_fieldname, new_fieldname) diff --git a/influxframework/model/utils/user_settings.py b/influxframework/model/utils/user_settings.py new file mode 100644 index 0000000..4fd4e38 --- /dev/null +++ b/influxframework/model/utils/user_settings.py @@ -0,0 +1,102 @@ +# Settings saved per user basis +# such as page_limit, filters, last_view + +import json + +import influxframework +from influxframework import safe_decode + +# dict for mapping the index and index type for the filters of different views +filter_dict = {"doctype": 0, "docfield": 1, "operator": 2, "value": 3} + + +def get_user_settings(doctype, for_update=False): + user_settings = influxframework.cache().hget("_user_settings", f"{doctype}::{influxframework.session.user}") + + if user_settings is None: + user_settings = influxframework.db.sql( + """select data from `__UserSettings` + where `user`=%s and `doctype`=%s""", + (influxframework.session.user, doctype), + ) + user_settings = user_settings and user_settings[0][0] or "{}" + + if not for_update: + update_user_settings(doctype, user_settings, True) + + return user_settings or "{}" + + +def update_user_settings(doctype, user_settings, for_update=False): + """update user settings in cache""" + + if for_update: + current = json.loads(user_settings) + else: + current = json.loads(get_user_settings(doctype, for_update=True)) + + if isinstance(current, str): + # corrupt due to old code, remove this in a future release + current = {} + + current.update(user_settings) + + influxframework.cache().hset("_user_settings", f"{doctype}::{influxframework.session.user}", json.dumps(current)) + + +def sync_user_settings(): + """Sync from cache to database (called asynchronously via the browser)""" + for key, data in influxframework.cache().hgetall("_user_settings").items(): + key = safe_decode(key) + doctype, user = key.split("::") # WTF? + influxframework.db.multisql( + { + "mariadb": """INSERT INTO `__UserSettings`(`user`, `doctype`, `data`) + VALUES (%s, %s, %s) + ON DUPLICATE key UPDATE `data`=%s""", + "postgres": """INSERT INTO `__UserSettings` (`user`, `doctype`, `data`) + VALUES (%s, %s, %s) + ON CONFLICT ("user", "doctype") DO UPDATE SET `data`=%s""", + }, + (user, doctype, data, data), + as_dict=1, + ) + + +@influxframework.whitelist() +def save(doctype, user_settings): + user_settings = json.loads(user_settings or "{}") + update_user_settings(doctype, user_settings) + return user_settings + + +@influxframework.whitelist() +def get(doctype): + return get_user_settings(doctype) + + +def update_user_settings_data( + user_setting, fieldname, old, new, condition_fieldname=None, condition_values=None +): + data = user_setting.get("data") + if data: + update = False + data = json.loads(data) + for view in ["List", "Gantt", "Kanban", "Calendar", "Image", "Inbox", "Report"]: + view_settings = data.get(view) + if view_settings and view_settings.get("filters"): + view_filters = view_settings.get("filters") + for view_filter in view_filters: + if condition_fieldname and view_filter[filter_dict[condition_fieldname]] != condition_values: + continue + if view_filter[filter_dict[fieldname]] == old: + view_filter[filter_dict[fieldname]] = new + update = True + if update: + influxframework.db.sql( + "update __UserSettings set data=%s where doctype=%s and user=%s", + (json.dumps(data), user_setting.doctype, user_setting.user), + ) + + # clear that user settings from the redis cache + influxframework.cache().hset("_user_settings", f"{user_setting.doctype}::{user_setting.user}", None) diff --git a/influxframework/model/virtual_doctype.py b/influxframework/model/virtual_doctype.py new file mode 100644 index 0000000..a62ad67 --- /dev/null +++ b/influxframework/model/virtual_doctype.py @@ -0,0 +1,52 @@ +from typing import Protocol + +import influxframework + + +class VirtualDoctype(Protocol): + """This class documents requirements that must be met by a doctype controller to function as virtual doctype + + + Additional requirements: + - DocType controller has to inherit from `influxframework.model.document.Document` class + + Note: + - "Backend" here means any storage service, it can be a database, flat file or network call to API. + """ + + # ============ class/static methods ============ + + @staticmethod + def get_list(args) -> list[influxframework._dict]: + """Similar to reportview.get_list""" + ... + + @staticmethod + def get_count(args) -> int: + """Similar to reportview.get_count, return total count of documents on listview.""" + ... + + @staticmethod + def get_stats(args): + """Similar to reportview.get_stats, return sidebar stats.""" + ... + + # ============ instance methods ============ + + def db_insert(self, *args, **kwargs) -> None: + """Serialize the `Document` object and insert it in backend.""" + ... + + def load_from_db(self) -> None: + """Using self.name initialize current document from backend data. + + This is responsible for updatinng __dict__ of class with all the fields on doctype.""" + ... + + def db_update(self, *args, **kwargs) -> None: + """Serialize the `Document` object and update existing document in backend.""" + ... + + def delete(self, *args, **kwargs) -> None: + """Delete the current document from backend""" + ... diff --git a/influxframework/model/workflow.py b/influxframework/model/workflow.py new file mode 100644 index 0000000..cd47bd2 --- /dev/null +++ b/influxframework/model/workflow.py @@ -0,0 +1,352 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import json + +import influxframework +from influxframework import _ +from influxframework.model.docstatus import DocStatus +from influxframework.utils import cint + + +class WorkflowStateError(influxframework.ValidationError): + pass + + +class WorkflowTransitionError(influxframework.ValidationError): + pass + + +class WorkflowPermissionError(influxframework.ValidationError): + pass + + +def get_workflow_name(doctype): + workflow_name = influxframework.cache().hget("workflow", doctype) + if workflow_name is None: + workflow_name = influxframework.db.get_value( + "Workflow", {"document_type": doctype, "is_active": 1}, "name" + ) + influxframework.cache().hset("workflow", doctype, workflow_name or "") + + return workflow_name + + +@influxframework.whitelist() +def get_transitions(doc, workflow=None, raise_exception=False): + """Return list of possible transitions for the given doc""" + doc = influxframework.get_doc(influxframework.parse_json(doc)) + + if doc.is_new(): + return [] + + doc.load_from_db() + + influxframework.has_permission(doc, "read", throw=True) + roles = influxframework.get_roles() + + if not workflow: + workflow = get_workflow(doc.doctype) + current_state = doc.get(workflow.workflow_state_field) + + if not current_state: + if raise_exception: + raise WorkflowStateError + else: + influxframework.throw(_("Workflow State not set"), WorkflowStateError) + + transitions = [] + for transition in workflow.transitions: + if transition.state == current_state and transition.allowed in roles: + if not is_transition_condition_satisfied(transition, doc): + continue + transitions.append(transition.as_dict()) + return transitions + + +def get_workflow_safe_globals(): + # access to influxframework.db.get_value, influxframework.db.get_list, and date time utils. + return dict( + influxframework=influxframework._dict( + db=influxframework._dict(get_value=influxframework.db.get_value, get_list=influxframework.db.get_list), + session=influxframework.session, + utils=influxframework._dict( + now_datetime=influxframework.utils.now_datetime, + add_to_date=influxframework.utils.add_to_date, + get_datetime=influxframework.utils.get_datetime, + now=influxframework.utils.now, + ), + ) + ) + + +def is_transition_condition_satisfied(transition, doc): + if not transition.condition: + return True + else: + return influxframework.safe_eval( + transition.condition, get_workflow_safe_globals(), dict(doc=doc.as_dict()) + ) + + +@influxframework.whitelist() +def apply_workflow(doc, action): + """Allow workflow action on the current doc""" + doc = influxframework.get_doc(influxframework.parse_json(doc)) + workflow = get_workflow(doc.doctype) + transitions = get_transitions(doc, workflow) + user = influxframework.session.user + + # find the transition + transition = None + for t in transitions: + if t.action == action: + transition = t + + if not transition: + influxframework.throw(_("Not a valid Workflow Action"), WorkflowTransitionError) + + if not has_approval_access(user, doc, transition): + influxframework.throw(_("Self approval is not allowed")) + + # update workflow state field + doc.set(workflow.workflow_state_field, transition.next_state) + + # find settings for the next state + next_state = [d for d in workflow.states if d.state == transition.next_state][0] + + # update any additional field + if next_state.update_field: + doc.set(next_state.update_field, next_state.update_value) + + new_docstatus = cint(next_state.doc_status) + if doc.docstatus.is_draft() and new_docstatus == DocStatus.draft(): + doc.save() + elif doc.docstatus.is_draft() and new_docstatus == DocStatus.submitted(): + doc.submit() + elif doc.docstatus.is_submitted() and new_docstatus == DocStatus.submitted(): + doc.save() + elif doc.docstatus.is_submitted() and new_docstatus == DocStatus.cancelled(): + doc.cancel() + else: + influxframework.throw(_("Illegal Document Status for {0}").format(next_state.state)) + + doc.add_comment("Workflow", _(next_state.state)) + + return doc + + +@influxframework.whitelist() +def can_cancel_document(doctype): + workflow = get_workflow(doctype) + for state_doc in workflow.states: + if state_doc.doc_status == "2": + for transition in workflow.transitions: + if transition.next_state == state_doc.state: + return False + return True + return True + + +def validate_workflow(doc): + """Validate Workflow State and Transition for the current user. + + - Check if user is allowed to edit in current state + - Check if user is allowed to transition to the next state (if changed) + """ + workflow = get_workflow(doc.doctype) + + current_state = None + if getattr(doc, "_doc_before_save", None): + current_state = doc._doc_before_save.get(workflow.workflow_state_field) + next_state = doc.get(workflow.workflow_state_field) + + if not next_state: + next_state = workflow.states[0].state + doc.set(workflow.workflow_state_field, next_state) + + if not current_state: + current_state = workflow.states[0].state + + state_row = [d for d in workflow.states if d.state == current_state] + if not state_row: + influxframework.throw( + _("{0} is not a valid Workflow State. Please update your Workflow and try again.").format( + influxframework.bold(current_state) + ) + ) + state_row = state_row[0] + + # if transitioning, check if user is allowed to transition + if current_state != next_state: + bold_current = influxframework.bold(current_state) + bold_next = influxframework.bold(next_state) + + if not doc._doc_before_save: + # transitioning directly to a state other than the first + # e.g from data import + influxframework.throw( + _("Workflow State transition not allowed from {0} to {1}").format(bold_current, bold_next), + WorkflowPermissionError, + ) + + transitions = get_transitions(doc._doc_before_save) + transition = [d for d in transitions if d.next_state == next_state] + if not transition: + influxframework.throw( + _("Workflow State transition not allowed from {0} to {1}").format(bold_current, bold_next), + WorkflowPermissionError, + ) + + +def get_workflow(doctype): + return influxframework.get_doc("Workflow", get_workflow_name(doctype)) + + +def has_approval_access(user, doc, transition): + return ( + user == "Administrator" or transition.get("allow_self_approval") or user != doc.get("owner") + ) + + +def get_workflow_state_field(workflow_name): + return get_workflow_field_value(workflow_name, "workflow_state_field") + + +def send_email_alert(workflow_name): + return get_workflow_field_value(workflow_name, "send_email_alert") + + +def get_workflow_field_value(workflow_name, field): + value = influxframework.cache().hget("workflow_" + workflow_name, field) + if value is None: + value = influxframework.db.get_value("Workflow", workflow_name, field) + influxframework.cache().hset("workflow_" + workflow_name, field, value) + return value + + +@influxframework.whitelist() +def bulk_workflow_approval(docnames, doctype, action): + from collections import defaultdict + + # dictionaries for logging + failed_transactions = defaultdict(list) + successful_transactions = defaultdict(list) + + # WARN: message log is cleared + print("Clearing influxframework.message_log...") + influxframework.clear_messages() + + docnames = json.loads(docnames) + for (idx, docname) in enumerate(docnames, 1): + message_dict = {} + try: + show_progress(docnames, _("Applying: {0}").format(action), idx, docname) + apply_workflow(influxframework.get_doc(doctype, docname), action) + influxframework.db.commit() + except Exception as e: + if not influxframework.message_log: + # Exception is raised manually and not from msgprint or throw + message = f"{e.__class__.__name__}" + if e.args: + message += f" : {e.args[0]}" + message_dict = {"docname": docname, "message": message} + failed_transactions[docname].append(message_dict) + + influxframework.db.rollback() + influxframework.log_error( + title=f"Workflow {action} threw an error for {doctype} {docname}", + reference_doctype="Workflow", + reference_name=action, + ) + finally: + if not message_dict: + if influxframework.message_log: + messages = influxframework.get_message_log() + for message in messages: + influxframework.message_log.pop() + message_dict = {"docname": docname, "message": message.get("message")} + + if message.get("raise_exception", False): + failed_transactions[docname].append(message_dict) + else: + successful_transactions[docname].append(message_dict) + else: + successful_transactions[docname].append({"docname": docname, "message": None}) + + if failed_transactions and successful_transactions: + indicator = "orange" + elif failed_transactions: + indicator = "red" + else: + indicator = "green" + + print_workflow_log(failed_transactions, _("Failed Transactions"), doctype, indicator) + print_workflow_log(successful_transactions, _("Successful Transactions"), doctype, indicator) + + +def print_workflow_log(messages, title, doctype, indicator): + if messages.keys(): + msg = f"

      {title}

      " + + for doc in messages.keys(): + if len(messages[doc]): + html = f"
      {influxframework.utils.get_link_to_form(doctype, doc)}" + for log in messages[doc]: + if log.get("message"): + html += "
      {}
      ".format( + log.get("message") + ) + html += "
      " + else: + html = f"
      {doc}
      " + msg += html + + influxframework.msgprint(msg, title=_("Workflow Status"), indicator=indicator, is_minimizable=True) + + +@influxframework.whitelist() +def get_common_transition_actions(docs, doctype): + common_actions = [] + if isinstance(docs, str): + docs = json.loads(docs) + try: + for (i, doc) in enumerate(docs, 1): + if not doc.get("doctype"): + doc["doctype"] = doctype + actions = [ + t.get("action") + for t in get_transitions(doc, raise_exception=True) + if has_approval_access(influxframework.session.user, doc, t) + ] + if not actions: + return [] + common_actions = actions if i == 1 else set(common_actions).intersection(actions) + if not common_actions: + return [] + except WorkflowStateError: + pass + + return list(common_actions) + + +def show_progress(docnames, message, i, description): + n = len(docnames) + if n >= 5: + influxframework.publish_progress(float(i) * 100 / n, title=message, description=description) + + +def set_workflow_state_on_action(doc, workflow_name, action): + workflow = influxframework.get_doc("Workflow", workflow_name) + workflow_state_field = workflow.workflow_state_field + + # If workflow state of doc is already correct, don't set workflow state + for state in workflow.states: + if state.state == doc.get(workflow_state_field) and doc.docstatus == cint(state.doc_status): + return + + action_map = {"update_after_submit": "1", "submit": "1", "cancel": "2"} + docstatus = action_map[action] + for state in workflow.states: + if state.doc_status == docstatus: + doc.set(workflow_state_field, state.state) + return diff --git a/influxframework/modules.txt b/influxframework/modules.txt new file mode 100644 index 0000000..fb7817f --- /dev/null +++ b/influxframework/modules.txt @@ -0,0 +1,13 @@ +Core +Website +Workflow +Email +Custom +Geo +Desk +Integrations +Printing +Contacts +Social +Automation +Event Streaming \ No newline at end of file diff --git a/influxframework/modules/__init__.py b/influxframework/modules/__init__.py new file mode 100644 index 0000000..16281fe --- /dev/null +++ b/influxframework/modules/__init__.py @@ -0,0 +1 @@ +from .utils import * diff --git a/influxframework/modules/export_file.py b/influxframework/modules/export_file.py new file mode 100644 index 0000000..58ddef9 --- /dev/null +++ b/influxframework/modules/export_file.py @@ -0,0 +1,158 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import os +import shutil + +import influxframework +import influxframework.model +from influxframework.modules import get_module_path, scrub, scrub_dt_dn + + +def export_doc(doc): + write_document_file(doc) + + +def export_to_files(record_list=None, record_module=None, verbose=0, create_init=None): + """ + Export record_list to files. record_list is a list of lists ([doctype, docname, folder name],) , + """ + if influxframework.flags.in_import: + return + + if record_list: + for record in record_list: + folder_name = record[2] if len(record) == 3 else None + write_document_file( + influxframework.get_doc(record[0], record[1]), + record_module, + create_init=create_init, + folder_name=folder_name, + ) + + +def write_document_file(doc, record_module=None, create_init=True, folder_name=None): + doc_export = doc.as_dict(no_nulls=True) + doc.run_method("before_export", doc_export) + + doc_export = strip_default_fields(doc, doc_export) + module = record_module or get_module_name(doc) + + # create folder + if folder_name: + folder = create_folder(module, folder_name, doc.name, create_init) + else: + folder = create_folder(module, doc.doctype, doc.name, create_init) + + fname = scrub(doc.name) + write_code_files(folder, fname, doc, doc_export) + + # write the data file + with open(os.path.join(folder, fname + ".json"), "w+") as txtfile: + txtfile.write(influxframework.as_json(doc_export)) + + +def strip_default_fields(doc, doc_export): + # strip out default fields from children + if doc.doctype == "DocType" and doc.migration_hash: + del doc_export["migration_hash"] + + for df in doc.meta.get_table_fields(): + for d in doc_export.get(df.fieldname): + for fieldname in influxframework.model.default_fields + influxframework.model.child_table_fields: + if fieldname in d: + del d[fieldname] + + return doc_export + + +def write_code_files(folder, fname, doc, doc_export): + """Export code files and strip from values""" + if hasattr(doc, "get_code_fields"): + for key, extn in doc.get_code_fields().items(): + if doc.get(key): + with open(os.path.join(folder, fname + "." + extn), "w+") as txtfile: + txtfile.write(doc.get(key)) + + # remove from exporting + del doc_export[key] + + +def get_module_name(doc): + if doc.doctype == "Module Def": + module = doc.name + elif doc.doctype == "Workflow": + module = influxframework.db.get_value("DocType", doc.document_type, "module") + elif hasattr(doc, "module"): + module = doc.module + else: + module = influxframework.db.get_value("DocType", doc.doctype, "module") + + return module + + +def delete_folder(module, dt, dn): + if influxframework.db.get_value("Module Def", module, "custom"): + module_path = get_custom_module_path(module) + else: + module_path = get_module_path(module) + + dt, dn = scrub_dt_dn(dt, dn) + + # delete folder + folder = os.path.join(module_path, dt, dn) + + if os.path.exists(folder): + shutil.rmtree(folder) + + +def create_folder(module, dt, dn, create_init): + if influxframework.db.get_value("Module Def", module, "custom"): + module_path = get_custom_module_path(module) + else: + module_path = get_module_path(module) + + dt, dn = scrub_dt_dn(dt, dn) + + # create folder + folder = os.path.join(module_path, dt, dn) + + influxframework.create_folder(folder) + + # create init_py_files + if create_init: + create_init_py(module_path, dt, dn) + + return folder + + +def get_custom_module_path(module): + package = influxframework.db.get_value("Module Def", module, "package") + if not package: + influxframework.throw(f"Package must be set for custom Module {module}") + + path = os.path.join(get_package_path(package), scrub(module)) + if not os.path.exists(path): + os.makedirs(path) + + return path + + +def get_package_path(package): + path = os.path.join( + influxframework.get_site_path("packages"), influxframework.db.get_value("Package", package, "package_name") + ) + if not os.path.exists(path): + os.makedirs(path) + return path + + +def create_init_py(module_path, dt, dn): + def create_if_not_exists(path): + initpy = os.path.join(path, "__init__.py") + if not os.path.exists(initpy): + open(initpy, "w").close() + + create_if_not_exists(os.path.join(module_path)) + create_if_not_exists(os.path.join(module_path, dt)) + create_if_not_exists(os.path.join(module_path, dt, dn)) diff --git a/influxframework/modules/import_file.py b/influxframework/modules/import_file.py new file mode 100644 index 0000000..34b83b1 --- /dev/null +++ b/influxframework/modules/import_file.py @@ -0,0 +1,290 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import hashlib +import json +import os + +import influxframework +from influxframework.model.base_document import get_controller +from influxframework.modules import get_module_path, scrub_dt_dn +from influxframework.query_builder import DocType +from influxframework.utils import get_datetime, now + + +def calculate_hash(path: str) -> str: + """Calculate md5 hash of the file in binary mode + + Args: + path (str): Path to the file to be hashed + + Returns: + str: The calculated hash + """ + hash_md5 = hashlib.md5() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_md5.update(chunk) + return hash_md5.hexdigest() + + +ignore_values = { + "Report": ["disabled", "prepared_report", "add_total_row"], + "Print Format": ["disabled"], + "Notification": ["enabled"], + "Print Style": ["disabled"], + "Module Onboarding": ["is_complete"], + "Onboarding Step": ["is_complete", "is_skipped"], +} + +ignore_doctypes = [""] + + +def import_files(module, dt=None, dn=None, force=False, pre_process=None, reset_permissions=False): + if type(module) is list: + out = [] + for m in module: + out.append( + import_file( + m[0], m[1], m[2], force=force, pre_process=pre_process, reset_permissions=reset_permissions + ) + ) + return out + else: + return import_file( + module, dt, dn, force=force, pre_process=pre_process, reset_permissions=reset_permissions + ) + + +def import_file(module, dt, dn, force=False, pre_process=None, reset_permissions=False): + """Sync a file from txt if modifed, return false if not updated""" + path = get_file_path(module, dt, dn) + ret = import_file_by_path( + path, force, pre_process=pre_process, reset_permissions=reset_permissions + ) + return ret + + +def get_file_path(module, dt, dn): + dt, dn = scrub_dt_dn(dt, dn) + + path = os.path.join(get_module_path(module), os.path.join(dt, dn, f"{dn}.json")) + + return path + + +def import_file_by_path( + path: str, + force: bool = False, + data_import: bool = False, + pre_process=None, + ignore_version: bool = None, + reset_permissions: bool = False, +): + """Import file from the given path + + Some conditions decide if a file should be imported or not. + Evaluation takes place in the order they are mentioned below. + + - Check if `force` is true. Import the file. If not, move ahead. + - Get `db_modified_timestamp`(value of the modified field in the database for the file). + If the return is `none,` this file doesn't exist in the DB, so Import the file. If not, move ahead. + - Check if there is a hash in DB for that file. If there is, Calculate the Hash of the file to import and compare it with the one in DB if they are not equal. + Import the file. If Hash doesn't exist, move ahead. + - Check if `db_modified_timestamp` is older than the timestamp in the file; if it is, we import the file. + + If timestamp comparison happens for doctypes, that means the Hash for it doesn't exist. + So, even if the timestamp is newer on DB (When comparing timestamps), we import the file and add the calculated Hash to the DB. + So in the subsequent imports, we can use hashes to compare. As a precautionary measure, the timestamp is updated to the current time as well. + + Args: + path (str): Path to the file. + force (bool, optional): Load the file without checking any conditions. Defaults to False. + data_import (bool, optional): [description]. Defaults to False. + pre_process ([type], optional): Any preprocesing that may need to take place on the doc. Defaults to None. + ignore_version (bool, optional): ignore current version. Defaults to None. + reset_permissions (bool, optional): reset permissions for the file. Defaults to False. + + Returns: + [bool]: True if import takes place. False if it wasn't imported. + """ + try: + docs = read_doc_from_file(path) + except OSError: + print(f"{path} missing") + return + + calculated_hash = calculate_hash(path) + + if docs: + if not isinstance(docs, list): + docs = [docs] + + for doc in docs: + # modified timestamp in db, none if doctype's first import + db_modified_timestamp = influxframework.db.get_value(doc["doctype"], doc["name"], "modified") + is_db_timestamp_latest = db_modified_timestamp and ( + get_datetime(doc.get("modified")) <= get_datetime(db_modified_timestamp) + ) + + if not force and db_modified_timestamp: + stored_hash = None + if doc["doctype"] == "DocType": + try: + stored_hash = influxframework.db.get_value(doc["doctype"], doc["name"], "migration_hash") + except Exception: + pass + + # if hash exists and is equal no need to update + if stored_hash and stored_hash == calculated_hash: + continue + + # if hash doesn't exist, check if db timestamp is same as json timestamp, add hash if from doctype + if is_db_timestamp_latest and doc["doctype"] != "DocType": + continue + + import_doc( + docdict=doc, + data_import=data_import, + pre_process=pre_process, + ignore_version=ignore_version, + reset_permissions=reset_permissions, + path=path, + ) + + if doc["doctype"] == "DocType": + doctype_table = DocType("DocType") + influxframework.qb.update(doctype_table).set(doctype_table.migration_hash, calculated_hash).where( + doctype_table.name == doc["name"] + ).run() + + new_modified_timestamp = doc.get("modified") + + # if db timestamp is newer, hash must have changed, must update db timestamp + if is_db_timestamp_latest and doc["doctype"] == "DocType": + new_modified_timestamp = now() + + if new_modified_timestamp: + update_modified(new_modified_timestamp, doc) + + return True + + +def read_doc_from_file(path): + doc = None + if os.path.exists(path): + with open(path) as f: + try: + doc = json.loads(f.read()) + except ValueError: + print(f"bad json: {path}") + raise + else: + raise OSError("%s missing" % path) + + return doc + + +def update_modified(original_modified, doc): + # since there is a new timestamp on the file, update timestamp in + if doc["doctype"] == doc["name"] and doc["name"] != "DocType": + singles_table = DocType("Singles") + + influxframework.qb.update(singles_table).set(singles_table.value, original_modified).where( + singles_table["field"] == "modified", # singles_table.field is a method of pypika Selectable + ).where(singles_table.doctype == doc["name"]).run() + else: + doctype_table = DocType(doc["doctype"]) + + influxframework.qb.update(doctype_table).set(doctype_table.modified, original_modified).where( + doctype_table.name == doc["name"] + ).run() + + +def import_doc( + docdict, + data_import=False, + pre_process=None, + ignore_version=None, + reset_permissions=False, + path=None, +): + influxframework.flags.in_import = True + docdict["__islocal"] = 1 + + controller = get_controller(docdict["doctype"]) + if ( + controller + and hasattr(controller, "prepare_for_import") + and callable(getattr(controller, "prepare_for_import")) + ): + controller.prepare_for_import(docdict) + + doc = influxframework.get_doc(docdict) + + reset_tree_properties(doc) + load_code_properties(doc, path) + + doc.run_method("before_import") + + doc.flags.ignore_version = ignore_version + if pre_process: + pre_process(doc) + + if influxframework.db.exists(doc.doctype, doc.name): + delete_old_doc(doc, reset_permissions) + + doc.flags.ignore_links = True + if not data_import: + doc.flags.ignore_validate = True + doc.flags.ignore_permissions = True + doc.flags.ignore_mandatory = True + + doc.insert() + + influxframework.flags.in_import = False + + return doc + + +def load_code_properties(doc, path): + """Load code files stored in separate files with extensions""" + if path: + if hasattr(doc, "get_code_fields"): + dirname, filename = os.path.split(path) + for key, extn in doc.get_code_fields().items(): + codefile = os.path.join(dirname, filename.split(".")[0] + "." + extn) + if os.path.exists(codefile): + with open(codefile) as txtfile: + doc.set(key, txtfile.read()) + + +def delete_old_doc(doc, reset_permissions): + ignore = [] + old_doc = influxframework.get_doc(doc.doctype, doc.name) + + if doc.doctype in ignore_values: + # update ignore values + for key in ignore_values.get(doc.doctype) or []: + doc.set(key, old_doc.get(key)) + + # update ignored docs into new doc + for df in doc.meta.get_table_fields(): + if df.options in ignore_doctypes and not reset_permissions: + doc.set(df.fieldname, []) + ignore.append(df.options) + + # delete old + influxframework.delete_doc(doc.doctype, doc.name, force=1, ignore_doctypes=ignore, for_reload=True) + + doc.flags.ignore_children_type = ignore + + +def reset_tree_properties(doc): + # Note on Tree DocTypes: + # The tree structure is maintained in the database via the fields "lft" and + # "rgt". They are automatically set and kept up-to-date. Importing them + # would destroy any existing tree structure. + if getattr(doc.meta, "is_tree", None) and any([doc.lft, doc.rgt]): + print(f'Ignoring values of `lft` and `rgt` for {doc.doctype} "{doc.name}"') + doc.lft = None + doc.rgt = None diff --git a/influxframework/modules/patch_handler.py b/influxframework/modules/patch_handler.py new file mode 100644 index 0000000..c2bc113 --- /dev/null +++ b/influxframework/modules/patch_handler.py @@ -0,0 +1,230 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE +""" Patch Handler. + +This file manages execution of manaully written patches. Patches are script +that apply changes in database schema or data to accomodate for changes in the +code. + +Ways to specify patches: + +1. patches.txt file specifies patches that run before doctype schema +migration. Each line represents one patch (old format). +2. patches.txt can alternatively also separate pre and post model sync +patches by using INI like file format: + ```patches.txt + [pre_model_sync] + app.module.patch1 + app.module.patch2 + + + [post_model_sync] + app.module.patch3 + ``` + + When different sections are specified patches are executed in this order: + 1. Run pre_model_sync patches + 2. Reload/resync all doctype schema + 3. Run post_model_sync patches + + Hence any patch that just needs to modify data but doesn't depend on + old schema should be added to post_model_sync section of file. + +3. simple python commands can be added by starting line with `execute:` +`execute:` example: `execute:print("hello world")` +""" + +import configparser +import time +from enum import Enum +from textwrap import dedent, indent + +import influxframework + + +class PatchError(Exception): + pass + + +class PatchType(Enum): + pre_model_sync = "pre_model_sync" + post_model_sync = "post_model_sync" + + +def run_all(skip_failing: bool = False, patch_type: PatchType | None = None) -> None: + """run all pending patches""" + executed = set(influxframework.get_all("Patch Log", fields="patch", pluck="patch")) + + influxframework.flags.final_patches = [] + + def run_patch(patch): + try: + if not run_single(patchmodule=patch): + print(patch + ": failed: STOPPED") + raise PatchError(patch) + except Exception: + if not skip_failing: + raise + else: + print("Failed to execute patch") + + patches = get_all_patches(patch_type=patch_type) + + for patch in patches: + if patch and (patch not in executed): + run_patch(patch) + + # patches to be run in the end + for patch in influxframework.flags.final_patches: + patch = patch.replace("finally:", "") + run_patch(patch) + + +def get_all_patches(patch_type: PatchType | None = None) -> list[str]: + + if patch_type and not isinstance(patch_type, PatchType): + influxframework.throw(f"Unsupported patch type specified: {patch_type}") + + patches = [] + for app in influxframework.get_installed_apps(): + patches.extend(get_patches_from_app(app, patch_type=patch_type)) + + return patches + + +def get_patches_from_app(app: str, patch_type: PatchType | None = None) -> list[str]: + """Get patches from an app's patches.txt + + patches.txt can be: + 1. ini like file with section for different patch_type + 2. plain text file with each line representing a patch. + """ + + patches_txt = influxframework.get_pymodule_path(app, "patches.txt") + + try: + # Attempt to parse as ini file with pre/post patches + # allow_no_value: patches are not key value pairs + # delimiters = '\n' to avoid treating default `:` and `=` in execute as k:v delimiter + parser = configparser.ConfigParser(allow_no_value=True, delimiters="\n") + # preserve case + parser.optionxform = str + parser.read(patches_txt) + + # empty file + if not parser.sections(): + return [] + + if not patch_type: + return [patch for patch in parser[PatchType.pre_model_sync.value]] + [ + patch for patch in parser[PatchType.post_model_sync.value] + ] + + if patch_type.value in parser.sections(): + return [patch for patch in parser[patch_type.value]] + else: + influxframework.throw(influxframework._("Patch type {} not found in patches.txt").format(patch_type)) + + except configparser.MissingSectionHeaderError: + # treat as old format with each line representing a single patch + # backward compatbility with old patches.txt format + if not patch_type or patch_type == PatchType.pre_model_sync: + return influxframework.get_file_items(patches_txt) + + return [] + + +def reload_doc(args): + import influxframework.modules + + run_single(method=influxframework.modules.reload_doc, methodargs=args) + + +def run_single(patchmodule=None, method=None, methodargs=None, force=False): + from influxframework import conf + + # don't write txt files + conf.developer_mode = 0 + + if force or method or not executed(patchmodule): + return execute_patch(patchmodule, method, methodargs) + else: + return True + + +def execute_patch(patchmodule, method=None, methodargs=None): + """execute the patch""" + _patch_mode(True) + + if patchmodule.startswith("execute:"): + has_patch_file = False + patch = patchmodule.split("execute:")[1] + docstring = "" + else: + has_patch_file = True + patch = f"{patchmodule.split()[0]}.execute" + _patch = influxframework.get_attr(patch) + docstring = _patch.__doc__ or "" + + if docstring: + docstring = "\n" + indent(dedent(docstring), "\t") + + print( + f"Executing {patchmodule or methodargs} in {influxframework.local.site} ({influxframework.db.cur_db_name}){docstring}" + ) + + start_time = time.time() + influxframework.db.begin() + influxframework.db.auto_commit_on_many_writes = 0 + try: + if patchmodule: + if patchmodule.startswith("finally:"): + # run run patch at the end + influxframework.flags.final_patches.append(patchmodule) + else: + if has_patch_file: + _patch() + else: + exec(patch, globals()) + update_patch_log(patchmodule) + + elif method: + method(**methodargs) + + except Exception: + influxframework.db.rollback() + raise + + else: + influxframework.db.commit() + end_time = time.time() + _patch_mode(False) + print(f"Success: Done in {round(end_time - start_time, 3)}s") + + return True + + +def update_patch_log(patchmodule): + """update patch_file in patch log""" + influxframework.get_doc({"doctype": "Patch Log", "patch": patchmodule}).insert(ignore_permissions=True) + + +def executed(patchmodule): + """return True if is executed""" + if patchmodule.startswith("finally:"): + # patches are saved without the finally: tag + patchmodule = patchmodule.replace("finally:", "") + return influxframework.db.get_value("Patch Log", {"patch": patchmodule}) + + +def _patch_mode(enable): + """stop/start execution till patch is run""" + influxframework.local.flags.in_patch = enable + influxframework.db.commit() + + +def check_session_stopped(): + """This function is deprecated. Use maintenance_mode in site config instead.""" + if influxframework.db.get_global("__session_status") == "stop": + influxframework.msgprint(influxframework.db.get_global("__session_status_message")) + raise influxframework.SessionStopped("Session Stopped") diff --git a/influxframework/modules/utils.py b/influxframework/modules/utils.py new file mode 100644 index 0000000..051c439 --- /dev/null +++ b/influxframework/modules/utils.py @@ -0,0 +1,337 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +""" + Utilities for using modules +""" +import json +import os + +import influxframework +import influxframework.utils +from influxframework import _ +from influxframework.utils import cint + + +def export_module_json(doc, is_standard, module): + """Make a folder for the given doc and add its json file (make it a standard + object that will be synced)""" + if not influxframework.flags.in_import and getattr(influxframework.get_conf(), "developer_mode", 0) and is_standard: + from influxframework.modules.export_file import export_to_files + + # json + export_to_files( + record_list=[[doc.doctype, doc.name]], record_module=module, create_init=is_standard + ) + + path = os.path.join( + influxframework.get_module_path(module), scrub(doc.doctype), scrub(doc.name), scrub(doc.name) + ) + + return path + + +def get_doc_module(module, doctype, name): + """Get custom module for given document""" + module_name = "{app}.{module}.{doctype}.{name}.{name}".format( + app=influxframework.local.module_app[scrub(module)], + doctype=scrub(doctype), + module=scrub(module), + name=scrub(name), + ) + return influxframework.get_module(module_name) + + +@influxframework.whitelist() +def export_customizations(module, doctype, sync_on_migrate=0, with_permissions=0): + """Export Custom Field and Property Setter for the current document to the app folder. + This will be synced with bench migrate""" + + sync_on_migrate = cint(sync_on_migrate) + with_permissions = cint(with_permissions) + + if not influxframework.get_conf().developer_mode: + raise Exception("Not developer mode") + + custom = { + "custom_fields": [], + "property_setters": [], + "custom_perms": [], + "links": [], + "doctype": doctype, + "sync_on_migrate": sync_on_migrate, + } + + def add(_doctype): + custom["custom_fields"] += influxframework.get_all("Custom Field", fields="*", filters={"dt": _doctype}) + custom["property_setters"] += influxframework.get_all( + "Property Setter", fields="*", filters={"doc_type": _doctype} + ) + custom["links"] += influxframework.get_all("DocType Link", fields="*", filters={"parent": _doctype}) + + add(doctype) + + if with_permissions: + custom["custom_perms"] = influxframework.get_all( + "Custom DocPerm", fields="*", filters={"parent": doctype} + ) + + # also update the custom fields and property setters for all child tables + for d in influxframework.get_meta(doctype).get_table_fields(): + export_customizations(module, d.options, sync_on_migrate, with_permissions) + + if custom["custom_fields"] or custom["property_setters"] or custom["custom_perms"]: + folder_path = os.path.join(get_module_path(module), "custom") + if not os.path.exists(folder_path): + os.makedirs(folder_path) + + path = os.path.join(folder_path, scrub(doctype) + ".json") + with open(path, "w") as f: + f.write(influxframework.as_json(custom)) + + influxframework.msgprint(_("Customizations for {0} exported to:
      {1}").format(doctype, path)) + + +def sync_customizations(app=None): + """Sync custom fields and property setters from custom folder in each app module""" + + if app: + apps = [app] + else: + apps = influxframework.get_installed_apps() + + for app_name in apps: + for module_name in influxframework.local.app_modules.get(app_name) or []: + folder = influxframework.get_app_path(app_name, module_name, "custom") + if os.path.exists(folder): + for fname in os.listdir(folder): + if fname.endswith(".json"): + with open(os.path.join(folder, fname)) as f: + data = json.loads(f.read()) + if data.get("sync_on_migrate"): + sync_customizations_for_doctype(data, folder) + + +def sync_customizations_for_doctype(data, folder): + """Sync doctype customzations for a particular data set""" + from influxframework.core.doctype.doctype.doctype import validate_fields_for_doctype + + doctype = data["doctype"] + update_schema = False + + def sync(key, custom_doctype, doctype_fieldname): + doctypes = list(set(map(lambda row: row.get(doctype_fieldname), data[key]))) + + # sync single doctype exculding the child doctype + def sync_single_doctype(doc_type): + def _insert(data): + if data.get(doctype_fieldname) == doc_type: + data["doctype"] = custom_doctype + doc = influxframework.get_doc(data) + doc.db_insert() + + if custom_doctype != "Custom Field": + influxframework.db.delete(custom_doctype, {doctype_fieldname: doc_type}) + + for d in data[key]: + _insert(d) + + else: + for d in data[key]: + field = influxframework.db.get_value("Custom Field", {"dt": doc_type, "fieldname": d["fieldname"]}) + if not field: + d["owner"] = "Administrator" + _insert(d) + else: + custom_field = influxframework.get_doc("Custom Field", field) + custom_field.flags.ignore_validate = True + custom_field.update(d) + custom_field.db_update() + + for doc_type in doctypes: + # only sync the parent doctype and child doctype if there isn't any other child table json file + if doc_type == doctype or not os.path.exists( + os.path.join(folder, influxframework.scrub(doc_type) + ".json") + ): + sync_single_doctype(doc_type) + + if data["custom_fields"]: + sync("custom_fields", "Custom Field", "dt") + update_schema = True + + if data["property_setters"]: + sync("property_setters", "Property Setter", "doc_type") + + if data.get("custom_perms"): + sync("custom_perms", "Custom DocPerm", "parent") + + print(f"Updating customizations for {doctype}") + validate_fields_for_doctype(doctype) + + if update_schema and not influxframework.db.get_value("DocType", doctype, "issingle"): + influxframework.db.updatedb(doctype) + + +def scrub(txt): + return influxframework.scrub(txt) + + +def scrub_dt_dn(dt, dn): + """Returns in lowercase and code friendly names of doctype and name for certain types""" + return scrub(dt), scrub(dn) + + +def get_module_path(module): + """Returns path of the given module""" + return influxframework.get_module_path(module) + + +def get_doc_path(module, doctype, name): + dt, dn = scrub_dt_dn(doctype, name) + return os.path.join(get_module_path(module), dt, dn) + + +def reload_doc(module, dt=None, dn=None, force=False, reset_permissions=False): + from influxframework.modules.import_file import import_files + + return import_files(module, dt, dn, force=force, reset_permissions=reset_permissions) + + +def export_doc(doctype, name, module=None): + """Write a doc to standard path.""" + from influxframework.modules.export_file import write_document_file + + print(doctype, name) + + if not module: + module = influxframework.db.get_value("DocType", name, "module") + write_document_file(influxframework.get_doc(doctype, name), module) + + +def get_doctype_module(doctype) -> str: + """Returns **Module Def** name of given doctype.""" + + def make_modules_dict(): + return dict(influxframework.db.sql("select name, module from tabDocType")) + + doctype_module_map = influxframework.cache().get_value("doctype_modules", make_modules_dict) + + if module_name := doctype_module_map.get(doctype): + return module_name + else: + influxframework.throw(_("DocType {} not found").format(doctype), exc=influxframework.DoesNotExistError) + + +doctype_python_modules = {} + + +def load_doctype_module(doctype, module=None, prefix="", suffix=""): + """Returns the module object for given doctype.""" + if not module: + module = get_doctype_module(doctype) + + app = get_module_app(module) + + key = (app, doctype, prefix, suffix) + + module_name = get_module_name(doctype, module, prefix, suffix) + + try: + if key not in doctype_python_modules: + doctype_python_modules[key] = influxframework.get_module(module_name) + except ImportError as e: + msg = f"Module import failed for {doctype}, the DocType you're trying to open might be deleted." + msg += f"
      Error: {e}" + raise ImportError(msg) from e + + return doctype_python_modules[key] + + +def get_module_name(doctype, module, prefix="", suffix="", app=None): + return "{app}.{module}.doctype.{doctype}.{prefix}{doctype}{suffix}".format( + app=scrub(app or get_module_app(module)), + module=scrub(module), + doctype=scrub(doctype), + prefix=prefix, + suffix=suffix, + ) + + +def get_module_app(module: str) -> str: + app = influxframework.local.module_app.get(scrub(module)) + if app is None: + influxframework.throw(_("Module {} not found").format(module), exc=influxframework.DoesNotExistError) + return app + + +def get_app_publisher(module: str) -> str: + app = get_module_app(module) + if not app: + influxframework.throw(_("App not found")) + app_publisher = influxframework.get_hooks(hook="app_publisher", app_name=app)[0] + return app_publisher + + +def make_boilerplate(template, doc, opts=None): + target_path = get_doc_path(doc.module, doc.doctype, doc.name) + template_name = template.replace("controller", scrub(doc.name)) + if template_name.endswith("._py"): + template_name = template_name[:-4] + ".py" + target_file_path = os.path.join(target_path, template_name) + + if not doc: + doc = {} + + app_publisher = get_app_publisher(doc.module) + + if not os.path.exists(target_file_path): + if not opts: + opts = {} + + base_class = "Document" + base_class_import = "from influxframework.model.document import Document" + if doc.get("is_tree"): + base_class = "NestedSet" + base_class_import = "from influxframework.utils.nestedset import NestedSet" + + custom_controller = "pass" + if doc.get("is_virtual"): + custom_controller = """ + def db_insert(self, *args, **kwargs): + pass + + def load_from_db(self): + pass + + def db_update(self, *args, **kwargs): + pass + + @staticmethod + def get_list(args): + pass + + @staticmethod + def get_count(args): + pass + + @staticmethod + def get_stats(args): + pass""" + + with open(target_file_path, "w") as target: + with open( + os.path.join(get_module_path("core"), "doctype", scrub(doc.doctype), "boilerplate", template), + ) as source: + target.write( + influxframework.as_unicode( + influxframework.utils.cstr(source.read()).format( + app_publisher=app_publisher, + year=influxframework.utils.nowdate()[:4], + classname=doc.name.replace(" ", "").replace("-", ""), + base_class_import=base_class_import, + base_class=base_class, + doctype=doc.name, + **opts, + custom_controller=custom_controller, + ) + ) + ) diff --git a/influxframework/monitor.py b/influxframework/monitor.py new file mode 100644 index 0000000..b7abd51 --- /dev/null +++ b/influxframework/monitor.py @@ -0,0 +1,124 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +import json +import os +import traceback +import uuid +from datetime import datetime + +import rq + +import influxframework + +MONITOR_REDIS_KEY = "monitor-transactions" +MONITOR_MAX_ENTRIES = 1000000 + + +def start(transaction_type="request", method=None, kwargs=None): + if influxframework.conf.monitor: + influxframework.local.monitor = Monitor(transaction_type, method, kwargs) + + +def stop(response=None): + if hasattr(influxframework.local, "monitor"): + influxframework.local.monitor.dump(response) + + +def add_data_to_monitor(**kwargs) -> None: + """Add additional custom key-value pairs along with monitor log. + Note: Key-value pairs should be simple JSON exportable types.""" + if hasattr(influxframework.local, "monitor"): + influxframework.local.monitor.add_custom_data(**kwargs) + + +def log_file(): + return os.path.join(influxframework.utils.get_bench_path(), "logs", "monitor.json.log") + + +class Monitor: + __slots__ = ("data",) + + def __init__(self, transaction_type, method, kwargs): + try: + self.data = influxframework._dict( + { + "site": influxframework.local.site, + "timestamp": datetime.utcnow(), + "transaction_type": transaction_type, + "uuid": str(uuid.uuid4()), + } + ) + + if transaction_type == "request": + self.collect_request_meta() + else: + self.collect_job_meta(method, kwargs) + except Exception: + traceback.print_exc() + + def collect_request_meta(self): + self.data.request = influxframework._dict( + { + "ip": influxframework.local.request_ip, + "method": influxframework.request.method, + "path": influxframework.request.path, + } + ) + + def collect_job_meta(self, method, kwargs): + self.data.job = influxframework._dict({"method": method, "scheduled": False, "wait": 0}) + if "run_scheduled_job" in method: + self.data.job.method = kwargs["job_type"] + self.data.job.scheduled = True + + job = rq.get_current_job() + if job: + self.data.uuid = job.id + waitdiff = self.data.timestamp - job.enqueued_at + self.data.job.wait = int(waitdiff.total_seconds() * 1000000) + + def add_custom_data(self, **kwargs): + if self.data: + self.data.update(kwargs) + + def dump(self, response=None): + try: + timediff = datetime.utcnow() - self.data.timestamp + # Obtain duration in microseconds + self.data.duration = int(timediff.total_seconds() * 1000000) + + if self.data.transaction_type == "request": + self.data.request.status_code = response.status_code + self.data.request.response_length = int(response.headers.get("Content-Length", 0)) + + if hasattr(influxframework.local, "rate_limiter"): + limiter = influxframework.local.rate_limiter + self.data.request.counter = limiter.counter + if limiter.rejected: + self.data.request.reset = limiter.reset + + self.store() + except Exception: + traceback.print_exc() + + def store(self): + if influxframework.cache().llen(MONITOR_REDIS_KEY) > MONITOR_MAX_ENTRIES: + influxframework.cache().ltrim(MONITOR_REDIS_KEY, 1, -1) + serialized = json.dumps(self.data, sort_keys=True, default=str, separators=(",", ":")) + influxframework.cache().rpush(MONITOR_REDIS_KEY, serialized) + + +def flush(): + try: + # Fetch all the logs without removing from cache + logs = influxframework.cache().lrange(MONITOR_REDIS_KEY, 0, -1) + if logs: + logs = list(map(influxframework.safe_decode, logs)) + with open(log_file(), "a", os.O_NONBLOCK) as f: + f.write("\n".join(logs)) + f.write("\n") + # Remove fetched entries from cache + influxframework.cache().ltrim(MONITOR_REDIS_KEY, len(logs) - 1, -1) + except Exception: + traceback.print_exc() diff --git a/influxframework/oauth.py b/influxframework/oauth.py new file mode 100644 index 0000000..30780ef --- /dev/null +++ b/influxframework/oauth.py @@ -0,0 +1,621 @@ +import base64 +import datetime +import hashlib +import re +from http import cookies +from urllib.parse import unquote, urlparse + +import jwt +import pytz +from oauthlib.openid import RequestValidator + +import influxframework +from influxframework.auth import LoginManager + + +class OAuthWebRequestValidator(RequestValidator): + + # Pre- and post-authorization. + def validate_client_id(self, client_id, request, *args, **kwargs): + # Simple validity check, does client exist? Not banned? + cli_id = influxframework.db.get_value("OAuth Client", {"name": client_id}) + if cli_id: + request.client = influxframework.get_doc("OAuth Client", client_id).as_dict() + return True + else: + return False + + def validate_redirect_uri(self, client_id, redirect_uri, request, *args, **kwargs): + # Is the client allowed to use the supplied redirect_uri? i.e. has + # the client previously registered this EXACT redirect uri. + + redirect_uris = influxframework.db.get_value("OAuth Client", client_id, "redirect_uris").split( + get_url_delimiter() + ) + + if redirect_uri in redirect_uris: + return True + else: + return False + + def get_default_redirect_uri(self, client_id, request, *args, **kwargs): + # The redirect used if none has been supplied. + # Prefer your clients to pre register a redirect uri rather than + # supplying one on each authorization request. + redirect_uri = influxframework.db.get_value("OAuth Client", client_id, "default_redirect_uri") + return redirect_uri + + def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs): + # Is the client allowed to access the requested scopes? + allowed_scopes = get_client_scopes(client_id) + return all(scope in allowed_scopes for scope in scopes) + + def get_default_scopes(self, client_id, request, *args, **kwargs): + # Scopes a client will authorize for if none are supplied in the + # authorization request. + scopes = get_client_scopes(client_id) + request.scopes = scopes # Apparently this is possible. + return scopes + + def validate_response_type(self, client_id, response_type, client, request, *args, **kwargs): + allowed_response_types = [ + # From OAuth Client response_type field + client.response_type.lower(), + # OIDC + "id_token", + "id_token token", + "code id_token", + "code token id_token", + ] + + return response_type in allowed_response_types + + # Post-authorization + + def save_authorization_code(self, client_id, code, request, *args, **kwargs): + + cookie_dict = get_cookie_dict_from_headers(request) + + oac = influxframework.new_doc("OAuth Authorization Code") + oac.scopes = get_url_delimiter().join(request.scopes) + oac.redirect_uri_bound_to_authorization_code = request.redirect_uri + oac.client = client_id + oac.user = unquote(cookie_dict["user_id"].value) + oac.authorization_code = code["code"] + + if request.nonce: + oac.nonce = request.nonce + + if request.code_challenge and request.code_challenge_method: + oac.code_challenge = request.code_challenge + oac.code_challenge_method = request.code_challenge_method.lower() + + oac.save(ignore_permissions=True) + influxframework.db.commit() + + def authenticate_client(self, request, *args, **kwargs): + # Get ClientID in URL + if request.client_id: + oc = influxframework.get_doc("OAuth Client", request.client_id) + else: + # Extract token, instantiate OAuth Bearer Token and use clientid from there. + if "refresh_token" in influxframework.form_dict: + oc = influxframework.get_doc( + "OAuth Client", + influxframework.db.get_value( + "OAuth Bearer Token", + {"refresh_token": influxframework.form_dict["refresh_token"]}, + "client", + ), + ) + elif "token" in influxframework.form_dict: + oc = influxframework.get_doc( + "OAuth Client", + influxframework.db.get_value("OAuth Bearer Token", influxframework.form_dict["token"], "client"), + ) + else: + oc = influxframework.get_doc( + "OAuth Client", + influxframework.db.get_value( + "OAuth Bearer Token", + influxframework.get_request_header("Authorization").split(" ")[1], + "client", + ), + ) + try: + request.client = request.client or oc.as_dict() + except Exception as e: + return generate_json_error_response(e) + + cookie_dict = get_cookie_dict_from_headers(request) + user_id = unquote(cookie_dict.get("user_id").value) if "user_id" in cookie_dict else "Guest" + return influxframework.session.user == user_id + + def authenticate_client_id(self, client_id, request, *args, **kwargs): + cli_id = influxframework.db.get_value("OAuth Client", client_id, "name") + if not cli_id: + # Don't allow public (non-authenticated) clients + return False + else: + request["client"] = influxframework.get_doc("OAuth Client", cli_id) + return True + + def validate_code(self, client_id, code, client, request, *args, **kwargs): + # Validate the code belongs to the client. Add associated scopes, + # state and user to request.scopes and request.user. + + validcodes = influxframework.get_all( + "OAuth Authorization Code", + filters={"client": client_id, "validity": "Valid"}, + ) + + checkcodes = [] + for vcode in validcodes: + checkcodes.append(vcode["name"]) + + if code in checkcodes: + request.scopes = influxframework.db.get_value("OAuth Authorization Code", code, "scopes").split( + get_url_delimiter() + ) + request.user = influxframework.db.get_value("OAuth Authorization Code", code, "user") + code_challenge_method = influxframework.db.get_value( + "OAuth Authorization Code", code, "code_challenge_method" + ) + code_challenge = influxframework.db.get_value("OAuth Authorization Code", code, "code_challenge") + + if code_challenge and not request.code_verifier: + if influxframework.db.exists("OAuth Authorization Code", code): + influxframework.delete_doc("OAuth Authorization Code", code, ignore_permissions=True) + influxframework.db.commit() + return False + + if code_challenge_method == "s256": + m = hashlib.sha256() + m.update(bytes(request.code_verifier, "utf-8")) + code_verifier = base64.b64encode(m.digest()).decode("utf-8") + code_verifier = re.sub(r"\+", "-", code_verifier) + code_verifier = re.sub(r"\/", "_", code_verifier) + code_verifier = re.sub(r"=", "", code_verifier) + return code_challenge == code_verifier + + elif code_challenge_method == "plain": + return code_challenge == request.code_verifier + + return True + + return False + + def confirm_redirect_uri(self, client_id, code, redirect_uri, client, *args, **kwargs): + saved_redirect_uri = influxframework.db.get_value("OAuth Client", client_id, "default_redirect_uri") + + redirect_uris = influxframework.db.get_value("OAuth Client", client_id, "redirect_uris") + + if redirect_uris: + redirect_uris = redirect_uris.split(get_url_delimiter()) + return redirect_uri in redirect_uris + + return saved_redirect_uri == redirect_uri + + def validate_grant_type(self, client_id, grant_type, client, request, *args, **kwargs): + # Clients should only be allowed to use one type of grant. + # In this case, it must be "authorization_code" or "refresh_token" + return grant_type in ["authorization_code", "refresh_token", "password"] + + def save_bearer_token(self, token, request, *args, **kwargs): + # Remember to associate it with request.scopes, request.user and + # request.client. The two former will be set when you validate + # the authorization code. Don't forget to save both the + # access_token and the refresh_token and set expiration for the + # access_token to now + expires_in seconds. + + otoken = influxframework.new_doc("OAuth Bearer Token") + otoken.client = request.client["name"] + try: + otoken.user = ( + request.user + if request.user + else influxframework.db.get_value( + "OAuth Bearer Token", + {"refresh_token": request.body.get("refresh_token")}, + "user", + ) + ) + except Exception: + otoken.user = influxframework.session.user + + otoken.scopes = get_url_delimiter().join(request.scopes) + otoken.access_token = token["access_token"] + otoken.refresh_token = token.get("refresh_token") + otoken.expires_in = token["expires_in"] + otoken.save(ignore_permissions=True) + influxframework.db.commit() + + default_redirect_uri = influxframework.db.get_value( + "OAuth Client", request.client["name"], "default_redirect_uri" + ) + return default_redirect_uri + + def invalidate_authorization_code(self, client_id, code, request, *args, **kwargs): + # Authorization codes are use once, invalidate it when a Bearer token + # has been acquired. + + influxframework.db.set_value("OAuth Authorization Code", code, "validity", "Invalid") + influxframework.db.commit() + + # Protected resource request + + def validate_bearer_token(self, token, scopes, request): + # Remember to check expiration and scope membership + otoken = influxframework.get_doc("OAuth Bearer Token", token) + token_expiration_local = otoken.expiration_time.replace( + tzinfo=pytz.timezone(influxframework.utils.get_time_zone()) + ) + token_expiration_utc = token_expiration_local.astimezone(pytz.utc) + is_token_valid = ( + influxframework.utils.datetime.datetime.utcnow().replace(tzinfo=pytz.utc) < token_expiration_utc + ) and otoken.status != "Revoked" + client_scopes = influxframework.db.get_value("OAuth Client", otoken.client, "scopes").split( + get_url_delimiter() + ) + are_scopes_valid = True + for scp in scopes: + are_scopes_valid = are_scopes_valid and True if scp in client_scopes else False + + return is_token_valid and are_scopes_valid + + # Token refresh request + + def get_original_scopes(self, refresh_token, request, *args, **kwargs): + # Obtain the token associated with the given refresh_token and + # return its scopes, these will be passed on to the refreshed + # access token if the client did not specify a scope during the + # request. + obearer_token = influxframework.get_doc("OAuth Bearer Token", {"refresh_token": refresh_token}) + return obearer_token.scopes + + def revoke_token(self, token, token_type_hint, request, *args, **kwargs): + """Revoke an access or refresh token. + + :param token: The token string. + :param token_type_hint: access_token or refresh_token. + :param request: The HTTP Request (oauthlib.common.Request) + + Method is used by: + - Revocation Endpoint + """ + if token_type_hint == "access_token": + influxframework.db.set_value("OAuth Bearer Token", token, "status", "Revoked") + elif token_type_hint == "refresh_token": + influxframework.db.set_value("OAuth Bearer Token", {"refresh_token": token}, "status", "Revoked") + else: + influxframework.db.set_value("OAuth Bearer Token", token, "status", "Revoked") + influxframework.db.commit() + + def validate_refresh_token(self, refresh_token, client, request, *args, **kwargs): + """Ensure the Bearer token is valid and authorized access to scopes. + + OBS! The request.user attribute should be set to the resource owner + associated with this refresh token. + + :param refresh_token: Unicode refresh token + :param client: Client object set by you, see authenticate_client. + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is used by: + - Authorization Code Grant (indirectly by issuing refresh tokens) + - Resource Owner Password Credentials Grant (also indirectly) + - Refresh Token Grant + """ + + otoken = influxframework.get_doc( + "OAuth Bearer Token", {"refresh_token": refresh_token, "status": "Active"} + ) + + if not otoken: + return False + else: + return True + + # OpenID Connect + + def finalize_id_token(self, id_token, token, token_handler, request): + # Check whether influxframework server URL is set + id_token_header = {"typ": "jwt", "alg": "HS256"} + + user = influxframework.get_doc("User", request.user) + + if request.nonce: + id_token["nonce"] = request.nonce + + userinfo = get_userinfo(user) + + if userinfo.get("iss"): + id_token["iss"] = userinfo.get("iss") + + if "openid" in request.scopes: + id_token.update(userinfo) + + id_token_encoded = jwt.encode( + payload=id_token, + key=request.client.client_secret, + algorithm="HS256", + headers=id_token_header, + ) + + return influxframework.safe_decode(id_token_encoded) + + def get_authorization_code_nonce(self, client_id, code, redirect_uri, request): + if influxframework.get_value("OAuth Authorization Code", code, "validity") == "Valid": + return influxframework.get_value("OAuth Authorization Code", code, "nonce") + + return None + + def get_authorization_code_scopes(self, client_id, code, redirect_uri, request): + scope = influxframework.get_value("OAuth Client", client_id, "scopes") + if not scope: + scope = [] + else: + scope = scope.split(get_url_delimiter()) + + return scope + + def get_jwt_bearer_token(self, token, token_handler, request): + now = datetime.datetime.now() + id_token = dict( + aud=token.client_id, + iat=round(now.timestamp()), + at_hash=calculate_at_hash(token.access_token, hashlib.sha256), + ) + return self.finalize_id_token(id_token, token, token_handler, request) + + def get_userinfo_claims(self, request): + user = influxframework.get_doc("User", influxframework.session.user) + userinfo = get_userinfo(user) + return userinfo + + def validate_id_token(self, token, scopes, request): + try: + id_token = influxframework.get_doc("OAuth Bearer Token", token) + if id_token.status == "Active": + return True + except Exception: + return False + + return False + + def validate_jwt_bearer_token(self, token, scopes, request): + try: + jwt = influxframework.get_doc("OAuth Bearer Token", token) + if jwt.status == "Active": + return True + except Exception: + return False + + return False + + def validate_silent_authorization(self, request): + """Ensure the logged in user has authorized silent OpenID authorization. + + Silent OpenID authorization allows access tokens and id tokens to be + granted to clients without any user prompt or interaction. + + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is used by: + - OpenIDConnectAuthCode + - OpenIDConnectImplicit + - OpenIDConnectHybrid + """ + if request.prompt == "login": + False + else: + True + + def validate_silent_login(self, request): + """Ensure session user has authorized silent OpenID login. + + If no user is logged in or has not authorized silent login, this + method should return False. + + If the user is logged in but associated with multiple accounts and + not selected which one to link to the token then this method should + raise an oauthlib.oauth2.AccountSelectionRequired error. + + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is used by: + - OpenIDConnectAuthCode + - OpenIDConnectImplicit + - OpenIDConnectHybrid + """ + if influxframework.session.user == "Guest" or request.prompt.lower() == "login": + return False + else: + return True + + def validate_user_match(self, id_token_hint, scopes, claims, request): + """Ensure client supplied user id hint matches session user. + + If the sub claim or id_token_hint is supplied then the session + user must match the given ID. + + :param id_token_hint: User identifier string. + :param scopes: List of OAuth 2 scopes and OpenID claims (strings). + :param claims: OpenID Connect claims dict. + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is used by: + - OpenIDConnectAuthCode + - OpenIDConnectImplicit + - OpenIDConnectHybrid + """ + if id_token_hint: + try: + user = None + payload = jwt.decode( + id_token_hint, + algorithms=["HS256"], + options={ + "verify_signature": False, + "verify_aud": False, + }, + ) + client_id, client_secret = influxframework.get_value( + "OAuth Client", + payload.get("aud"), + ["client_id", "client_secret"], + ) + + if payload.get("sub") and client_id and client_secret: + user = influxframework.db.get_value( + "User Social Login", + {"userid": payload.get("sub"), "provider": "influxframework"}, + "parent", + ) + user = influxframework.get_doc("User", user) + verified_payload = jwt.decode( + id_token_hint, + key=client_secret, + audience=client_id, + algorithms=["HS256"], + options={ + "verify_exp": False, + }, + ) + + if verified_payload: + return user.name == influxframework.session.user + + except Exception: + return False + + elif influxframework.session.user != "Guest": + return True + + return False + + def validate_user(self, username, password, client, request, *args, **kwargs): + """Ensure the username and password is valid. + + Method is used by: + - Resource Owner Password Credentials Grant + """ + login_manager = LoginManager() + login_manager.authenticate(username, password) + + if login_manager.user == "Guest": + return False + + request.user = login_manager.user + return True + + +def get_cookie_dict_from_headers(r): + cookie = cookies.BaseCookie() + if r.headers.get("Cookie"): + cookie.load(r.headers.get("Cookie")) + return cookie + + +def calculate_at_hash(access_token, hash_alg): + """Helper method for calculating an access token + hash, as described in http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken + Its value is the base64url encoding of the left-most half of the hash of the octets + of the ASCII representation of the access_token value, where the hash algorithm + used is the hash algorithm used in the alg Header Parameter of the ID Token's JOSE + Header. For instance, if the alg is RS256, hash the access_token value with SHA-256, + then take the left-most 128 bits and base64url encode them. The at_hash value is a + case sensitive string. + Args: + access_token (str): An access token string. + hash_alg (callable): A callable returning a hash object, e.g. hashlib.sha256 + """ + hash_digest = hash_alg(access_token.encode("utf-8")).digest() + cut_at = int(len(hash_digest) / 2) + truncated = hash_digest[:cut_at] + from jwt.utils import base64url_encode + + at_hash = base64url_encode(truncated) + return at_hash.decode("utf-8") + + +def delete_oauth2_data(): + # Delete Invalid Authorization Code and Revoked Token + commit_code, commit_token = False, False + code_list = influxframework.get_all("OAuth Authorization Code", filters={"validity": "Invalid"}) + token_list = influxframework.get_all("OAuth Bearer Token", filters={"status": "Revoked"}) + if len(code_list) > 0: + commit_code = True + if len(token_list) > 0: + commit_token = True + for code in code_list: + influxframework.delete_doc("OAuth Authorization Code", code["name"]) + for token in token_list: + influxframework.delete_doc("OAuth Bearer Token", token["name"]) + if commit_code or commit_token: + influxframework.db.commit() + + +def get_client_scopes(client_id): + scopes_string = influxframework.db.get_value("OAuth Client", client_id, "scopes") + return scopes_string.split() + + +def get_userinfo(user): + picture = None + influxframework_server_url = get_server_url() + valid_url_schemes = ("http", "https", "ftp", "ftps") + + if user.user_image: + if influxframework.utils.validate_url(user.user_image, valid_schemes=valid_url_schemes): + picture = user.user_image + else: + picture = influxframework_server_url + "/" + user.user_image + + userinfo = influxframework._dict( + { + "sub": influxframework.db.get_value( + "User Social Login", + {"parent": user.name, "provider": "influxframework"}, + "userid", + ), + "name": " ".join(filter(None, [user.first_name, user.last_name])), + "given_name": user.first_name, + "family_name": user.last_name, + "email": user.email, + "picture": picture, + "roles": influxframework.get_roles(user.name), + "iss": influxframework_server_url, + } + ) + + return userinfo + + +def get_url_delimiter(separator_character=" "): + return separator_character + + +def generate_json_error_response(e): + if not e: + e = influxframework._dict({}) + + influxframework.local.response = influxframework._dict( + { + "description": getattr(e, "description", "Internal Server Error"), + "status_code": getattr(e, "status_code", 500), + "error": getattr(e, "error", "internal_server_error"), + } + ) + influxframework.local.response["http_status_code"] = getattr(e, "status_code", 500) + return + + +def get_server_url(): + request_url = urlparse(influxframework.request.url) + request_url = f"{request_url.scheme}://{request_url.netloc}" + return influxframework.get_value("Social Login Key", "influxframework", "base_url") or request_url diff --git a/influxframework/parallel_test_runner.py b/influxframework/parallel_test_runner.py new file mode 100644 index 0000000..6490eb3 --- /dev/null +++ b/influxframework/parallel_test_runner.py @@ -0,0 +1,308 @@ +import json +import os +import re +import sys +import time +import unittest + +import click +import requests + +import influxframework + +from .test_runner import SLOW_TEST_THRESHOLD, make_test_records, set_test_email_config + +click_ctx = click.get_current_context(True) +if click_ctx: + click_ctx.color = True + + +class ParallelTestRunner: + def __init__(self, app, site, build_number=1, total_builds=1, dry_run=False): + self.app = app + self.site = site + self.build_number = influxframework.utils.cint(build_number) or 1 + self.total_builds = influxframework.utils.cint(total_builds) + self.dry_run = dry_run + self.setup_test_site() + self.run_tests() + + def setup_test_site(self): + influxframework.init(site=self.site) + if not influxframework.db: + influxframework.connect() + + if self.dry_run: + return + + influxframework.flags.in_test = True + influxframework.clear_cache() + influxframework.utils.scheduler.disable_scheduler() + set_test_email_config() + self.before_test_setup() + + def before_test_setup(self): + start_time = time.time() + for fn in influxframework.get_hooks("before_tests", app_name=self.app): + influxframework.get_attr(fn)() + + test_module = influxframework.get_module(f"{self.app}.tests") + + if hasattr(test_module, "global_test_dependencies"): + for doctype in test_module.global_test_dependencies: + make_test_records(doctype, commit=True) + + elapsed = time.time() - start_time + elapsed = click.style(f" ({elapsed:.03}s)", fg="red") + click.echo(f"Before Test {elapsed}") + + def run_tests(self): + self.test_result = ParallelTestResult(stream=sys.stderr, descriptions=True, verbosity=2) + + for test_file_info in self.get_test_file_list(): + self.run_tests_for_file(test_file_info) + + self.print_result() + + def run_tests_for_file(self, file_info): + if not file_info: + return + + if self.dry_run: + print("running tests from", "/".join(file_info)) + return + + influxframework.set_user("Administrator") + path, filename = file_info + module = self.get_module(path, filename) + self.create_test_dependency_records(module, path, filename) + test_suite = unittest.TestSuite() + module_test_cases = unittest.TestLoader().loadTestsFromModule(module) + test_suite.addTest(module_test_cases) + test_suite(self.test_result) + + def create_test_dependency_records(self, module, path, filename): + if hasattr(module, "test_dependencies"): + for doctype in module.test_dependencies: + make_test_records(doctype, commit=True) + + if os.path.basename(os.path.dirname(path)) == "doctype": + # test_data_migration_connector.py > data_migration_connector.json + test_record_filename = re.sub("^test_", "", filename).replace(".py", ".json") + test_record_file_path = os.path.join(path, test_record_filename) + if os.path.exists(test_record_file_path): + with open(test_record_file_path) as f: + doc = json.loads(f.read()) + doctype = doc["name"] + make_test_records(doctype, commit=True) + + def get_module(self, path, filename): + app_path = influxframework.get_pymodule_path(self.app) + relative_path = os.path.relpath(path, app_path) + if relative_path == ".": + module_name = self.app + else: + relative_path = relative_path.replace("/", ".") + module_name = os.path.splitext(filename)[0] + module_name = f"{self.app}.{relative_path}.{module_name}" + + return influxframework.get_module(module_name) + + def print_result(self): + self.test_result.printErrors() + click.echo(self.test_result) + if self.test_result.failures or self.test_result.errors: + if os.environ.get("CI"): + sys.exit(1) + + def get_test_file_list(self): + # Load balance based on total # of tests ~ each runner should get roughly same # of tests. + test_list = get_all_tests(self.app) + + test_counts = [self.get_test_count(test) for test in test_list] + test_chunks = split_by_weight(test_list, test_counts, chunk_count=self.total_builds) + + return test_chunks[self.build_number - 1] + + @staticmethod + def get_test_count(test): + """Get approximate count of tests inside a file""" + file_name = "/".join(test) + + with open(file_name) as f: + test_count = f.read().count("def test_") + + return test_count + + +def split_by_weight(work, weights, chunk_count): + """Roughly split work by respective weight while keep ordering.""" + expected_weight = sum(weights) // chunk_count + + chunks = [[] for _ in range(chunk_count)] + + chunk_no = 0 + chunk_weight = 0 + + for task, weight in zip(work, weights): + if chunk_weight > expected_weight: + chunk_weight = 0 + chunk_no += 1 + assert chunk_no < chunk_count + + chunks[chunk_no].append(task) + chunk_weight += weight + + assert len(work) == sum(len(chunk) for chunk in chunks) + assert len(chunks) == chunk_count + + return chunks + + +class ParallelTestResult(unittest.TextTestResult): + def startTest(self, test): + self.tb_locals = True + self._started_at = time.time() + super(unittest.TextTestResult, self).startTest(test) + test_class = unittest.util.strclass(test.__class__) + if not hasattr(self, "current_test_class") or self.current_test_class != test_class: + click.echo(f"\n{unittest.util.strclass(test.__class__)}") + self.current_test_class = test_class + + def getTestMethodName(self, test): + return test._testMethodName if hasattr(test, "_testMethodName") else str(test) + + def addSuccess(self, test): + super(unittest.TextTestResult, self).addSuccess(test) + elapsed = time.time() - self._started_at + threshold_passed = elapsed >= SLOW_TEST_THRESHOLD + elapsed = click.style(f" ({elapsed:.03}s)", fg="red") if threshold_passed else "" + click.echo(f" {click.style(' ✔ ', fg='green')} {self.getTestMethodName(test)}{elapsed}") + + def addError(self, test, err): + super(unittest.TextTestResult, self).addError(test, err) + click.echo(f" {click.style(' ✖ ', fg='red')} {self.getTestMethodName(test)}") + + def addFailure(self, test, err): + super(unittest.TextTestResult, self).addFailure(test, err) + click.echo(f" {click.style(' ✖ ', fg='red')} {self.getTestMethodName(test)}") + + def addSkip(self, test, reason): + super(unittest.TextTestResult, self).addSkip(test, reason) + click.echo(f" {click.style(' = ', fg='white')} {self.getTestMethodName(test)}") + + def addExpectedFailure(self, test, err): + super(unittest.TextTestResult, self).addExpectedFailure(test, err) + click.echo(f" {click.style(' ✖ ', fg='red')} {self.getTestMethodName(test)}") + + def addUnexpectedSuccess(self, test): + super(unittest.TextTestResult, self).addUnexpectedSuccess(test) + click.echo(f" {click.style(' ✔ ', fg='green')} {self.getTestMethodName(test)}") + + def printErrors(self): + click.echo("\n") + self.printErrorList(" ERROR ", self.errors, "red") + self.printErrorList(" FAIL ", self.failures, "red") + + def printErrorList(self, flavour, errors, color): + for test, err in errors: + click.echo(self.separator1) + click.echo(f"{click.style(flavour, bg=color)} {self.getDescription(test)}") + click.echo(self.separator2) + click.echo(err) + + def __str__(self): + return f"Tests: {self.testsRun}, Failing: {len(self.failures)}, Errors: {len(self.errors)}" + + +def get_all_tests(app): + test_file_list = [] + for path, folders, files in os.walk(influxframework.get_pymodule_path(app)): + for dontwalk in ("locals", ".git", "public", "__pycache__"): + if dontwalk in folders: + folders.remove(dontwalk) + + # for predictability + folders.sort() + files.sort() + + if os.path.sep.join(["doctype", "doctype", "boilerplate"]) in path: + # in /doctype/doctype/boilerplate/ + continue + + for filename in files: + if filename.startswith("test_") and filename.endswith(".py") and filename != "test_runner.py": + test_file_list.append([path, filename]) + + return test_file_list + + +class ParallelTestWithOrchestrator(ParallelTestRunner): + """ + This can be used to balance-out test time across multiple instances + This is dependent on external orchestrator which returns next test to run + + orchestrator endpoints + - register-instance (, , test_spec_list) + - get-next-test-spec (, ) + - test-completed (, ) + """ + + def __init__(self, app, site): + self.orchestrator_url = os.environ.get("ORCHESTRATOR_URL") + if not self.orchestrator_url: + click.echo("ORCHESTRATOR_URL environment variable not found!") + click.echo("Pass public URL after hosting https://github.com/influxframework/test-orchestrator") + sys.exit(1) + + self.ci_build_id = os.environ.get("CI_BUILD_ID") + self.ci_instance_id = os.environ.get("CI_INSTANCE_ID") or influxframework.generate_hash(length=10) + if not self.ci_build_id: + click.echo("CI_BUILD_ID environment variable not found!") + sys.exit(1) + + ParallelTestRunner.__init__(self, app, site) + + def run_tests(self): + self.test_status = "ongoing" + self.register_instance() + super().run_tests() + + def get_test_file_list(self): + while self.test_status == "ongoing": + yield self.get_next_test() + + def register_instance(self): + test_spec_list = get_all_tests(self.app) + response_data = self.call_orchestrator( + "register-instance", data={"test_spec_list": test_spec_list} + ) + self.is_master = response_data.get("is_master") + + def get_next_test(self): + response_data = self.call_orchestrator("get-next-test-spec") + self.test_status = response_data.get("status") + return response_data.get("next_test") + + def print_result(self): + self.call_orchestrator("test-completed") + return super().print_result() + + def call_orchestrator(self, endpoint, data=None): + if data is None: + data = {} + # add repo token header + # build id in header + headers = { + "CI-BUILD-ID": self.ci_build_id, + "CI-INSTANCE-ID": self.ci_instance_id, + "REPO-TOKEN": "2948288382838DE", + } + url = f"{self.orchestrator_url}/{endpoint}" + res = requests.get(url, json=data, headers=headers) + res.raise_for_status() + response_data = {} + if "application/json" in res.headers.get("content-type"): + response_data = res.json() + + return response_data diff --git a/influxframework/patches.txt b/influxframework/patches.txt new file mode 100644 index 0000000..183aa54 --- /dev/null +++ b/influxframework/patches.txt @@ -0,0 +1,216 @@ +[pre_model_sync] +influxframework.patches.v12_0.remove_deprecated_fields_from_doctype #3 +execute:influxframework.utils.global_search.setup_global_search_table() +execute:influxframework.reload_doc('core', 'doctype', 'doctype_action', force=True) #2019-09-23 +execute:influxframework.reload_doc('core', 'doctype', 'doctype_link', force=True) #2020-10-17 +execute:influxframework.reload_doc('core', 'doctype', 'doctype_state', force=True) #2021-12-15 +execute:influxframework.reload_doc('core', 'doctype', 'doctype', force=True) #2017-09-22 +execute:influxframework.reload_doc('core', 'doctype', 'docfield', force=True) #2018-02-20 +influxframework.patches.v11_0.drop_column_apply_user_permissions +execute:influxframework.reload_doc('core', 'doctype', 'custom_docperm') +execute:influxframework.reload_doc('core', 'doctype', 'docperm') #2018-05-29 +execute:influxframework.reload_doc('core', 'doctype', 'comment') +execute:influxframework.reload_doc('core', 'doctype', 'document_naming_rule', force=True) +execute:influxframework.reload_doc('core', 'doctype', 'module_def') #2020-08-28 +execute:influxframework.reload_doc('core', 'doctype', 'version') #2017-04-01 +execute:influxframework.reload_doc('email', 'doctype', 'document_follow') +execute:influxframework.reload_doc('core', 'doctype', 'communication_link') #2019-10-02 +execute:influxframework.reload_doc('core', 'doctype', 'has_role') +execute:influxframework.reload_doc('core', 'doctype', 'communication') #2019-10-02 +execute:influxframework.reload_doc('core', 'doctype', 'server_script') +influxframework.patches.v11_0.replicate_old_user_permissions +influxframework.patches.v11_0.reload_and_rename_view_log #2019-01-03 +influxframework.patches.v11_0.copy_fetch_data_from_options +influxframework.patches.v11_0.change_email_signature_fieldtype +execute:influxframework.reload_doc('core', 'doctype', 'activity_log') +execute:influxframework.reload_doc('core', 'doctype', 'deleted_document') +execute:influxframework.reload_doc('core', 'doctype', 'domain_settings') +influxframework.patches.v13_0.rename_custom_client_script +execute:influxframework.reload_doc('core', 'doctype', 'role') #2017-05-23 +execute:influxframework.reload_doc('core', 'doctype', 'user') #2017-10-27 +execute:influxframework.reload_doc('core', 'doctype', 'report_column') +execute:influxframework.reload_doc('core', 'doctype', 'report_filter') +execute:influxframework.reload_doc('core', 'doctype', 'report') #2020-08-25 +execute:influxframework.reload_doc('core', 'doctype', 'error_snapshot') +execute:influxframework.get_doc("User", "Guest").save() +execute:influxframework.delete_doc("DocType", "Control Panel", force=1) +execute:influxframework.delete_doc("DocType", "Tag") +execute:influxframework.db.sql("delete from `tabProperty Setter` where `property` in ('idx', '_idx')") +execute:influxframework.db.sql("update tabUser set new_password='' where ifnull(new_password, '')!=''") +execute:influxframework.permissions.reset_perms("DocType") +execute:influxframework.db.sql("delete from `tabProperty Setter` where `property` = 'idx'") +execute:influxframework.db.sql("delete from tabSessions where user is null") +execute:influxframework.delete_doc("DocType", "Backup Manager") +execute:influxframework.permissions.reset_perms("Web Page") +execute:influxframework.permissions.reset_perms("Error Snapshot") +execute:influxframework.db.sql("delete from `tabWeb Page` where ifnull(template_path, '')!=''") +execute:influxframework.core.doctype.language.language.update_language_names() # 2017-04-12 +execute:influxframework.db.set_value("Print Settings", "Print Settings", "add_draft_heading", 1) +execute:influxframework.db.set_default('language', '') +execute:influxframework.db.sql("update tabCommunication set communication_date = creation where time(communication_date) = 0") +execute:influxframework.rename_doc('Country', 'Macedonia, Republic of', 'Macedonia', ignore_if_exists=True) +execute:influxframework.rename_doc('Country', 'Iran, Islamic Republic of', 'Iran', ignore_if_exists=True) +execute:influxframework.rename_doc('Country', 'Tanzania, United Republic of', 'Tanzania', ignore_if_exists=True) +execute:influxframework.rename_doc('Country', 'Syrian Arab Republic', 'Syria', ignore_if_exists=True) +execute:influxframework.reload_doc('desk', 'doctype', 'notification_log') +execute:influxframework.db.sql('update tabReport set module="Desk" where name="ToDo"') +execute:influxframework.delete_doc('Page', 'data-import-tool', ignore_missing=True) +influxframework.patches.v10_0.reload_countries_and_currencies # 2021-02-03 +influxframework.patches.v10_0.refactor_social_login_keys +influxframework.patches.v10_0.enable_chat_by_default_within_system_settings +influxframework.patches.v10_0.remove_custom_field_for_disabled_domain +execute:influxframework.delete_doc("Page", "chat") +influxframework.patches.v10_0.migrate_passwords_passlib +influxframework.patches.v11_0.rename_standard_reply_to_email_template +execute:influxframework.delete_doc_if_exists('Page', 'user-permissions') +influxframework.patches.v10_0.set_no_copy_to_workflow_state +influxframework.patches.v10_0.increase_single_table_column_length +influxframework.patches.v11_0.create_contact_for_user +influxframework.patches.v11_0.update_list_user_settings +influxframework.patches.v11_0.rename_workflow_action_to_workflow_action_master #13-06-2018 +influxframework.patches.v11_0.rename_email_alert_to_notification #13-06-2018 +influxframework.patches.v11_0.delete_duplicate_user_permissions +influxframework.patches.v11_0.set_dropbox_file_backup +influxframework.patches.v10_0.set_default_locking_time +influxframework.patches.v11_0.rename_google_maps_doctype +influxframework.patches.v10_0.modify_smallest_currency_fraction +influxframework.patches.v10_0.modify_naming_series_table +influxframework.patches.v10_0.enhance_security +influxframework.patches.v11_0.multiple_references_in_events +influxframework.patches.v11_0.set_allow_self_approval_in_workflow +influxframework.patches.v11_0.remove_skip_for_doctype +influxframework.patches.v11_0.migrate_report_settings_for_new_listview +influxframework.patches.v11_0.delete_all_prepared_reports +influxframework.patches.v11_0.fix_order_by_in_reports_json +execute:influxframework.delete_doc('Page', 'applications', ignore_missing=True) +influxframework.patches.v11_0.set_missing_creation_and_modified_value_for_user_permissions +influxframework.patches.v11_0.set_default_letter_head_source +influxframework.patches.v12_0.set_primary_key_in_series +execute:influxframework.delete_doc("Page", "modules", ignore_missing=True) +influxframework.patches.v12_0.setup_comments_from_communications +influxframework.patches.v12_0.replace_null_values_in_tables +influxframework.patches.v12_0.reset_home_settings +influxframework.patches.v12_0.update_print_format_type +influxframework.patches.v11_0.remove_doctype_user_permissions_for_page_and_report #2019-05-01 +influxframework.patches.v11_0.apply_customization_to_custom_doctype +influxframework.patches.v12_0.remove_feedback_rating +influxframework.patches.v12_0.move_form_attachments_to_attachments_folder +influxframework.patches.v12_0.move_timeline_links_to_dynamic_links +influxframework.patches.v12_0.delete_feedback_request_if_exists #1 +influxframework.patches.v12_0.rename_events_repeat_on +influxframework.patches.v12_0.fix_public_private_files +influxframework.patches.v12_0.move_email_and_phone_to_child_table +influxframework.patches.v12_0.delete_duplicate_indexes +influxframework.patches.v12_0.set_default_incoming_email_port +influxframework.patches.v12_0.update_global_search +influxframework.patches.v12_0.setup_tags +influxframework.patches.v12_0.update_auto_repeat_status_and_not_submittable +influxframework.patches.v12_0.create_notification_settings_for_user +influxframework.patches.v11_0.make_all_prepared_report_attachments_private #2019-11-26 +influxframework.patches.v12_0.setup_email_linking +influxframework.patches.v12_0.change_existing_dashboard_chart_filters +influxframework.patches.v12_0.set_correct_assign_value_in_docs #2020-07-13 +execute:influxframework.delete_doc('DocType', 'Test Runner') # 2022-05-19 +execute:influxframework.delete_doc_if_exists('DocType', 'Google Maps Settings') +execute:influxframework.db.set_default('desktop:home_page', 'workspace') +execute:influxframework.delete_doc_if_exists('DocType', 'GSuite Settings') +execute:influxframework.delete_doc_if_exists('DocType', 'GSuite Templates') +execute:influxframework.delete_doc_if_exists('DocType', 'GCalendar Account') +execute:influxframework.delete_doc_if_exists('DocType', 'GCalendar Settings') +influxframework.patches.v12_0.remove_example_email_thread_notify +execute:from influxframework.desk.page.setup_wizard.install_fixtures import update_genders;update_genders() +influxframework.patches.v12_0.set_correct_url_in_files +execute:influxframework.reload_doc('core', 'doctype', 'doctype') #2022-06-21 +execute:influxframework.reload_doc('custom', 'doctype', 'property_setter') +influxframework.patches.v13_0.remove_invalid_options_for_data_fields +influxframework.patches.v13_0.website_theme_custom_scss +influxframework.patches.v13_0.make_user_type +influxframework.patches.v13_0.set_existing_dashboard_charts_as_public +influxframework.patches.v13_0.set_path_for_homepage_in_web_page_view +influxframework.patches.v13_0.migrate_translation_column_data +influxframework.patches.v13_0.set_read_times +influxframework.patches.v13_0.remove_web_view +influxframework.patches.v13_0.site_wise_logging +influxframework.patches.v13_0.set_unique_for_page_view +influxframework.patches.v13_0.remove_tailwind_from_page_builder +influxframework.patches.v13_0.rename_onboarding +influxframework.patches.v13_0.email_unsubscribe +execute:influxframework.delete_doc("Web Template", "Section with Left Image", force=1) +execute:influxframework.delete_doc("DocType", "Onboarding Slide") +execute:influxframework.delete_doc("DocType", "Onboarding Slide Field") +execute:influxframework.delete_doc("DocType", "Onboarding Slide Help Link") +influxframework.patches.v13_0.update_date_filters_in_user_settings +influxframework.patches.v13_0.update_duration_options +influxframework.patches.v13_0.replace_old_data_import # 2020-06-24 +influxframework.patches.v13_0.create_custom_dashboards_cards_and_charts +influxframework.patches.v13_0.rename_is_custom_field_in_dashboard_chart +influxframework.patches.v13_0.add_standard_navbar_items # 2020-12-15 +influxframework.patches.v13_0.generate_theme_files_in_public_folder +influxframework.patches.v13_0.increase_password_length +influxframework.patches.v12_0.fix_email_id_formatting +influxframework.patches.v13_0.add_toggle_width_in_navbar_settings +influxframework.patches.v13_0.rename_notification_fields +influxframework.patches.v13_0.remove_duplicate_navbar_items +influxframework.patches.v13_0.set_social_icons +influxframework.patches.v12_0.set_default_password_reset_limit +influxframework.patches.v13_0.set_route_for_blog_category +influxframework.patches.v13_0.enable_custom_script +influxframework.patches.v13_0.update_newsletter_content_type +execute:influxframework.db.set_value('Website Settings', 'Website Settings', {'navbar_template': 'Standard Navbar', 'footer_template': 'Standard Footer'}) +influxframework.patches.v13_0.delete_event_producer_and_consumer_keys +influxframework.patches.v13_0.web_template_set_module #2020-10-05 +influxframework.patches.v13_0.remove_custom_link +execute:influxframework.delete_doc("DocType", "Footer Item") +execute:influxframework.reload_doctype('user') +execute:influxframework.reload_doctype('docperm') +influxframework.patches.v13_0.replace_field_target_with_open_in_new_tab +influxframework.core.doctype.role.patches.v13_set_default_desk_properties +influxframework.patches.v13_0.add_switch_theme_to_navbar_settings +influxframework.patches.v13_0.update_icons_in_customized_desk_pages +execute:influxframework.db.set_default('desktop:home_page', 'space') +execute:influxframework.delete_doc_if_exists('Page', 'workspace') +execute:influxframework.delete_doc_if_exists('Page', 'dashboard', force=1) +influxframework.core.doctype.page.patches.drop_unused_pages +execute:influxframework.get_doc('Role', 'Guest').save() # remove desk access +influxframework.patches.v13_0.remove_chat +influxframework.patches.v13_0.rename_desk_page_to_workspace # 02.02.2021 +influxframework.patches.v13_0.delete_package_publish_tool +influxframework.patches.v13_0.rename_list_view_setting_to_list_view_settings +influxframework.patches.v13_0.remove_twilio_settings +influxframework.patches.v12_0.rename_uploaded_files_with_proper_name +influxframework.patches.v13_0.queryreport_columns +influxframework.patches.v13_0.jinja_hook +influxframework.patches.v13_0.update_notification_channel_if_empty +influxframework.patches.v13_0.set_first_day_of_the_week +influxframework.patches.v13_0.encrypt_2fa_secrets +influxframework.patches.v13_0.reset_corrupt_defaults +execute:influxframework.reload_doc('custom', 'doctype', 'custom_field') +influxframework.patches.v14_0.update_workspace2 # 20.09.2021 +influxframework.patches.v14_0.save_ratings_in_fraction #23-12-2021 +influxframework.patches.v14_0.transform_todo_schema +influxframework.patches.v14_0.remove_post_and_post_comment +influxframework.patches.v14_0.reset_creation_datetime +influxframework.patches.v14_0.remove_is_first_startup +influxframework.patches.v14_0.clear_long_pending_stale_logs +influxframework.patches.v14_0.log_settings_migration +influxframework.patches.v14_0.setup_likes_from_feedback +influxframework.patches.v14_0.update_webforms +influxframework.patches.v14_0.delete_payment_gateways +influxframework.patches.v14_0.event_streaming_deprecation_warning + +[post_model_sync] +influxframework.patches.v14_0.drop_data_import_legacy +influxframework.patches.v14_0.copy_mail_data #08.03.21 +influxframework.patches.v14_0.update_github_endpoints #08-11-2021 +influxframework.patches.v14_0.remove_db_aggregation +influxframework.patches.v14_0.update_color_names_in_kanban_board_column +influxframework.patches.v14_0.update_is_system_generated_flag +influxframework.patches.v14_0.update_auto_account_deletion_duration +influxframework.patches.v14_0.update_integration_request +influxframework.patches.v14_0.set_document_expiry_default +influxframework.patches.v14_0.delete_data_migration_tool +influxframework.patches.v14_0.set_suspend_email_queue_default +influxframework.patches.v14_0.different_encryption_key +influxframework.patches.v14_0.update_multistep_webforms +influxframework.patches.v14_0.drop_unused_indexes +influxframework.patches.v14_0.add_manage_subscriptions_in_navbar_settings diff --git a/influxframework/patches/__init__.py b/influxframework/patches/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/patches/v10_0/__init__.py b/influxframework/patches/v10_0/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/patches/v10_0/enable_chat_by_default_within_system_settings.py b/influxframework/patches/v10_0/enable_chat_by_default_within_system_settings.py new file mode 100644 index 0000000..69ec074 --- /dev/null +++ b/influxframework/patches/v10_0/enable_chat_by_default_within_system_settings.py @@ -0,0 +1,13 @@ +import influxframework + + +def execute(): + influxframework.reload_doctype("System Settings") + doc = influxframework.get_single("System Settings") + doc.enable_chat = 1 + + # Changes prescribed by Nabin Hait (nabin@influxframework.io) + doc.flags.ignore_mandatory = True + doc.flags.ignore_permissions = True + + doc.save() diff --git a/influxframework/patches/v10_0/enhance_security.py b/influxframework/patches/v10_0/enhance_security.py new file mode 100644 index 0000000..a72bbe1 --- /dev/null +++ b/influxframework/patches/v10_0/enhance_security.py @@ -0,0 +1,32 @@ +import influxframework +from influxframework.utils import cint + + +def execute(): + """ + The motive of this patch is to increase the overall security in influxframework framework + + Existing passwords won't be affected, however, newly created accounts + will have to adheare to the new password policy guidelines. Once can always + loosen up the security by modifying the values in System Settings, however, + we strongly advice against doing so! + + Security is something we take very seriously at influxframework, + and hence we chose to make security tighter by default. + """ + doc = influxframework.get_single("System Settings") + + # Enforce a Password Policy + if cint(doc.enable_password_policy) == 0: + doc.enable_password_policy = 1 + + # Enforce a password score as calculated by zxcvbn + if cint(doc.minimum_password_score) <= 2: + doc.minimum_password_score = 2 + + # Disallow more than 3 consecutive login attempts in a span of 60 seconds + if cint(doc.allow_consecutive_login_attempts) <= 3: + doc.allow_consecutive_login_attempts = 3 + + doc.flags.ignore_mandatory = True + doc.save() diff --git a/influxframework/patches/v10_0/increase_single_table_column_length.py b/influxframework/patches/v10_0/increase_single_table_column_length.py new file mode 100644 index 0000000..19d2139 --- /dev/null +++ b/influxframework/patches/v10_0/increase_single_table_column_length.py @@ -0,0 +1,9 @@ +""" +Run this after updating country_info.json and or +""" +import influxframework + + +def execute(): + for col in ("field", "doctype"): + influxframework.db.sql_ddl(f"alter table `tabSingles` modify column `{col}` varchar(255)") diff --git a/influxframework/patches/v10_0/migrate_passwords_passlib.py b/influxframework/patches/v10_0/migrate_passwords_passlib.py new file mode 100644 index 0000000..89a9db7 --- /dev/null +++ b/influxframework/patches/v10_0/migrate_passwords_passlib.py @@ -0,0 +1,23 @@ +import influxframework +from influxframework.utils.password import LegacyPassword + + +def execute(): + all_auths = influxframework.db.sql( + """SELECT `name`, `password`, `salt` FROM `__Auth` + WHERE doctype='User' AND `fieldname`='password'""", + as_dict=True, + ) + + for auth in all_auths: + if auth.salt and auth.salt != "": + pwd = LegacyPassword.hash(auth.password, salt=auth.salt.encode("UTF-8")) + influxframework.db.sql( + """UPDATE `__Auth` SET `password`=%(pwd)s, `salt`=NULL + WHERE `doctype`='User' AND `fieldname`='password' AND `name`=%(user)s""", + {"pwd": pwd, "user": auth.name}, + ) + + influxframework.reload_doctype("User") + + influxframework.db.sql_ddl("""ALTER TABLE `__Auth` DROP COLUMN `salt`""") diff --git a/influxframework/patches/v10_0/modify_naming_series_table.py b/influxframework/patches/v10_0/modify_naming_series_table.py new file mode 100644 index 0000000..e2b0e2c --- /dev/null +++ b/influxframework/patches/v10_0/modify_naming_series_table.py @@ -0,0 +1,10 @@ +""" + Modify the Integer 10 Digits Value to BigInt 20 Digit value + to generate long Naming Series + +""" +import influxframework + + +def execute(): + influxframework.db.sql(""" ALTER TABLE `tabSeries` MODIFY current BIGINT """) diff --git a/influxframework/patches/v10_0/modify_smallest_currency_fraction.py b/influxframework/patches/v10_0/modify_smallest_currency_fraction.py new file mode 100644 index 0000000..f6b415a --- /dev/null +++ b/influxframework/patches/v10_0/modify_smallest_currency_fraction.py @@ -0,0 +1,8 @@ +# Copyright (c) 2018, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework + + +def execute(): + influxframework.db.set_value("Currency", "USD", "smallest_currency_fraction_value", "0.01") diff --git a/influxframework/patches/v10_0/refactor_social_login_keys.py b/influxframework/patches/v10_0/refactor_social_login_keys.py new file mode 100644 index 0000000..29f7f43 --- /dev/null +++ b/influxframework/patches/v10_0/refactor_social_login_keys.py @@ -0,0 +1,159 @@ +import influxframework +from influxframework.utils import cstr + + +def execute(): + # Update Social Logins in User + run_patch() + + # Create Social Login Key(s) from Social Login Keys + influxframework.reload_doc("integrations", "doctype", "social_login_key", force=True) + + if not influxframework.db.exists("DocType", "Social Login Keys"): + return + + social_login_keys = influxframework.get_doc("Social Login Keys", "Social Login Keys") + if social_login_keys.get("facebook_client_id") or social_login_keys.get("facebook_client_secret"): + facebook_login_key = influxframework.new_doc("Social Login Key") + facebook_login_key.get_social_login_provider("Facebook", initialize=True) + facebook_login_key.social_login_provider = "Facebook" + facebook_login_key.client_id = social_login_keys.get("facebook_client_id") + facebook_login_key.client_secret = social_login_keys.get("facebook_client_secret") + if not (facebook_login_key.client_secret and facebook_login_key.client_id): + facebook_login_key.enable_social_login = 0 + facebook_login_key.save() + + if social_login_keys.get("influxframework_server_url"): + influxframework_login_key = influxframework.new_doc("Social Login Key") + influxframework_login_key.get_social_login_provider("InfluxFramework", initialize=True) + influxframework_login_key.social_login_provider = "InfluxFramework" + influxframework_login_key.base_url = social_login_keys.get("influxframework_server_url") + influxframework_login_key.client_id = social_login_keys.get("influxframework_client_id") + influxframework_login_key.client_secret = social_login_keys.get("influxframework_client_secret") + if not ( + influxframework_login_key.client_secret and influxframework_login_key.client_id and influxframework_login_key.base_url + ): + influxframework_login_key.enable_social_login = 0 + influxframework_login_key.save() + + if social_login_keys.get("github_client_id") or social_login_keys.get("github_client_secret"): + github_login_key = influxframework.new_doc("Social Login Key") + github_login_key.get_social_login_provider("GitHub", initialize=True) + github_login_key.social_login_provider = "GitHub" + github_login_key.client_id = social_login_keys.get("github_client_id") + github_login_key.client_secret = social_login_keys.get("github_client_secret") + if not (github_login_key.client_secret and github_login_key.client_id): + github_login_key.enable_social_login = 0 + github_login_key.save() + + if social_login_keys.get("google_client_id") or social_login_keys.get("google_client_secret"): + google_login_key = influxframework.new_doc("Social Login Key") + google_login_key.get_social_login_provider("Google", initialize=True) + google_login_key.social_login_provider = "Google" + google_login_key.client_id = social_login_keys.get("google_client_id") + google_login_key.client_secret = social_login_keys.get("google_client_secret") + if not (google_login_key.client_secret and google_login_key.client_id): + google_login_key.enable_social_login = 0 + google_login_key.save() + + influxframework.delete_doc("DocType", "Social Login Keys") + + +def run_patch(): + influxframework.reload_doc("core", "doctype", "user", force=True) + influxframework.reload_doc("core", "doctype", "user_social_login", force=True) + + users = influxframework.get_all( + "User", fields=["*"], filters={"name": ("not in", ["Administrator", "Guest"])} + ) + + for user in users: + idx = 0 + if user.influxframework_userid: + insert_user_social_login(user.name, user.modified_by, "influxframework", idx, userid=user.influxframework_userid) + idx += 1 + + if user.fb_userid or user.fb_username: + insert_user_social_login( + user.name, user.modified_by, "facebook", idx, userid=user.fb_userid, username=user.fb_username + ) + idx += 1 + + if user.github_userid or user.github_username: + insert_user_social_login( + user.name, + user.modified_by, + "github", + idx, + userid=user.github_userid, + username=user.github_username, + ) + idx += 1 + + if user.google_userid: + insert_user_social_login(user.name, user.modified_by, "google", idx, userid=user.google_userid) + idx += 1 + + +def insert_user_social_login(user, modified_by, provider, idx, userid=None, username=None): + source_cols = get_standard_cols() + + creation_time = influxframework.utils.get_datetime_str(influxframework.utils.get_datetime()) + values = [ + influxframework.generate_hash(length=10), + creation_time, + creation_time, + user, + modified_by, + user, + "User", + "social_logins", + cstr(idx), + provider, + ] + + if userid: + source_cols.append("userid") + values.append(userid) + + if username: + source_cols.append("username") + values.append(username) + + query = """INSERT INTO `tabUser Social Login` (`{source_cols}`) + VALUES ({values}) + """.format( + source_cols="`, `".join(source_cols), values=", ".join([influxframework.db.escape(d) for d in values]) + ) + + influxframework.db.sql(query) + + +def get_provider_field_map(): + return influxframework._dict( + { + "influxframework": ["influxframework_userid"], + "facebook": ["fb_userid", "fb_username"], + "github": ["github_userid", "github_username"], + "google": ["google_userid"], + } + ) + + +def get_provider_fields(provider): + return get_provider_field_map().get(provider) + + +def get_standard_cols(): + return [ + "name", + "creation", + "modified", + "owner", + "modified_by", + "parent", + "parenttype", + "parentfield", + "idx", + "provider", + ] diff --git a/influxframework/patches/v10_0/reload_countries_and_currencies.py b/influxframework/patches/v10_0/reload_countries_and_currencies.py new file mode 100644 index 0000000..54bc155 --- /dev/null +++ b/influxframework/patches/v10_0/reload_countries_and_currencies.py @@ -0,0 +1,8 @@ +""" +Run this after updating country_info.json and or +""" +from influxframework.utils.install import import_country_and_currency + + +def execute(): + import_country_and_currency() diff --git a/influxframework/patches/v10_0/remove_custom_field_for_disabled_domain.py b/influxframework/patches/v10_0/remove_custom_field_for_disabled_domain.py new file mode 100644 index 0000000..2771323 --- /dev/null +++ b/influxframework/patches/v10_0/remove_custom_field_for_disabled_domain.py @@ -0,0 +1,14 @@ +import influxframework + + +def execute(): + influxframework.reload_doc("core", "doctype", "domain") + influxframework.reload_doc("core", "doctype", "has_domain") + active_domains = influxframework.get_active_domains() + all_domains = influxframework.get_all("Domain") + + for d in all_domains: + if d.name not in active_domains: + inactive_domain = influxframework.get_doc("Domain", d.name) + inactive_domain.setup_data() + inactive_domain.remove_custom_field() diff --git a/influxframework/patches/v10_0/set_default_locking_time.py b/influxframework/patches/v10_0/set_default_locking_time.py new file mode 100644 index 0000000..cd4b957 --- /dev/null +++ b/influxframework/patches/v10_0/set_default_locking_time.py @@ -0,0 +1,9 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework + + +def execute(): + influxframework.reload_doc("core", "doctype", "system_settings") + influxframework.db.set_value("System Settings", None, "allow_login_after_fail", 60) diff --git a/influxframework/patches/v10_0/set_no_copy_to_workflow_state.py b/influxframework/patches/v10_0/set_no_copy_to_workflow_state.py new file mode 100644 index 0000000..9d40374 --- /dev/null +++ b/influxframework/patches/v10_0/set_no_copy_to_workflow_state.py @@ -0,0 +1,13 @@ +import influxframework + + +def execute(): + for dt in influxframework.get_all("Workflow", fields=["name", "document_type", "workflow_state_field"]): + fieldname = influxframework.db.get_value( + "Custom Field", filters={"dt": dt.document_type, "fieldname": dt.workflow_state_field} + ) + + if fieldname: + custom_field = influxframework.get_doc("Custom Field", fieldname) + custom_field.no_copy = 1 + custom_field.save() diff --git a/influxframework/patches/v11_0/__init__.py b/influxframework/patches/v11_0/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/patches/v11_0/apply_customization_to_custom_doctype.py b/influxframework/patches/v11_0/apply_customization_to_custom_doctype.py new file mode 100644 index 0000000..a75afb1 --- /dev/null +++ b/influxframework/patches/v11_0/apply_customization_to_custom_doctype.py @@ -0,0 +1,52 @@ +import influxframework +from influxframework.utils import cint + +# This patch aims to apply & delete all the customization +# on custom doctypes done through customize form + +# This is required because customize form in now blocked +# for custom doctypes and user may not be able to +# see previous customization + + +def execute(): + custom_doctypes = influxframework.get_all("DocType", filters={"custom": 1}) + + for doctype in custom_doctypes: + property_setters = influxframework.get_all( + "Property Setter", + filters={"doc_type": doctype.name, "doctype_or_field": "DocField"}, + fields=["name", "property", "value", "property_type", "field_name"], + ) + + custom_fields = influxframework.get_all("Custom Field", filters={"dt": doctype.name}, fields=["*"]) + + property_setter_map = {} + + for prop in property_setters: + property_setter_map[prop.field_name] = prop + influxframework.db.delete("Property Setter", {"name": prop.name}) + + meta = influxframework.get_meta(doctype.name) + + for df in meta.fields: + ps = property_setter_map.get(df.fieldname, None) + if ps: + value = cint(ps.value) if ps.property_type == "Int" else ps.value + df.set(ps.property, value) + + for cf in custom_fields: + cf.pop("parenttype") + cf.pop("parentfield") + cf.pop("parent") + cf.pop("name") + field = meta.get_field(cf.fieldname) + if field: + field.update(cf) + else: + df = influxframework.new_doc("DocField", meta, "fields") + df.update(cf) + meta.fields.append(df) + influxframework.db.delete("Custom Field", {"name": cf.name}) + + meta.save() diff --git a/influxframework/patches/v11_0/change_email_signature_fieldtype.py b/influxframework/patches/v11_0/change_email_signature_fieldtype.py new file mode 100644 index 0000000..8b50571 --- /dev/null +++ b/influxframework/patches/v11_0/change_email_signature_fieldtype.py @@ -0,0 +1,16 @@ +# Copyright (c) 2018, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework + + +def execute(): + signatures = influxframework.db.get_list( + "User", {"email_signature": ["!=", ""]}, ["name", "email_signature"] + ) + influxframework.reload_doc("core", "doctype", "user") + for d in signatures: + signature = d.get("email_signature") + signature = signature.replace("\n", "
      ") + signature = "
      " + signature + "
      " + influxframework.db.set_value("User", d.get("name"), "email_signature", signature) diff --git a/influxframework/patches/v11_0/copy_fetch_data_from_options.py b/influxframework/patches/v11_0/copy_fetch_data_from_options.py new file mode 100644 index 0000000..05b61b4 --- /dev/null +++ b/influxframework/patches/v11_0/copy_fetch_data_from_options.py @@ -0,0 +1,38 @@ +import influxframework + + +def execute(): + influxframework.reload_doc("core", "doctype", "docfield", force=True) + influxframework.reload_doc("custom", "doctype", "custom_field", force=True) + influxframework.reload_doc("custom", "doctype", "customize_form_field", force=True) + influxframework.reload_doc("custom", "doctype", "property_setter", force=True) + + influxframework.db.sql( + """ + update `tabDocField` + set fetch_from = options, options='' + where options like '%.%' and (fetch_from is NULL OR fetch_from='') + and fieldtype in ('Data', 'Read Only', 'Text', 'Small Text', 'Text Editor', 'Code', 'Link', 'Check') + and fieldname!='naming_series' + """ + ) + + influxframework.db.sql( + """ + update `tabCustom Field` + set fetch_from = options, options='' + where options like '%.%' and (fetch_from is NULL OR fetch_from='') + and fieldtype in ('Data', 'Read Only', 'Text', 'Small Text', 'Text Editor', 'Code', 'Link', 'Check') + and fieldname!='naming_series' + """ + ) + + influxframework.db.sql( + """ + update `tabProperty Setter` + set property="fetch_from", name=concat(doc_type, '-', field_name, '-', property) + where property="options" and value like '%.%' + and property_type in ('Data', 'Read Only', 'Text', 'Small Text', 'Text Editor', 'Code', 'Link', 'Check') + and field_name!='naming_series' + """ + ) diff --git a/influxframework/patches/v11_0/create_contact_for_user.py b/influxframework/patches/v11_0/create_contact_for_user.py new file mode 100644 index 0000000..e077fda --- /dev/null +++ b/influxframework/patches/v11_0/create_contact_for_user.py @@ -0,0 +1,28 @@ +import re + +import influxframework +from influxframework.core.doctype.user.user import create_contact + + +def execute(): + """Create Contact for each User if not present""" + influxframework.reload_doc("integrations", "doctype", "google_contacts") + influxframework.reload_doc("contacts", "doctype", "contact") + influxframework.reload_doc("core", "doctype", "dynamic_link") + + contact_meta = influxframework.get_meta("Contact") + if contact_meta.has_field("phone_nos") and contact_meta.has_field("email_ids"): + influxframework.reload_doc("contacts", "doctype", "contact_phone") + influxframework.reload_doc("contacts", "doctype", "contact_email") + + users = influxframework.get_all("User", filters={"name": ("not in", "Administrator, Guest")}, fields=["*"]) + for user in users: + if influxframework.db.exists("Contact", {"email_id": user.email}) or influxframework.db.exists( + "Contact Email", {"email_id": user.email} + ): + continue + if user.first_name: + user.first_name = re.sub("[<>]+", "", influxframework.safe_decode(user.first_name)) + if user.last_name: + user.last_name = re.sub("[<>]+", "", influxframework.safe_decode(user.last_name)) + create_contact(user, ignore_links=True, ignore_mandatory=True) diff --git a/influxframework/patches/v11_0/delete_all_prepared_reports.py b/influxframework/patches/v11_0/delete_all_prepared_reports.py new file mode 100644 index 0000000..f5a7ebb --- /dev/null +++ b/influxframework/patches/v11_0/delete_all_prepared_reports.py @@ -0,0 +1,9 @@ +import influxframework + + +def execute(): + if influxframework.db.table_exists("Prepared Report"): + influxframework.reload_doc("core", "doctype", "prepared_report") + prepared_reports = influxframework.get_all("Prepared Report") + for report in prepared_reports: + influxframework.delete_doc("Prepared Report", report.name) diff --git a/influxframework/patches/v11_0/delete_duplicate_user_permissions.py b/influxframework/patches/v11_0/delete_duplicate_user_permissions.py new file mode 100644 index 0000000..fab6905 --- /dev/null +++ b/influxframework/patches/v11_0/delete_duplicate_user_permissions.py @@ -0,0 +1,20 @@ +import influxframework + + +def execute(): + duplicateRecords = influxframework.db.sql( + """select count(name) as `count`, allow, user, for_value + from `tabUser Permission` + group by allow, user, for_value + having count(*) > 1 """, + as_dict=1, + ) + + for record in duplicateRecords: + influxframework.db.sql( + """delete from `tabUser Permission` + where allow=%s and user=%s and for_value=%s limit {}""".format( + record.count - 1 + ), + (record.allow, record.user, record.for_value), + ) diff --git a/influxframework/patches/v11_0/drop_column_apply_user_permissions.py b/influxframework/patches/v11_0/drop_column_apply_user_permissions.py new file mode 100644 index 0000000..691f979 --- /dev/null +++ b/influxframework/patches/v11_0/drop_column_apply_user_permissions.py @@ -0,0 +1,14 @@ +import influxframework + + +def execute(): + column = "apply_user_permissions" + to_remove = ["DocPerm", "Custom DocPerm"] + + for doctype in to_remove: + if influxframework.db.table_exists(doctype): + if column in influxframework.db.get_table_columns(doctype): + influxframework.db.sql(f"alter table `tab{doctype}` drop column {column}") + + influxframework.reload_doc("core", "doctype", "docperm", force=True) + influxframework.reload_doc("core", "doctype", "custom_docperm", force=True) diff --git a/influxframework/patches/v11_0/fix_order_by_in_reports_json.py b/influxframework/patches/v11_0/fix_order_by_in_reports_json.py new file mode 100644 index 0000000..567fce8 --- /dev/null +++ b/influxframework/patches/v11_0/fix_order_by_in_reports_json.py @@ -0,0 +1,35 @@ +import json + +import influxframework + + +def execute(): + reports_data = influxframework.get_all( + "Report", + filters={ + "json": ["not like", '%%%"order_by": "`tab%%%'], + "report_type": "Report Builder", + "is_standard": "No", + }, + fields=["name"], + ) + + for d in reports_data: + doc = influxframework.get_doc("Report", d.get("name")) + + if not doc.get("json"): + continue + + json_data = json.loads(doc.get("json")) + + parts = [] + if ("order_by" in json_data) and ("." in json_data.get("order_by")): + parts = json_data.get("order_by").split(".") + + sort_by = parts[1].split(" ") + + json_data["order_by"] = f"`tab{doc.ref_doctype}`.`{sort_by[0]}`" + json_data["order_by"] += f" {sort_by[1]}" if len(sort_by) > 1 else "" + + doc.json = json.dumps(json_data) + doc.save() diff --git a/influxframework/patches/v11_0/make_all_prepared_report_attachments_private.py b/influxframework/patches/v11_0/make_all_prepared_report_attachments_private.py new file mode 100644 index 0000000..b7fa39b --- /dev/null +++ b/influxframework/patches/v11_0/make_all_prepared_report_attachments_private.py @@ -0,0 +1,31 @@ +import influxframework + + +def execute(): + if ( + influxframework.db.count("File", filters={"attached_to_doctype": "Prepared Report", "is_private": 0}) + > 10000 + ): + influxframework.db.auto_commit_on_many_writes = True + + files = influxframework.get_all( + "File", + fields=["name", "attached_to_name"], + filters={"attached_to_doctype": "Prepared Report", "is_private": 0}, + ) + for file_dict in files: + # For some reason Prepared Report doc might not exist, check if it exists first + if influxframework.db.exists("Prepared Report", file_dict.attached_to_name): + try: + file_doc = influxframework.get_doc("File", file_dict.name) + file_doc.is_private = 1 + file_doc.save() + except Exception: + # File might not exist on the file system in that case delete both Prepared Report and File doc + influxframework.delete_doc("Prepared Report", file_dict.attached_to_name) + else: + # If Prepared Report doc doesn't exist then the file doc is useless. Delete it. + influxframework.delete_doc("File", file_dict.name) + + if influxframework.db.auto_commit_on_many_writes: + influxframework.db.auto_commit_on_many_writes = False diff --git a/influxframework/patches/v11_0/migrate_report_settings_for_new_listview.py b/influxframework/patches/v11_0/migrate_report_settings_for_new_listview.py new file mode 100644 index 0000000..383d014 --- /dev/null +++ b/influxframework/patches/v11_0/migrate_report_settings_for_new_listview.py @@ -0,0 +1,34 @@ +import json + +import influxframework + + +def execute(): + """ + Migrate JSON field of Report according to changes in New ListView + Rename key columns to fields + Rename key add_total_row to add_totals_row + Convert sort_by and sort_order to order_by + """ + + reports = influxframework.get_all("Report", {"report_type": "Report Builder"}) + + for report_name in reports: + settings = influxframework.db.get_value("Report", report_name, "json") + if not settings: + continue + + settings = influxframework._dict(json.loads(settings)) + + # columns -> fields + settings.fields = settings.columns or [] + settings.pop("columns", None) + + # sort_by + order_by -> order_by + settings.order_by = (settings.sort_by or "modified") + " " + (settings.order_by or "desc") + + # add_total_row -> add_totals_row + settings.add_totals_row = settings.add_total_row + settings.pop("add_total_row", None) + + influxframework.db.set_value("Report", report_name, "json", json.dumps(settings)) diff --git a/influxframework/patches/v11_0/multiple_references_in_events.py b/influxframework/patches/v11_0/multiple_references_in_events.py new file mode 100644 index 0000000..62130dd --- /dev/null +++ b/influxframework/patches/v11_0/multiple_references_in_events.py @@ -0,0 +1,24 @@ +import influxframework + + +def execute(): + influxframework.reload_doctype("Event") + # Rename "Cancel" to "Cancelled" + influxframework.db.sql("""UPDATE tabEvent set event_type='Cancelled' where event_type='Cancel'""") + # Move references to Participants table + events = influxframework.db.sql( + """SELECT name, ref_type, ref_name FROM tabEvent WHERE ref_type!=''""", as_dict=True + ) + for event in events: + if event.ref_type and event.ref_name: + try: + e = influxframework.get_doc("Event", event.name) + e.append( + "event_participants", + {"reference_doctype": event.ref_type, "reference_docname": event.ref_name}, + ) + e.flags.ignore_mandatory = True + e.flags.ignore_permissions = True + e.save() + except Exception: + influxframework.log_error(influxframework.get_traceback()) diff --git a/influxframework/patches/v11_0/reload_and_rename_view_log.py b/influxframework/patches/v11_0/reload_and_rename_view_log.py new file mode 100644 index 0000000..480ddab --- /dev/null +++ b/influxframework/patches/v11_0/reload_and_rename_view_log.py @@ -0,0 +1,28 @@ +import influxframework + + +def execute(): + if influxframework.db.table_exists("View log"): + # for mac users direct renaming would not work since mysql for mac saves table name in lower case + # so while renaming `tabView log` to `tabView Log` we get "Table 'tabView Log' already exists" error + # more info https://stackoverflow.com/a/44753093/5955589 , + # https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_lower_case_table_names + + # here we are creating a temp table to store view log data + influxframework.db.sql("CREATE TABLE `ViewLogTemp` AS SELECT * FROM `tabView log`") + + # deleting old View log table + influxframework.db.sql("DROP table `tabView log`") + influxframework.delete_doc("DocType", "View log") + + # reloading view log doctype to create `tabView Log` table + influxframework.reload_doc("core", "doctype", "view_log") + + # Move the data to newly created `tabView Log` table + influxframework.db.sql("INSERT INTO `tabView Log` SELECT * FROM `ViewLogTemp`") + influxframework.db.commit() + + # Delete temporary table + influxframework.db.sql("DROP table `ViewLogTemp`") + else: + influxframework.reload_doc("core", "doctype", "view_log") diff --git a/influxframework/patches/v11_0/remove_doctype_user_permissions_for_page_and_report.py b/influxframework/patches/v11_0/remove_doctype_user_permissions_for_page_and_report.py new file mode 100644 index 0000000..f7da663 --- /dev/null +++ b/influxframework/patches/v11_0/remove_doctype_user_permissions_for_page_and_report.py @@ -0,0 +1,8 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework + + +def execute(): + influxframework.delete_doc_if_exists("DocType", "User Permission for Page and Report") diff --git a/influxframework/patches/v11_0/remove_skip_for_doctype.py b/influxframework/patches/v11_0/remove_skip_for_doctype.py new file mode 100644 index 0000000..672d196 --- /dev/null +++ b/influxframework/patches/v11_0/remove_skip_for_doctype.py @@ -0,0 +1,92 @@ +import influxframework +from influxframework.desk.form.linked_with import get_linked_doctypes +from influxframework.patches.v11_0.replicate_old_user_permissions import get_doctypes_to_skip +from influxframework.query_builder import Field + +# `skip_for_doctype` was a un-normalized way of storing for which +# doctypes the user permission was applicable. +# in this patch, we normalize this into `applicable_for` where +# a new record will be created for each doctype where the user permission +# is applicable +# +# if the user permission is applicable for all doctypes, then only +# one record is created + + +def execute(): + influxframework.reload_doctype("User Permission") + + # to check if we need to migrate from skip_for_doctype + has_skip_for_doctype = influxframework.db.has_column("User Permission", "skip_for_doctype") + skip_for_doctype_map = {} + + new_user_permissions_list = [] + + user_permissions_to_delete = [] + + for user_permission in influxframework.get_all("User Permission", fields=["*"]): + skip_for_doctype = [] + + # while migrating from v11 -> v11 + if has_skip_for_doctype: + if not user_permission.skip_for_doctype: + continue + skip_for_doctype = user_permission.skip_for_doctype.split("\n") + else: # while migrating from v10 -> v11 + if skip_for_doctype_map.get((user_permission.allow, user_permission.user)) is None: + skip_for_doctype = get_doctypes_to_skip(user_permission.allow, user_permission.user) + # cache skip for doctype for same user and doctype + skip_for_doctype_map[(user_permission.allow, user_permission.user)] = skip_for_doctype + else: + skip_for_doctype = skip_for_doctype_map[(user_permission.allow, user_permission.user)] + + if skip_for_doctype: + # only specific doctypes are selected + # split this into multiple records and delete + linked_doctypes = get_linked_doctypes(user_permission.allow, True).keys() + + linked_doctypes = list(linked_doctypes) + + # append the doctype for which we have build the user permission + linked_doctypes += [user_permission.allow] + + applicable_for_doctypes = list(set(linked_doctypes) - set(skip_for_doctype)) + + user_permissions_to_delete.append(user_permission.name) + user_permission.name = None + user_permission.skip_for_doctype = None + for doctype in applicable_for_doctypes: + if doctype: + # Maintain sequence (name, user, allow, for_value, applicable_for, apply_to_all_doctypes, creation, modified) + new_user_permissions_list.append( + ( + influxframework.generate_hash("", 10), + user_permission.user, + user_permission.allow, + user_permission.for_value, + doctype, + 0, + user_permission.creation, + user_permission.modified, + ) + ) + else: + # No skip_for_doctype found! Just update apply_to_all_doctypes. + influxframework.db.set_value("User Permission", user_permission.name, "apply_to_all_doctypes", 1) + + if new_user_permissions_list: + influxframework.qb.into("User Permission").columns( + "name", + "user", + "allow", + "for_value", + "applicable_for", + "apply_to_all_doctypes", + "creation", + "modified", + ).insert(*new_user_permissions_list).run() + + if user_permissions_to_delete: + influxframework.db.delete( + "User Permission", filters=(Field("name").isin(tuple(user_permissions_to_delete))) + ) diff --git a/influxframework/patches/v11_0/rename_email_alert_to_notification.py b/influxframework/patches/v11_0/rename_email_alert_to_notification.py new file mode 100644 index 0000000..5553b0d --- /dev/null +++ b/influxframework/patches/v11_0/rename_email_alert_to_notification.py @@ -0,0 +1,14 @@ +import influxframework +from influxframework.model.rename_doc import rename_doc + + +def execute(): + if influxframework.db.table_exists("Email Alert Recipient") and not influxframework.db.table_exists( + "Notification Recipient" + ): + rename_doc("DocType", "Email Alert Recipient", "Notification Recipient") + influxframework.reload_doc("email", "doctype", "notification_recipient") + + if influxframework.db.table_exists("Email Alert") and not influxframework.db.table_exists("Notification"): + rename_doc("DocType", "Email Alert", "Notification") + influxframework.reload_doc("email", "doctype", "notification") diff --git a/influxframework/patches/v11_0/rename_google_maps_doctype.py b/influxframework/patches/v11_0/rename_google_maps_doctype.py new file mode 100644 index 0000000..0625275 --- /dev/null +++ b/influxframework/patches/v11_0/rename_google_maps_doctype.py @@ -0,0 +1,9 @@ +import influxframework +from influxframework.model.rename_doc import rename_doc + + +def execute(): + if influxframework.db.exists("DocType", "Google Maps") and not influxframework.db.exists( + "DocType", "Google Maps Settings" + ): + rename_doc("DocType", "Google Maps", "Google Maps Settings") diff --git a/influxframework/patches/v11_0/rename_standard_reply_to_email_template.py b/influxframework/patches/v11_0/rename_standard_reply_to_email_template.py new file mode 100644 index 0000000..2181e1e --- /dev/null +++ b/influxframework/patches/v11_0/rename_standard_reply_to_email_template.py @@ -0,0 +1,8 @@ +import influxframework +from influxframework.model.rename_doc import rename_doc + + +def execute(): + if influxframework.db.table_exists("Standard Reply") and not influxframework.db.table_exists("Email Template"): + rename_doc("DocType", "Standard Reply", "Email Template") + influxframework.reload_doc("email", "doctype", "email_template") diff --git a/influxframework/patches/v11_0/rename_workflow_action_to_workflow_action_master.py b/influxframework/patches/v11_0/rename_workflow_action_to_workflow_action_master.py new file mode 100644 index 0000000..aa02141 --- /dev/null +++ b/influxframework/patches/v11_0/rename_workflow_action_to_workflow_action_master.py @@ -0,0 +1,10 @@ +import influxframework +from influxframework.model.rename_doc import rename_doc + + +def execute(): + if influxframework.db.table_exists("Workflow Action") and not influxframework.db.table_exists( + "Workflow Action Master" + ): + rename_doc("DocType", "Workflow Action", "Workflow Action Master") + influxframework.reload_doc("workflow", "doctype", "workflow_action_master") diff --git a/influxframework/patches/v11_0/replicate_old_user_permissions.py b/influxframework/patches/v11_0/replicate_old_user_permissions.py new file mode 100644 index 0000000..85c5b5b --- /dev/null +++ b/influxframework/patches/v11_0/replicate_old_user_permissions.py @@ -0,0 +1,100 @@ +import json + +import influxframework +from influxframework.permissions import get_valid_perms +from influxframework.utils import cint + + +def execute(): + influxframework.reload_doctype("User Permission") + user_permissions = influxframework.get_all("User Permission", fields=["allow", "name", "user"]) + + doctype_to_skip_map = {} + + for permission in user_permissions: + if (permission.allow, permission.user) not in doctype_to_skip_map: + doctype_to_skip_map[(permission.allow, permission.user)] = get_doctypes_to_skip( + permission.allow, permission.user + ) + + if not doctype_to_skip_map: + return + for key, doctype_to_skip in doctype_to_skip_map.items(): + if not doctype_to_skip: + continue + if not influxframework.db.has_column("User Permission", "applicable_for") and influxframework.db.has_column( + "User Permission", "skip_for_doctype" + ): + doctype_to_skip = "\n".join(doctype_to_skip) + influxframework.db.sql( + """ + update `tabUser Permission` + set skip_for_doctype = %s + where user=%s and allow=%s + """, + (doctype_to_skip, key[1], key[0]), + ) + + +def get_doctypes_to_skip(doctype, user): + """Returns doctypes to be skipped from user permission check""" + doctypes_to_skip = [] + valid_perms = get_user_valid_perms(user) or [] + for perm in valid_perms: + parent_doctype = perm.parent + try: + linked_doctypes = get_linked_doctypes(parent_doctype) + if doctype not in linked_doctypes: + continue + except influxframework.DoesNotExistError: + # if doctype not found (may be due to rename) it should not be considered for skip + continue + + if not cint(perm.apply_user_permissions): + # add doctype to skip list if any of the perm does not apply user permission + doctypes_to_skip.append(parent_doctype) + + elif parent_doctype not in doctypes_to_skip: + + user_permission_doctypes = get_user_permission_doctypes(perm) + + # "No doctypes present" indicates that user permission will be applied to each link field + if not user_permission_doctypes: + continue + + elif doctype in user_permission_doctypes: + continue + + else: + doctypes_to_skip.append(parent_doctype) + # to remove possible duplicates + doctypes_to_skip = list(set(doctypes_to_skip)) + + return doctypes_to_skip + + +# store user's valid perms to avoid repeated query +user_valid_perm = {} + + +def get_user_valid_perms(user): + if not user_valid_perm.get(user): + user_valid_perm[user] = get_valid_perms(user=user) + return user_valid_perm.get(user) + + +def get_user_permission_doctypes(perm): + try: + return json.loads(perm.user_permission_doctypes or "[]") + except ValueError: + return [] + + +def get_linked_doctypes(doctype): + from influxframework.permissions import get_linked_doctypes + + linked_doctypes = get_linked_doctypes(doctype) + child_doctypes = [d.options for d in influxframework.get_meta(doctype).get_table_fields()] + for child_dt in child_doctypes: + linked_doctypes += get_linked_doctypes(child_dt) + return linked_doctypes diff --git a/influxframework/patches/v11_0/set_allow_self_approval_in_workflow.py b/influxframework/patches/v11_0/set_allow_self_approval_in_workflow.py new file mode 100644 index 0000000..6cbf71a --- /dev/null +++ b/influxframework/patches/v11_0/set_allow_self_approval_in_workflow.py @@ -0,0 +1,6 @@ +import influxframework + + +def execute(): + influxframework.reload_doc("workflow", "doctype", "workflow_transition") + influxframework.db.sql("update `tabWorkflow Transition` set allow_self_approval=1") diff --git a/influxframework/patches/v11_0/set_default_letter_head_source.py b/influxframework/patches/v11_0/set_default_letter_head_source.py new file mode 100644 index 0000000..80842f5 --- /dev/null +++ b/influxframework/patches/v11_0/set_default_letter_head_source.py @@ -0,0 +1,8 @@ +import influxframework + + +def execute(): + influxframework.reload_doctype("Letter Head") + + # source of all existing letter heads must be HTML + influxframework.db.sql("update `tabLetter Head` set source = 'HTML'") diff --git a/influxframework/patches/v11_0/set_dropbox_file_backup.py b/influxframework/patches/v11_0/set_dropbox_file_backup.py new file mode 100644 index 0000000..1b61f97 --- /dev/null +++ b/influxframework/patches/v11_0/set_dropbox_file_backup.py @@ -0,0 +1,9 @@ +import influxframework +from influxframework.utils import cint + + +def execute(): + influxframework.reload_doctype("Dropbox Settings") + check_dropbox_enabled = cint(influxframework.db.get_single_value("Dropbox Settings", "enabled")) + if check_dropbox_enabled == 1: + influxframework.db.set_value("Dropbox Settings", None, "file_backup", 1) diff --git a/influxframework/patches/v11_0/set_missing_creation_and_modified_value_for_user_permissions.py b/influxframework/patches/v11_0/set_missing_creation_and_modified_value_for_user_permissions.py new file mode 100644 index 0000000..bf70724 --- /dev/null +++ b/influxframework/patches/v11_0/set_missing_creation_and_modified_value_for_user_permissions.py @@ -0,0 +1,9 @@ +import influxframework + + +def execute(): + influxframework.db.sql( + """UPDATE `tabUser Permission` + SET `modified`=NOW(), `creation`=NOW() + WHERE `creation` IS NULL""" + ) diff --git a/influxframework/patches/v11_0/update_list_user_settings.py b/influxframework/patches/v11_0/update_list_user_settings.py new file mode 100644 index 0000000..9511e49 --- /dev/null +++ b/influxframework/patches/v11_0/update_list_user_settings.py @@ -0,0 +1,35 @@ +import json + +import influxframework +from influxframework.model.utils.user_settings import sync_user_settings, update_user_settings + + +def execute(): + """Update list_view's order by property from __UserSettings""" + + users = influxframework.db.sql("select distinct(user) from `__UserSettings`", as_dict=True) + + for user in users: + # get user_settings for each user + settings = influxframework.db.sql( + "select * from `__UserSettings` \ + where user={}".format( + influxframework.db.escape(user.user) + ), + as_dict=True, + ) + + # traverse through each doctype's settings for a user + for d in settings: + data = json.loads(d["data"]) + if data and ("List" in data) and ("order_by" in data["List"]) and data["List"]["order_by"]: + # convert order_by to sort_order & sort_by and delete order_by + order_by = data["List"]["order_by"] + if "`" in order_by and "." in order_by: + order_by = order_by.replace("`", "").split(".")[1] + + data["List"]["sort_by"], data["List"]["sort_order"] = order_by.split(" ") + data["List"].pop("order_by") + update_user_settings(d["doctype"], json.dumps(data), for_update=True) + + sync_user_settings() diff --git a/influxframework/patches/v12_0/__init__.py b/influxframework/patches/v12_0/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/patches/v12_0/change_existing_dashboard_chart_filters.py b/influxframework/patches/v12_0/change_existing_dashboard_chart_filters.py new file mode 100644 index 0000000..3ddf9fe --- /dev/null +++ b/influxframework/patches/v12_0/change_existing_dashboard_chart_filters.py @@ -0,0 +1,31 @@ +import json + +import influxframework + + +def execute(): + if not influxframework.db.table_exists("Dashboard Chart"): + return + + charts_to_modify = influxframework.get_all( + "Dashboard Chart", + fields=["name", "filters_json", "document_type"], + filters={"chart_type": ["not in", ["Report", "Custom"]]}, + ) + + for chart in charts_to_modify: + old_filters = influxframework.parse_json(chart.filters_json) + + if chart.filters_json and isinstance(old_filters, dict): + new_filters = [] + doctype = chart.document_type + + for key in old_filters.keys(): + filter_value = old_filters[key] + if isinstance(filter_value, list): + new_filters.append([doctype, key, filter_value[0], filter_value[1], 0]) + else: + new_filters.append([doctype, key, "=", filter_value, 0]) + + new_filters_json = json.dumps(new_filters) + influxframework.db.set_value("Dashboard Chart", chart.name, "filters_json", new_filters_json) diff --git a/influxframework/patches/v12_0/create_notification_settings_for_user.py b/influxframework/patches/v12_0/create_notification_settings_for_user.py new file mode 100644 index 0000000..f3debcd --- /dev/null +++ b/influxframework/patches/v12_0/create_notification_settings_for_user.py @@ -0,0 +1,13 @@ +import influxframework +from influxframework.desk.doctype.notification_settings.notification_settings import ( + create_notification_settings, +) + + +def execute(): + influxframework.reload_doc("desk", "doctype", "notification_settings") + influxframework.reload_doc("desk", "doctype", "notification_subscribed_document") + + users = influxframework.get_all("User", fields=["name"]) + for user in users: + create_notification_settings(user.name) diff --git a/influxframework/patches/v12_0/delete_duplicate_indexes.py b/influxframework/patches/v12_0/delete_duplicate_indexes.py new file mode 100644 index 0000000..f40e0d0 --- /dev/null +++ b/influxframework/patches/v12_0/delete_duplicate_indexes.py @@ -0,0 +1,53 @@ +import influxframework + +# This patch deletes all the duplicate indexes created for same column +# The patch only checks for indexes with UNIQUE constraints + + +def execute(): + if influxframework.db.db_type != "mariadb": + return + + all_tables = influxframework.db.get_tables() + final_deletion_map = influxframework._dict() + + for table in all_tables: + indexes_to_keep_map = influxframework._dict() + indexes_to_delete = [] + index_info = influxframework.db.sql( + """ + SELECT + column_name, + index_name, + non_unique + FROM information_schema.STATISTICS + WHERE table_name=%s + AND column_name!='name' + AND non_unique=0 + ORDER BY index_name; + """, + table, + as_dict=1, + ) + + for index in index_info: + if not indexes_to_keep_map.get(index.column_name): + indexes_to_keep_map[index.column_name] = index + else: + indexes_to_delete.append(index.index_name) + if indexes_to_delete: + final_deletion_map[table] = indexes_to_delete + + # build drop index query + for (table_name, index_list) in final_deletion_map.items(): + query_list = [] + alter_query = f"ALTER TABLE `{table_name}`" + + for index in index_list: + query_list.append(f"{alter_query} DROP INDEX `{index}`") + + for query in query_list: + try: + influxframework.db.sql(query) + except influxframework.db.InternalError: + pass diff --git a/influxframework/patches/v12_0/delete_feedback_request_if_exists.py b/influxframework/patches/v12_0/delete_feedback_request_if_exists.py new file mode 100644 index 0000000..999aee7 --- /dev/null +++ b/influxframework/patches/v12_0/delete_feedback_request_if_exists.py @@ -0,0 +1,5 @@ +import influxframework + + +def execute(): + influxframework.db.delete("DocType", {"name": "Feedback Request"}) diff --git a/influxframework/patches/v12_0/fix_email_id_formatting.py b/influxframework/patches/v12_0/fix_email_id_formatting.py new file mode 100644 index 0000000..f5fe080 --- /dev/null +++ b/influxframework/patches/v12_0/fix_email_id_formatting.py @@ -0,0 +1,61 @@ +import influxframework + + +def execute(): + fix_communications() + fix_show_as_cc_email_queue() + fix_email_queue_recipients() + + +def fix_communications(): + for communication in influxframework.db.sql( + """select name, recipients, cc, bcc from tabCommunication + where creation > '2020-06-01' + and communication_medium='Email' + and communication_type='Communication' + and (cc like '%<%' or bcc like '%<%' or recipients like '%<%') + """, + as_dict=1, + ): + + communication["recipients"] = format_email_id(communication.recipients) + communication["cc"] = format_email_id(communication.cc) + communication["bcc"] = format_email_id(communication.bcc) + + influxframework.db.sql( + """update `tabCommunication` set recipients=%s,cc=%s,bcc=%s + where name =%s """, + (communication["recipients"], communication["cc"], communication["bcc"], communication["name"]), + ) + + +def fix_show_as_cc_email_queue(): + for queue in influxframework.get_all( + "Email Queue", + {"creation": [">", "2020-06-01"], "status": "Not Sent", "show_as_cc": ["like", "%<%"]}, + ["name", "show_as_cc"], + ): + + influxframework.db.set_value( + "Email Queue", queue["name"], "show_as_cc", format_email_id(queue["show_as_cc"]) + ) + + +def fix_email_queue_recipients(): + for recipient in influxframework.db.sql( + """select recipient, name from + `tabEmail Queue Recipient` where recipient like '%<%' + and status='Not Sent' and creation > '2020-06-01' """, + as_dict=1, + ): + + influxframework.db.set_value( + "Email Queue Recipient", recipient["name"], "recipient", format_email_id(recipient["recipient"]) + ) + + +def format_email_id(email): + if email and ("<" in email and ">" in email): + return email.replace(">", ">").replace("<", "<") + + return email diff --git a/influxframework/patches/v12_0/fix_public_private_files.py b/influxframework/patches/v12_0/fix_public_private_files.py new file mode 100644 index 0000000..ad65250 --- /dev/null +++ b/influxframework/patches/v12_0/fix_public_private_files.py @@ -0,0 +1,36 @@ +import influxframework + + +def execute(): + files = influxframework.get_all( + "File", fields=["is_private", "file_url", "name"], filters={"is_folder": 0} + ) + + for file in files: + file_url = file.file_url or "" + if file.is_private: + if not file_url.startswith("/private/files/"): + generate_file(file.name) + else: + if file_url.startswith("/private/files/"): + generate_file(file.name) + + +def generate_file(file_name): + try: + file_doc = influxframework.get_doc("File", file_name) + # private + new_doc = influxframework.new_doc("File") + new_doc.is_private = file_doc.is_private + new_doc.file_name = file_doc.file_name + # to create copy of file in right location + # if the file doc is private then the file will be created in /private folder + # if the file doc is public then the file will be created in /files folder + new_doc.save_file(content=file_doc.get_content(), ignore_existing_file_check=True) + + file_doc.file_url = new_doc.file_url + file_doc.save() + except OSError: + pass + except Exception as e: + print(e) diff --git a/influxframework/patches/v12_0/move_email_and_phone_to_child_table.py b/influxframework/patches/v12_0/move_email_and_phone_to_child_table.py new file mode 100644 index 0000000..cbcaa41 --- /dev/null +++ b/influxframework/patches/v12_0/move_email_and_phone_to_child_table.py @@ -0,0 +1,110 @@ +import influxframework + + +def execute(): + influxframework.reload_doc("contacts", "doctype", "contact_email") + influxframework.reload_doc("contacts", "doctype", "contact_phone") + influxframework.reload_doc("contacts", "doctype", "contact") + + contact_details = influxframework.db.sql( + """ + SELECT + `name`, `email_id`, `phone`, `mobile_no`, `modified_by`, `creation`, `modified` + FROM `tabContact` + where not exists (select * from `tabContact Email` + where `tabContact Email`.parent=`tabContact`.name + and `tabContact Email`.email_id=`tabContact`.email_id) + """, + as_dict=True, + ) + + email_values = [] + phone_values = [] + for count, contact_detail in enumerate(contact_details): + phone_counter = 1 + is_primary = 1 + if contact_detail.email_id: + email_values.append( + ( + 1, + influxframework.generate_hash(contact_detail.email_id, 10), + contact_detail.email_id, + "email_ids", + "Contact", + contact_detail.name, + 1, + contact_detail.creation, + contact_detail.modified, + contact_detail.modified_by, + ) + ) + + if contact_detail.phone: + is_primary_phone = 1 if phone_counter == 1 else 0 + phone_values.append( + ( + phone_counter, + influxframework.generate_hash(contact_detail.email_id, 10), + contact_detail.phone, + "phone_nos", + "Contact", + contact_detail.name, + is_primary_phone, + 0, + contact_detail.creation, + contact_detail.modified, + contact_detail.modified_by, + ) + ) + phone_counter += 1 + + if contact_detail.mobile_no: + is_primary_mobile_no = 1 if phone_counter == 1 else 0 + phone_values.append( + ( + phone_counter, + influxframework.generate_hash(contact_detail.email_id, 10), + contact_detail.mobile_no, + "phone_nos", + "Contact", + contact_detail.name, + 0, + is_primary_mobile_no, + contact_detail.creation, + contact_detail.modified, + contact_detail.modified_by, + ) + ) + + if email_values and (count % 10000 == 0 or count == len(contact_details) - 1): + influxframework.db.sql( + """ + INSERT INTO `tabContact Email` + (`idx`, `name`, `email_id`, `parentfield`, `parenttype`, `parent`, `is_primary`, `creation`, + `modified`, `modified_by`) + VALUES {} + """.format( + ", ".join(["%s"] * len(email_values)) + ), + tuple(email_values), + ) + + email_values = [] + + if phone_values and (count % 10000 == 0 or count == len(contact_details) - 1): + influxframework.db.sql( + """ + INSERT INTO `tabContact Phone` + (`idx`, `name`, `phone`, `parentfield`, `parenttype`, `parent`, `is_primary_phone`, `is_primary_mobile_no`, `creation`, + `modified`, `modified_by`) + VALUES {} + """.format( + ", ".join(["%s"] * len(phone_values)) + ), + tuple(phone_values), + ) + + phone_values = [] + + influxframework.db.add_index("Contact Phone", ["phone"]) + influxframework.db.add_index("Contact Email", ["email_id"]) diff --git a/influxframework/patches/v12_0/move_form_attachments_to_attachments_folder.py b/influxframework/patches/v12_0/move_form_attachments_to_attachments_folder.py new file mode 100644 index 0000000..551528e --- /dev/null +++ b/influxframework/patches/v12_0/move_form_attachments_to_attachments_folder.py @@ -0,0 +1,12 @@ +import influxframework + + +def execute(): + influxframework.db.sql( + """ + UPDATE tabFile + SET folder = 'Home/Attachments' + WHERE ifnull(attached_to_doctype, '') != '' + AND folder = 'Home' + """ + ) diff --git a/influxframework/patches/v12_0/move_timeline_links_to_dynamic_links.py b/influxframework/patches/v12_0/move_timeline_links_to_dynamic_links.py new file mode 100644 index 0000000..4c5f41f --- /dev/null +++ b/influxframework/patches/v12_0/move_timeline_links_to_dynamic_links.py @@ -0,0 +1,66 @@ +import influxframework + + +def execute(): + communications = influxframework.db.sql( + """ + SELECT + `tabCommunication`.name, `tabCommunication`.creation, `tabCommunication`.modified, + `tabCommunication`.modified_by,`tabCommunication`.timeline_doctype, `tabCommunication`.timeline_name, + `tabCommunication`.link_doctype, `tabCommunication`.link_name + FROM `tabCommunication` + WHERE `tabCommunication`.communication_medium='Email' + """, + as_dict=True, + ) + + name = 1000000000 + values = [] + + for count, communication in enumerate(communications): + counter = 1 + if communication.timeline_doctype and communication.timeline_name: + name += 1 + values.append( + """({}, "{}", "timeline_links", "Communication", "{}", "{}", "{}", "{}", "{}", "{}")""".format( + counter, + str(name), + influxframework.db.escape(communication.name), + influxframework.db.escape(communication.timeline_doctype), + influxframework.db.escape(communication.timeline_name), + communication.creation, + communication.modified, + communication.modified_by, + ) + ) + counter += 1 + if communication.link_doctype and communication.link_name: + name += 1 + values.append( + """({}, "{}", "timeline_links", "Communication", "{}", "{}", "{}", "{}", "{}", "{}")""".format( + counter, + str(name), + influxframework.db.escape(communication.name), + influxframework.db.escape(communication.link_doctype), + influxframework.db.escape(communication.link_name), + communication.creation, + communication.modified, + communication.modified_by, + ) + ) + + if values and (count % 10000 == 0 or count == len(communications) - 1): + influxframework.db.sql( + """ + INSERT INTO `tabCommunication Link` + (`idx`, `name`, `parentfield`, `parenttype`, `parent`, `link_doctype`, `link_name`, `creation`, + `modified`, `modified_by`) + VALUES {} + """.format( + ", ".join([d for d in values]) + ) + ) + + values = [] + + influxframework.db.add_index("Communication Link", ["link_doctype", "link_name"]) diff --git a/influxframework/patches/v12_0/remove_deprecated_fields_from_doctype.py b/influxframework/patches/v12_0/remove_deprecated_fields_from_doctype.py new file mode 100644 index 0000000..a1b1d76 --- /dev/null +++ b/influxframework/patches/v12_0/remove_deprecated_fields_from_doctype.py @@ -0,0 +1,12 @@ +import influxframework + + +def execute(): + influxframework.reload_doc("core", "doctype", "doctype_link") + influxframework.reload_doc("core", "doctype", "doctype_action") + influxframework.reload_doc("core", "doctype", "doctype") + influxframework.model.delete_fields( + {"DocType": ["hide_heading", "image_view", "read_only_onload"]}, delete=1 + ) + + influxframework.db.delete("Property Setter", {"property": "read_only_onload"}) diff --git a/influxframework/patches/v12_0/remove_example_email_thread_notify.py b/influxframework/patches/v12_0/remove_example_email_thread_notify.py new file mode 100644 index 0000000..dea565d --- /dev/null +++ b/influxframework/patches/v12_0/remove_example_email_thread_notify.py @@ -0,0 +1,10 @@ +import influxframework + + +def execute(): + # remove all example.com email user accounts from notifications + influxframework.db.sql( + """UPDATE `tabUser` + SET thread_notify=0, send_me_a_copy=0 + WHERE email like '%@example.com'""" + ) diff --git a/influxframework/patches/v12_0/remove_feedback_rating.py b/influxframework/patches/v12_0/remove_feedback_rating.py new file mode 100644 index 0000000..47fe70e --- /dev/null +++ b/influxframework/patches/v12_0/remove_feedback_rating.py @@ -0,0 +1,10 @@ +import influxframework + + +def execute(): + """ + Deprecate Feedback Trigger and Rating. This feature was not customizable. + Now can be achieved via custom Web Forms + """ + influxframework.delete_doc("DocType", "Feedback Trigger") + influxframework.delete_doc("DocType", "Feedback Rating") diff --git a/influxframework/patches/v12_0/rename_events_repeat_on.py b/influxframework/patches/v12_0/rename_events_repeat_on.py new file mode 100644 index 0000000..de2680f --- /dev/null +++ b/influxframework/patches/v12_0/rename_events_repeat_on.py @@ -0,0 +1,37 @@ +import influxframework +from influxframework.utils import get_datetime + + +def execute(): + weekdays = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"] + + weekly_events = influxframework.get_list( + "Event", + filters={"repeat_this_event": 1, "repeat_on": "Every Week"}, + fields=["name", "starts_on"], + ) + influxframework.reload_doc("desk", "doctype", "event") + + # Initially Daily Events had option to choose days, but now Weekly does, so just changing from Daily -> Weekly does the job + influxframework.db.sql( + """UPDATE `tabEvent` SET `tabEvent`.repeat_on='Weekly' WHERE `tabEvent`.repeat_on='Every Day'""" + ) + influxframework.db.sql( + """UPDATE `tabEvent` SET `tabEvent`.repeat_on='Weekly' WHERE `tabEvent`.repeat_on='Every Week'""" + ) + influxframework.db.sql( + """UPDATE `tabEvent` SET `tabEvent`.repeat_on='Monthly' WHERE `tabEvent`.repeat_on='Every Month'""" + ) + influxframework.db.sql( + """UPDATE `tabEvent` SET `tabEvent`.repeat_on='Yearly' WHERE `tabEvent`.repeat_on='Every Year'""" + ) + + for weekly_event in weekly_events: + # Set WeekDay based on the starts_on so that event can repeat Weekly + influxframework.db.set_value( + "Event", + weekly_event.name, + weekdays[get_datetime(weekly_event.starts_on).weekday()], + 1, + update_modified=False, + ) diff --git a/influxframework/patches/v12_0/rename_uploaded_files_with_proper_name.py b/influxframework/patches/v12_0/rename_uploaded_files_with_proper_name.py new file mode 100644 index 0000000..cea60ab --- /dev/null +++ b/influxframework/patches/v12_0/rename_uploaded_files_with_proper_name.py @@ -0,0 +1,33 @@ +import os + +import influxframework + + +def execute(): + file_names_with_url = influxframework.get_all( + "File", + filters={"is_folder": 0, "file_name": ["like", "%/%"]}, + fields=["name", "file_name", "file_url"], + ) + + for f in file_names_with_url: + filename = f.file_name.rsplit("/", 1)[-1] + + if not f.file_url: + f.file_url = f.file_name + + try: + if not file_exists(f.file_url): + continue + influxframework.db.set_value( + "File", f.name, {"file_name": filename, "file_url": f.file_url}, update_modified=False + ) + except Exception: + continue + + +def file_exists(file_path): + file_path = influxframework.utils.get_files_path( + file_path.rsplit("/", 1)[-1], is_private=file_path.startswith("/private") + ) + return os.path.exists(file_path) diff --git a/influxframework/patches/v12_0/replace_null_values_in_tables.py b/influxframework/patches/v12_0/replace_null_values_in_tables.py new file mode 100644 index 0000000..a3cae2f --- /dev/null +++ b/influxframework/patches/v12_0/replace_null_values_in_tables.py @@ -0,0 +1,30 @@ +import re + +import influxframework + + +def execute(): + fields = influxframework.db.sql( + """ + SELECT COLUMN_NAME , TABLE_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS + WHERE DATA_TYPE IN ('INT', 'FLOAT', 'DECIMAL') AND IS_NULLABLE = 'YES' + """, + as_dict=1, + ) + + update_column_table_map = {} + + for field in fields: + update_column_table_map.setdefault(field.TABLE_NAME, []) + + update_column_table_map[field.TABLE_NAME].append( + "`{fieldname}`=COALESCE(`{fieldname}`, 0)".format(fieldname=field.COLUMN_NAME) + ) + + for table in influxframework.db.get_tables(): + if update_column_table_map.get(table) and influxframework.db.exists("DocType", re.sub("^tab", "", table)): + influxframework.db.sql( + """UPDATE `{table}` SET {columns}""".format( + table=table, columns=", ".join(update_column_table_map.get(table)) + ) + ) diff --git a/influxframework/patches/v12_0/reset_home_settings.py b/influxframework/patches/v12_0/reset_home_settings.py new file mode 100644 index 0000000..bea0020 --- /dev/null +++ b/influxframework/patches/v12_0/reset_home_settings.py @@ -0,0 +1,12 @@ +import influxframework + + +def execute(): + influxframework.reload_doc("core", "doctype", "user") + influxframework.db.sql( + """ + UPDATE `tabUser` + SET `home_settings` = '' + WHERE `user_type` = 'System User' + """ + ) diff --git a/influxframework/patches/v12_0/set_correct_assign_value_in_docs.py b/influxframework/patches/v12_0/set_correct_assign_value_in_docs.py new file mode 100644 index 0000000..f6ebbf5 --- /dev/null +++ b/influxframework/patches/v12_0/set_correct_assign_value_in_docs.py @@ -0,0 +1,28 @@ +import influxframework +from influxframework.query_builder.functions import Coalesce, GroupConcat + + +def execute(): + influxframework.reload_doc("desk", "doctype", "todo") + + ToDo = influxframework.qb.DocType("ToDo") + assignees = GroupConcat("owner").distinct().as_("assignees") + + assignments = ( + influxframework.qb.from_(ToDo) + .select(ToDo.name, ToDo.reference_type, assignees) + .where(Coalesce(ToDo.reference_type, "") != "") + .where(Coalesce(ToDo.reference_name, "") != "") + .where(ToDo.status != "Cancelled") + .groupby(ToDo.reference_type, ToDo.reference_name) + ).run(as_dict=True) + + for doc in assignments: + assignments = doc.assignees.split(",") + influxframework.db.set_value( + doc.reference_type, + doc.reference_name, + "_assign", + influxframework.as_json(assignments), + update_modified=False, + ) diff --git a/influxframework/patches/v12_0/set_correct_url_in_files.py b/influxframework/patches/v12_0/set_correct_url_in_files.py new file mode 100644 index 0000000..07b738f --- /dev/null +++ b/influxframework/patches/v12_0/set_correct_url_in_files.py @@ -0,0 +1,39 @@ +import os + +import influxframework + + +def execute(): + files = influxframework.get_all( + "File", + fields=["name", "file_name", "file_url"], + filters={ + "is_folder": 0, + "file_url": ["!=", ""], + }, + ) + + private_file_path = influxframework.get_site_path("private", "files") + public_file_path = influxframework.get_site_path("public", "files") + + for file in files: + file_path = file.file_url + file_name = file_path.split("/")[-1] + + if not file_path.startswith(("/private/", "/files/")): + continue + + file_is_private = file_path.startswith("/private/files/") + full_path = influxframework.utils.get_files_path(file_name, is_private=file_is_private) + + if not os.path.exists(full_path): + if file_is_private: + public_file_url = os.path.join(public_file_path, file_name) + if os.path.exists(public_file_url): + influxframework.db.set_value("File", file.name, {"file_url": f"/files/{file_name}", "is_private": 0}) + else: + private_file_url = os.path.join(private_file_path, file_name) + if os.path.exists(private_file_url): + influxframework.db.set_value( + "File", file.name, {"file_url": f"/private/files/{file_name}", "is_private": 1} + ) diff --git a/influxframework/patches/v12_0/set_default_incoming_email_port.py b/influxframework/patches/v12_0/set_default_incoming_email_port.py new file mode 100644 index 0000000..a50dbff --- /dev/null +++ b/influxframework/patches/v12_0/set_default_incoming_email_port.py @@ -0,0 +1,43 @@ +import influxframework +from influxframework.email.utils import get_port + + +def execute(): + """ + 1. Set default incoming email port in email domain + 2. Set default incoming email port in all email account (for those account where domain is missing) + """ + influxframework.reload_doc("email", "doctype", "email_domain", force=True) + influxframework.reload_doc("email", "doctype", "email_account", force=True) + + setup_incoming_email_port_in_email_domains() + setup_incoming_email_port_in_email_accounts() + + +def setup_incoming_email_port_in_email_domains(): + email_domains = influxframework.get_all("Email Domain", ["incoming_port", "use_imap", "use_ssl", "name"]) + for domain in email_domains: + if not domain.incoming_port: + incoming_port = get_port(domain) + influxframework.db.set_value( + "Email Domain", domain.name, "incoming_port", incoming_port, update_modified=False + ) + + # update incoming email port in all + influxframework.db.sql( + """update `tabEmail Account` set incoming_port=%s where domain = %s""", + (domain.incoming_port, domain.name), + ) + + +def setup_incoming_email_port_in_email_accounts(): + email_accounts = influxframework.get_all( + "Email Account", ["incoming_port", "use_imap", "use_ssl", "name", "enable_incoming"] + ) + + for account in email_accounts: + if account.enable_incoming and not account.incoming_port: + incoming_port = get_port(account) + influxframework.db.set_value( + "Email Account", account.name, "incoming_port", incoming_port, update_modified=False + ) diff --git a/influxframework/patches/v12_0/set_default_password_reset_limit.py b/influxframework/patches/v12_0/set_default_password_reset_limit.py new file mode 100644 index 0000000..774b87c --- /dev/null +++ b/influxframework/patches/v12_0/set_default_password_reset_limit.py @@ -0,0 +1,9 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework + + +def execute(): + influxframework.reload_doc("core", "doctype", "system_settings", force=1) + influxframework.db.set_value("System Settings", None, "password_reset_limit", 3) diff --git a/influxframework/patches/v12_0/set_primary_key_in_series.py b/influxframework/patches/v12_0/set_primary_key_in_series.py new file mode 100644 index 0000000..485b455 --- /dev/null +++ b/influxframework/patches/v12_0/set_primary_key_in_series.py @@ -0,0 +1,26 @@ +import influxframework + + +def execute(): + # if current = 0, simply delete the key as it'll be recreated on first entry + influxframework.db.delete("Series", {"current": 0}) + + duplicate_keys = influxframework.db.sql( + """ + SELECT name, max(current) as current + from + `tabSeries` + group by + name + having count(name) > 1 + """, + as_dict=True, + ) + + for row in duplicate_keys: + influxframework.db.delete("Series", {"name": row.name}) + if row.current: + influxframework.db.sql("insert into `tabSeries`(`name`, `current`) values (%(name)s, %(current)s)", row) + influxframework.db.commit() + + influxframework.db.sql("ALTER table `tabSeries` ADD PRIMARY KEY IF NOT EXISTS (name)") diff --git a/influxframework/patches/v12_0/setup_comments_from_communications.py b/influxframework/patches/v12_0/setup_comments_from_communications.py new file mode 100644 index 0000000..19a8309 --- /dev/null +++ b/influxframework/patches/v12_0/setup_comments_from_communications.py @@ -0,0 +1,34 @@ +import influxframework + + +def execute(): + influxframework.reload_doctype("Comment") + + if influxframework.db.count("Communication", filters=dict(communication_type="Comment")) > 20000: + influxframework.db.auto_commit_on_many_writes = True + + for comment in influxframework.get_all( + "Communication", fields=["*"], filters=dict(communication_type="Comment") + ): + + new_comment = influxframework.new_doc("Comment") + new_comment.comment_type = comment.comment_type + new_comment.comment_email = comment.sender + new_comment.comment_by = comment.sender_full_name + new_comment.subject = comment.subject + new_comment.content = comment.content or comment.subject + new_comment.reference_doctype = comment.reference_doctype + new_comment.reference_name = comment.reference_name + new_comment.link_doctype = comment.link_doctype + new_comment.link_name = comment.link_name + new_comment.creation = comment.creation + new_comment.modified = comment.modified + new_comment.owner = comment.owner + new_comment.modified_by = comment.modified_by + new_comment.db_insert() + + if influxframework.db.auto_commit_on_many_writes: + influxframework.db.auto_commit_on_many_writes = False + + # clean up + influxframework.db.delete("Communication", {"communication_type": "Comment"}) diff --git a/influxframework/patches/v12_0/setup_email_linking.py b/influxframework/patches/v12_0/setup_email_linking.py new file mode 100644 index 0000000..2b61f49 --- /dev/null +++ b/influxframework/patches/v12_0/setup_email_linking.py @@ -0,0 +1,5 @@ +from influxframework.desk.page.setup_wizard.install_fixtures import setup_email_linking + + +def execute(): + setup_email_linking() diff --git a/influxframework/patches/v12_0/setup_tags.py b/influxframework/patches/v12_0/setup_tags.py new file mode 100644 index 0000000..27ca970 --- /dev/null +++ b/influxframework/patches/v12_0/setup_tags.py @@ -0,0 +1,47 @@ +import influxframework + + +def execute(): + influxframework.delete_doc_if_exists("DocType", "Tag Category") + influxframework.delete_doc_if_exists("DocType", "Tag Doc Category") + + influxframework.reload_doc("desk", "doctype", "tag") + influxframework.reload_doc("desk", "doctype", "tag_link") + + tag_list = [] + tag_links = [] + time = influxframework.utils.get_datetime() + + for doctype in influxframework.get_list("DocType", filters={"istable": 0, "issingle": 0}): + if not influxframework.db.count(doctype.name) or not influxframework.db.has_column(doctype.name, "_user_tags"): + continue + + for _user_tags in influxframework.db.sql( + f"select `name`, `_user_tags` from `tab{doctype.name}`", as_dict=True + ): + if not _user_tags.get("_user_tags"): + continue + + for tag in _user_tags.get("_user_tags").split(",") if _user_tags.get("_user_tags") else []: + if not tag: + continue + + tag_list.append((tag.strip(), time, time, "Administrator")) + + tag_link_name = influxframework.generate_hash(_user_tags.name + tag.strip() + doctype.name, 10) + tag_links.append( + (tag_link_name, doctype.name, _user_tags.name, tag.strip(), time, time, "Administrator") + ) + + influxframework.db.bulk_insert( + "Tag", + fields=["name", "creation", "modified", "modified_by"], + values=set(tag_list), + ignore_duplicates=True, + ) + influxframework.db.bulk_insert( + "Tag Link", + fields=["name", "document_type", "document_name", "tag", "creation", "modified", "modified_by"], + values=set(tag_links), + ignore_duplicates=True, + ) diff --git a/influxframework/patches/v12_0/update_auto_repeat_status_and_not_submittable.py b/influxframework/patches/v12_0/update_auto_repeat_status_and_not_submittable.py new file mode 100644 index 0000000..483d06f --- /dev/null +++ b/influxframework/patches/v12_0/update_auto_repeat_status_and_not_submittable.py @@ -0,0 +1,34 @@ +import influxframework +from influxframework.custom.doctype.custom_field.custom_field import create_custom_field + + +def execute(): + # auto repeat is not submittable in v12 + influxframework.reload_doc("automation", "doctype", "Auto Repeat") + influxframework.db.sql("update `tabDocPerm` set submit=0, cancel=0, amend=0 where parent='Auto Repeat'") + influxframework.db.sql("update `tabAuto Repeat` set docstatus=0 where docstatus=1 or docstatus=2") + + for entry in influxframework.get_all("Auto Repeat"): + doc = influxframework.get_doc("Auto Repeat", entry.name) + + # create custom field for allow auto repeat + fields = influxframework.get_meta(doc.reference_doctype).fields + insert_after = fields[len(fields) - 1].fieldname + df = dict( + fieldname="auto_repeat", + label="Auto Repeat", + fieldtype="Link", + insert_after=insert_after, + options="Auto Repeat", + hidden=1, + print_hide=1, + read_only=1, + ) + create_custom_field(doc.reference_doctype, df) + + if doc.status in ["Draft", "Stopped", "Cancelled"]: + doc.disabled = 1 + + doc.flags.ignore_links = 1 + # updates current status as Active, Disabled or Completed on validate + doc.save() diff --git a/influxframework/patches/v12_0/update_global_search.py b/influxframework/patches/v12_0/update_global_search.py new file mode 100644 index 0000000..62cf8a1 --- /dev/null +++ b/influxframework/patches/v12_0/update_global_search.py @@ -0,0 +1,8 @@ +import influxframework +from influxframework.desk.page.setup_wizard.install_fixtures import update_global_search_doctypes + + +def execute(): + influxframework.reload_doc("desk", "doctype", "global_search_doctype") + influxframework.reload_doc("desk", "doctype", "global_search_settings") + update_global_search_doctypes() diff --git a/influxframework/patches/v12_0/update_print_format_type.py b/influxframework/patches/v12_0/update_print_format_type.py new file mode 100644 index 0000000..b93e989 --- /dev/null +++ b/influxframework/patches/v12_0/update_print_format_type.py @@ -0,0 +1,18 @@ +import influxframework + + +def execute(): + influxframework.db.sql( + """ + UPDATE `tabPrint Format` + SET `print_format_type` = 'Jinja' + WHERE `print_format_type` in ('Server', 'Client') + """ + ) + influxframework.db.sql( + """ + UPDATE `tabPrint Format` + SET `print_format_type` = 'JS' + WHERE `print_format_type` = 'Js' + """ + ) diff --git a/influxframework/patches/v13_0/__init__.py b/influxframework/patches/v13_0/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/patches/v13_0/add_standard_navbar_items.py b/influxframework/patches/v13_0/add_standard_navbar_items.py new file mode 100644 index 0000000..4e2f6ed --- /dev/null +++ b/influxframework/patches/v13_0/add_standard_navbar_items.py @@ -0,0 +1,9 @@ +import influxframework +from influxframework.utils.install import add_standard_navbar_items + + +def execute(): + # Add standard navbar items for InfluxERP in Navbar Settings + influxframework.reload_doc("core", "doctype", "navbar_settings") + influxframework.reload_doc("core", "doctype", "navbar_item") + add_standard_navbar_items() diff --git a/influxframework/patches/v13_0/add_switch_theme_to_navbar_settings.py b/influxframework/patches/v13_0/add_switch_theme_to_navbar_settings.py new file mode 100644 index 0000000..f2a0421 --- /dev/null +++ b/influxframework/patches/v13_0/add_switch_theme_to_navbar_settings.py @@ -0,0 +1,24 @@ +import influxframework + + +def execute(): + navbar_settings = influxframework.get_single("Navbar Settings") + + if influxframework.db.exists("Navbar Item", {"item_label": "Toggle Theme"}): + return + + for navbar_item in navbar_settings.settings_dropdown[6:]: + navbar_item.idx = navbar_item.idx + 1 + + navbar_settings.append( + "settings_dropdown", + { + "item_label": "Toggle Theme", + "item_type": "Action", + "action": "new influxframework.ui.ThemeSwitcher().show()", + "is_standard": 1, + "idx": 7, + }, + ) + + navbar_settings.save() diff --git a/influxframework/patches/v13_0/add_toggle_width_in_navbar_settings.py b/influxframework/patches/v13_0/add_toggle_width_in_navbar_settings.py new file mode 100644 index 0000000..f8ff899 --- /dev/null +++ b/influxframework/patches/v13_0/add_toggle_width_in_navbar_settings.py @@ -0,0 +1,24 @@ +import influxframework + + +def execute(): + navbar_settings = influxframework.get_single("Navbar Settings") + + if influxframework.db.exists("Navbar Item", {"item_label": "Toggle Full Width"}): + return + + for navbar_item in navbar_settings.settings_dropdown[5:]: + navbar_item.idx = navbar_item.idx + 1 + + navbar_settings.append( + "settings_dropdown", + { + "item_label": "Toggle Full Width", + "item_type": "Action", + "action": "influxframework.ui.toolbar.toggle_full_width()", + "is_standard": 1, + "idx": 6, + }, + ) + + navbar_settings.save() diff --git a/influxframework/patches/v13_0/create_custom_dashboards_cards_and_charts.py b/influxframework/patches/v13_0/create_custom_dashboards_cards_and_charts.py new file mode 100644 index 0000000..d0ba266 --- /dev/null +++ b/influxframework/patches/v13_0/create_custom_dashboards_cards_and_charts.py @@ -0,0 +1,48 @@ +import influxframework +from influxframework.model.naming import append_number_if_name_exists +from influxframework.utils.dashboard import get_dashboards_with_link + + +def execute(): + if ( + not influxframework.db.table_exists("Dashboard Chart") + or not influxframework.db.table_exists("Number Card") + or not influxframework.db.table_exists("Dashboard") + ): + return + + influxframework.reload_doc("desk", "doctype", "dashboard_chart") + influxframework.reload_doc("desk", "doctype", "number_card") + influxframework.reload_doc("desk", "doctype", "dashboard") + + modified_charts = get_modified_docs("Dashboard Chart") + modified_cards = get_modified_docs("Number Card") + modified_dashboards = [doc.name for doc in get_modified_docs("Dashboard")] + + for chart in modified_charts: + modified_dashboards += get_dashboards_with_link(chart.name, "Dashboard Chart") + rename_modified_doc(chart.name, "Dashboard Chart") + + for card in modified_cards: + modified_dashboards += get_dashboards_with_link(card.name, "Number Card") + rename_modified_doc(card.name, "Number Card") + + modified_dashboards = list(set(modified_dashboards)) + + for dashboard in modified_dashboards: + rename_modified_doc(dashboard, "Dashboard") + + +def get_modified_docs(doctype): + return influxframework.get_all( + doctype, filters={"owner": "Administrator", "modified_by": ["!=", "Administrator"]} + ) + + +def rename_modified_doc(docname, doctype): + new_name = docname + " Custom" + try: + influxframework.rename_doc(doctype, docname, new_name) + except influxframework.ValidationError: + new_name = append_number_if_name_exists(doctype, new_name) + influxframework.rename_doc(doctype, docname, new_name) diff --git a/influxframework/patches/v13_0/delete_event_producer_and_consumer_keys.py b/influxframework/patches/v13_0/delete_event_producer_and_consumer_keys.py new file mode 100644 index 0000000..138942c --- /dev/null +++ b/influxframework/patches/v13_0/delete_event_producer_and_consumer_keys.py @@ -0,0 +1,11 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework + + +def execute(): + if influxframework.db.exists("DocType", "Event Producer"): + influxframework.db.sql("""UPDATE `tabEvent Producer` SET api_key='', api_secret=''""") + if influxframework.db.exists("DocType", "Event Consumer"): + influxframework.db.sql("""UPDATE `tabEvent Consumer` SET api_key='', api_secret=''""") diff --git a/influxframework/patches/v13_0/delete_package_publish_tool.py b/influxframework/patches/v13_0/delete_package_publish_tool.py new file mode 100644 index 0000000..82e06ed --- /dev/null +++ b/influxframework/patches/v13_0/delete_package_publish_tool.py @@ -0,0 +1,10 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework + + +def execute(): + influxframework.delete_doc("DocType", "Package Publish Tool", ignore_missing=True) + influxframework.delete_doc("DocType", "Package Document Type", ignore_missing=True) + influxframework.delete_doc("DocType", "Package Publish Target", ignore_missing=True) diff --git a/influxframework/patches/v13_0/email_unsubscribe.py b/influxframework/patches/v13_0/email_unsubscribe.py new file mode 100644 index 0000000..96a4c74 --- /dev/null +++ b/influxframework/patches/v13_0/email_unsubscribe.py @@ -0,0 +1,14 @@ +import influxframework + + +def execute(): + email_unsubscribe = [ + {"email": "admin@example.com", "global_unsubscribe": 1}, + {"email": "guest@example.com", "global_unsubscribe": 1}, + ] + + for unsubscribe in email_unsubscribe: + if not influxframework.get_all("Email Unsubscribe", filters=unsubscribe): + doc = influxframework.new_doc("Email Unsubscribe") + doc.update(unsubscribe) + doc.insert(ignore_permissions=True) diff --git a/influxframework/patches/v13_0/enable_custom_script.py b/influxframework/patches/v13_0/enable_custom_script.py new file mode 100644 index 0000000..86d0f8b --- /dev/null +++ b/influxframework/patches/v13_0/enable_custom_script.py @@ -0,0 +1,14 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework + + +def execute(): + """Enable all the existing Client script""" + + influxframework.db.sql( + """ + UPDATE `tabClient Script` SET enabled=1 + """ + ) diff --git a/influxframework/patches/v13_0/encrypt_2fa_secrets.py b/influxframework/patches/v13_0/encrypt_2fa_secrets.py new file mode 100644 index 0000000..734b2c2 --- /dev/null +++ b/influxframework/patches/v13_0/encrypt_2fa_secrets.py @@ -0,0 +1,45 @@ +import influxframework +import influxframework.defaults +from influxframework.cache_manager import clear_defaults_cache +from influxframework.twofactor import PARENT_FOR_DEFAULTS +from influxframework.utils.password import encrypt + +DOCTYPE = "DefaultValue" +OLD_PARENT = "__default" + + +def execute(): + table = influxframework.qb.DocType(DOCTYPE) + + # set parent for `*_otplogin` + ( + influxframework.qb.update(table) + .set(table.parent, PARENT_FOR_DEFAULTS) + .where(table.parent == OLD_PARENT) + .where(table.defkey.like("%_otplogin")) + ).run() + + # update records for `*_otpsecret` + secrets = { + key: value + for key, value in influxframework.defaults.get_defaults_for(parent=OLD_PARENT).items() + if key.endswith("_otpsecret") + } + + if not secrets: + return + + defvalue_cases = influxframework.qb.terms.Case() + + for key, value in secrets.items(): + defvalue_cases.when(table.defkey == key, encrypt(value)) + + ( + influxframework.qb.update(table) + .set(table.parent, PARENT_FOR_DEFAULTS) + .set(table.defvalue, defvalue_cases) + .where(table.parent == OLD_PARENT) + .where(table.defkey.like("%_otpsecret")) + ).run() + + clear_defaults_cache() diff --git a/influxframework/patches/v13_0/generate_theme_files_in_public_folder.py b/influxframework/patches/v13_0/generate_theme_files_in_public_folder.py new file mode 100644 index 0000000..064856c --- /dev/null +++ b/influxframework/patches/v13_0/generate_theme_files_in_public_folder.py @@ -0,0 +1,19 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework + + +def execute(): + influxframework.reload_doc("website", "doctype", "website_theme_ignore_app") + themes = influxframework.get_all( + "Website Theme", filters={"theme_url": ("not like", "/files/website_theme/%")} + ) + for theme in themes: + doc = influxframework.get_doc("Website Theme", theme.name) + try: + doc.generate_bootstrap_theme() + doc.save() + except Exception: + print("Ignoring....") + print(influxframework.get_traceback()) diff --git a/influxframework/patches/v13_0/increase_password_length.py b/influxframework/patches/v13_0/increase_password_length.py new file mode 100644 index 0000000..b7d6611 --- /dev/null +++ b/influxframework/patches/v13_0/increase_password_length.py @@ -0,0 +1,5 @@ +import influxframework + + +def execute(): + influxframework.db.change_column_type("__Auth", column="password", type="TEXT") diff --git a/influxframework/patches/v13_0/jinja_hook.py b/influxframework/patches/v13_0/jinja_hook.py new file mode 100644 index 0000000..2354bcf --- /dev/null +++ b/influxframework/patches/v13_0/jinja_hook.py @@ -0,0 +1,17 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +from click import secho + +import influxframework + + +def execute(): + if influxframework.get_hooks("jenv"): + print() + secho( + 'WARNING: The hook "jenv" is deprecated. Follow the migration guide to use the new "jinja" hook.', + fg="yellow", + ) + secho("https://github.com/influxframework/influxframework/wiki/Migrating-to-Version-13", fg="yellow") + print() diff --git a/influxframework/patches/v13_0/make_user_type.py b/influxframework/patches/v13_0/make_user_type.py new file mode 100644 index 0000000..9e2985f --- /dev/null +++ b/influxframework/patches/v13_0/make_user_type.py @@ -0,0 +1,12 @@ +import influxframework +from influxframework.utils.install import create_user_type + + +def execute(): + influxframework.reload_doc("core", "doctype", "role") + influxframework.reload_doc("core", "doctype", "user_document_type") + influxframework.reload_doc("core", "doctype", "user_type_module") + influxframework.reload_doc("core", "doctype", "user_select_document_type") + influxframework.reload_doc("core", "doctype", "user_type") + + create_user_type() diff --git a/influxframework/patches/v13_0/migrate_translation_column_data.py b/influxframework/patches/v13_0/migrate_translation_column_data.py new file mode 100644 index 0000000..acba218 --- /dev/null +++ b/influxframework/patches/v13_0/migrate_translation_column_data.py @@ -0,0 +1,8 @@ +import influxframework + + +def execute(): + influxframework.reload_doctype("Translation") + influxframework.db.sql( + "UPDATE `tabTranslation` SET `translated_text`=`target_name`, `source_text`=`source_name`, `contributed`=0" + ) diff --git a/influxframework/patches/v13_0/queryreport_columns.py b/influxframework/patches/v13_0/queryreport_columns.py new file mode 100644 index 0000000..9950d6f --- /dev/null +++ b/influxframework/patches/v13_0/queryreport_columns.py @@ -0,0 +1,18 @@ +# Copyright (c) 2021, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +import influxframework + + +def execute(): + """Convert Query Report json to support other content""" + records = influxframework.get_all("Report", filters={"json": ["!=", ""]}, fields=["name", "json"]) + for record in records: + jstr = record["json"] + data = json.loads(jstr) + if isinstance(data, list): + # double escape braces + jstr = f'{{"columns":{jstr}}}' + influxframework.db.update("Report", record["name"], "json", jstr) diff --git a/influxframework/patches/v13_0/remove_chat.py b/influxframework/patches/v13_0/remove_chat.py new file mode 100644 index 0000000..2acac2f --- /dev/null +++ b/influxframework/patches/v13_0/remove_chat.py @@ -0,0 +1,19 @@ +import click + +import influxframework + + +def execute(): + influxframework.delete_doc_if_exists("DocType", "Chat Message") + influxframework.delete_doc_if_exists("DocType", "Chat Message Attachment") + influxframework.delete_doc_if_exists("DocType", "Chat Profile") + influxframework.delete_doc_if_exists("DocType", "Chat Token") + influxframework.delete_doc_if_exists("DocType", "Chat Room User") + influxframework.delete_doc_if_exists("DocType", "Chat Room") + influxframework.delete_doc_if_exists("Module Def", "Chat") + + click.secho( + "Chat Module is moved to a separate app and is removed from InfluxFramework in version-13.\n" + "Please install the app to continue using the chat feature: https://github.com/influxframework/chat", + fg="yellow", + ) diff --git a/influxframework/patches/v13_0/remove_custom_link.py b/influxframework/patches/v13_0/remove_custom_link.py new file mode 100644 index 0000000..6376285 --- /dev/null +++ b/influxframework/patches/v13_0/remove_custom_link.py @@ -0,0 +1,18 @@ +import influxframework + + +def execute(): + """ + Remove the doctype "Custom Link" that was used to add Custom Links to the + Dashboard since this is now managed by Customize Form. + Update `parent` property to the DocType and delte the doctype + """ + influxframework.reload_doctype("DocType Link") + if influxframework.db.has_table("Custom Link"): + for custom_link in influxframework.get_all("Custom Link", ["name", "document_type"]): + influxframework.db.sql( + "update `tabDocType Link` set custom=1, parent=%s where parent=%s", + (custom_link.document_type, custom_link.name), + ) + + influxframework.delete_doc("DocType", "Custom Link") diff --git a/influxframework/patches/v13_0/remove_duplicate_navbar_items.py b/influxframework/patches/v13_0/remove_duplicate_navbar_items.py new file mode 100644 index 0000000..5cf6208 --- /dev/null +++ b/influxframework/patches/v13_0/remove_duplicate_navbar_items.py @@ -0,0 +1,14 @@ +import influxframework + + +def execute(): + navbar_settings = influxframework.get_single("Navbar Settings") + duplicate_items = [] + + for navbar_item in navbar_settings.settings_dropdown: + if navbar_item.item_label == "Toggle Full Width": + duplicate_items.append(navbar_item) + + if len(duplicate_items) > 1: + navbar_settings.remove(duplicate_items[0]) + navbar_settings.save() diff --git a/influxframework/patches/v13_0/remove_invalid_options_for_data_fields.py b/influxframework/patches/v13_0/remove_invalid_options_for_data_fields.py new file mode 100644 index 0000000..626a433 --- /dev/null +++ b/influxframework/patches/v13_0/remove_invalid_options_for_data_fields.py @@ -0,0 +1,15 @@ +# Copyright (c) 2022, InfluxFramework and Contributors +# License: MIT. See LICENSE + + +import influxframework +from influxframework.model import data_field_options + + +def execute(): + custom_field = influxframework.qb.DocType("Custom Field") + ( + influxframework.qb.update(custom_field) + .set(custom_field.options, None) + .where((custom_field.fieldtype == "Data") & (custom_field.options.notin(data_field_options))) + ).run() diff --git a/influxframework/patches/v13_0/remove_tailwind_from_page_builder.py b/influxframework/patches/v13_0/remove_tailwind_from_page_builder.py new file mode 100644 index 0000000..dd61080 --- /dev/null +++ b/influxframework/patches/v13_0/remove_tailwind_from_page_builder.py @@ -0,0 +1,11 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework + + +def execute(): + influxframework.reload_doc("website", "doctype", "web_page_block") + # remove unused templates + influxframework.delete_doc("Web Template", "Navbar with Links on Right", force=1) + influxframework.delete_doc("Web Template", "Footer Horizontal", force=1) diff --git a/influxframework/patches/v13_0/remove_twilio_settings.py b/influxframework/patches/v13_0/remove_twilio_settings.py new file mode 100644 index 0000000..0fffb19 --- /dev/null +++ b/influxframework/patches/v13_0/remove_twilio_settings.py @@ -0,0 +1,20 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework + + +def execute(): + """Add missing Twilio patch. + + While making Twilio as a standaone app, we missed to delete Twilio records from DB through migration. Adding the missing patch. + """ + influxframework.delete_doc_if_exists("DocType", "Twilio Number Group") + if twilio_settings_doctype_in_integrations(): + influxframework.delete_doc_if_exists("DocType", "Twilio Settings") + influxframework.db.delete("Singles", {"doctype": "Twilio Settings"}) + + +def twilio_settings_doctype_in_integrations() -> bool: + """Check Twilio Settings doctype exists in integrations module or not.""" + return influxframework.db.exists("DocType", {"name": "Twilio Settings", "module": "Integrations"}) diff --git a/influxframework/patches/v13_0/remove_web_view.py b/influxframework/patches/v13_0/remove_web_view.py new file mode 100644 index 0000000..9bed381 --- /dev/null +++ b/influxframework/patches/v13_0/remove_web_view.py @@ -0,0 +1,7 @@ +import influxframework + + +def execute(): + influxframework.delete_doc_if_exists("DocType", "Web View") + influxframework.delete_doc_if_exists("DocType", "Web View Component") + influxframework.delete_doc_if_exists("DocType", "CSS Class") diff --git a/influxframework/patches/v13_0/rename_custom_client_script.py b/influxframework/patches/v13_0/rename_custom_client_script.py new file mode 100644 index 0000000..b1aa64e --- /dev/null +++ b/influxframework/patches/v13_0/rename_custom_client_script.py @@ -0,0 +1,13 @@ +import influxframework +from influxframework.model.rename_doc import rename_doc + + +def execute(): + if influxframework.db.exists("DocType", "Client Script"): + return + + influxframework.flags.ignore_route_conflict_validation = True + rename_doc("DocType", "Custom Script", "Client Script") + influxframework.flags.ignore_route_conflict_validation = False + + influxframework.reload_doctype("Client Script", force=True) diff --git a/influxframework/patches/v13_0/rename_desk_page_to_workspace.py b/influxframework/patches/v13_0/rename_desk_page_to_workspace.py new file mode 100644 index 0000000..a2bd354 --- /dev/null +++ b/influxframework/patches/v13_0/rename_desk_page_to_workspace.py @@ -0,0 +1,22 @@ +import influxframework +from influxframework.model.rename_doc import rename_doc + + +def execute(): + if influxframework.db.exists("DocType", "Desk Page"): + if influxframework.db.exists("DocType", "Workspace"): + # this patch was not added initially, so this page might still exist + influxframework.delete_doc("DocType", "Desk Page") + else: + influxframework.flags.ignore_route_conflict_validation = True + rename_doc("DocType", "Desk Page", "Workspace") + influxframework.flags.ignore_route_conflict_validation = False + + rename_doc("DocType", "Desk Chart", "Workspace Chart", ignore_if_exists=True) + rename_doc("DocType", "Desk Shortcut", "Workspace Shortcut", ignore_if_exists=True) + rename_doc("DocType", "Desk Link", "Workspace Link", ignore_if_exists=True) + + influxframework.reload_doc("desk", "doctype", "workspace", force=True) + influxframework.reload_doc("desk", "doctype", "workspace_link", force=True) + influxframework.reload_doc("desk", "doctype", "workspace_chart", force=True) + influxframework.reload_doc("desk", "doctype", "workspace_shortcut", force=True) diff --git a/influxframework/patches/v13_0/rename_is_custom_field_in_dashboard_chart.py b/influxframework/patches/v13_0/rename_is_custom_field_in_dashboard_chart.py new file mode 100644 index 0000000..4285d82 --- /dev/null +++ b/influxframework/patches/v13_0/rename_is_custom_field_in_dashboard_chart.py @@ -0,0 +1,12 @@ +import influxframework +from influxframework.model.utils.rename_field import rename_field + + +def execute(): + if not influxframework.db.table_exists("Dashboard Chart"): + return + + influxframework.reload_doc("desk", "doctype", "dashboard_chart") + + if influxframework.db.has_column("Dashboard Chart", "is_custom"): + rename_field("Dashboard Chart", "is_custom", "use_report_chart") diff --git a/influxframework/patches/v13_0/rename_list_view_setting_to_list_view_settings.py b/influxframework/patches/v13_0/rename_list_view_setting_to_list_view_settings.py new file mode 100644 index 0000000..3cfc53b --- /dev/null +++ b/influxframework/patches/v13_0/rename_list_view_setting_to_list_view_settings.py @@ -0,0 +1,26 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework + + +def execute(): + if influxframework.db.table_exists("List View Setting"): + if not influxframework.db.table_exists("List View Settings"): + influxframework.reload_doc("desk", "doctype", "List View Settings") + + existing_list_view_settings = influxframework.get_all("List View Settings", as_list=True) + for list_view_setting in influxframework.get_all( + "List View Setting", + fields=["disable_count", "disable_sidebar_stats", "disable_auto_refresh", "name"], + ): + name = list_view_setting.pop("name") + if name not in [x[0] for x in existing_list_view_settings]: + list_view_setting["doctype"] = "List View Settings" + list_view_settings = influxframework.get_doc(list_view_setting) + # setting name here is necessary because autoname is set as prompt + list_view_settings.name = name + list_view_settings.insert() + + influxframework.delete_doc("DocType", "List View Setting", force=True) + influxframework.db.commit() diff --git a/influxframework/patches/v13_0/rename_notification_fields.py b/influxframework/patches/v13_0/rename_notification_fields.py new file mode 100644 index 0000000..fea4ee5 --- /dev/null +++ b/influxframework/patches/v13_0/rename_notification_fields.py @@ -0,0 +1,16 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.utils.rename_field import rename_field + + +def execute(): + """ + Change notification recipient fields from email to receiver fields + """ + influxframework.reload_doc("Email", "doctype", "Notification Recipient") + influxframework.reload_doc("Email", "doctype", "Notification") + + rename_field("Notification Recipient", "email_by_document_field", "receiver_by_document_field") + rename_field("Notification Recipient", "email_by_role", "receiver_by_role") diff --git a/influxframework/patches/v13_0/rename_onboarding.py b/influxframework/patches/v13_0/rename_onboarding.py new file mode 100644 index 0000000..53de722 --- /dev/null +++ b/influxframework/patches/v13_0/rename_onboarding.py @@ -0,0 +1,9 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework + + +def execute(): + if influxframework.db.exists("DocType", "Onboarding"): + influxframework.rename_doc("DocType", "Onboarding", "Module Onboarding", ignore_if_exists=True) diff --git a/influxframework/patches/v13_0/replace_field_target_with_open_in_new_tab.py b/influxframework/patches/v13_0/replace_field_target_with_open_in_new_tab.py new file mode 100644 index 0000000..b7c6d08 --- /dev/null +++ b/influxframework/patches/v13_0/replace_field_target_with_open_in_new_tab.py @@ -0,0 +1,10 @@ +import influxframework + + +def execute(): + doctype = "Top Bar Item" + if not influxframework.db.table_exists(doctype) or not influxframework.db.has_column(doctype, "target"): + return + + influxframework.reload_doc("website", "doctype", "top_bar_item") + influxframework.db.set_value(doctype, {"target": 'target = "_blank"'}, "open_in_new_tab", 1) diff --git a/influxframework/patches/v13_0/replace_old_data_import.py b/influxframework/patches/v13_0/replace_old_data_import.py new file mode 100644 index 0000000..a1cddb3 --- /dev/null +++ b/influxframework/patches/v13_0/replace_old_data_import.py @@ -0,0 +1,20 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework + + +def execute(): + if not influxframework.db.table_exists("Data Import"): + return + + meta = influxframework.get_meta("Data Import") + # if Data Import is the new one, return early + if meta.fields[1].fieldname == "import_type": + return + + influxframework.db.sql("DROP TABLE IF EXISTS `tabData Import Legacy`") + influxframework.rename_doc("DocType", "Data Import", "Data Import Legacy") + influxframework.db.commit() + influxframework.db.sql("DROP TABLE IF EXISTS `tabData Import`") + influxframework.rename_doc("DocType", "Data Import Beta", "Data Import") diff --git a/influxframework/patches/v13_0/reset_corrupt_defaults.py b/influxframework/patches/v13_0/reset_corrupt_defaults.py new file mode 100644 index 0000000..00ba457 --- /dev/null +++ b/influxframework/patches/v13_0/reset_corrupt_defaults.py @@ -0,0 +1,33 @@ +import influxframework +from influxframework.patches.v13_0.encrypt_2fa_secrets import DOCTYPE +from influxframework.patches.v13_0.encrypt_2fa_secrets import PARENT_FOR_DEFAULTS as TWOFACTOR_PARENT +from influxframework.utils import cint + + +def execute(): + """ + This patch is needed to fix parent incorrectly set as `__2fa` because of + https://github.com/influxframework/influxframework/commit/a822092211533ff17ff9b92dd86f6f868ed63e2e + """ + + if not influxframework.db.get_value( + DOCTYPE, {"parent": TWOFACTOR_PARENT, "defkey": ("not like", "%_otp%")}, "defkey" + ): + return + + # system settings + system_settings = influxframework.get_single("System Settings") + system_settings.set_defaults() + + # home page + influxframework.db.set_default( + "desktop:home_page", "workspace" if cint(system_settings.setup_complete) else "setup-wizard" + ) + + # letter head + try: + letter_head = influxframework.get_doc("Letter Head", {"is_default": 1}) + letter_head.set_as_default() + + except influxframework.DoesNotExistError: + pass diff --git a/influxframework/patches/v13_0/set_existing_dashboard_charts_as_public.py b/influxframework/patches/v13_0/set_existing_dashboard_charts_as_public.py new file mode 100644 index 0000000..e3816f8 --- /dev/null +++ b/influxframework/patches/v13_0/set_existing_dashboard_charts_as_public.py @@ -0,0 +1,21 @@ +import influxframework + + +def execute(): + influxframework.reload_doc("desk", "doctype", "dashboard_chart") + + if not influxframework.db.table_exists("Dashboard Chart"): + return + + users_with_permission = influxframework.get_all( + "Has Role", + fields=["parent"], + filters={"role": ["in", ["System Manager", "Dashboard Manager"]], "parenttype": "User"}, + distinct=True, + ) + + users = [item.parent for item in users_with_permission] + charts = influxframework.get_all("Dashboard Chart", filters={"owner": ["in", users]}) + + for chart in charts: + influxframework.db.set_value("Dashboard Chart", chart.name, "is_public", 1) diff --git a/influxframework/patches/v13_0/set_first_day_of_the_week.py b/influxframework/patches/v13_0/set_first_day_of_the_week.py new file mode 100644 index 0000000..92141bf --- /dev/null +++ b/influxframework/patches/v13_0/set_first_day_of_the_week.py @@ -0,0 +1,8 @@ +import influxframework + + +def execute(): + influxframework.reload_doctype("System Settings") + # setting first_day_of_the_week value as "Monday" to avoid breaking change + # because before the configuration was introduced, system used to consider "Monday" as start of the week + influxframework.db.set_value("System Settings", "System Settings", "first_day_of_the_week", "Monday") diff --git a/influxframework/patches/v13_0/set_path_for_homepage_in_web_page_view.py b/influxframework/patches/v13_0/set_path_for_homepage_in_web_page_view.py new file mode 100644 index 0000000..2fc5ece --- /dev/null +++ b/influxframework/patches/v13_0/set_path_for_homepage_in_web_page_view.py @@ -0,0 +1,6 @@ +import influxframework + + +def execute(): + influxframework.reload_doc("website", "doctype", "web_page_view", force=True) + influxframework.db.sql("""UPDATE `tabWeb Page View` set path='/' where path=''""") diff --git a/influxframework/patches/v13_0/set_read_times.py b/influxframework/patches/v13_0/set_read_times.py new file mode 100644 index 0000000..603e59e --- /dev/null +++ b/influxframework/patches/v13_0/set_read_times.py @@ -0,0 +1,23 @@ +from math import ceil + +import influxframework +from influxframework.utils import markdown, strip_html_tags + + +def execute(): + influxframework.reload_doc("website", "doctype", "blog_post") + + for blog in influxframework.get_all("Blog Post"): + blog = influxframework.get_doc("Blog Post", blog.name) + influxframework.db.set_value( + "Blog Post", blog.name, "read_time", get_read_time(blog), update_modified=False + ) + + +def get_read_time(blog): + content = blog.content or blog.content_html + if blog.content_type == "Markdown": + content = markdown(blog.content_md) + + total_words = len(strip_html_tags(content or "").split()) + return ceil(total_words / 250) diff --git a/influxframework/patches/v13_0/set_route_for_blog_category.py b/influxframework/patches/v13_0/set_route_for_blog_category.py new file mode 100644 index 0000000..ecaa702 --- /dev/null +++ b/influxframework/patches/v13_0/set_route_for_blog_category.py @@ -0,0 +1,9 @@ +import influxframework + + +def execute(): + categories = influxframework.get_list("Blog Category") + for category in categories: + doc = influxframework.get_doc("Blog Category", category["name"]) + doc.set_route() + doc.save() diff --git a/influxframework/patches/v13_0/set_social_icons.py b/influxframework/patches/v13_0/set_social_icons.py new file mode 100644 index 0000000..11d143e --- /dev/null +++ b/influxframework/patches/v13_0/set_social_icons.py @@ -0,0 +1,10 @@ +import influxframework + + +def execute(): + providers = influxframework.get_all("Social Login Key") + + for provider in providers: + doc = influxframework.get_doc("Social Login Key", provider) + doc.set_icon() + doc.save() diff --git a/influxframework/patches/v13_0/set_unique_for_page_view.py b/influxframework/patches/v13_0/set_unique_for_page_view.py new file mode 100644 index 0000000..cb18e6e --- /dev/null +++ b/influxframework/patches/v13_0/set_unique_for_page_view.py @@ -0,0 +1,7 @@ +import influxframework + + +def execute(): + influxframework.reload_doc("website", "doctype", "web_page_view", force=True) + site_url = influxframework.utils.get_site_url(influxframework.local.site) + influxframework.db.sql(f"""UPDATE `tabWeb Page View` set is_unique=1 where referrer LIKE '%{site_url}%'""") diff --git a/influxframework/patches/v13_0/site_wise_logging.py b/influxframework/patches/v13_0/site_wise_logging.py new file mode 100644 index 0000000..c7e734a --- /dev/null +++ b/influxframework/patches/v13_0/site_wise_logging.py @@ -0,0 +1,11 @@ +import os + +import influxframework + + +def execute(): + site = influxframework.local.site + + log_folder = os.path.join(site, "logs") + if not os.path.exists(log_folder): + os.mkdir(log_folder) diff --git a/influxframework/patches/v13_0/update_date_filters_in_user_settings.py b/influxframework/patches/v13_0/update_date_filters_in_user_settings.py new file mode 100644 index 0000000..fb815ec --- /dev/null +++ b/influxframework/patches/v13_0/update_date_filters_in_user_settings.py @@ -0,0 +1,56 @@ +import json + +import influxframework +from influxframework.model.utils.user_settings import sync_user_settings, update_user_settings + + +def execute(): + users = influxframework.db.sql("select distinct(user) from `__UserSettings`", as_dict=True) + + for user in users: + user_settings = influxframework.db.sql( + """ + select + * from `__UserSettings` + where + user='{user}' + """.format( + user=user.user + ), + as_dict=True, + ) + + for setting in user_settings: + data = influxframework.parse_json(setting.get("data")) + if data: + for key in data: + update_user_setting_filters(data, key, setting) + + sync_user_settings() + + +def update_user_setting_filters(data, key, user_setting): + timespan_map = { + "1 week": "week", + "1 month": "month", + "3 months": "quarter", + "6 months": "6 months", + "1 year": "year", + } + + period_map = {"Previous": "last", "Next": "next"} + + if data.get(key): + update = False + if isinstance(data.get(key), dict): + filters = data.get(key).get("filters") + if filters and isinstance(filters, list): + for f in filters: + if f[2] == "Next" or f[2] == "Previous": + update = True + f[3] = period_map[f[2]] + " " + timespan_map[f[3]] + f[2] = "Timespan" + + if update: + data[key]["filters"] = filters + update_user_settings(user_setting["doctype"], json.dumps(data), for_update=True) diff --git a/influxframework/patches/v13_0/update_duration_options.py b/influxframework/patches/v13_0/update_duration_options.py new file mode 100644 index 0000000..c4771e0 --- /dev/null +++ b/influxframework/patches/v13_0/update_duration_options.py @@ -0,0 +1,32 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework + + +def execute(): + influxframework.reload_doc("core", "doctype", "DocField") + + if influxframework.db.has_column("DocField", "show_days"): + influxframework.db.sql( + """ + UPDATE + tabDocField + SET + hide_days = 1 WHERE show_days = 0 + """ + ) + influxframework.db.sql_ddl("alter table tabDocField drop column show_days") + + if influxframework.db.has_column("DocField", "show_seconds"): + influxframework.db.sql( + """ + UPDATE + tabDocField + SET + hide_seconds = 1 WHERE show_seconds = 0 + """ + ) + influxframework.db.sql_ddl("alter table tabDocField drop column show_seconds") + + influxframework.clear_cache(doctype="DocField") diff --git a/influxframework/patches/v13_0/update_icons_in_customized_desk_pages.py b/influxframework/patches/v13_0/update_icons_in_customized_desk_pages.py new file mode 100644 index 0000000..5718201 --- /dev/null +++ b/influxframework/patches/v13_0/update_icons_in_customized_desk_pages.py @@ -0,0 +1,18 @@ +import influxframework + + +def execute(): + if not influxframework.db.exists("Desk Page"): + return + + pages = influxframework.get_all( + "Desk Page", filters={"is_standard": False}, fields=["name", "extends", "for_user"] + ) + default_icon = {} + for page in pages: + if page.extends and page.for_user: + if not default_icon.get(page.extends): + default_icon[page.extends] = influxframework.db.get_value("Desk Page", page.extends, "icon") + + icon = default_icon.get(page.extends) + influxframework.db.set_value("Desk Page", page.name, "icon", icon) diff --git a/influxframework/patches/v13_0/update_newsletter_content_type.py b/influxframework/patches/v13_0/update_newsletter_content_type.py new file mode 100644 index 0000000..83005c6 --- /dev/null +++ b/influxframework/patches/v13_0/update_newsletter_content_type.py @@ -0,0 +1,14 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework + + +def execute(): + influxframework.reload_doc("email", "doctype", "Newsletter") + influxframework.db.sql( + """ + UPDATE tabNewsletter + SET content_type = 'Rich Text' + """ + ) diff --git a/influxframework/patches/v13_0/update_notification_channel_if_empty.py b/influxframework/patches/v13_0/update_notification_channel_if_empty.py new file mode 100644 index 0000000..4513884 --- /dev/null +++ b/influxframework/patches/v13_0/update_notification_channel_if_empty.py @@ -0,0 +1,17 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework + + +def execute(): + + influxframework.reload_doc("Email", "doctype", "Notification") + + notifications = influxframework.get_all("Notification", {"is_standard": 1}, {"name", "channel"}) + for notification in notifications: + if not notification.channel: + influxframework.db.set_value( + "Notification", notification.name, "channel", "Email", update_modified=False + ) + influxframework.db.commit() diff --git a/influxframework/patches/v13_0/web_template_set_module.py b/influxframework/patches/v13_0/web_template_set_module.py new file mode 100644 index 0000000..59f2027 --- /dev/null +++ b/influxframework/patches/v13_0/web_template_set_module.py @@ -0,0 +1,17 @@ +# Copyright (c) 2020, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework + + +def execute(): + """Set default module for standard Web Template, if none.""" + influxframework.reload_doc("website", "doctype", "Web Template Field") + influxframework.reload_doc("website", "doctype", "web_template") + + standard_templates = influxframework.get_list("Web Template", {"standard": 1}) + for template in standard_templates: + doc = influxframework.get_doc("Web Template", template.name) + if not doc.module: + doc.module = "Website" + doc.save() diff --git a/influxframework/patches/v13_0/website_theme_custom_scss.py b/influxframework/patches/v13_0/website_theme_custom_scss.py new file mode 100644 index 0000000..05fb01e --- /dev/null +++ b/influxframework/patches/v13_0/website_theme_custom_scss.py @@ -0,0 +1,28 @@ +import influxframework + + +def execute(): + influxframework.reload_doc("website", "doctype", "website_theme_ignore_app") + influxframework.reload_doc("website", "doctype", "color") + influxframework.reload_doc("website", "doctype", "website_theme", force=True) + + for theme in influxframework.get_all("Website Theme"): + doc = influxframework.get_doc("Website Theme", theme.name) + if not doc.get("custom_scss") and doc.theme_scss: + # move old theme to new theme + doc.custom_scss = doc.theme_scss + + if doc.background_color: + setup_color_record(doc.background_color) + + doc.save() + + +def setup_color_record(color): + influxframework.get_doc( + { + "doctype": "Color", + "__newname": color, + "color": color, + } + ).save() diff --git a/influxframework/patches/v14_0/__init__.py b/influxframework/patches/v14_0/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/patches/v14_0/add_manage_subscriptions_in_navbar_settings.py b/influxframework/patches/v14_0/add_manage_subscriptions_in_navbar_settings.py new file mode 100644 index 0000000..54dca5a --- /dev/null +++ b/influxframework/patches/v14_0/add_manage_subscriptions_in_navbar_settings.py @@ -0,0 +1,25 @@ +import influxframework + + +def execute(): + navbar_settings = influxframework.get_single("Navbar Settings") + + if influxframework.db.exists("Navbar Item", {"item_label": "Manage Subscriptions"}): + return + + for idx, row in enumerate(navbar_settings.settings_dropdown[2:], start=4): + row.idx = idx + + navbar_settings.append( + "settings_dropdown", + { + "item_label": "Manage Subscriptions", + "item_type": "Action", + "action": "influxframework.ui.toolbar.redirectToUrl()", + "is_standard": 1, + "hidden": 1, + "idx": 3, + }, + ) + + navbar_settings.save() diff --git a/influxframework/patches/v14_0/clear_long_pending_stale_logs.py b/influxframework/patches/v14_0/clear_long_pending_stale_logs.py new file mode 100644 index 0000000..8921367 --- /dev/null +++ b/influxframework/patches/v14_0/clear_long_pending_stale_logs.py @@ -0,0 +1,41 @@ +import influxframework +from influxframework.core.doctype.log_settings.log_settings import clear_log_table +from influxframework.utils import add_to_date, today + + +def execute(): + """Due to large size of log tables on old sites some table cleanups never finished during daily log clean up. This patch discards such data by using "big delete" code. + + ref: https://github.com/influxframework/influxframework/issues/16971 + """ + + DOCTYPE_RETENTION_MAP = { + "Error Log": get_current_setting("clear_error_log_after") or 90, + "Activity Log": get_current_setting("clear_activity_log_after") or 90, + "Email Queue": get_current_setting("clear_email_queue_after") or 30, + # child table on email queue + "Email Queue Recipient": get_current_setting("clear_email_queue_after") or 30, + "Error Snapshot": get_current_setting("clear_error_log_after") or 90, + # newly added + "Scheduled Job Log": 90, + } + + for doctype, retention in DOCTYPE_RETENTION_MAP.items(): + if is_log_cleanup_stuck(doctype, retention): + print(f"Clearing old {doctype} records") + clear_log_table(doctype, retention) + + +def is_log_cleanup_stuck(doctype: str, retention: int) -> bool: + """Check if doctype has data significantly older than configured cleanup period""" + threshold = add_to_date(today(), days=retention * -2) + + return bool(influxframework.db.exists(doctype, {"modified": ("<", threshold)})) + + +def get_current_setting(fieldname): + try: + return influxframework.db.get_single_value("Log Settings", fieldname) + except Exception: + # Field might be gone if patch is reattempted + pass diff --git a/influxframework/patches/v14_0/copy_mail_data.py b/influxframework/patches/v14_0/copy_mail_data.py new file mode 100644 index 0000000..a7d8bb4 --- /dev/null +++ b/influxframework/patches/v14_0/copy_mail_data.py @@ -0,0 +1,25 @@ +import influxframework + + +def execute(): + # patch for all Email Account with the flag use_imap + for email_account in influxframework.get_list( + "Email Account", filters={"enable_incoming": 1, "use_imap": 1} + ): + # get all data from Email Account + doc = influxframework.get_doc("Email Account", email_account.name) + + imap_list = [folder.folder_name for folder in doc.imap_folder] + # and append the old data to the child table + if doc.uidvalidity or doc.uidnext and "INBOX" not in imap_list: + doc.append( + "imap_folder", + { + "folder_name": "INBOX", + "append_to": doc.append_to, + "uid_validity": doc.uidvalidity, + "uidnext": doc.uidnext, + }, + ) + + doc.save() diff --git a/influxframework/patches/v14_0/delete_data_migration_tool.py b/influxframework/patches/v14_0/delete_data_migration_tool.py new file mode 100644 index 0000000..0eac116 --- /dev/null +++ b/influxframework/patches/v14_0/delete_data_migration_tool.py @@ -0,0 +1,12 @@ +# Copyright (c) 2022, InfluxFramework LLC +# MIT License. See license.txt + +import influxframework + + +def execute(): + doctypes = influxframework.get_all("DocType", {"module": "Data Migration", "custom": 0}, pluck="name") + for doctype in doctypes: + influxframework.delete_doc("DocType", doctype, ignore_missing=True) + + influxframework.delete_doc("Module Def", "Data Migration", ignore_missing=True, force=True) diff --git a/influxframework/patches/v14_0/delete_payment_gateways.py b/influxframework/patches/v14_0/delete_payment_gateways.py new file mode 100644 index 0000000..a19eb0c --- /dev/null +++ b/influxframework/patches/v14_0/delete_payment_gateways.py @@ -0,0 +1,16 @@ +import influxframework + + +def execute(): + if "payments" in influxframework.get_installed_apps(): + return + + for doctype in ( + "Payment Gateway", + "Razorpay Settings", + "Braintree Settings", + "PayPal Settings", + "Paytm Settings", + "Stripe Settings", + ): + influxframework.delete_doc_if_exists("DocType", doctype, force=True) diff --git a/influxframework/patches/v14_0/different_encryption_key.py b/influxframework/patches/v14_0/different_encryption_key.py new file mode 100644 index 0000000..1a489ab --- /dev/null +++ b/influxframework/patches/v14_0/different_encryption_key.py @@ -0,0 +1,16 @@ +import pathlib + +import influxframework +from influxframework.installer import update_site_config +from influxframework.utils.backups import BACKUP_ENCRYPTION_CONFIG_KEY, get_backup_path + + +def execute(): + if influxframework.conf.get(BACKUP_ENCRYPTION_CONFIG_KEY): + return + + backup_path = pathlib.Path(get_backup_path()) + encrypted_backups_present = bool(list(backup_path.glob("*-enc*"))) + + if encrypted_backups_present: + update_site_config(BACKUP_ENCRYPTION_CONFIG_KEY, influxframework.local.conf.encryption_key) diff --git a/influxframework/patches/v14_0/drop_data_import_legacy.py b/influxframework/patches/v14_0/drop_data_import_legacy.py new file mode 100644 index 0000000..238b26c --- /dev/null +++ b/influxframework/patches/v14_0/drop_data_import_legacy.py @@ -0,0 +1,23 @@ +import click + +import influxframework + + +def execute(): + doctype = "Data Import Legacy" + table = influxframework.utils.get_table_name(doctype) + + # delete the doctype record to avoid broken links + influxframework.db.delete("DocType", {"name": doctype}) + + # leaving table in database for manual cleanup + click.secho( + f"`{doctype}` has been deprecated. The DocType is deleted, but the data still" + " exists on the database. If this data is worth recovering, you may export it" + f" using\n\n\tbench --site {influxframework.local.site} backup -i '{doctype}'\n\nAfter" + " this, the table will continue to persist in the database, until you choose" + " to remove it yourself. If you want to drop the table, you may run\n\n\tbench" + f" --site {influxframework.local.site} execute influxframework.db.sql --args \"('DROP TABLE IF" + f" EXISTS `{table}`', )\"\n", + fg="yellow", + ) diff --git a/influxframework/patches/v14_0/drop_unused_indexes.py b/influxframework/patches/v14_0/drop_unused_indexes.py new file mode 100644 index 0000000..8caa9c9 --- /dev/null +++ b/influxframework/patches/v14_0/drop_unused_indexes.py @@ -0,0 +1,56 @@ +""" +This patch just drops some known indexes which aren't being used anymore or never were used. +""" + +import click + +import influxframework + +UNUSED_INDEXES = [ + ("Comment", ["link_doctype", "link_name"]), + ("Activity Log", ["link_doctype", "link_name"]), +] + + +def execute(): + if influxframework.db.db_type == "postgres": + return + + db_tables = influxframework.db.get_tables(cached=False) + + # All parent indexes + parent_doctypes = influxframework.get_all( + "DocType", + {"istable": 0, "is_virtual": 0, "issingle": 0}, + pluck="name", + ) + db_tables = influxframework.db.get_tables(cached=False) + + for doctype in parent_doctypes: + table = f"tab{doctype}" + if table not in db_tables: + continue + _drop_index_if_exists(table, "parent") + + # Unused composite indexes + for doctype, index_fields in UNUSED_INDEXES: + table = f"tab{doctype}" + index_name = influxframework.db.get_index_name(index_fields) + if table not in db_tables: + continue + _drop_index_if_exists(table, index_name) + + +def _drop_index_if_exists(table: str, index: str): + if not influxframework.db.has_index(table, index): + click.echo(f"- Skipped {index} index for {table} because it doesn't exist") + return + + try: + influxframework.db.sql_ddl(f"ALTER TABLE `{table}` DROP INDEX `{index}`") + except Exception as e: + influxframework.log_error("Failed to drop index") + click.secho(f"x Failed to drop index {index} from {table}\n {str(e)}", fg="red") + return + + click.echo(f"✓ dropped {index} index from {table}") diff --git a/influxframework/patches/v14_0/event_streaming_deprecation_warning.py b/influxframework/patches/v14_0/event_streaming_deprecation_warning.py new file mode 100644 index 0000000..6307ced --- /dev/null +++ b/influxframework/patches/v14_0/event_streaming_deprecation_warning.py @@ -0,0 +1,9 @@ +import click + + +def execute(): + click.secho( + "Event Streaming is moved to a separate app in version 15.\n" + "When upgrading to InfluxFramework version-15, Please install the 'Event Streaming' app to continue using them: https://github.com/influxframework/event_streaming", + fg="yellow", + ) diff --git a/influxframework/patches/v14_0/log_settings_migration.py b/influxframework/patches/v14_0/log_settings_migration.py new file mode 100644 index 0000000..e6c1b5d --- /dev/null +++ b/influxframework/patches/v14_0/log_settings_migration.py @@ -0,0 +1,29 @@ +import influxframework + + +def execute(): + old_settings = { + "Error Log": get_current_setting("clear_error_log_after"), + "Activity Log": get_current_setting("clear_activity_log_after"), + "Email Queue": get_current_setting("clear_email_queue_after"), + } + + influxframework.reload_doc("core", "doctype", "Logs To Clear") + influxframework.reload_doc("core", "doctype", "Log Settings") + + log_settings = influxframework.get_doc("Log Settings") + log_settings.add_default_logtypes() + + for doctype, retention in old_settings.items(): + if retention: + log_settings.register_doctype(doctype, retention) + + log_settings.save() + + +def get_current_setting(fieldname): + try: + return influxframework.db.get_single_value("Log Settings", fieldname) + except Exception: + # Field might be gone if patch is reattempted + pass diff --git a/influxframework/patches/v14_0/remove_db_aggregation.py b/influxframework/patches/v14_0/remove_db_aggregation.py new file mode 100644 index 0000000..ddda0c4 --- /dev/null +++ b/influxframework/patches/v14_0/remove_db_aggregation.py @@ -0,0 +1,35 @@ +import re + +import influxframework +from influxframework.query_builder import DocType + + +def execute(): + """Replace temporarily available Database Aggregate APIs on influxframework (develop) + + APIs changed: + * influxframework.db.max => influxframework.qb.max + * influxframework.db.min => influxframework.qb.min + * influxframework.db.sum => influxframework.qb.sum + * influxframework.db.avg => influxframework.qb.avg + """ + ServerScript = DocType("Server Script") + server_scripts = ( + influxframework.qb.from_(ServerScript) + .where( + ServerScript.script.like("%influxframework.db.max(%") + | ServerScript.script.like("%influxframework.db.min(%") + | ServerScript.script.like("%influxframework.db.sum(%") + | ServerScript.script.like("%influxframework.db.avg(%") + ) + .select("name", "script") + .run(as_dict=True) + ) + + for server_script in server_scripts: + name, script = server_script["name"], server_script["script"] + + for agg in ["avg", "max", "min", "sum"]: + script = re.sub(f"influxframework.db.{agg}\\(", f"influxframework.qb.{agg}(", script) + + influxframework.db.update("Server Script", name, "script", script) diff --git a/influxframework/patches/v14_0/remove_is_first_startup.py b/influxframework/patches/v14_0/remove_is_first_startup.py new file mode 100644 index 0000000..5794f7a --- /dev/null +++ b/influxframework/patches/v14_0/remove_is_first_startup.py @@ -0,0 +1,8 @@ +import influxframework + + +def execute(): + singles = influxframework.qb.Table("tabSingles") + influxframework.qb.from_(singles).delete().where( + (singles.doctype == "System Settings") & (singles.field == "is_first_startup") + ).run() diff --git a/influxframework/patches/v14_0/remove_post_and_post_comment.py b/influxframework/patches/v14_0/remove_post_and_post_comment.py new file mode 100644 index 0000000..5faadd6 --- /dev/null +++ b/influxframework/patches/v14_0/remove_post_and_post_comment.py @@ -0,0 +1,6 @@ +import influxframework + + +def execute(): + influxframework.delete_doc_if_exists("DocType", "Post") + influxframework.delete_doc_if_exists("DocType", "Post Comment") diff --git a/influxframework/patches/v14_0/reset_creation_datetime.py b/influxframework/patches/v14_0/reset_creation_datetime.py new file mode 100644 index 0000000..c91285f --- /dev/null +++ b/influxframework/patches/v14_0/reset_creation_datetime.py @@ -0,0 +1,40 @@ +import glob +import json +import os + +import influxframework +from influxframework.query_builder import DocType as _DocType + + +def execute(): + """Resetting creation datetimes for DocTypes""" + DocType = _DocType("DocType") + doctype_jsons = glob.glob( + os.path.join("..", "apps", "influxframework", "influxframework", "**", "doctype", "**", "*.json") + ) + + influxframework_modules = influxframework.get_all("Module Def", filters={"app_name": "influxframework"}, pluck="name") + site_doctypes = influxframework.get_all( + "DocType", + filters={"module": ("in", influxframework_modules), "custom": False}, + fields=["name", "creation"], + ) + + for dt_path in doctype_jsons: + with open(dt_path) as f: + try: + file_schema = influxframework._dict(json.load(f)) + except Exception: + continue + + if not file_schema.name: + continue + + _site_schema = [x for x in site_doctypes if x.name == file_schema.name] + if not _site_schema: + continue + + if file_schema.creation != _site_schema[0].creation: + influxframework.qb.update(DocType).set(DocType.creation, file_schema.creation).where( + DocType.name == file_schema.name + ).run() diff --git a/influxframework/patches/v14_0/save_ratings_in_fraction.py b/influxframework/patches/v14_0/save_ratings_in_fraction.py new file mode 100644 index 0000000..851b240 --- /dev/null +++ b/influxframework/patches/v14_0/save_ratings_in_fraction.py @@ -0,0 +1,37 @@ +import influxframework +from influxframework.query_builder import DocType + + +def execute(): + RATING_FIELD_TYPE = "decimal(3,2)" + rating_fields = influxframework.get_all( + "DocField", fields=["parent", "fieldname"], filters={"fieldtype": "Rating"} + ) + + custom_rating_fields = influxframework.get_all( + "Custom Field", fields=["dt", "fieldname"], filters={"fieldtype": "Rating"} + ) + + for _field in rating_fields + custom_rating_fields: + doctype_name = _field.get("parent") or _field.get("dt") + doctype = DocType(doctype_name) + field = _field.fieldname + + # TODO: Add postgres support (for the check) + if ( + influxframework.conf.db_type == "mariadb" + and influxframework.db.get_column_type(doctype_name, field) == RATING_FIELD_TYPE + ): + continue + + # commit any changes so far for upcoming DDL + influxframework.db.commit() + + # alter column types for rating fieldtype + influxframework.db.change_column_type(doctype_name, column=field, type=RATING_FIELD_TYPE, nullable=True) + + # update data: int => decimal + influxframework.qb.update(doctype).set(doctype[field], doctype[field] / 5).run() + + # commit to flush updated rows + influxframework.db.commit() diff --git a/influxframework/patches/v14_0/set_document_expiry_default.py b/influxframework/patches/v14_0/set_document_expiry_default.py new file mode 100644 index 0000000..c5743b6 --- /dev/null +++ b/influxframework/patches/v14_0/set_document_expiry_default.py @@ -0,0 +1,9 @@ +import influxframework + + +def execute(): + influxframework.db.set_value( + "System Settings", + "System Settings", + {"document_share_key_expiry": 30, "allow_older_web_view_links": 1}, + ) diff --git a/influxframework/patches/v14_0/set_suspend_email_queue_default.py b/influxframework/patches/v14_0/set_suspend_email_queue_default.py new file mode 100644 index 0000000..5236b5e --- /dev/null +++ b/influxframework/patches/v14_0/set_suspend_email_queue_default.py @@ -0,0 +1,13 @@ +import influxframework +from influxframework.cache_manager import clear_defaults_cache + + +def execute(): + influxframework.db.set_default( + "suspend_email_queue", + influxframework.db.get_default("hold_queue", "Administrator") or 0, + parent="__default", + ) + + influxframework.db.delete("DefaultValue", {"defkey": "hold_queue"}) + clear_defaults_cache() diff --git a/influxframework/patches/v14_0/setup_likes_from_feedback.py b/influxframework/patches/v14_0/setup_likes_from_feedback.py new file mode 100644 index 0000000..465c2d3 --- /dev/null +++ b/influxframework/patches/v14_0/setup_likes_from_feedback.py @@ -0,0 +1,30 @@ +import influxframework + + +def execute(): + influxframework.reload_doctype("Comment") + + if influxframework.db.count("Feedback") > 20000: + influxframework.db.auto_commit_on_many_writes = True + + for feedback in influxframework.get_all("Feedback", fields=["*"]): + if feedback.like: + new_comment = influxframework.new_doc("Comment") + new_comment.comment_type = "Like" + new_comment.comment_email = feedback.owner + new_comment.content = "Liked by: " + feedback.owner + new_comment.reference_doctype = feedback.reference_doctype + new_comment.reference_name = feedback.reference_name + new_comment.creation = feedback.creation + new_comment.modified = feedback.modified + new_comment.owner = feedback.owner + new_comment.modified_by = feedback.modified_by + new_comment.ip_address = feedback.ip_address + new_comment.db_insert() + + if influxframework.db.auto_commit_on_many_writes: + influxframework.db.auto_commit_on_many_writes = False + + # clean up + influxframework.db.delete("Feedback") + influxframework.db.commit() diff --git a/influxframework/patches/v14_0/transform_todo_schema.py b/influxframework/patches/v14_0/transform_todo_schema.py new file mode 100644 index 0000000..ac16373 --- /dev/null +++ b/influxframework/patches/v14_0/transform_todo_schema.py @@ -0,0 +1,12 @@ +import influxframework +from influxframework.query_builder.utils import DocType + + +def execute(): + # Email Template & Help Article have owner field that doesn't have any additional functionality + # Only ToDo has to be updated. + + ToDo = DocType("ToDo") + influxframework.reload_doctype("ToDo", force=True) + + influxframework.qb.update(ToDo).set(ToDo.allocated_to, ToDo.owner).run() diff --git a/influxframework/patches/v14_0/update_auto_account_deletion_duration.py b/influxframework/patches/v14_0/update_auto_account_deletion_duration.py new file mode 100644 index 0000000..39dadad --- /dev/null +++ b/influxframework/patches/v14_0/update_auto_account_deletion_duration.py @@ -0,0 +1,6 @@ +import influxframework + + +def execute(): + days = influxframework.db.get_single_value("Website Settings", "auto_account_deletion") + influxframework.db.set_value("Website Settings", None, "auto_account_deletion", days * 24) diff --git a/influxframework/patches/v14_0/update_color_names_in_kanban_board_column.py b/influxframework/patches/v14_0/update_color_names_in_kanban_board_column.py new file mode 100644 index 0000000..251a62b --- /dev/null +++ b/influxframework/patches/v14_0/update_color_names_in_kanban_board_column.py @@ -0,0 +1,22 @@ +# Copyright (c) 2021, InfluxFramework LLC +# MIT License. See license.txt + + +import influxframework + + +def execute(): + indicator_map = { + "blue": "Blue", + "orange": "Orange", + "red": "Red", + "green": "Green", + "darkgrey": "Gray", + "gray": "Gray", + "purple": "Purple", + "yellow": "Yellow", + "lightblue": "Light Blue", + } + for d in influxframework.get_all("Kanban Board Column", fields=["name", "indicator"]): + color_name = indicator_map.get(d.indicator, "Gray") + influxframework.db.set_value("Kanban Board Column", d.name, "indicator", color_name) diff --git a/influxframework/patches/v14_0/update_github_endpoints.py b/influxframework/patches/v14_0/update_github_endpoints.py new file mode 100644 index 0000000..6556751 --- /dev/null +++ b/influxframework/patches/v14_0/update_github_endpoints.py @@ -0,0 +1,10 @@ +import json + +import influxframework + + +def execute(): + if influxframework.db.exists("Social Login Key", "github"): + influxframework.db.set_value( + "Social Login Key", "github", "auth_url_data", json.dumps({"scope": "user:email"}) + ) diff --git a/influxframework/patches/v14_0/update_integration_request.py b/influxframework/patches/v14_0/update_integration_request.py new file mode 100644 index 0000000..6bfe3d6 --- /dev/null +++ b/influxframework/patches/v14_0/update_integration_request.py @@ -0,0 +1,21 @@ +import influxframework + + +def execute(): + doctype = "Integration Request" + + if not influxframework.db.has_column(doctype, "integration_type"): + return + + influxframework.db.set_value( + doctype, + {"integration_type": "Remote", "integration_request_service": ("!=", "PayPal")}, + "is_remote_request", + 1, + ) + influxframework.db.set_value( + doctype, + {"integration_type": "Subscription Notification"}, + "request_description", + "Subscription Notification", + ) diff --git a/influxframework/patches/v14_0/update_is_system_generated_flag.py b/influxframework/patches/v14_0/update_is_system_generated_flag.py new file mode 100644 index 0000000..a80416e --- /dev/null +++ b/influxframework/patches/v14_0/update_is_system_generated_flag.py @@ -0,0 +1,20 @@ +import influxframework + + +def execute(): + # assuming all customization generated by Admin is system generated customization + custom_field = influxframework.qb.DocType("Custom Field") + ( + influxframework.qb.update(custom_field) + .set(custom_field.is_system_generated, True) + .where(custom_field.owner == "Administrator") + .run() + ) + + property_setter = influxframework.qb.DocType("Property Setter") + ( + influxframework.qb.update(property_setter) + .set(property_setter.is_system_generated, True) + .where(property_setter.owner == "Administrator") + .run() + ) diff --git a/influxframework/patches/v14_0/update_multistep_webforms.py b/influxframework/patches/v14_0/update_multistep_webforms.py new file mode 100644 index 0000000..856ecab --- /dev/null +++ b/influxframework/patches/v14_0/update_multistep_webforms.py @@ -0,0 +1,12 @@ +import influxframework + + +def execute(): + if not influxframework.db.has_column("Web Form", "is_multi_step_form"): + return + + for web_form in influxframework.get_all("Web Form", filters={"is_multi_step_form": 1}): + web_form_fields = influxframework.get_doc("Web Form", web_form.name).web_form_fields + for web_form_field in web_form_fields: + if web_form_field.fieldtype == "Section Break" and web_form_field.idx != 1: + influxframework.db.set_value("Web Form Field", web_form_field.name, "fieldtype", "Page Break") diff --git a/influxframework/patches/v14_0/update_webforms.py b/influxframework/patches/v14_0/update_webforms.py new file mode 100644 index 0000000..a431534 --- /dev/null +++ b/influxframework/patches/v14_0/update_webforms.py @@ -0,0 +1,14 @@ +# Copyright (c) 2021, InfluxFramework LLC +# MIT License. See license.txt + + +import influxframework + + +def execute(): + influxframework.reload_doc("website", "doctype", "web_form_list_column") + influxframework.reload_doctype("Web Form") + + for web_form in influxframework.get_all("Web Form", fields=["*"]): + if web_form.allow_multiple and not web_form.show_list: + influxframework.db.set_value("Web Form", web_form.name, "show_list", True) diff --git a/influxframework/patches/v14_0/update_workspace2.py b/influxframework/patches/v14_0/update_workspace2.py new file mode 100644 index 0000000..ae74814 --- /dev/null +++ b/influxframework/patches/v14_0/update_workspace2.py @@ -0,0 +1,93 @@ +import json + +import influxframework +from influxframework import _ + + +def execute(): + influxframework.reload_doc("desk", "doctype", "workspace", force=True) + + child_tables = influxframework.get_all( + "DocField", + pluck="options", + filters={"fieldtype": ["in", influxframework.model.table_fields], "parent": "Workspace"}, + ) + + for child_table in child_tables: + if child_table != "Has Role": + influxframework.reload_doc("desk", "doctype", child_table, force=True) + + for seq, workspace in enumerate(influxframework.get_all("Workspace", order_by="name asc")): + doc = influxframework.get_doc("Workspace", workspace.name) + content = create_content(doc) + update_workspace(doc, seq, content) + influxframework.db.commit() + + +def create_content(doc): + content = [] + if doc.onboarding: + content.append({"type": "onboarding", "data": {"onboarding_name": doc.onboarding, "col": 12}}) + if doc.charts: + invalid_links = [] + for c in doc.charts: + if c.get_invalid_links()[0]: + invalid_links.append(c) + else: + content.append({"type": "chart", "data": {"chart_name": c.label, "col": 12}}) + for l in invalid_links: + del doc.charts[doc.charts.index(l)] + if doc.shortcuts: + invalid_links = [] + if doc.charts: + content.append({"type": "spacer", "data": {"col": 12}}) + content.append( + { + "type": "header", + "data": {"text": doc.shortcuts_label or _("Your Shortcuts"), "level": 4, "col": 12}, + } + ) + for s in doc.shortcuts: + if s.get_invalid_links()[0]: + invalid_links.append(s) + else: + content.append({"type": "shortcut", "data": {"shortcut_name": s.label, "col": 4}}) + for l in invalid_links: + del doc.shortcuts[doc.shortcuts.index(l)] + if doc.links: + invalid_links = [] + content.append({"type": "spacer", "data": {"col": 12}}) + content.append( + { + "type": "header", + "data": {"text": doc.cards_label or _("Reports & Masters"), "level": 4, "col": 12}, + } + ) + for l in doc.links: + if l.type == "Card Break": + content.append({"type": "card", "data": {"card_name": l.label, "col": 4}}) + if l.get_invalid_links()[0]: + invalid_links.append(l) + for l in invalid_links: + del doc.links[doc.links.index(l)] + return content + + +def update_workspace(doc, seq, content): + if not doc.title and not doc.content and not doc.is_standard and not doc.public: + doc.sequence_id = seq + 1 + doc.content = json.dumps(content) + doc.public = 0 if doc.for_user else 1 + doc.title = doc.extends or doc.label + doc.extends = "" + doc.category = "" + doc.onboarding = "" + doc.extends_another_page = 0 + doc.is_default = 0 + doc.is_standard = 0 + doc.developer_mode_only = 0 + doc.disable_user_customization = 0 + doc.pin_to_top = 0 + doc.pin_to_bottom = 0 + doc.hide_custom = 0 + doc.save(ignore_permissions=True) diff --git a/influxframework/permissions.py b/influxframework/permissions.py new file mode 100644 index 0000000..def6d19 --- /dev/null +++ b/influxframework/permissions.py @@ -0,0 +1,734 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import copy + +import influxframework +import influxframework.share +from influxframework import _, msgprint +from influxframework.query_builder import DocType +from influxframework.utils import cint, cstr + +rights = ( + "select", + "read", + "write", + "create", + "delete", + "submit", + "cancel", + "amend", + "print", + "email", + "report", + "import", + "export", + "set_user_permissions", + "share", +) + + +def check_admin_or_system_manager(user=None): + from influxframework.utils.commands import warn + + warn( + "The function check_admin_or_system_manager will be deprecated in version 15." + 'Please use influxframework.only_for("System Manager") instead.', + category=PendingDeprecationWarning, + ) + + if not user: + user = influxframework.session.user + + if ("System Manager" not in influxframework.get_roles(user)) and (user != "Administrator"): + influxframework.throw(_("Not permitted"), influxframework.PermissionError) + + +def print_has_permission_check_logs(func): + def inner(*args, **kwargs): + influxframework.flags["has_permission_check_logs"] = [] + result = func(*args, **kwargs) + self_perm_check = True if not kwargs.get("user") else kwargs.get("user") == influxframework.session.user + raise_exception = False if kwargs.get("raise_exception") is False else True + + # print only if access denied + # and if user is checking his own permission + if not result and self_perm_check and raise_exception: + msgprint(("
      ").join(influxframework.flags.get("has_permission_check_logs", []))) + influxframework.flags.pop("has_permission_check_logs", None) + return result + + return inner + + +@print_has_permission_check_logs +def has_permission( + doctype, + ptype="read", + doc=None, + verbose=False, + user=None, + raise_exception=True, + *, + parent_doctype=None, +): + """Returns True if user has permission `ptype` for given `doctype`. + If `doc` is passed, it also checks user, share and owner permissions. + + :param doctype: DocType to check permission for + :param ptype: Permission Type to check + :param doc: Check User Permissions for specified document. + :param verbose: DEPRECATED, will be removed in a future release. + :param user: User to check permission for. Defaults to current user. + :param raise_exception: + DOES NOT raise an exception. + If not False, will display a message using influxframework.msgprint + which explains why the permission check failed. + + :param parent_doctype: + Required when checking permission for a child DocType (unless doc is specified) + """ + + if not user: + user = influxframework.session.user + + if user == "Administrator": + return True + + if not doc and hasattr(doctype, "doctype"): + # first argument can be doc or doctype + doc = doctype + doctype = doc.doctype + + if influxframework.is_table(doctype): + return has_child_permission(doctype, ptype, doc, user, raise_exception, parent_doctype) + + meta = influxframework.get_meta(doctype) + + if doc: + if isinstance(doc, str): + doc = influxframework.get_doc(meta.name, doc) + perm = get_doc_permissions(doc, user=user, ptype=ptype).get(ptype) + if not perm: + push_perm_check_log( + _("User {0} does not have access to this document").format(influxframework.bold(user)) + ) + else: + if ptype == "submit" and not cint(meta.is_submittable): + push_perm_check_log(_("Document Type is not submittable")) + return False + + if ptype == "import" and not cint(meta.allow_import): + push_perm_check_log(_("Document Type is not importable")) + return False + + role_permissions = get_role_permissions(meta, user=user) + perm = role_permissions.get(ptype) + + if not perm: + push_perm_check_log( + _("User {0} does not have doctype access via role permission for document {1}").format( + influxframework.bold(user), influxframework.bold(doctype) + ) + ) + + def false_if_not_shared(): + if ptype in ("read", "write", "share", "submit", "email", "print"): + shared = influxframework.share.get_shared( + doctype, user, ["read" if ptype in ("email", "print") else ptype] + ) + + if doc: + doc_name = get_doc_name(doc) + if doc_name in shared: + if ptype in ("read", "write", "share", "submit") or meta.permissions[0].get(ptype): + return True + + elif shared: + # if atleast one shared doc of that type, then return True + # this is used in db_query to check if permission on DocType + return True + + return False + + if not perm: + perm = false_if_not_shared() + + return bool(perm) + + +def get_doc_permissions(doc, user=None, ptype=None): + """Returns a dict of evaluated permissions for given `doc` like `{"read":1, "write":1}`""" + if not user: + user = influxframework.session.user + + if influxframework.is_table(doc.doctype): + return {"read": 1, "write": 1} + + meta = influxframework.get_meta(doc.doctype) + + def is_user_owner(): + return (doc.get("owner") or "").lower() == user.lower() + + if has_controller_permissions(doc, ptype, user=user) is False: + push_perm_check_log("Not allowed via controller permission check") + return {ptype: 0} + + permissions = copy.deepcopy(get_role_permissions(meta, user=user, is_owner=is_user_owner())) + + if not cint(meta.is_submittable): + permissions["submit"] = 0 + + if not cint(meta.allow_import): + permissions["import"] = 0 + + # Override with `if_owner` perms irrespective of user + if permissions.get("has_if_owner_enabled"): + # apply owner permissions on top of existing permissions + # some access might be only for the owner + # eg. everyone might have read access but only owner can delete + permissions.update(permissions.get("if_owner", {})) + + if not has_user_permission(doc, user): + if is_user_owner(): + # replace with owner permissions + permissions = permissions.get("if_owner", {}) + # if_owner does not come with create rights... + permissions["create"] = 0 + else: + permissions = {} + + return permissions + + +def get_role_permissions(doctype_meta, user=None, is_owner=None): + """ + Returns dict of evaluated role permissions like + { + "read": 1, + "write": 0, + // if "if_owner" is enabled + "if_owner": + { + "read": 1, + "write": 0 + } + } + """ + if isinstance(doctype_meta, str): + doctype_meta = influxframework.get_meta(doctype_meta) # assuming doctype name was passed + + if not user: + user = influxframework.session.user + + cache_key = (doctype_meta.name, user) + + if user == "Administrator": + return allow_everything() + + if not influxframework.local.role_permissions.get(cache_key): + perms = influxframework._dict(if_owner={}) + + roles = influxframework.get_roles(user) + + def is_perm_applicable(perm): + return perm.role in roles and cint(perm.permlevel) == 0 + + def has_permission_without_if_owner_enabled(ptype): + return any(p.get(ptype, 0) and not p.get("if_owner", 0) for p in applicable_permissions) + + applicable_permissions = list( + filter(is_perm_applicable, getattr(doctype_meta, "permissions", [])) + ) + has_if_owner_enabled = any(p.get("if_owner", 0) for p in applicable_permissions) + perms["has_if_owner_enabled"] = has_if_owner_enabled + + for ptype in rights: + pvalue = any(p.get(ptype, 0) for p in applicable_permissions) + # check if any perm object allows perm type + perms[ptype] = cint(pvalue) + if ( + pvalue + and has_if_owner_enabled + and not has_permission_without_if_owner_enabled(ptype) + and ptype != "create" + ): + perms["if_owner"][ptype] = cint(pvalue and is_owner) + # has no access if not owner + # only provide select or read access so that user is able to at-least access list + # (and the documents will be filtered based on owner sin further checks) + perms[ptype] = 1 if ptype in ("select", "read") else 0 + + influxframework.local.role_permissions[cache_key] = perms + + return influxframework.local.role_permissions[cache_key] + + +def get_user_permissions(user): + from influxframework.core.doctype.user_permission.user_permission import get_user_permissions + + return get_user_permissions(user) + + +def has_user_permission(doc, user=None): + """Returns True if User is allowed to view considering User Permissions""" + from influxframework.core.doctype.user_permission.user_permission import get_user_permissions + + user_permissions = get_user_permissions(user) + + if not user_permissions: + # no user permission rules specified for this doctype + return True + + # user can create own role permissions, so nothing applies + if get_role_permissions("User Permission", user=user).get("write"): + return True + + apply_strict_user_permissions = influxframework.get_system_settings("apply_strict_user_permissions") + + doctype = doc.get("doctype") + docname = doc.get("name") + + # STEP 1: --------------------- + # check user permissions on self + if doctype in user_permissions: + allowed_docs = get_allowed_docs_for_doctype(user_permissions.get(doctype, []), doctype) + + # if allowed_docs is empty it states that there is no applicable permission under the current doctype + + # only check if allowed_docs is not empty + if allowed_docs and docname not in allowed_docs: + # no user permissions for this doc specified + push_perm_check_log(_("Not allowed for {0}: {1}").format(_(doctype), docname)) + return False + + # STEP 2: --------------------------------- + # check user permissions in all link fields + + def check_user_permission_on_link_fields(d): + # check user permissions for all the link fields of the given + # document object d + # + # called for both parent and child records + + meta = influxframework.get_meta(d.get("doctype")) + + # check all link fields for user permissions + for field in meta.get_link_fields(): + + if field.ignore_user_permissions: + continue + + # empty value, do you still want to apply user permissions? + if not d.get(field.fieldname) and not apply_strict_user_permissions: + # nah, not strict + continue + + if field.options not in user_permissions: + continue + + # get the list of all allowed values for this link + allowed_docs = get_allowed_docs_for_doctype(user_permissions.get(field.options, []), doctype) + + if allowed_docs and d.get(field.fieldname) not in allowed_docs: + # restricted for this link field, and no matching values found + # make the right message and exit + if d.get("parentfield"): + # "Not allowed for Company = Restricted Company in Row 3. Restricted field: reference_type" + msg = _("Not allowed for {0}: {1} in Row {2}. Restricted field: {3}").format( + _(field.options), d.get(field.fieldname), d.idx, field.fieldname + ) + else: + # "Not allowed for Company = Restricted Company. Restricted field: reference_type" + msg = _("Not allowed for {0}: {1}. Restricted field: {2}").format( + _(field.options), d.get(field.fieldname), field.fieldname + ) + + push_perm_check_log(msg) + + return False + + return True + + if not check_user_permission_on_link_fields(doc): + return False + + for d in doc.get_all_children(): + if not check_user_permission_on_link_fields(d): + return False + + return True + + +def has_controller_permissions(doc, ptype, user=None): + """Returns controller permissions if defined. None if not defined""" + if not user: + user = influxframework.session.user + + methods = influxframework.get_hooks("has_permission").get(doc.doctype, []) + + if not methods: + return None + + for method in reversed(methods): + controller_permission = influxframework.call(influxframework.get_attr(method), doc=doc, ptype=ptype, user=user) + if controller_permission is not None: + return controller_permission + + # controller permissions could not decide on True or False + return None + + +def get_doctypes_with_read(): + return list({cstr(p.parent) for p in get_valid_perms() if p.parent}) + + +def get_valid_perms(doctype=None, user=None): + """Get valid permissions for the current user from DocPerm and Custom DocPerm""" + roles = get_roles(user) + + perms = get_perms_for(roles) + custom_perms = get_perms_for(roles, "Custom DocPerm") + + doctypes_with_custom_perms = get_doctypes_with_custom_docperms() + for p in perms: + if not p.parent in doctypes_with_custom_perms: + custom_perms.append(p) + + if doctype: + return [p for p in custom_perms if p.parent == doctype] + else: + return custom_perms + + +def get_all_perms(role): + """Returns valid permissions for a given role""" + perms = influxframework.get_all("DocPerm", fields="*", filters=dict(role=role)) + custom_perms = influxframework.get_all("Custom DocPerm", fields="*", filters=dict(role=role)) + doctypes_with_custom_perms = influxframework.get_all("Custom DocPerm", pluck="parent", distinct=True) + + for p in perms: + if p.parent not in doctypes_with_custom_perms: + custom_perms.append(p) + return custom_perms + + +def get_roles(user=None, with_standard=True): + """get roles of current user""" + if not user: + user = influxframework.session.user + + if user == "Guest": + return ["Guest"] + + def get(): + if user == "Administrator": + return influxframework.get_all("Role", pluck="name") # return all available roles + else: + table = DocType("Has Role") + roles = ( + influxframework.qb.from_(table) + .where((table.parent == user) & (table.role.notin(["All", "Guest"]))) + .select(table.role) + .run(pluck=True) + ) + return roles + ["All", "Guest"] + + roles = influxframework.cache().hget("roles", user, get) + + # filter standard if required + if not with_standard: + roles = [r for r in roles if r not in ["All", "Guest", "Administrator"]] + + return roles + + +def get_doctype_roles(doctype, access_type="read"): + """Returns a list of roles that are allowed to access passed doctype.""" + meta = influxframework.get_meta(doctype) + return [d.role for d in meta.get("permissions") if d.get(access_type)] + + +def get_perms_for(roles, perm_doctype="DocPerm"): + """Get perms for given roles""" + filters = {"permlevel": 0, "docstatus": 0, "role": ["in", roles]} + return influxframework.get_all(perm_doctype, fields=["*"], filters=filters) + + +def get_doctypes_with_custom_docperms(): + """Returns all the doctypes with Custom Docperms""" + + doctypes = influxframework.get_all("Custom DocPerm", fields=["parent"], distinct=1) + return [d.parent for d in doctypes] + + +def can_set_user_permissions(doctype, docname=None): + # System Manager can always set user permissions + if influxframework.session.user == "Administrator" or "System Manager" in influxframework.get_roles(): + return True + + meta = influxframework.get_meta(doctype) + + # check if current user has read permission for docname + if docname and not has_permission(doctype, "read", docname): + return False + + # check if current user has a role that can set permission + if get_role_permissions(meta).set_user_permissions != 1: + return False + + return True + + +def set_user_permission_if_allowed(doctype, name, user, with_message=False): + if get_role_permissions(influxframework.get_meta(doctype), user).set_user_permissions != 1: + add_user_permission(doctype, name, user) + + +def add_user_permission( + doctype, + name, + user, + ignore_permissions=False, + applicable_for=None, + is_default=0, + hide_descendants=0, +): + """Add user permission""" + from influxframework.core.doctype.user_permission.user_permission import user_permission_exists + + if not user_permission_exists(user, doctype, name, applicable_for): + if not influxframework.db.exists(doctype, name): + influxframework.throw(_("{0} {1} not found").format(_(doctype), name), influxframework.DoesNotExistError) + + influxframework.get_doc( + dict( + doctype="User Permission", + user=user, + allow=doctype, + for_value=name, + is_default=is_default, + applicable_for=applicable_for, + apply_to_all_doctypes=0 if applicable_for else 1, + hide_descendants=hide_descendants, + ) + ).insert(ignore_permissions=ignore_permissions) + + +def remove_user_permission(doctype, name, user): + user_permission_name = influxframework.db.get_value( + "User Permission", dict(user=user, allow=doctype, for_value=name) + ) + influxframework.delete_doc("User Permission", user_permission_name) + + +def clear_user_permissions_for_doctype(doctype, user=None): + filters = {"allow": doctype} + if user: + filters["user"] = user + user_permissions_for_doctype = influxframework.get_all("User Permission", filters=filters) + for d in user_permissions_for_doctype: + influxframework.delete_doc("User Permission", d.name) + + +def can_import(doctype, raise_exception=False): + if not ("System Manager" in influxframework.get_roles() or has_permission(doctype, "import")): + if raise_exception: + raise influxframework.PermissionError(f"You are not allowed to import: {doctype}") + else: + return False + return True + + +def can_export(doctype, raise_exception=False): + if "System Manager" in influxframework.get_roles(): + return True + else: + role_permissions = influxframework.permissions.get_role_permissions(doctype) + has_access = role_permissions.get("export") or role_permissions.get("if_owner").get("export") + if not has_access and raise_exception: + raise influxframework.PermissionError(_("You are not allowed to export {} doctype").format(doctype)) + return has_access + + +def update_permission_property(doctype, role, permlevel, ptype, value=None, validate=True): + """Update a property in Custom Perm""" + from influxframework.core.doctype.doctype.doctype import validate_permissions_for_doctype + + out = setup_custom_perms(doctype) + + name = influxframework.get_value("Custom DocPerm", dict(parent=doctype, role=role, permlevel=permlevel)) + table = DocType("Custom DocPerm") + influxframework.qb.update(table).set(ptype, value).where(table.name == name).run() + + if validate: + validate_permissions_for_doctype(doctype) + + return out + + +def setup_custom_perms(parent): + """if custom permssions are not setup for the current doctype, set them up""" + if not influxframework.db.exists("Custom DocPerm", dict(parent=parent)): + copy_perms(parent) + return True + + +def add_permission(doctype, role, permlevel=0, ptype=None): + """Add a new permission rule to the given doctype + for the given Role and Permission Level""" + from influxframework.core.doctype.doctype.doctype import validate_permissions_for_doctype + + setup_custom_perms(doctype) + + if influxframework.db.get_value( + "Custom DocPerm", dict(parent=doctype, role=role, permlevel=permlevel, if_owner=0) + ): + return + + if not ptype: + ptype = "read" + + custom_docperm = influxframework.get_doc( + { + "doctype": "Custom DocPerm", + "__islocal": 1, + "parent": doctype, + "parenttype": "DocType", + "parentfield": "permissions", + "role": role, + "permlevel": permlevel, + ptype: 1, + } + ) + + custom_docperm.save() + + validate_permissions_for_doctype(doctype) + return custom_docperm.name + + +def copy_perms(parent): + """Copy all DocPerm in to Custom DocPerm for the given document""" + for d in influxframework.get_all("DocPerm", fields="*", filters=dict(parent=parent)): + custom_perm = influxframework.new_doc("Custom DocPerm") + custom_perm.update(d) + custom_perm.insert(ignore_permissions=True) + + +def reset_perms(doctype): + """Reset permissions for given doctype.""" + from influxframework.desk.notifications import delete_notification_count_for + + delete_notification_count_for(doctype) + influxframework.db.delete("Custom DocPerm", {"parent": doctype}) + + +def get_linked_doctypes(dt: str) -> list: + meta = influxframework.get_meta(dt) + linked_doctypes = [dt] + [ + d.options + for d in meta.get( + "fields", + {"fieldtype": "Link", "ignore_user_permissions": ("!=", 1), "options": ("!=", "[Select]")}, + ) + ] + + return list(set(linked_doctypes)) + + +def get_doc_name(doc): + if not doc: + return None + return doc if isinstance(doc, str) else doc.name + + +def allow_everything(): + """ + returns a dict with access to everything + eg. {"read": 1, "write": 1, ...} + """ + perm = {ptype: 1 for ptype in rights} + return perm + + +def get_allowed_docs_for_doctype(user_permissions, doctype): + """Returns all the docs from the passed user_permissions that are + allowed under provided doctype""" + return filter_allowed_docs_for_doctype(user_permissions, doctype, with_default_doc=False) + + +def filter_allowed_docs_for_doctype(user_permissions, doctype, with_default_doc=True): + """Returns all the docs from the passed user_permissions that are + allowed under provided doctype along with default doc value if with_default_doc is set""" + allowed_doc = [] + default_doc = None + for doc in user_permissions: + if not doc.get("applicable_for") or doc.get("applicable_for") == doctype: + allowed_doc.append(doc.get("doc")) + if doc.get("is_default") or len(user_permissions) == 1 and with_default_doc: + default_doc = doc.get("doc") + + return (allowed_doc, default_doc) if with_default_doc else allowed_doc + + +def push_perm_check_log(log): + if influxframework.flags.get("has_permission_check_logs") is None: + return + + influxframework.flags.get("has_permission_check_logs").append(_(log)) + + +def has_child_permission( + child_doctype, + ptype="read", + child_doc=None, + user=None, + raise_exception=True, + parent_doctype=None, +): + if isinstance(child_doc, str): + child_doc = influxframework.db.get_value( + child_doctype, + child_doc, + ("parent", "parenttype", "parentfield"), + as_dict=True, + ) + + if child_doc: + parent_doctype = child_doc.parenttype + + if not parent_doctype: + push_perm_check_log( + _("Please specify a valid parent DocType for {0}").format(influxframework.bold(child_doctype)) + ) + return False + + parent_meta = influxframework.get_meta(parent_doctype) + + if parent_meta.istable or all( + df.options != child_doctype for df in parent_meta.get_table_fields() + ): + push_perm_check_log( + _("{0} is not a valid parent DocType for {1}").format( + influxframework.bold(parent_doctype), influxframework.bold(child_doctype) + ) + ) + return False + + if ( + child_doc + and (permlevel := parent_meta.get_field(child_doc.parentfield).permlevel) > 0 + and permlevel not in parent_meta.get_permlevel_access(ptype, user=user) + ): + push_perm_check_log( + _("Insufficient Permission Level for {0}").format(influxframework.bold(parent_doctype)) + ) + return False + + return has_permission( + parent_doctype, + ptype=ptype, + doc=child_doc and getattr(child_doc, "parent_doc", child_doc.parent), + user=user, + raise_exception=raise_exception, + ) diff --git a/influxframework/printing/__init__.py b/influxframework/printing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/printing/doctype/__init__.py b/influxframework/printing/doctype/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/printing/doctype/letter_head/__init__.py b/influxframework/printing/doctype/letter_head/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/printing/doctype/letter_head/letter_head.js b/influxframework/printing/doctype/letter_head/letter_head.js new file mode 100644 index 0000000..7ba0553 --- /dev/null +++ b/influxframework/printing/doctype/letter_head/letter_head.js @@ -0,0 +1,8 @@ +// Copyright (c) 2017, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Letter Head", { + refresh: function (frm) { + frm.flag_public_attachments = true; + }, +}); diff --git a/influxframework/printing/doctype/letter_head/letter_head.json b/influxframework/printing/doctype/letter_head/letter_head.json new file mode 100644 index 0000000..d49b65a --- /dev/null +++ b/influxframework/printing/doctype/letter_head/letter_head.json @@ -0,0 +1,198 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:letter_head_name", + "creation": "2012-11-22 17:45:46", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "letter_head_name", + "source", + "footer_source", + "column_break_3", + "disabled", + "is_default", + "letter_head_image_section", + "image", + "image_height", + "image_width", + "align", + "header_section", + "content", + "footer_section", + "footer", + "footer_image_section", + "footer_image", + "footer_image_height", + "footer_image_width", + "footer_align" + ], + "fields": [ + { + "fieldname": "letter_head_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Letter Head Name", + "oldfieldname": "letter_head_name", + "oldfieldtype": "Data", + "reqd": 1, + "unique": 1 + }, + { + "depends_on": "letter_head_name", + "fieldname": "source", + "fieldtype": "Select", + "label": "Letter Head Based On", + "options": "Image\nHTML" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "letter_head_name", + "fieldname": "disabled", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Disabled", + "oldfieldname": "disabled", + "oldfieldtype": "Check" + }, + { + "default": "0", + "depends_on": "letter_head_name", + "fieldname": "is_default", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Default Letter Head", + "oldfieldname": "is_default", + "oldfieldtype": "Check", + "search_index": 1 + }, + { + "depends_on": "eval:doc.letter_head_name && doc.source === 'Image'", + "fieldname": "letter_head_image_section", + "fieldtype": "Section Break", + "label": "Letter Head Image" + }, + { + "depends_on": "eval:doc.letter_head_name && doc.source === 'Image'", + "fieldname": "image", + "fieldtype": "Attach Image", + "label": "Image" + }, + { + "depends_on": "eval:doc.source==='HTML' && doc.letter_head_name", + "fieldname": "header_section", + "fieldtype": "Section Break", + "label": "Header" + }, + { + "depends_on": "eval:!doc.__islocal && doc.source==='HTML'", + "description": "Letter Head in HTML", + "fieldname": "content", + "fieldtype": "HTML Editor", + "label": "Header HTML", + "oldfieldname": "content", + "oldfieldtype": "Text Editor" + }, + { + "depends_on": "eval:doc.footer_source==='HTML' && doc.letter_head_name", + "fieldname": "footer_section", + "fieldtype": "Section Break", + "label": "Footer" + }, + { + "depends_on": "eval:!doc.__islocal", + "description": "Footer will display correctly only in PDF", + "fieldname": "footer", + "fieldtype": "HTML Editor", + "label": "Footer HTML" + }, + { + "default": "Left", + "fieldname": "align", + "fieldtype": "Select", + "label": "Align", + "options": "Left\nRight\nCenter" + }, + { + "fieldname": "image_height", + "fieldtype": "Float", + "label": "Image Height" + }, + { + "fieldname": "image_width", + "fieldtype": "Float", + "label": "Image Width" + }, + { + "depends_on": "eval:doc.footer_source==='Image' && doc.letter_head_name", + "fieldname": "footer_image_section", + "fieldtype": "Section Break", + "label": "Footer Image" + }, + { + "fieldname": "footer_image", + "fieldtype": "Attach Image", + "label": "Image" + }, + { + "fieldname": "footer_image_height", + "fieldtype": "Float", + "label": "Image Height" + }, + { + "fieldname": "footer_image_width", + "fieldtype": "Float", + "label": "Image Width" + }, + { + "fieldname": "footer_align", + "fieldtype": "Select", + "label": "Align", + "options": "Left\nRight\nCenter" + }, + { + "default": "HTML", + "depends_on": "letter_head_name", + "fieldname": "footer_source", + "fieldtype": "Select", + "label": "Footer Based On", + "options": "Image\nHTML" + } + ], + "icon": "fa fa-font", + "idx": 1, + "links": [], + "max_attachments": 3, + "modified": "2022-06-16 23:10:46.852116", + "modified_by": "Administrator", + "module": "Printing", + "name": "Letter Head", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "read": 1, + "role": "All" + } + ], + "sort_field": "modified", + "sort_order": "ASC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/printing/doctype/letter_head/letter_head.py b/influxframework/printing/doctype/letter_head/letter_head.py new file mode 100644 index 0000000..cd667ef --- /dev/null +++ b/influxframework/printing/doctype/letter_head/letter_head.py @@ -0,0 +1,98 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework import _ +from influxframework.model.document import Document +from influxframework.utils import flt, is_image + + +class LetterHead(Document): + def before_insert(self): + # for better UX, let user set from attachment + self.source = "Image" + + def validate(self): + self.set_image() + self.validate_disabled_and_default() + + def validate_disabled_and_default(self): + if self.disabled and self.is_default: + influxframework.throw(_("Letter Head cannot be both disabled and default")) + + if not self.is_default and not self.disabled: + if not influxframework.db.exists("Letter Head", dict(is_default=1)): + self.is_default = 1 + + def set_image(self): + if self.source == "Image": + self.set_image_as_html( + field="image", + width="image_width", + height="image_height", + align="align", + html_field="content", + dimension_prefix="image_", + success_msg=_("Header HTML set from attachment {0}").format(self.image), + failure_msg=_("Please attach an image file to set HTML for Letter Head."), + ) + + if self.footer_source == "Image": + self.set_image_as_html( + field="footer_image", + width="footer_image_width", + height="footer_image_height", + align="footer_align", + html_field="footer", + dimension_prefix="footer_image_", + success_msg=_("Footer HTML set from attachment {0}").format(self.footer_image), + failure_msg=_("Please attach an image file to set HTML for Footer."), + ) + + def set_image_as_html( + self, field, width, height, dimension_prefix, align, html_field, success_msg, failure_msg + ): + if not self.get(field) or not is_image(self.get(field)): + influxframework.msgprint(failure_msg, alert=True, indicator="orange") + return + + self.set(width, flt(self.get(width))) + self.set(height, flt(self.get(height))) + + # To preserve the aspect ratio of the image, apply constraints only on + # the greater dimension and allow the other to scale accordingly + dimension = "width" if self.get(width) > self.get(height) else "height" + dimension_value = self.get(f"{dimension_prefix}{dimension}") + + if not dimension_value: + dimension_value = "" + + self.set( + html_field, + f"""
      +{self.get( +
      """, + ) + + influxframework.msgprint(success_msg, alert=True) + + def on_update(self): + self.set_as_default() + + # clear the cache so that the new letter head is uploaded + influxframework.clear_cache() + + def set_as_default(self): + from influxframework.utils import set_default + + if self.is_default: + influxframework.db.sql("update `tabLetter Head` set is_default=0 where name != %s", self.name) + + set_default("letter_head", self.name) + + # update control panel - so it loads new letter directly + influxframework.db.set_default("default_letter_head_content", self.content) + else: + influxframework.defaults.clear_default("letter_head", self.name) + influxframework.defaults.clear_default("default_letter_head_content", self.content) diff --git a/influxframework/printing/doctype/letter_head/test_letter_head.py b/influxframework/printing/doctype/letter_head/test_letter_head.py new file mode 100644 index 0000000..6692d70 --- /dev/null +++ b/influxframework/printing/doctype/letter_head/test_letter_head.py @@ -0,0 +1,14 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestLetterHead(InfluxFrameworkTestCase): + def test_auto_image(self): + letter_head = influxframework.get_doc( + dict(doctype="Letter Head", letter_head_name="Test", source="Image", image="/public/test.png") + ).insert() + + # test if image is automatically set + self.assertTrue(letter_head.image in letter_head.content) diff --git a/influxframework/printing/doctype/network_printer_settings/__init__.py b/influxframework/printing/doctype/network_printer_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/printing/doctype/network_printer_settings/network_printer_settings.js b/influxframework/printing/doctype/network_printer_settings/network_printer_settings.js new file mode 100644 index 0000000..cb2cda1 --- /dev/null +++ b/influxframework/printing/doctype/network_printer_settings/network_printer_settings.js @@ -0,0 +1,29 @@ +// Copyright (c) 2021, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Network Printer Settings", { + onload(frm) { + frm.trigger("connect_print_server"); + }, + server_ip(frm) { + frm.trigger("connect_print_server"); + }, + port(frm) { + frm.trigger("connect_print_server"); + }, + connect_print_server(frm) { + if (frm.doc.server_ip && frm.doc.port) { + influxframework.call({ + doc: frm.doc, + method: "get_printers_list", + args: { + ip: frm.doc.server_ip, + port: frm.doc.port, + }, + callback: function (data) { + frm.set_df_property("printer_name", "options", [""].concat(data.message)); + }, + }); + } + }, +}); diff --git a/influxframework/printing/doctype/network_printer_settings/network_printer_settings.json b/influxframework/printing/doctype/network_printer_settings/network_printer_settings.json new file mode 100644 index 0000000..11f1382 --- /dev/null +++ b/influxframework/printing/doctype/network_printer_settings/network_printer_settings.json @@ -0,0 +1,76 @@ +{ + "actions": [], + "autoname": "Prompt", + "creation": "2021-09-17 11:26:06.943999", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "server_ip", + "port", + "column_break_4", + "printer_name" + ], + "fields": [ + { + "default": "localhost", + "fieldname": "server_ip", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Server IP", + "reqd": 1 + }, + { + "default": "631", + "fieldname": "port", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Port", + "reqd": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "printer_name", + "fieldtype": "Select", + "label": "Printer Name", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-10-07 11:23:13.799402", + "modified_by": "Administrator", + "module": "Printing", + "name": "Network Printer Settings", + "naming_rule": "Set by user", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/printing/doctype/network_printer_settings/network_printer_settings.py b/influxframework/printing/doctype/network_printer_settings/network_printer_settings.py new file mode 100644 index 0000000..e47dcc1 --- /dev/null +++ b/influxframework/printing/doctype/network_printer_settings/network_printer_settings.py @@ -0,0 +1,40 @@ +# Copyright (c) 2021, InfluxFramework LLC +# For license information, please see license.txt + +import influxframework +from influxframework import _ +from influxframework.model.document import Document + + +class NetworkPrinterSettings(Document): + @influxframework.whitelist() + def get_printers_list(self, ip="localhost", port=631): + printer_list = [] + try: + import cups + except ImportError: + influxframework.throw( + _( + """This feature can not be used as dependencies are missing. + Please contact your system manager to enable this by installing pycups!""" + ) + ) + return + try: + cups.setServer(self.server_ip) + cups.setPort(self.port) + conn = cups.Connection() + printers = conn.getPrinters() + for printer_id, printer in printers.items(): + printer_list.append({"value": printer_id, "label": printer["printer-make-and-model"]}) + + except RuntimeError: + influxframework.throw(_("Failed to connect to server")) + except influxframework.ValidationError: + influxframework.throw(_("Failed to connect to server")) + return printer_list + + +@influxframework.whitelist() +def get_network_printer_settings(): + return influxframework.db.get_list("Network Printer Settings", pluck="name") diff --git a/influxframework/printing/doctype/network_printer_settings/test_network_printer_settings.py b/influxframework/printing/doctype/network_printer_settings/test_network_printer_settings.py new file mode 100644 index 0000000..5ae2e03 --- /dev/null +++ b/influxframework/printing/doctype/network_printer_settings/test_network_printer_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, InfluxFramework LLC +# See license.txt + +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestNetworkPrinterSettings(InfluxFrameworkTestCase): + pass diff --git a/influxframework/printing/doctype/print_format/__init__.py b/influxframework/printing/doctype/print_format/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/printing/doctype/print_format/print_format.js b/influxframework/printing/doctype/print_format/print_format.js new file mode 100644 index 0000000..f19ff60 --- /dev/null +++ b/influxframework/printing/doctype/print_format/print_format.js @@ -0,0 +1,85 @@ +// Copyright (c) 2017, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Print Format", "onload", function (frm) { + frm.add_fetch("doc_type", "module", "module"); +}); + +influxframework.ui.form.on("Print Format", { + refresh: function (frm) { + frm.set_intro(""); + frm.toggle_enable(["html", "doc_type", "module"], false); + if (influxframework.session.user === "Administrator" || frm.doc.standard === "No") { + frm.toggle_enable(["html", "doc_type", "module"], true); + frm.enable_save(); + } + + if (frm.doc.standard === "Yes" && influxframework.session.user !== "Administrator") { + frm.set_intro(__("Please duplicate this to make changes")); + } + frm.trigger("render_buttons"); + frm.toggle_display("standard", influxframework.boot.developer_mode); + frm.trigger("hide_absolute_value_field"); + }, + render_buttons: function (frm) { + frm.page.clear_inner_toolbar(); + if (!frm.is_new()) { + if (!frm.doc.custom_format) { + frm.add_custom_button(__("Edit Format"), function () { + if (!frm.doc.doc_type) { + influxframework.msgprint(__("Please select DocType first")); + return; + } + if (frm.doc.print_format_builder_beta) { + influxframework.set_route("print-format-builder-beta", frm.doc.name); + } else { + influxframework.set_route("print-format-builder", frm.doc.name); + } + }); + } else if (frm.doc.custom_format && !frm.doc.raw_printing) { + frm.set_df_property("html", "reqd", 1); + } + if (influxframework.model.can_read(frm.doc.doc_type)) { + influxframework.db.get_value("DocType", frm.doc.doc_type, "default_print_format", (r) => { + if (r.default_print_format != frm.doc.name) { + frm.add_custom_button(__("Set as Default"), function () { + influxframework.call({ + method: "influxframework.printing.doctype.print_format.print_format.make_default", + args: { + name: frm.doc.name, + }, + callback: function () { + frm.refresh(); + }, + }); + }); + } + }); + } + } + }, + custom_format: function (frm) { + var value = frm.doc.custom_format ? 0 : 1; + frm.set_value("align_labels_right", value); + frm.set_value("show_section_headings", value); + frm.set_value("line_breaks", value); + frm.trigger("render_buttons"); + }, + doc_type: function (frm) { + frm.trigger("hide_absolute_value_field"); + }, + hide_absolute_value_field: function (frm) { + // TODO: make it work with frm.doc.doc_type + // Problem: frm isn't updated in some random cases + const doctype = locals[frm.doc.doctype][frm.doc.name].doc_type; + if (doctype) { + influxframework.model.with_doctype(doctype, () => { + const meta = influxframework.get_meta(doctype); + const has_int_float_currency_field = meta.fields.filter((df) => + in_list(["Int", "Float", "Currency"], df.fieldtype) + ); + frm.toggle_display("absolute_value", has_int_float_currency_field.length); + }); + } + }, +}); diff --git a/influxframework/printing/doctype/print_format/print_format.json b/influxframework/printing/doctype/print_format/print_format.json new file mode 100644 index 0000000..d630ff3 --- /dev/null +++ b/influxframework/printing/doctype/print_format/print_format.json @@ -0,0 +1,284 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "Prompt", + "creation": "2013-01-23 19:54:43", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "doc_type", + "module", + "default_print_language", + "column_break_3", + "standard", + "custom_format", + "disabled", + "section_break_6", + "print_format_type", + "raw_printing", + "html", + "raw_commands", + "section_break_9", + "margin_top", + "margin_bottom", + "margin_left", + "margin_right", + "align_labels_right", + "show_section_headings", + "line_breaks", + "absolute_value", + "column_break_11", + "font_size", + "font", + "page_number", + "css_section", + "css", + "custom_html_help", + "section_break_13", + "print_format_help", + "format_data", + "print_format_builder", + "print_format_builder_beta" + ], + "fields": [ + { + "fieldname": "doc_type", + "fieldtype": "Link", + "in_filter": 1, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "DocType", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "module", + "fieldtype": "Link", + "label": "Module", + "options": "Module Def" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "default": "No", + "fieldname": "standard", + "fieldtype": "Select", + "in_filter": 1, + "label": "Standard", + "no_copy": 1, + "oldfieldname": "standard", + "oldfieldtype": "Select", + "options": "No\nYes", + "reqd": 1, + "search_index": 1 + }, + { + "default": "0", + "fieldname": "custom_format", + "fieldtype": "Check", + "label": "Custom Format" + }, + { + "depends_on": "custom_format", + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "default": "Jinja", + "depends_on": "custom_format", + "fieldname": "print_format_type", + "fieldtype": "Select", + "label": "Print Format Type", + "options": "Jinja\nJS" + }, + { + "default": "0", + "fieldname": "raw_printing", + "fieldtype": "Check", + "label": "Raw Printing" + }, + { + "depends_on": "eval:!doc.raw_printing", + "fieldname": "html", + "fieldtype": "Code", + "label": "HTML", + "oldfieldname": "html", + "oldfieldtype": "Text Editor", + "options": "HTML" + }, + { + "depends_on": "raw_printing", + "description": "Any string-based printer languages can be used. Writing raw commands requires knowledge of the printer's native language provided by the printer manufacturer. Please refer to the developer manual provided by the printer manufacturer on how to write their native commands. These commands are rendered on the server side using the Jinja Templating Language.", + "fieldname": "raw_commands", + "fieldtype": "Code", + "label": "Raw Commands" + }, + { + "depends_on": "eval:!doc.custom_format", + "fieldname": "section_break_9", + "fieldtype": "Section Break", + "label": "Style Settings" + }, + { + "default": "0", + "fieldname": "align_labels_right", + "fieldtype": "Check", + "label": "Align Labels to the Right" + }, + { + "default": "0", + "fieldname": "show_section_headings", + "fieldtype": "Check", + "label": "Show Section Headings" + }, + { + "default": "0", + "fieldname": "line_breaks", + "fieldtype": "Check", + "label": "Show Line Breaks after Sections" + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "fieldname": "default_print_language", + "fieldtype": "Link", + "label": "Default Print Language", + "options": "Language" + }, + { + "depends_on": "eval:!doc.custom_format", + "fieldname": "font", + "fieldtype": "Data", + "label": "Google Font" + }, + { + "depends_on": "eval:!doc.raw_printing", + "fieldname": "css_section", + "fieldtype": "Section Break" + }, + { + "fieldname": "css", + "fieldtype": "Code", + "label": "Custom CSS", + "options": "CSS" + }, + { + "fieldname": "custom_html_help", + "fieldtype": "HTML", + "label": "Custom HTML Help", + "options": "

      Custom CSS Help

      \n\n

      Notes:

      \n\n
        \n
      1. All field groups (label + value) are set attributes data-fieldtype and data-fieldname
      2. \n
      3. All values are given class value
      4. \n
      5. All Section Breaks are given class section-break
      6. \n
      7. All Column Breaks are given class column-break
      8. \n
      \n\n

      Examples

      \n\n

      1. Left align integers

      \n\n
      [data-fieldtype=\"Int\"] .value { text-align: left; }
      \n\n

      1. Add border to sections except the last section

      \n\n
      .section-break { padding: 30px 0px; border-bottom: 1px solid #eee; }\n.section-break:last-child { padding-bottom: 0px; border-bottom: 0px;  }
      \n" + }, + { + "depends_on": "custom_format", + "fieldname": "section_break_13", + "fieldtype": "Section Break" + }, + { + "depends_on": "custom_format", + "fieldname": "print_format_help", + "fieldtype": "HTML", + "label": "Print Format Help", + "options": "

      Print Format Help

      \n
      \n

      Introduction

      \n

      Print Formats are rendered on the server side using the Jinja Templating Language. All forms have access to the doc object which contains information about the document that is being formatted. You can also access common utilities via the influxframework module.

      \n

      For styling, the Boostrap CSS framework is provided and you can enjoy the full range of classes.

      \n
      \n

      References

      \n
        \n\t
      1. Jinja Templating Language
      2. \n\t
      3. Bootstrap CSS Framework
      4. \n
      \n
      \n

      Example

      \n
      <h3>{{ doc.select_print_heading or \"Invoice\" }}</h3>\n<div class=\"row\">\n\t<div class=\"col-md-3 text-right\">Customer Name</div>\n\t<div class=\"col-md-9\">{{ doc.customer_name }}</div>\n</div>\n<div class=\"row\">\n\t<div class=\"col-md-3 text-right\">Date</div>\n\t<div class=\"col-md-9\">{{ doc.get_formatted(\"invoice_date\") }}</div>\n</div>\n<table class=\"table table-bordered\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<th>Sr</th>\n\t\t\t<th>Item Name</th>\n\t\t\t<th>Description</th>\n\t\t\t<th class=\"text-right\">Qty</th>\n\t\t\t<th class=\"text-right\">Rate</th>\n\t\t\t<th class=\"text-right\">Amount</th>\n\t\t</tr>\n\t\t{%- for row in doc.items -%}\n\t\t<tr>\n\t\t\t<td style=\"width: 3%;\">{{ row.idx }}</td>\n\t\t\t<td style=\"width: 20%;\">\n\t\t\t\t{{ row.item_name }}\n\t\t\t\t{% if row.item_code != row.item_name -%}\n\t\t\t\t<br>Item Code: {{ row.item_code}}\n\t\t\t\t{%- endif %}\n\t\t\t</td>\n\t\t\t<td style=\"width: 37%;\">\n\t\t\t\t<div style=\"border: 0px;\">{{ row.description }}</div></td>\n\t\t\t<td style=\"width: 10%; text-align: right;\">{{ row.qty }} {{ row.uom or row.stock_uom }}</td>\n\t\t\t<td style=\"width: 15%; text-align: right;\">{{\n\t\t\t\trow.get_formatted(\"rate\", doc) }}</td>\n\t\t\t<td style=\"width: 15%; text-align: right;\">{{\n\t\t\t\trow.get_formatted(\"amount\", doc) }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>
      \n
      \n

      Common Functions

      \n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n
      doc.get_formatted(\"[fieldname]\", [parent_doc])Get document value formatted as Date, Currency, etc. Pass parent doc for currency type fields.
      influxframework.db.get_value(\"[doctype]\", \"[name]\", \"fieldname\")Get value from another document.
      \n" + }, + { + "fieldname": "format_data", + "fieldtype": "Code", + "hidden": 1, + "label": "Format Data" + }, + { + "default": "0", + "fieldname": "print_format_builder", + "fieldtype": "Check", + "hidden": 1, + "label": "Print Format Builder" + }, + { + "default": "0", + "depends_on": "doc_type", + "description": "If checked, negative numeric values of Currency, Quantity or Count would be shown as positive", + "fieldname": "absolute_value", + "fieldtype": "Check", + "label": "Show Absolute Values" + }, + { + "default": "0", + "fieldname": "print_format_builder_beta", + "fieldtype": "Check", + "label": "Print Format Builder Beta" + }, + { + "default": "15", + "fieldname": "margin_top", + "fieldtype": "Float", + "label": "Margin Top" + }, + { + "default": "15", + "fieldname": "margin_bottom", + "fieldtype": "Float", + "label": "Margin Bottom" + }, + { + "default": "15", + "fieldname": "margin_left", + "fieldtype": "Float", + "label": "Margin Left" + }, + { + "default": "15", + "fieldname": "margin_right", + "fieldtype": "Float", + "label": "Margin Right" + }, + { + "default": "14", + "fieldname": "font_size", + "fieldtype": "Int", + "label": "Font Size" + }, + { + "default": "Hide", + "fieldname": "page_number", + "fieldtype": "Select", + "label": "Page Number", + "options": "Hide\nTop Left\nTop Center\nTop Right\nBottom Left\nBottom Center\nBottom Right" + } + ], + "icon": "fa fa-print", + "idx": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-10-12 17:52:41.167107", + "modified_by": "Administrator", + "module": "Printing", + "name": "Print Format", + "naming_rule": "Set by user", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} diff --git a/influxframework/printing/doctype/print_format/print_format.py b/influxframework/printing/doctype/print_format/print_format.py new file mode 100644 index 0000000..aca8dec --- /dev/null +++ b/influxframework/printing/doctype/print_format/print_format.py @@ -0,0 +1,132 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE + +import json + +import influxframework +import influxframework.utils +from influxframework import _ +from influxframework.model.document import Document +from influxframework.utils.jinja import validate_template +from influxframework.utils.weasyprint import download_pdf, get_html + + +class PrintFormat(Document): + def onload(self): + templates = influxframework.get_all( + "Print Format Field Template", + fields=["template", "field", "name"], + filters={"document_type": self.doc_type}, + ) + self.set_onload("print_templates", templates) + + def get_html(self, docname, letterhead=None): + return get_html(self.doc_type, docname, self.name, letterhead) + + def download_pdf(self, docname, letterhead=None): + return download_pdf(self.doc_type, docname, self.name, letterhead) + + def validate(self): + if ( + self.standard == "Yes" + and not influxframework.local.conf.get("developer_mode") + and not (influxframework.flags.in_import or influxframework.flags.in_test) + ): + + influxframework.throw(influxframework._("Standard Print Format cannot be updated")) + + # old_doc_type is required for clearing item cache + self.old_doc_type = influxframework.db.get_value("Print Format", self.name, "doc_type") + + self.extract_images() + + if not self.module: + self.module = influxframework.db.get_value("DocType", self.doc_type, "module") + + if self.html and self.print_format_type != "JS": + validate_template(self.html) + + if self.custom_format and self.raw_printing and not self.raw_commands: + influxframework.throw( + _("{0} are required").format(influxframework.bold(_("Raw Commands"))), influxframework.MandatoryError + ) + + if self.custom_format and not self.html and not self.raw_printing: + influxframework.throw(_("{0} is required").format(influxframework.bold(_("HTML"))), influxframework.MandatoryError) + + def extract_images(self): + from influxframework.core.doctype.file.utils import extract_images_from_html + + if self.print_format_builder_beta: + return + + if self.format_data: + data = json.loads(self.format_data) + for df in data: + if df.get("fieldtype") and df["fieldtype"] in ("HTML", "Custom HTML") and df.get("options"): + df["options"] = extract_images_from_html(self, df["options"]) + self.format_data = json.dumps(data) + + def on_update(self): + if hasattr(self, "old_doc_type") and self.old_doc_type: + influxframework.clear_cache(doctype=self.old_doc_type) + if self.doc_type: + influxframework.clear_cache(doctype=self.doc_type) + + self.export_doc() + + def after_rename(self, old: str, new: str, *args, **kwargs): + if self.doc_type: + influxframework.clear_cache(doctype=self.doc_type) + + # update property setter default_print_format if set + influxframework.db.set_value( + "Property Setter", + { + "doctype_or_field": "DocType", + "doc_type": self.doc_type, + "property": "default_print_format", + "value": old, + }, + "value", + new, + ) + + def export_doc(self): + from influxframework.modules.utils import export_module_json + + export_module_json(self, self.standard == "Yes", self.module) + + def on_trash(self): + if self.doc_type: + influxframework.clear_cache(doctype=self.doc_type) + + +@influxframework.whitelist() +def make_default(name): + """Set print format as default""" + influxframework.has_permission("Print Format", "write") + + print_format = influxframework.get_doc("Print Format", name) + + if (influxframework.conf.get("developer_mode") or 0) == 1: + # developer mode, set it default in doctype + doctype = influxframework.get_doc("DocType", print_format.doc_type) + doctype.default_print_format = name + doctype.save() + else: + # customization + influxframework.make_property_setter( + { + "doctype_or_field": "DocType", + "doctype": print_format.doc_type, + "property": "default_print_format", + "value": name, + } + ) + + influxframework.msgprint( + influxframework._("{0} is now default print format for {1} doctype").format( + influxframework.bold(name), influxframework.bold(print_format.doc_type) + ) + ) diff --git a/influxframework/printing/doctype/print_format/test_print_format.py b/influxframework/printing/doctype/print_format/test_print_format.py new file mode 100644 index 0000000..b321fe3 --- /dev/null +++ b/influxframework/printing/doctype/print_format/test_print_format.py @@ -0,0 +1,32 @@ +# Copyright (c) 2015, InfluxFramework LLC +# License: MIT. See LICENSE +import re + +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + +test_records = influxframework.get_test_records("Print Format") + + +class TestPrintFormat(InfluxFrameworkTestCase): + def test_print_user(self, style=None): + print_html = influxframework.get_print("User", "Administrator", style=style) + self.assertTrue("" in print_html) + self.assertTrue( + re.findall(r'
      [\s]*administrator[\s]*
      ', print_html) + ) + return print_html + + def test_print_user_standard(self): + print_html = self.test_print_user("Standard") + self.assertTrue(re.findall(r"\.print-format {[\s]*font-size: 9pt;", print_html)) + self.assertFalse(re.findall(r"th {[\s]*background-color: #eee;[\s]*}", print_html)) + self.assertFalse("font-family: serif;" in print_html) + + def test_print_user_modern(self): + print_html = self.test_print_user("Modern") + self.assertTrue("/* modern format: for-test */" in print_html) + + def test_print_user_classic(self): + print_html = self.test_print_user("Classic") + self.assertTrue("/* classic format: for-test */" in print_html) diff --git a/influxframework/printing/doctype/print_format/test_records.json b/influxframework/printing/doctype/print_format/test_records.json new file mode 100644 index 0000000..c772383 --- /dev/null +++ b/influxframework/printing/doctype/print_format/test_records.json @@ -0,0 +1,9 @@ +[ + { + "doctype": "Print Format", + "name": "_Test Print Format 1", + "module": "Core", + "doc_type": "User", + "html": "" + } +] diff --git a/influxframework/printing/doctype/print_format_field_template/__init__.py b/influxframework/printing/doctype/print_format_field_template/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/printing/doctype/print_format_field_template/print_format_field_template.js b/influxframework/printing/doctype/print_format_field_template/print_format_field_template.js new file mode 100644 index 0000000..5895931 --- /dev/null +++ b/influxframework/printing/doctype/print_format_field_template/print_format_field_template.js @@ -0,0 +1,7 @@ +// Copyright (c) 2021, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Print Format Field Template", { + // refresh: function(frm) { + // } +}); diff --git a/influxframework/printing/doctype/print_format_field_template/print_format_field_template.json b/influxframework/printing/doctype/print_format_field_template/print_format_field_template.json new file mode 100644 index 0000000..3b79aae --- /dev/null +++ b/influxframework/printing/doctype/print_format_field_template/print_format_field_template.json @@ -0,0 +1,101 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "Prompt", + "creation": "2021-10-05 14:23:56.508499", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type", + "field", + "template_file", + "column_break_3", + "module", + "standard", + "section_break_5", + "template" + ], + "fields": [ + { + "depends_on": "eval:!doc.multiple", + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Document Type", + "mandatory_depends_on": "eval:!doc.multiple", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "field", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Default Template For Field" + }, + { + "depends_on": "eval:!doc.standard", + "fieldname": "template", + "fieldtype": "Code", + "label": "Template", + "mandatory_depends_on": "eval:!doc.standard", + "options": "HTML" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "depends_on": "standard", + "fieldname": "module", + "fieldtype": "Link", + "label": "Module", + "options": "Module Def" + }, + { + "default": "0", + "fieldname": "standard", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Standard" + }, + { + "depends_on": "eval:doc.standard", + "fieldname": "template_file", + "fieldtype": "Data", + "label": "Template File", + "mandatory_depends_on": "eval:doc.standard" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-10-19 17:47:59.577949", + "modified_by": "Administrator", + "module": "Printing", + "name": "Print Format Field Template", + "naming_rule": "Set by user", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/printing/doctype/print_format_field_template/print_format_field_template.py b/influxframework/printing/doctype/print_format_field_template/print_format_field_template.py new file mode 100644 index 0000000..390060a --- /dev/null +++ b/influxframework/printing/doctype/print_format_field_template/print_format_field_template.py @@ -0,0 +1,43 @@ +# Copyright (c) 2021, InfluxFramework LLC +# For license information, please see license.txt + +import influxframework +from influxframework import _ +from influxframework.model.document import Document + + +class PrintFormatFieldTemplate(Document): + def validate(self): + if self.standard and not (influxframework.conf.developer_mode or influxframework.flags.in_patch): + influxframework.throw(_("Enable developer mode to create a standard Print Template")) + + def before_insert(self): + self.validate_duplicate() + + def on_update(self): + self.validate_duplicate() + self.export_doc() + + def validate_duplicate(self): + if not self.standard: + return + if not self.field: + return + + filters = {"document_type": self.document_type, "field": self.field} + if not self.is_new(): + filters.update({"name": ("!=", self.name)}) + result = influxframework.get_all("Print Format Field Template", filters=filters, limit=1) + if result: + influxframework.throw( + _("A template already exists for field {0} of {1}").format( + influxframework.bold(self.field), influxframework.bold(self.document_type) + ), + influxframework.DuplicateEntryError, + title=_("Duplicate Entry"), + ) + + def export_doc(self): + from influxframework.modules.utils import export_module_json + + export_module_json(self, self.standard, self.module) diff --git a/influxframework/printing/doctype/print_format_field_template/test_print_format_field_template.py b/influxframework/printing/doctype/print_format_field_template/test_print_format_field_template.py new file mode 100644 index 0000000..81790c3 --- /dev/null +++ b/influxframework/printing/doctype/print_format_field_template/test_print_format_field_template.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, InfluxFramework LLC +# See license.txt + +# import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestPrintFormatFieldTemplate(InfluxFrameworkTestCase): + pass diff --git a/influxframework/printing/doctype/print_heading/__init__.py b/influxframework/printing/doctype/print_heading/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/printing/doctype/print_heading/print_heading.js b/influxframework/printing/doctype/print_heading/print_heading.js new file mode 100644 index 0000000..f33fc2a --- /dev/null +++ b/influxframework/printing/doctype/print_heading/print_heading.js @@ -0,0 +1,6 @@ +// Copyright (c) 2017, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Print Heading", { + refresh: function (frm) {}, +}); diff --git a/influxframework/printing/doctype/print_heading/print_heading.json b/influxframework/printing/doctype/print_heading/print_heading.json new file mode 100644 index 0000000..418429f --- /dev/null +++ b/influxframework/printing/doctype/print_heading/print_heading.json @@ -0,0 +1,145 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:print_heading", + "beta": 0, + "creation": "2013-01-10 16:34:24", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 0, + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "print_heading", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 1, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Print Heading", + "length": 0, + "no_copy": 0, + "oldfieldname": "print_heading", + "oldfieldtype": "Data", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "description", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Description", + "length": 0, + "no_copy": 0, + "oldfieldname": "description", + "oldfieldtype": "Small Text", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, + "width": "300px" + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "icon": "fa fa-font", + "idx": 1, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2017-05-03 05:59:09.131569", + "modified_by": "Administrator", + "module": "Printing", + "name": "Print Heading", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + }, + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 0, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 0, + "read": 1, + "report": 0, + "role": "All", + "set_user_permissions": 0, + "share": 0, + "submit": 0, + "write": 0 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "search_fields": "print_heading", + "show_name_in_global_search": 0, + "sort_order": "DESC", + "track_changes": 0, + "track_seen": 0 +} \ No newline at end of file diff --git a/influxframework/printing/doctype/print_heading/print_heading.py b/influxframework/printing/doctype/print_heading/print_heading.py new file mode 100644 index 0000000..9e32778 --- /dev/null +++ b/influxframework/printing/doctype/print_heading/print_heading.py @@ -0,0 +1,9 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document + + +class PrintHeading(Document): + pass diff --git a/influxframework/printing/doctype/print_heading/test_print_heading.py b/influxframework/printing/doctype/print_heading/test_print_heading.py new file mode 100644 index 0000000..b080e37 --- /dev/null +++ b/influxframework/printing/doctype/print_heading/test_print_heading.py @@ -0,0 +1,8 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestPrintHeading(InfluxFrameworkTestCase): + pass diff --git a/influxframework/printing/doctype/print_settings/__init__.py b/influxframework/printing/doctype/print_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/printing/doctype/print_settings/print_settings.js b/influxframework/printing/doctype/print_settings/print_settings.js new file mode 100644 index 0000000..e7ededf --- /dev/null +++ b/influxframework/printing/doctype/print_settings/print_settings.js @@ -0,0 +1,23 @@ +// Copyright (c) 2018, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Print Settings", { + print_style: function (frm) { + influxframework.db.get_value("Print Style", frm.doc.print_style, "preview").then((r) => { + if (r.message.preview) { + frm.get_field("print_style_preview").$wrapper.html( + `` + ); + } else { + frm.get_field("print_style_preview").$wrapper.html( + `

      ${__( + "No Preview" + )}

      ` + ); + } + }); + }, + onload: function (frm) { + frm.script_manager.trigger("print_style"); + }, +}); diff --git a/influxframework/printing/doctype/print_settings/print_settings.json b/influxframework/printing/doctype/print_settings/print_settings.json new file mode 100644 index 0000000..f45de76 --- /dev/null +++ b/influxframework/printing/doctype/print_settings/print_settings.json @@ -0,0 +1,197 @@ +{ + "actions": [], + "creation": "2014-07-17 06:54:20.782907", + "doctype": "DocType", + "document_type": "System", + "engine": "InnoDB", + "field_order": [ + "pdf_settings", + "send_print_as_pdf", + "repeat_header_footer", + "column_break_4", + "pdf_page_size", + "pdf_page_height", + "pdf_page_width", + "view_link_in_email", + "with_letterhead", + "allow_print_for_draft", + "add_draft_heading", + "column_break_10", + "allow_page_break_inside_tables", + "allow_print_for_cancelled", + "server_printer", + "enable_print_server", + "raw_printing_section", + "enable_raw_printing", + "print_style_section", + "print_style", + "print_style_preview", + "section_break_8", + "font", + "font_size" + ], + "fields": [ + { + "fieldname": "pdf_settings", + "fieldtype": "Section Break", + "label": "PDF Settings" + }, + { + "default": "1", + "description": "Send Email Print Attachments as PDF (Recommended)", + "fieldname": "send_print_as_pdf", + "fieldtype": "Check", + "label": "Send Print as PDF" + }, + { + "default": "1", + "fieldname": "repeat_header_footer", + "fieldtype": "Check", + "label": "Repeat Header and Footer in PDF" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "default": "A4", + "fieldname": "pdf_page_size", + "fieldtype": "Select", + "label": "PDF Page Size", + "options": "A0\nA1\nA2\nA3\nA4\nA5\nA6\nA7\nA8\nA9\nB0\nB1\nB2\nB3\nB4\nB5\nB6\nB7\nB8\nB9\nB10\nC5E\nComm10E\nDLE\nExecutive\nFolio\nLedger\nLegal\nLetter\nTabloid\nCustom" + }, + { + "fieldname": "view_link_in_email", + "fieldtype": "Section Break", + "label": "Page Settings" + }, + { + "default": "1", + "fieldname": "with_letterhead", + "fieldtype": "Check", + "label": "Print with letterhead" + }, + { + "default": "1", + "fieldname": "allow_print_for_draft", + "fieldtype": "Check", + "label": "Allow Print for Draft" + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break" + }, + { + "default": "1", + "fieldname": "add_draft_heading", + "fieldtype": "Check", + "label": "Always add \"Draft\" Heading for printing draft documents" + }, + { + "default": "0", + "fieldname": "allow_page_break_inside_tables", + "fieldtype": "Check", + "label": "Allow page break inside tables" + }, + { + "default": "0", + "fieldname": "allow_print_for_cancelled", + "fieldtype": "Check", + "label": "Allow Print for Cancelled" + }, + { + "fieldname": "server_printer", + "fieldtype": "Section Break", + "label": "Print Server" + }, + { + "default": "0", + "depends_on": "enable_print_server", + "fieldname": "enable_print_server", + "fieldtype": "Check", + "label": "Enable Print Server", + "mandatory_depends_on": "enable_print_server" + }, + { + "fieldname": "raw_printing_section", + "fieldtype": "Section Break", + "label": "Raw Printing" + }, + { + "default": "0", + "fieldname": "enable_raw_printing", + "fieldtype": "Check", + "label": "Enable Raw Printing" + }, + { + "fieldname": "print_style_section", + "fieldtype": "Section Break", + "label": "Print Style" + }, + { + "default": "Redesign", + "fieldname": "print_style", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Print Style", + "options": "Print Style" + }, + { + "fieldname": "print_style_preview", + "fieldtype": "HTML", + "label": "Print Style Preview" + }, + { + "fieldname": "section_break_8", + "fieldtype": "Section Break", + "label": "Fonts" + }, + { + "default": "Default", + "fieldname": "font", + "fieldtype": "Select", + "label": "Font", + "options": "Default\nHelvetica Neue\nArial\nHelvetica\nInter\nVerdana\nMonospace" + }, + { + "description": "In points. Default is 9.", + "fieldname": "font_size", + "fieldtype": "Float", + "label": "Font Size" + }, + { + "depends_on": "eval:doc.pdf_page_size == \"Custom\"", + "fieldname": "pdf_page_height", + "fieldtype": "Float", + "label": "PDF Page Height (in mm)" + }, + { + "depends_on": "eval:doc.pdf_page_size == \"Custom\"", + "fieldname": "pdf_page_width", + "fieldtype": "Float", + "label": "PDF Page Width (in mm)" + } + ], + "icon": "fa fa-cog", + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2021-09-17 12:59:14.783694", + "modified_by": "Administrator", + "module": "Printing", + "name": "Print Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/influxframework/printing/doctype/print_settings/print_settings.py b/influxframework/printing/doctype/print_settings/print_settings.py new file mode 100644 index 0000000..279173a --- /dev/null +++ b/influxframework/printing/doctype/print_settings/print_settings.py @@ -0,0 +1,26 @@ +# Copyright (c) 2018, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework import _ +from influxframework.model.document import Document +from influxframework.utils import cint + + +class PrintSettings(Document): + def validate(self): + if self.pdf_page_size == "Custom" and not (self.pdf_page_height and self.pdf_page_width): + influxframework.throw(_("Page height and width cannot be zero")) + + def on_update(self): + influxframework.clear_cache() + + +@influxframework.whitelist() +def is_print_server_enabled(): + if not hasattr(influxframework.local, "enable_print_server"): + influxframework.local.enable_print_server = cint( + influxframework.db.get_single_value("Print Settings", "enable_print_server") + ) + + return influxframework.local.enable_print_server diff --git a/influxframework/printing/doctype/print_settings/test_print_settings.py b/influxframework/printing/doctype/print_settings/test_print_settings.py new file mode 100644 index 0000000..5d5658c --- /dev/null +++ b/influxframework/printing/doctype/print_settings/test_print_settings.py @@ -0,0 +1,7 @@ +# Copyright (c) 2018, InfluxFramework LLC +# License: MIT. See LICENSE +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestPrintSettings(InfluxFrameworkTestCase): + pass diff --git a/influxframework/printing/doctype/print_style/__init__.py b/influxframework/printing/doctype/print_style/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/printing/doctype/print_style/print_style.js b/influxframework/printing/doctype/print_style/print_style.js new file mode 100644 index 0000000..21f4b82 --- /dev/null +++ b/influxframework/printing/doctype/print_style/print_style.js @@ -0,0 +1,10 @@ +// Copyright (c) 2017, InfluxFramework LLC +// For license information, please see license.txt + +influxframework.ui.form.on("Print Style", { + refresh: function (frm) { + frm.add_custom_button(__("Print Settings"), () => { + influxframework.set_route("Form", "Print Settings"); + }); + }, +}); diff --git a/influxframework/printing/doctype/print_style/print_style.json b/influxframework/printing/doctype/print_style/print_style.json new file mode 100644 index 0000000..29e88a4 --- /dev/null +++ b/influxframework/printing/doctype/print_style/print_style.json @@ -0,0 +1,214 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 1, + "autoname": "field:print_style_name", + "beta": 0, + "creation": "2017-08-17 01:25:56.910716", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "print_style_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Print Style Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "disabled", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Disabled", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "standard", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Standard", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "css", + "fieldtype": "Code", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "CSS", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "preview", + "fieldtype": "Attach Image", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Preview", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_field": "preview", + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2017-08-17 02:18:08.132853", + "modified_by": "Administrator", + "module": "Printing", + "name": "Print Style", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/influxframework/printing/doctype/print_style/print_style.py b/influxframework/printing/doctype/print_style/print_style.py new file mode 100644 index 0000000..aabf9bf --- /dev/null +++ b/influxframework/printing/doctype/print_style/print_style.py @@ -0,0 +1,25 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE + +import influxframework +from influxframework.model.document import Document + + +class PrintStyle(Document): + def validate(self): + if ( + self.standard == 1 + and not influxframework.local.conf.get("developer_mode") + and not (influxframework.flags.in_import or influxframework.flags.in_test) + ): + + influxframework.throw(influxframework._("Standard Print Style cannot be changed. Please duplicate to edit.")) + + def on_update(self): + self.export_doc() + + def export_doc(self): + # export + from influxframework.modules.utils import export_module_json + + export_module_json(self, self.standard == 1, "Printing") diff --git a/influxframework/printing/doctype/print_style/test_print_style.py b/influxframework/printing/doctype/print_style/test_print_style.py new file mode 100644 index 0000000..7363509 --- /dev/null +++ b/influxframework/printing/doctype/print_style/test_print_style.py @@ -0,0 +1,8 @@ +# Copyright (c) 2017, InfluxFramework LLC +# License: MIT. See LICENSE +import influxframework +from influxframework.tests.utils import InfluxFrameworkTestCase + + +class TestPrintStyle(InfluxFrameworkTestCase): + pass diff --git a/influxframework/printing/form_tour/letter_head/letter_head.json b/influxframework/printing/form_tour/letter_head/letter_head.json new file mode 100644 index 0000000..66730b4 --- /dev/null +++ b/influxframework/printing/form_tour/letter_head/letter_head.json @@ -0,0 +1,53 @@ +{ + "creation": "2021-11-22 15:26:53.878805", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-11-22 15:26:53.878805", + "modified_by": "Administrator", + "module": "Printing", + "name": "Letter Head", + "owner": "Administrator", + "reference_doctype": "Letter Head", + "save_on_complete": 1, + "steps": [ + { + "description": "Let's name your first Letter Head with your company's name", + "field": "", + "fieldname": "letter_head_name", + "fieldtype": "Data", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Letter Head Name", + "parent_field": "", + "position": "Right", + "title": "Letter Head Name" + }, + { + "description": "Select the image containing only header part of your letter Head.", + "field": "", + "fieldname": "image", + "fieldtype": "Attach Image", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Image", + "parent_field": "", + "position": "Right", + "title": "Image" + }, + { + "description": "You can mark the Letter Head as default", + "field": "", + "fieldname": "is_default", + "fieldtype": "Check", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Default Letter Head", + "parent_field": "", + "position": "Right", + "title": "Default Letter Head" + } + ], + "title": "Letter Head" +} \ No newline at end of file diff --git a/influxframework/printing/form_tour/print_format/print_format.json b/influxframework/printing/form_tour/print_format/print_format.json new file mode 100644 index 0000000..0f2b303 --- /dev/null +++ b/influxframework/printing/form_tour/print_format/print_format.json @@ -0,0 +1,94 @@ +{ + "creation": "2021-11-24 17:31:44.978996", + "docstatus": 0, + "doctype": "Form Tour", + "first_document": 0, + "idx": 0, + "include_name_field": 1, + "is_standard": 1, + "modified": "2021-11-24 17:58:00.807972", + "modified_by": "Administrator", + "module": "Printing", + "name": "Print Format", + "owner": "Administrator", + "reference_doctype": "Print Format", + "save_on_complete": 1, + "steps": [ + { + "description": "Select a Doctype for which you want to create a Print Format", + "field": "", + "fieldname": "doc_type", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "DocType", + "parent_field": "", + "position": "Right", + "title": "Doctype" + }, + { + "description": "You can modify the style of the Print Format from this section", + "field": "", + "fieldname": "section_break_9", + "fieldtype": "Section Break", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Style Settings", + "parent_field": "", + "position": "Top", + "title": "Style Settings" + }, + { + "description": "You can add custom css for your Print Format from this section", + "field": "", + "fieldname": "css", + "fieldtype": "Code", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Custom CSS", + "parent_field": "", + "position": "Top", + "title": "Custom CSS" + }, + { + "description": "Check this if you want to add custom Jinja Code or JavaScript to your Print Format", + "field": "", + "fieldname": "custom_format", + "fieldtype": "Check", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Custom Format", + "next_step_condition": "eval: doc.custom_format", + "parent_field": "", + "position": "Left", + "title": "Custom Format" + }, + { + "description": "Select the type of Print Format", + "field": "", + "fieldname": "print_format_type", + "fieldtype": "Select", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Print Format Type", + "next_step_condition": "eval: doc.custom_format", + "parent_field": "", + "position": "Right", + "title": "Print Format Type" + }, + { + "description": "Enter the code based on the Print Format Type you selected above", + "field": "", + "fieldname": "html", + "fieldtype": "Code", + "has_next_condition": 1, + "is_table_field": 0, + "label": "HTML", + "next_step_condition": "eval:doc.html", + "parent_field": "", + "position": "Right", + "title": "Code" + } + ], + "title": "Print Format" +} \ No newline at end of file diff --git a/influxframework/printing/page/__init__.py b/influxframework/printing/page/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/printing/page/print/__init__.py b/influxframework/printing/page/print/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/printing/page/print/print.js b/influxframework/printing/page/print/print.js new file mode 100644 index 0000000..91821d8 --- /dev/null +++ b/influxframework/printing/page/print/print.js @@ -0,0 +1,863 @@ +influxframework.pages["print"].on_page_load = function (wrapper) { + influxframework.ui.make_app_page({ + parent: wrapper, + }); + + let print_view = new influxframework.ui.form.PrintView(wrapper); + + $(wrapper).bind("show", () => { + const route = influxframework.get_route(); + const doctype = route[1]; + const docname = route.slice(2).join("/"); + if (!influxframework.route_options || !influxframework.route_options.frm) { + influxframework.model.with_doc(doctype, docname, () => { + let frm = { doctype: doctype, docname: docname }; + frm.doc = influxframework.get_doc(doctype, docname); + influxframework.model.with_doctype(doctype, () => { + frm.meta = influxframework.get_meta(route[1]); + print_view.show(frm); + }); + }); + } else { + print_view.frm = influxframework.route_options.frm.doctype + ? influxframework.route_options.frm + : influxframework.route_options.frm.frm; + influxframework.route_options.frm = null; + print_view.show(print_view.frm); + } + }); +}; + +influxframework.ui.form.PrintView = class { + constructor(wrapper) { + this.wrapper = $(wrapper); + this.page = wrapper.page; + this.make(); + } + + make() { + this.print_wrapper = this.page.main.empty().html( + ` +
      + +
      + ` + ); + + this.print_settings = influxframework.model.get_doc(":Print Settings", "Print Settings"); + this.setup_menu(); + this.setup_toolbar(); + this.setup_sidebar(); + this.setup_keyboard_shortcuts(); + } + + set_title() { + this.page.set_title(this.frm.docname); + } + + setup_toolbar() { + this.page.set_primary_action(__("Print"), () => this.printit(), "printer"); + + this.page.add_button(__("Full Page"), () => this.render_page("/printview?"), { + icon: "full-page", + }); + + this.page.add_button(__("PDF"), () => this.render_pdf(), { icon: "small-file" }); + + this.page.add_button(__("Refresh"), () => this.refresh_print_format(), { + icon: "refresh", + }); + + this.page.add_action_icon( + "file", + () => { + this.go_to_form_view(); + }, + "", + __("Form") + ); + } + + setup_sidebar() { + this.sidebar = this.page.sidebar.addClass("print-preview-sidebar"); + + this.print_sel = this.add_sidebar_item({ + fieldtype: "Select", + fieldname: "print_format", + label: "Print Format", + options: [this.get_default_option_for_select(__("Select Print Format"))], + change: () => this.refresh_print_format(), + default: __("Select Print Format"), + }).$input; + + this.language_sel = this.add_sidebar_item({ + fieldtype: "Select", + fieldname: "language", + placeholder: "Language", + options: [ + this.get_default_option_for_select(__("Select Language")), + ...this.get_language_options(), + ], + default: __("Select Language"), + change: () => { + this.set_user_lang(); + this.preview(); + }, + }).$input; + + this.letterhead_selector_df = this.add_sidebar_item({ + fieldtype: "Autocomplete", + fieldname: "letterhead", + label: __("Select Letterhead"), + placeholder: __("Select Letterhead"), + options: [__("No Letterhead")], + change: () => this.preview(), + default: this.print_settings.with_letterhead + ? __("No Letterhead") + : __("Select Letterhead"), + }); + this.letterhead_selector = this.letterhead_selector_df.$input; + this.sidebar_dynamic_section = $(`
      `).appendTo( + this.sidebar + ); + } + + add_sidebar_item(df, is_dynamic) { + if (df.fieldtype == "Select") { + df.input_class = "btn btn-default btn-sm text-left"; + } + + let field = influxframework.ui.form.make_control({ + df: df, + parent: is_dynamic ? this.sidebar_dynamic_section : this.sidebar, + render_input: 1, + }); + + if (df.default != null) { + field.set_input(df.default); + } + + return field; + } + + get_default_option_for_select(value) { + return { + label: value, + value: value, + disabled: true, + }; + } + + setup_menu() { + this.page.clear_menu(); + + this.page.add_menu_item(__("Print Settings"), () => { + influxframework.set_route("Form", "Print Settings"); + }); + + if (this.print_settings.enable_raw_printing == "1") { + this.page.add_menu_item(__("Raw Printing Setting"), () => { + this.printer_setting_dialog(); + }); + } + + if (influxframework.model.can_create("Print Format")) { + this.page.add_menu_item(__("Customize"), () => this.edit_print_format()); + } + + if (cint(this.print_settings.enable_print_server)) { + this.page.add_menu_item(__("Select Network Printer"), () => + this.network_printer_setting_dialog() + ); + } + } + + show(frm) { + this.frm = frm; + this.set_title(); + this.set_breadcrumbs(); + this.setup_customize_dialog(); + + // print format builder beta + this.page.add_inner_message(` + + ${__("Try the new Print Format Builder")} + + `); + + let tasks = [ + this.refresh_print_options, + this.set_default_print_language, + this.set_letterhead_options, + this.preview, + ].map((fn) => fn.bind(this)); + + this.setup_additional_settings(); + return influxframework.run_serially(tasks); + } + + set_breadcrumbs() { + influxframework.breadcrumbs.add(this.frm.meta.module, this.frm.doctype); + } + + setup_additional_settings() { + this.additional_settings = {}; + this.sidebar_dynamic_section.empty(); + influxframework + .xcall("influxframework.printing.page.print.print.get_print_settings_to_show", { + doctype: this.frm.doc.doctype, + docname: this.frm.doc.name, + }) + .then((settings) => this.add_settings_to_sidebar(settings)); + } + + add_settings_to_sidebar(settings) { + for (let df of settings) { + let field = this.add_sidebar_item( + { + ...df, + change: () => { + const val = field.get_value(); + this.additional_settings[field.df.fieldname] = val; + this.preview(); + }, + }, + true + ); + } + } + + edit_print_format() { + let print_format = this.get_print_format(); + let is_custom_format = + print_format.name && + (print_format.print_format_builder || print_format.print_format_builder_beta) && + print_format.standard === "No"; + let is_standard_but_editable = print_format.name && print_format.custom_format; + + if (is_standard_but_editable) { + influxframework.set_route("Form", "Print Format", print_format.name); + return; + } + if (is_custom_format) { + if (print_format.print_format_builder_beta) { + influxframework.set_route("print-format-builder-beta", print_format.name); + } else { + influxframework.set_route("print-format-builder", print_format.name); + } + return; + } + // start a new print format + influxframework.prompt( + [ + { + label: __("New Print Format Name"), + fieldname: "print_format_name", + fieldtype: "Data", + reqd: 1, + }, + { + label: __("Based On"), + fieldname: "based_on", + fieldtype: "Read Only", + default: print_format.name || "Standard", + }, + { + label: __("Use the new Print Format Builder"), + fieldname: "beta", + fieldtype: "Check", + }, + ], + (data) => { + influxframework.route_options = { + make_new: true, + doctype: this.frm.doctype, + name: data.print_format_name, + based_on: data.based_on, + beta: data.beta, + }; + influxframework.set_route("print-format-builder"); + this.print_sel.val(data.print_format_name); + }, + __("New Custom Print Format"), + __("Start") + ); + } + + refresh_print_format() { + this.set_default_print_language(); + this.toggle_raw_printing(); + this.preview(); + } + + // bind_events () { + // // // hide print view on pressing escape, only if there is no focus on any input + // // $(document).on("keydown", function (e) { + // // if (e.which === 27 && me.frm && e.target === document.body) { + // // me.hide(); + // // } + // // }); + // } + + setup_customize_dialog() { + let print_format = this.get_print_format(); + $(document).on("new-print-format", (e) => { + this.refresh_print_options(); + if (e.print_format) { + this.print_sel.val(e.print_format); + } + // start a new print format + influxframework.prompt( + [ + { + label: __("New Print Format Name"), + fieldname: "print_format_name", + fieldtype: "Data", + reqd: 1, + }, + { + label: __("Based On"), + fieldname: "based_on", + fieldtype: "Read Only", + default: print_format.name || "Standard", + }, + ], + (data) => { + influxframework.route_options = { + make_new: true, + doctype: this.frm.doctype, + name: data.print_format_name, + based_on: data.based_on, + }; + influxframework.set_route("print-format-builder"); + }, + __("New Custom Print Format"), + __("Start") + ); + }); + } + + setup_keyboard_shortcuts() { + this.wrapper.find(".print-toolbar a.btn-default").each((i, el) => { + influxframework.ui.keys.get_shortcut_group(this.frm.page).add($(el)); + }); + } + + set_letterhead_options() { + let letterhead_options = [__("No Letterhead")]; + let default_letterhead; + let doc_letterhead = this.frm.doc.letter_head; + + return influxframework.db + .get_list("Letter Head", { + filters: { disabled: 0 }, + fields: ["name", "is_default"], + limit: 0, + }) + .then((letterheads) => { + letterheads.map((letterhead) => { + if (letterhead.is_default) default_letterhead = letterhead.name; + return letterhead_options.push(letterhead.name); + }); + + this.letterhead_selector_df.set_data(letterhead_options); + let selected_letterhead = doc_letterhead || default_letterhead; + if (selected_letterhead) this.letterhead_selector.val(selected_letterhead); + }); + } + + set_user_lang() { + this.lang_code = this.language_sel.val(); + } + + get_language_options() { + return influxframework.get_languages(); + } + + set_default_print_language() { + let print_format = this.get_print_format(); + this.lang_code = + print_format.default_print_language || this.frm.doc.language || influxframework.boot.lang; + this.language_sel.val(this.lang_code); + } + + toggle_raw_printing() { + const is_raw_printing = this.is_raw_printing(); + this.wrapper.find(".btn-print-preview").toggle(!is_raw_printing); + this.wrapper.find(".btn-download-pdf").toggle(!is_raw_printing); + } + + preview() { + let print_format = this.get_print_format(); + if (print_format.print_format_builder_beta) { + this.print_wrapper.find(".print-preview-wrapper").hide(); + this.print_wrapper.find(".preview-beta-wrapper").show(); + this.preview_beta(); + return; + } + + this.print_wrapper.find(".preview-beta-wrapper").hide(); + this.print_wrapper.find(".print-preview-wrapper").show(); + + const $print_format = this.print_wrapper.find("iframe"); + this.$print_format_body = $print_format.contents(); + this.get_print_html((out) => { + if (!out.html) { + out.html = this.get_no_preview_html(); + } + + this.setup_print_format_dom(out, $print_format); + + const print_height = $print_format.get(0).offsetHeight; + const $message = this.wrapper.find(".page-break-message"); + + const print_height_inches = influxframework.dom.pixel_to_inches(print_height); + // if contents are large enough, indicate that it will get printed on multiple pages + // Maximum height for an A4 document is 11.69 inches + if (print_height_inches > 11.69) { + $message.text(__("This may get printed on multiple pages")); + } else { + $message.text(""); + } + }); + } + + preview_beta() { + let print_format = this.get_print_format(); + const iframe = this.print_wrapper.find(".preview-beta-wrapper iframe"); + let params = new URLSearchParams({ + doctype: this.frm.doc.doctype, + name: this.frm.doc.name, + print_format: print_format.name, + }); + let letterhead = this.get_letterhead(); + if (letterhead) { + params.append("letterhead", letterhead); + } + iframe.prop("src", `/printpreview?${params.toString()}`); + } + + setup_print_format_dom(out, $print_format) { + this.print_wrapper.find(".print-format-skeleton").remove(); + let base_url = influxframework.urllib.get_base_url(); + let print_css = influxframework.assets.bundled_asset( + "print.bundle.css", + influxframework.utils.is_rtl(this.lang_code) + ); + this.$print_format_body + .find("html") + .attr("dir", influxframework.utils.is_rtl(this.lang_code) ? "rtl" : "ltr"); + this.$print_format_body.find("html").attr("lang", this.lang_code); + this.$print_format_body.find("head").html( + ` + ` + ); + + this.$print_format_body + .find("body") + .html(``); + + this.show_footer(); + + this.$print_format_body.find(".print-format").css({ + display: "flex", + flexDirection: "column", + }); + + this.$print_format_body.find(".page-break").css({ + display: "flex", + "flex-direction": "column", + flex: "1", + }); + + setTimeout(() => { + $print_format.height(this.$print_format_body.find(".print-format").outerHeight()); + }, 500); + } + + hide() { + if (this.frm.setup_done && this.frm.page.current_view_name === "print") { + this.frm.page.set_view( + this.frm.page.previous_view_name === "print" + ? "main" + : this.frm.page.previous_view_name || "main" + ); + } + } + + go_to_form_view() { + influxframework.route_options = { + frm: this, + }; + influxframework.set_route("Form", this.frm.doctype, this.frm.docname); + } + + show_footer() { + // footer is hidden by default as reqd by pdf generation + // simple hack to show it in print preview + + this.$print_format_body.find("#footer-html").attr( + "style", + ` + display: block !important; + order: 1; + margin-top: auto; + padding-top: var(--padding-xl) + ` + ); + } + + printit() { + let me = this; + + if (cint(me.print_settings.enable_print_server)) { + if (localStorage.getItem("network_printer")) { + me.print_by_server(); + } else { + me.network_printer_setting_dialog(() => me.print_by_server()); + } + } else if (me.get_mapped_printer().length === 1) { + // printer is already mapped in localstorage (applies for both raw and pdf ) + if (me.is_raw_printing()) { + me.get_raw_commands(function (out) { + influxframework.ui.form + .qz_connect() + .then(function () { + let printer_map = me.get_mapped_printer()[0]; + let data = [out.raw_commands]; + let config = qz.configs.create(printer_map.printer); + return qz.print(config, data); + }) + .then(influxframework.ui.form.qz_success) + .catch((err) => { + influxframework.ui.form.qz_fail(err); + }); + }); + } else { + influxframework.show_alert( + { + message: __('PDF printing via "Raw Print" is not supported.'), + subtitle: __( + "Please remove the printer mapping in Printer Settings and try again." + ), + indicator: "info", + }, + 14 + ); + //Note: need to solve "Error: Cannot parse (FILE) as a PDF file" to enable qz pdf printing. + } + } else if (me.is_raw_printing()) { + // printer not mapped in localstorage and the current print format is raw printing + influxframework.show_alert( + { + message: __("Printer mapping not set."), + subtitle: __( + "Please set a printer mapping for this print format in the Printer Settings" + ), + indicator: "warning", + }, + 14 + ); + me.printer_setting_dialog(); + } else { + me.render_page("/printview?", true); + } + } + + print_by_server() { + let me = this; + if (localStorage.getItem("network_printer")) { + influxframework.call({ + method: "influxframework.utils.print_format.print_by_server", + args: { + doctype: me.frm.doc.doctype, + name: me.frm.doc.name, + printer_setting: localStorage.getItem("network_printer"), + print_format: me.selected_format(), + no_letterhead: me.with_letterhead(), + letterhead: me.get_letterhead(), + }, + callback: function () {}, + }); + } + } + network_printer_setting_dialog(callback) { + influxframework.call({ + method: "influxframework.printing.doctype.network_printer_settings.network_printer_settings.get_network_printer_settings", + callback: function (r) { + if (r.message) { + let d = new influxframework.ui.Dialog({ + title: __("Select Network Printer"), + fields: [ + { + label: "Printer", + fieldname: "printer", + fieldtype: "Select", + reqd: 1, + options: r.message, + }, + ], + primary_action: function () { + localStorage.setItem("network_printer", d.get_values().printer); + if (typeof callback == "function") { + callback(); + } + d.hide(); + }, + primary_action_label: __("Select"), + }); + d.show(); + } + }, + }); + } + + render_pdf() { + let print_format = this.get_print_format(); + if (print_format.print_format_builder_beta) { + let params = new URLSearchParams({ + doctype: this.frm.doc.doctype, + name: this.frm.doc.name, + print_format: print_format.name, + letterhead: this.get_letterhead(), + }); + let w = window.open(`/api/method/influxframework.utils.weasyprint.download_pdf?${params}`); + if (!w) { + influxframework.msgprint(__("Please enable pop-ups")); + return; + } + } else { + this.render_page("/api/method/influxframework.utils.print_format.download_pdf?"); + } + } + + render_page(method, printit = false) { + let w = window.open( + influxframework.urllib.get_full_url( + method + + "doctype=" + + encodeURIComponent(this.frm.doc.doctype) + + "&name=" + + encodeURIComponent(this.frm.doc.name) + + (printit ? "&trigger_print=1" : "") + + "&format=" + + encodeURIComponent(this.selected_format()) + + "&no_letterhead=" + + (this.with_letterhead() ? "0" : "1") + + "&letterhead=" + + encodeURIComponent(this.get_letterhead()) + + "&settings=" + + encodeURIComponent(JSON.stringify(this.additional_settings)) + + (this.lang_code ? "&_lang=" + this.lang_code : "") + ) + ); + if (!w) { + influxframework.msgprint(__("Please enable pop-ups")); + return; + } + } + + get_print_html(callback) { + let print_format = this.get_print_format(); + if (print_format.raw_printing) { + callback({ + html: this.get_no_preview_html(), + }); + return; + } + if (this._req) { + this._req.abort(); + } + this._req = influxframework.call({ + method: "influxframework.www.printview.get_html_and_style", + args: { + doc: this.frm.doc, + print_format: this.selected_format(), + no_letterhead: !this.with_letterhead() ? 1 : 0, + letterhead: this.get_letterhead(), + settings: this.additional_settings, + _lang: this.lang_code, + }, + callback: function (r) { + if (!r.exc) { + callback(r.message); + } + }, + }); + } + + get_letterhead() { + return this.letterhead_selector.val(); + } + + get_no_preview_html() { + return `
      + ${__("No Preview Available")} +
      `; + } + + get_raw_commands(callback) { + // fetches rendered raw commands from the server for the current print format. + influxframework.call({ + method: "influxframework.www.printview.get_rendered_raw_commands", + args: { + doc: this.frm.doc, + print_format: this.selected_format(), + _lang: this.lang_code, + }, + callback: function (r) { + if (!r.exc) { + callback(r.message); + } + }, + }); + } + + get_mapped_printer() { + // returns a list of "print format: printer" mapping filtered by the current print format + let print_format_printer_map = this.get_print_format_printer_map(); + if (print_format_printer_map[this.frm.doctype]) { + return print_format_printer_map[this.frm.doctype].filter( + (printer_map) => printer_map.print_format == this.selected_format() + ); + } else { + return []; + } + } + + get_print_format_printer_map() { + // returns the whole object "print_format_printer_map" stored in the localStorage. + try { + let print_format_printer_map = JSON.parse(localStorage.print_format_printer_map); + return print_format_printer_map; + } catch (e) { + return {}; + } + } + + refresh_print_options() { + this.print_formats = influxframework.meta.get_print_formats(this.frm.doctype); + const print_format_select_val = this.print_sel.val(); + this.print_sel + .empty() + .add_options([ + this.get_default_option_for_select(__("Select Print Format")), + ...this.print_formats, + ]); + return ( + this.print_formats.includes(print_format_select_val) && + this.print_sel.val(print_format_select_val) + ); + } + + selected_format() { + return this.print_sel.val() || this.frm.meta.default_print_format || "Standard"; + } + + is_raw_printing(format) { + return this.get_print_format(format).raw_printing === 1; + } + + get_print_format(format) { + let print_format = {}; + if (!format) { + format = this.selected_format(); + } + + if (locals["Print Format"] && locals["Print Format"][format]) { + print_format = locals["Print Format"][format]; + } + + return print_format; + } + + with_letterhead() { + return cint(this.get_letterhead() !== __("No Letterhead")); + } + + set_style(style) { + influxframework.dom.set_style(style || influxframework.boot.print_css, "print-style"); + } + + printer_setting_dialog() { + // dialog for the Printer Settings + this.print_format_printer_map = this.get_print_format_printer_map(); + this.data = this.print_format_printer_map[this.frm.doctype] || []; + this.printer_list = []; + influxframework.ui.form.qz_get_printer_list().then((data) => { + this.printer_list = data; + const dialog = new influxframework.ui.Dialog({ + title: __("Printer Settings"), + fields: [ + { + fieldtype: "Section Break", + }, + { + fieldname: "printer_mapping", + fieldtype: "Table", + label: __("Printer Mapping"), + in_place_edit: true, + data: this.data, + get_data: () => { + return this.data; + }, + fields: [ + { + fieldtype: "Select", + fieldname: "print_format", + default: 0, + options: this.print_formats, + read_only: 0, + in_list_view: 1, + label: __("Print Format"), + }, + { + fieldtype: "Select", + fieldname: "printer", + default: 0, + options: this.printer_list, + read_only: 0, + in_list_view: 1, + label: __("Printer"), + }, + ], + }, + ], + primary_action: () => { + let printer_mapping = dialog.get_values()["printer_mapping"]; + if (printer_mapping && printer_mapping.length) { + let print_format_list = printer_mapping.map((a) => a.print_format); + let has_duplicate = print_format_list.some( + (item, idx) => print_format_list.indexOf(item) != idx + ); + if (has_duplicate) + influxframework.throw( + __( + "Cannot have multiple printers mapped to a single print format." + ) + ); + } else { + printer_mapping = []; + } + dialog.print_format_printer_map = this.get_print_format_printer_map(); + dialog.print_format_printer_map[this.frm.doctype] = printer_mapping; + localStorage.print_format_printer_map = JSON.stringify( + dialog.print_format_printer_map + ); + dialog.hide(); + }, + primary_action_label: __("Save"), + }); + dialog.show(); + if (!(this.printer_list && this.printer_list.length)) { + influxframework.throw(__("No Printer is Available.")); + } + }); + } +}; diff --git a/influxframework/printing/page/print/print.json b/influxframework/printing/page/print/print.json new file mode 100644 index 0000000..bea659c --- /dev/null +++ b/influxframework/printing/page/print/print.json @@ -0,0 +1,18 @@ +{ + "content": null, + "creation": "2020-10-09 17:23:15.163030", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2020-10-09 17:23:15.163030", + "modified_by": "Administrator", + "module": "Printing", + "name": "print", + "owner": "Administrator", + "page_name": "Print", + "roles": [], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 0 +} \ No newline at end of file diff --git a/influxframework/printing/page/print/print.py b/influxframework/printing/page/print/print.py new file mode 100644 index 0000000..8247b82 --- /dev/null +++ b/influxframework/printing/page/print/print.py @@ -0,0 +1,22 @@ +import influxframework + + +@influxframework.whitelist() +def get_print_settings_to_show(doctype, docname): + doc = influxframework.get_doc(doctype, docname) + print_settings = influxframework.get_single("Print Settings") + + if hasattr(doc, "get_print_settings"): + fields = doc.get_print_settings() or [] + else: + return [] + + print_settings_fields = [] + for fieldname in fields: + df = print_settings.meta.get_field(fieldname) + if not df: + continue + df.default = print_settings.get(fieldname) + print_settings_fields.append(df) + + return print_settings_fields diff --git a/influxframework/printing/page/print/print_skeleton_loading.html b/influxframework/printing/page/print/print_skeleton_loading.html new file mode 100644 index 0000000..c1e6a0d --- /dev/null +++ b/influxframework/printing/page/print/print_skeleton_loading.html @@ -0,0 +1,164 @@ + diff --git a/influxframework/printing/page/print_format_builder/__init__.py b/influxframework/printing/page/print_format_builder/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/printing/page/print_format_builder/print_format_builder.css b/influxframework/printing/page/print_format_builder/print_format_builder.css new file mode 100644 index 0000000..0f77965 --- /dev/null +++ b/influxframework/printing/page/print_format_builder/print_format_builder.css @@ -0,0 +1,160 @@ +.print-format-builder-section { + border: 1px solid var(--dark-border-color); + border-radius: var(--border-radius); + margin: 0px; + margin-bottom: var(--margin-md); +} + +.print-format-builder-add-section, .print-format-builder-header { + border: 1px dashed var(--dark-border-color); + border-radius: var(--border-radius); + padding: var(--padding-sm); + width: 100%; + display: inline-block; + background: var(--bg-color); + cursor: pointer; +} + +.print-format-builder-header-edit { + margin-bottom: var(--margin-sm); +} + +.print-format-builder-header { + margin-bottom: var(--margin-md); +} + +.print-format-builder-add-section { + color: var(--text-light); + align-items: center; + padding: var(--padding-lg); + display: flex; + justify-content: center; +} + +.print-format-builder-add-section .icon { + margin-right: var(--margin-sm); +} + +.print-format-builder-column { + border-radius: var(--border-radius); +} + +.print-format-builder-section .section-column { + padding: var(--padding-xs) 0 var(--padding-md) var(--padding-md); +} + +.print-format-builder-section .section-column:last-child { + padding-right: var(--padding-md); +} + +.print-format-builder-field { + padding: 8px; + width: 100%; + display: inline-block; + border-radius: var(--border-radius); + background: var(--bg-light-gray); + margin-bottom: var(--margin-sm); + font-size: var(--text-md); + color: var(--text-color); +} + +.print-format-builder-field:last-child { + margin-bottom: 0; +} + +.print-format-builder-field .field-label { + vertical-align: middle; +} + +.print-format-builder-column .print-format-builder-field { + cursor: move; +} + +.print-format-builder-section-head .section-label { + font-size: var(--text-lg); + color: var(--text-color); + font-weight: 500; + vertical-align: middle; + margin-left: var(--margin-sm); +} + +.print-format-builder-section-head { + cursor: move; + padding: var(--padding-md) calc(var(--padding-md) + 8px) + var(--padding-sm) calc(var(--padding-md) + 8px); +} + +.column-selector-row { + margin-bottom: var(--margin-xs); + padding: var(--padding-xs) 0; + cursor: grab; +} + +.column-selector-row:hover { + background-color: var(--highlight-color); +} + +.column-selector-row .drag-handle { + margin-right: var(--margin-sm); +} + +.print-format-builder-field .drag-handle { + margin-right: var(--margin-sm); +} + +.print-format-builder-sidebar .sidebar-field { + width: 100%; + padding: 4px 8px; + color: var(--text-on-light-gray); + /* color: var(--text-light); */ + text-align: left; + cursor: grab; +} + +.print-format-builder-sidebar .sidebar-custom-field { + background-color: var(--gray-300); +} + +.print-format-builder-sidebar { + top: calc(var(--navbar-height) + 70px); + position: sticky; +} + +.print-format-builder-sidebar-fields { + padding: var(--padding-xs); + overflow-y: auto; + height: 75vh; +} + +.print-format-builder-field-placeholder { + margin-bottom: var(--margin-sm); +} + +.print-format-builder-field-placeholder:last-child { + margin-bottom: 0; +} + +.print-format-builder-field-placeholder .drag-handle { + margin-right: var(--margin-sm); +} + +.filter-searchbox { + padding: 0 var(--padding-xs); + margin-bottom: var(--margin-sm); +} + +.filter-searchbox input { + background-color: var(--control-bg-on-gray); +} + +.print-format-builder-main { + display: inline-block; + vertical-align: top; + border-top: 0px; + padding: var(--padding-lg); +} + +.print-format-help-message { + font-size: var(--text-md); + margin-bottom: var(--margin-md); +} diff --git a/influxframework/printing/page/print_format_builder/print_format_builder.js b/influxframework/printing/page/print_format_builder/print_format_builder.js new file mode 100644 index 0000000..87556c4 --- /dev/null +++ b/influxframework/printing/page/print_format_builder/print_format_builder.js @@ -0,0 +1,851 @@ +influxframework.pages["print-format-builder"].on_page_load = function (wrapper) { + influxframework.print_format_builder = new influxframework.PrintFormatBuilder(wrapper); + influxframework.breadcrumbs.add("Setup", "Print Format"); +}; + +influxframework.pages["print-format-builder"].on_page_show = function (wrapper) { + var route = influxframework.get_route(); + if (route.length > 1) { + influxframework.model.with_doc("Print Format", route[1], function () { + influxframework.print_format_builder.print_format = influxframework.get_doc("Print Format", route[1]); + influxframework.print_format_builder.refresh(); + }); + } else if (influxframework.route_options) { + if (influxframework.route_options.make_new) { + let { doctype, name, based_on, beta } = influxframework.route_options; + influxframework.route_options = null; + influxframework.print_format_builder.setup_new_print_format(doctype, name, based_on, beta); + } else { + influxframework.print_format_builder.print_format = influxframework.route_options.doc; + influxframework.route_options = null; + influxframework.print_format_builder.refresh(); + } + } +}; + +influxframework.PrintFormatBuilder = class PrintFormatBuilder { + constructor(parent) { + this.parent = parent; + this.make(); + this.refresh(); + } + refresh() { + this.custom_html_count = 0; + if (!this.print_format) { + this.show_start(); + } else { + this.page.set_title(this.print_format.name); + this.setup_print_format(); + } + } + make() { + this.page = influxframework.ui.make_app_page({ + parent: this.parent, + title: __("Print Format Builder"), + }); + + this.page.main.css({ "border-color": "transparent" }); + + this.page.sidebar = $('').appendTo( + this.page.sidebar + ); + this.page.main = $( + '' + ).appendTo(this.page.main); + + // future-bindings for buttons on sections / fields + // bind only once + this.setup_section_settings(); + this.setup_column_selector(); + this.setup_edit_custom_html(); + // $(this.page.sidebar).css({"position": 'fixed'}); + // $(this.page.main).parent().css({"margin-left": '16.67%'}); + } + show_start() { + this.page.main.html(influxframework.render_template("print_format_builder_start", {})); + this.page.clear_actions(); + this.page.set_title(__("Print Format Builder")); + this.start_edit_print_format(); + this.start_new_print_format(); + } + start_edit_print_format() { + // print format control + var me = this; + this.print_format_input = influxframework.ui.form.make_control({ + parent: this.page.main.find(".print-format-selector"), + df: { + fieldtype: "Link", + options: "Print Format", + filters: { + print_format_builder: 1, + }, + label: __("Select Print Format to Edit"), + only_select: true, + }, + render_input: true, + }); + + // create a new print format. + this.page.main.find(".btn-edit-print-format").on("click", function () { + var name = me.print_format_input.get_value(); + if (!name) return; + influxframework.model.with_doc("Print Format", name, function (doc) { + influxframework.set_route("print-format-builder", name); + }); + }); + } + start_new_print_format() { + var me = this; + this.doctype_input = influxframework.ui.form.make_control({ + parent: this.page.main.find(".doctype-selector"), + df: { + fieldtype: "Link", + options: "DocType", + filters: { + istable: 0, + issingle: 0, + }, + label: __("Select a DocType to make a new format"), + }, + render_input: true, + }); + + this.name_input = influxframework.ui.form.make_control({ + parent: this.page.main.find(".name-selector"), + df: { + fieldtype: "Data", + label: __("Name of the new Print Format"), + }, + render_input: true, + }); + + this.page.main.find(".btn-new-print-format").on("click", function () { + var doctype = me.doctype_input.get_value(), + name = me.name_input.get_value(); + if (!(doctype && name)) { + influxframework.msgprint(__("Both DocType and Name required")); + return; + } + me.setup_new_print_format(doctype, name); + }); + } + setup_new_print_format(doctype, name, based_on, beta) { + influxframework.call({ + method: "influxframework.printing.page.print_format_builder.print_format_builder.create_custom_format", + args: { + doctype: doctype, + name: name, + based_on: based_on, + beta: Boolean(beta), + }, + callback: (r) => { + if (r.message) { + let print_format = r.message; + if (print_format.print_format_builder_beta) { + influxframework.set_route("print-format-builder-beta", print_format.name); + } else { + this.print_format = print_format; + this.refresh(); + } + } + }, + }); + } + setup_print_format() { + var me = this; + influxframework.model.with_doctype(this.print_format.doc_type, function (doctype) { + me.meta = influxframework.get_meta(me.print_format.doc_type); + me.setup_sidebar(); + me.render_layout(); + me.page.set_primary_action(__("Save"), function () { + me.save_print_format(); + }); + me.page.clear_menu(); + me.page.add_menu_item( + __("Start new Format"), + function () { + me.print_format = null; + me.refresh(); + }, + true + ); + me.page.clear_inner_toolbar(); + me.page.add_inner_button(__("Edit Properties"), function () { + influxframework.set_route("Form", "Print Format", me.print_format.name); + }); + }); + } + setup_sidebar() { + // prepend custom HTML field + var fields = [this.get_custom_html_field()].concat(this.meta.fields); + this.page.sidebar.html( + $(influxframework.render_template("print_format_builder_sidebar", { fields: fields })) + ); + this.setup_field_filter(); + } + get_custom_html_field() { + return { + fieldtype: "Custom HTML", + fieldname: "_custom_html", + label: __("Custom HTML"), + }; + } + render_layout() { + this.page.main.empty(); + this.prepare_data(); + $( + influxframework.render_template("print_format_builder_layout", { + data: this.layout_data, + me: this, + }) + ).appendTo(this.page.main); + this.setup_sortable(); + this.setup_add_section(); + this.setup_edit_heading(); + this.setup_field_settings(); + this.setup_html_data(); + } + prepare_data() { + this.print_heading_template = null; + this.data = JSON.parse(this.print_format.format_data || "[]"); + if (!this.data.length) { + // new layout + this.data = this.meta.fields; + } else { + // extract print_heading_template if found + if (this.data[0].fieldname === "print_heading_template") { + this.print_heading_template = this.data[0].options; + this.data = this.data.splice(1); + } + } + + if (!this.print_heading_template) { + // default print heading template + this.print_heading_template = + ''; + } + + this.layout_data = []; + this.fields_dict = {}; + this.custom_html_dict = {}; + var section = null, + column = null, + me = this, + custom_html_count = 0; + + // create a new placeholder for column and set + // it as "column" + var set_column = function () { + if (!section) set_section(); + column = me.get_new_column(); + section.columns.push(column); + section.no_of_columns += 1; + }; + + var set_section = function (label) { + section = me.get_new_section(); + if (label) section.label = label; + column = null; + me.layout_data.push(section); + }; + + // break the layout into sections and columns + // so that it is easier to render in a template + $.each(this.data, function (i, f) { + me.fields_dict[f.fieldname] = f; + if (!f.name && f.fieldname) { + // from format_data (designed format) + // print_hide should always be false + if (f.fieldname === "_custom_html") { + f.label = "Custom HTML"; + f.fieldtype = "Custom HTML"; + + // set custom html id to map data properties later + custom_html_count++; + f.custom_html_id = custom_html_count; + me.custom_html_dict[f.custom_html_id] = f; + } else { + f = $.extend( + influxframework.meta.get_docfield(me.print_format.doc_type, f.fieldname) || {}, + f + ); + } + } + + if (f.fieldtype === "Section Break") { + set_section(f.label); + } else if (f.fieldtype === "Column Break") { + set_column(); + } else if ( + !in_list(["Section Break", "Column Break", "Tab Break", "Fold"], f.fieldtype) && + f.label + ) { + if (!column) set_column(); + + if (f.fieldtype === "Table") { + me.add_table_properties(f); + } + + if (!f.print_hide) { + column.fields.push(f); + section.has_fields = true; + } + } + }); + + // strip out empty sections + this.layout_data = $.map(this.layout_data, function (s) { + return s.has_fields ? s : null; + }); + } + get_new_section() { + return { columns: [], no_of_columns: 0, label: "" }; + } + get_new_column() { + return { fields: [] }; + } + add_table_properties(f) { + // build table columns and widths in a dict + // visible_columns + var me = this; + if (!f.visible_columns) { + me.init_visible_columns(f); + } + } + init_visible_columns(f) { + f.visible_columns = []; + $.each(influxframework.get_meta(f.options).fields, function (i, _f) { + if ( + !in_list(["Section Break", "Column Break", "Tab Break"], _f.fieldtype) && + !_f.print_hide && + f.label + ) { + // column names set as fieldname|width + f.visible_columns.push({ + fieldname: _f.fieldname, + print_width: _f.width || "", + print_hide: 0, + }); + } + }); + } + setup_sortable() { + var me = this; + + // drag from fields library + Sortable.create(this.page.sidebar.find(".print-format-builder-sidebar-fields").get(0), { + group: { + name: "field", + put: true, + pull: "clone", + }, + sort: false, + onAdd: function (evt) { + // on drop, trash! + $(evt.item).fadeOut(); + }, + }); + + // sort, drag and drop between columns + this.page.main.find(".print-format-builder-column").each(function () { + me.setup_sortable_for_column(this); + }); + + // section sorting + Sortable.create(this.page.main.find(".print-format-builder-layout").get(0), { + handle: ".print-format-builder-section-head", + }); + } + setup_sortable_for_column(col) { + var me = this; + Sortable.create(col, { + group: { + name: "field", + put: true, + pull: true, + }, + onAdd: function (evt) { + // on drop, change the HTML + + var $item = $(evt.item); + if (!$item.hasClass("print-format-builder-field")) { + var fieldname = $item.attr("data-fieldname"); + + if (fieldname === "_custom_html") { + var field = me.get_custom_html_field(); + } else { + var field = influxframework.meta.get_docfield(me.print_format.doc_type, fieldname); + } + + var html = influxframework.render_template("print_format_builder_field", { + field: field, + me: me, + }); + + $item.replaceWith(html); + } + }, + }); + } + setup_field_filter() { + var me = this; + this.page.sidebar.find(".filter-fields").on("keyup", function () { + var text = $(this).val(); + me.page.sidebar.find(".field-label").each(function () { + var show = + !text || $(this).text().toLowerCase().indexOf(text.toLowerCase()) !== -1; + $(this).parent().toggle(show); + }); + }); + } + setup_section_settings() { + var me = this; + this.page.main.on("click", ".section-settings", function () { + var section = $(this).parent().parent(); + var no_of_columns = section.find(".section-column").length; + var label = section.attr("data-label"); + + // new dialog + var d = new influxframework.ui.Dialog({ + title: "Edit Section", + fields: [ + { + label: __("No of Columns"), + fieldname: "no_of_columns", + fieldtype: "Select", + options: ["1", "2", "3", "4"], + }, + { + label: __("Section Heading"), + fieldname: "label", + fieldtype: "Data", + description: __("Will only be shown if section headings are enabled"), + }, + { + label: __("Remove Section"), + fieldname: "remove_section", + fieldtype: "Button", + click: function () { + d.hide(); + section.fadeOut(function () { + section.remove(); + }); + }, + input_class: "btn-danger", + input_css: { + "margin-top": "20px", + }, + }, + ], + }); + + d.set_input("no_of_columns", no_of_columns + ""); + d.set_input("label", label || ""); + + d.set_primary_action(__("Update"), function () { + // resize number of columns + me.update_columns_in_section( + section, + no_of_columns, + cint(d.get_value("no_of_columns")) + ); + + section.attr("data-label", d.get_value("label") || ""); + section.find(".section-label").html(d.get_value("label") || ""); + + d.hide(); + }); + + d.show(); + + return false; + }); + } + setup_field_settings() { + this.page.main.find(".field-settings").on("click", (e) => { + const field = $(e.currentTarget).parent(); + // new dialog + var d = new influxframework.ui.Dialog({ + title: __("Set Properties"), + fields: [ + { + label: __("Label"), + fieldname: "label", + fieldtype: "Data", + }, + { + label: __("Align Value"), + fieldname: "align", + fieldtype: "Select", + options: [ + { label: __("Left", null, "alignment"), value: "left" }, + { label: __("Right", null, "alignment"), value: "right" }, + ], + }, + { + label: __("Remove Field"), + fieldtype: "Button", + click: function () { + d.hide(); + field.remove(); + }, + input_class: "btn-danger", + }, + ], + }); + + d.set_value("label", field.attr("data-label")); + + d.set_primary_action(__("Update"), function () { + field.attr("data-align", d.get_value("align")); + field.attr("data-label", d.get_value("label")); + field.find(".field-label").html(d.get_value("label")); + d.hide(); + }); + + // set current value + if (field.attr("data-align")) { + d.set_value("align", field.attr("data-align")); + } else { + d.set_value("align", "left"); + } + + d.show(); + + return false; + }); + } + setup_html_data() { + // set JQuery `data` for Custom HTML fields, since editing the HTML + // directly causes problem becuase of HTML reformatting + // + // this is based on a dummy attribute custom_html_id, since all custom html + // fields have the same fieldname `_custom_html` + var me = this; + this.page.main.find('[data-fieldtype="Custom HTML"]').each(function () { + var fieldname = $(this).attr("data-fieldname"); + var content = $($(this).find(".html-content")[0]); + var html = me.custom_html_dict[parseInt(content.attr("data-custom-html-id"))].options; + content.data("content", html); + }); + } + update_columns_in_section(section, no_of_columns, new_no_of_columns) { + var col_size = 12 / new_no_of_columns, + me = this, + resize = function () { + section + .find(".section-column") + .removeClass() + .addClass("section-column") + .addClass("col-md-" + col_size); + }; + + if (new_no_of_columns < no_of_columns) { + // move contents of last n columns to previous column + for (var i = no_of_columns; i > new_no_of_columns; i--) { + var $col = $(section.find(".print-format-builder-column").get(i - 1)); + var prev = section.find(".print-format-builder-column").get(i - 2); + + // append each field to prev + $col.parent().addClass("to-drop"); + $col.find(".print-format-builder-field").each(function () { + $(this).appendTo(prev); + }); + } + + // drop columns + section.find(".to-drop").remove(); + + // resize + resize(); + } else if (new_no_of_columns > no_of_columns) { + // add empty column and resize old columns + for (var i = no_of_columns; i < new_no_of_columns; i++) { + var col = $( + '
      \ +
      ' + ).appendTo(section); + me.setup_sortable_for_column(col.find(".print-format-builder-column").get(0)); + } + // resize + resize(); + } + } + setup_add_section() { + var me = this; + this.page.main.find(".print-format-builder-add-section").on("click", function () { + // boostrap new section info + var section = me.get_new_section(); + section.columns.push(me.get_new_column()); + section.no_of_columns = 1; + + var $section = $( + influxframework.render_template("print_format_builder_section", { + section: section, + me: me, + }) + ).appendTo(me.page.main.find(".print-format-builder-layout")); + + me.setup_sortable_for_column($section.find(".print-format-builder-column").get(0)); + }); + } + setup_edit_heading() { + var me = this; + var $heading = this.page.main.find(".print-format-builder-print-heading"); + + // set content property + $heading.data("content", this.print_heading_template); + + this.page.main.find(".edit-heading").on("click", function () { + var d = me.get_edit_html_dialog(__("Edit Heading"), __("Heading"), $heading); + }); + } + setup_column_selector() { + var me = this; + this.page.main.on("click", ".select-columns", function () { + var parent = $(this).parents(".print-format-builder-field:first"), + doctype = parent.attr("data-doctype"), + label = parent.attr("data-label"), + columns = parent.attr("data-columns").split(","), + column_names = $.map(columns, function (v) { + return v.split("|")[0]; + }), + widths = {}; + + $.each(columns, function (i, v) { + var parts = v.split("|"); + widths[parts[0]] = parts[1] || ""; + }); + + var d = new influxframework.ui.Dialog({ + title: __("Select Table Columns for {0}", [label]), + }); + + var $body = $(d.body); + + var doc_fields = influxframework.get_meta(doctype).fields; + var docfields_by_name = {}; + + // docfields by fieldname + $.each(doc_fields, function (j, f) { + if (f) docfields_by_name[f.fieldname] = f; + }); + + // add field which are in column_names first to preserve order + var fields = []; + $.each(column_names, function (i, v) { + if (in_list(Object.keys(docfields_by_name), v)) { + fields.push(docfields_by_name[v]); + } + }); + // add remaining fields + $.each(doc_fields, function (j, f) { + if ( + f && + !in_list(column_names, f.fieldname) && + !in_list(["Section Break", "Column Break", "Tab Break"], f.fieldtype) && + f.label + ) { + fields.push(f); + } + }); + // render checkboxes + $( + influxframework.render_template("print_format_builder_column_selector", { + fields: fields, + column_names: column_names, + widths: widths, + }) + ).appendTo(d.body); + + Sortable.create($body.find(".column-selector-list").get(0)); + + var get_width_input = function (fieldname) { + return $body.find(".column-width[data-fieldname='" + fieldname + "']"); + }; + + // update data-columns property on update + d.set_primary_action(__("Update"), function () { + var visible_columns = []; + $body.find("input:checked").each(function () { + var fieldname = $(this).attr("data-fieldname"), + width = get_width_input(fieldname).val() || ""; + visible_columns.push(fieldname + "|" + width); + }); + parent.attr("data-columns", visible_columns.join(",")); + d.hide(); + }); + + let update_column_count_message = () => { + // show a warning if user selects more than 10 columns for a table + let columns_count = $body.find("input:checked").length; + $body.find(".help-message").toggle(columns_count > 10); + }; + update_column_count_message(); + + // enable / disable input based on selection + $body.on("click", "input[type='checkbox']", function () { + var disabled = !$(this).prop("checked"), + input = get_width_input($(this).attr("data-fieldname")); + + input.prop("disabled", disabled); + if (disabled) input.val(""); + + update_column_count_message(); + }); + + d.show(); + + return false; + }); + } + get_visible_columns_string(f) { + if (!f.visible_columns) { + this.init_visible_columns(f); + } + return $.map(f.visible_columns, function (v) { + return v.fieldname + "|" + (v.print_width || ""); + }).join(","); + } + get_no_content() { + return __("Edit to add content"); + } + setup_edit_custom_html() { + var me = this; + this.page.main.on("click", ".edit-html", function () { + me.get_edit_html_dialog( + __("Edit Custom HTML"), + __("Custom HTML"), + $(this).parents(".print-format-builder-field:first").find(".html-content") + ); + }); + } + get_edit_html_dialog(title, label, $content) { + var me = this; + var d = new influxframework.ui.Dialog({ + title: title, + fields: [ + { + fieldname: "content", + fieldtype: "Code", + label: label, + options: "HTML", + }, + { + fieldname: "help", + fieldtype: "HTML", + options: + "

      " + + __( + "You can add dynamic properties from the document by using Jinja templating." + ) + + __("For example: If you want to include the document ID, use {0}", [ + "{{ doc.name }}", + ]) + + "

      ", + }, + ], + }); + + // set existing content in input + var content = $content.data("content") || ""; + if (content.indexOf(me.get_no_content()) !== -1) content = ""; + d.set_input("content", content); + + d.set_primary_action(__("Update"), function () { + $($content[0]).data("content", d.get_value("content")); + $content.html(d.get_value("content")); + d.hide(); + }); + + d.show(); + + return d; + } + save_print_format() { + var data = [], + me = this; + + // add print heading as the first field + // this will be removed and set as a doc property + // before rendering + data.push({ + fieldname: "print_heading_template", + fieldtype: "Custom HTML", + options: this.page.main.find(".print-format-builder-print-heading").data("content"), + }); + + // add pages + this.page.main.find(".print-format-builder-section").each(function () { + var section = { fieldtype: "Section Break", label: $(this).attr("data-label") || "" }; + data.push(section); + $(this) + .find(".print-format-builder-column") + .each(function () { + data.push({ fieldtype: "Column Break" }); + $(this) + .find(".print-format-builder-field") + .each(function () { + var $this = $(this), + fieldtype = $this.attr("data-fieldtype"), + align = $this.attr("data-align"), + label = $this.attr("data-label"), + df = { + fieldname: $this.attr("data-fieldname"), + print_hide: 0, + }; + + if (align) { + df.align = align; + } + + if (label) { + df.label = label; + } + + if (fieldtype === "Table") { + // append the user selected columns to visible_columns + var columns = $this.attr("data-columns").split(","); + df.visible_columns = []; + $.each(columns, function (i, c) { + var parts = c.split("|"); + df.visible_columns.push({ + fieldname: parts[0], + print_width: parts[1], + print_hide: 0, + }); + }); + } + if (fieldtype === "Custom HTML") { + // custom html as HTML field + df.fieldtype = "HTML"; + df.options = $($this.find(".html-content")[0]).data("content"); + } + data.push(df); + }); + }); + }); + + // save format_data + influxframework.call({ + method: "influxframework.client.set_value", + args: { + doctype: "Print Format", + name: this.print_format.name, + fieldname: "format_data", + value: JSON.stringify(data), + }, + freeze: true, + btn: this.page.btn_primary, + callback: function (r) { + me.print_format = r.message; + locals["Print Format"][me.print_format.name] = r.message; + influxframework.show_alert({ message: __("Saved"), indicator: "green" }); + }, + }); + } +}; diff --git a/influxframework/printing/page/print_format_builder/print_format_builder.json b/influxframework/printing/page/print_format_builder/print_format_builder.json new file mode 100644 index 0000000..81cb213 --- /dev/null +++ b/influxframework/printing/page/print_format_builder/print_format_builder.json @@ -0,0 +1,23 @@ +{ + "content": null, + "creation": "2015-01-27 04:35:43.872918", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2017-05-03 05:59:33.702308", + "modified_by": "Administrator", + "module": "Printing", + "name": "print-format-builder", + "owner": "Administrator", + "page_name": "print-format-builder", + "roles": [ + { + "role": "System Manager" + } + ], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 0, + "title": "Print Format Builder" +} \ No newline at end of file diff --git a/influxframework/printing/page/print_format_builder/print_format_builder.py b/influxframework/printing/page/print_format_builder/print_format_builder.py new file mode 100644 index 0000000..35d45f5 --- /dev/null +++ b/influxframework/printing/page/print_format_builder/print_format_builder.py @@ -0,0 +1,19 @@ +import influxframework + + +@influxframework.whitelist() +def create_custom_format(doctype, name, based_on="Standard", beta=False): + doc = influxframework.new_doc("Print Format") + doc.doc_type = doctype + doc.name = name + beta = influxframework.parse_json(beta) + + if beta: + doc.print_format_builder_beta = 1 + else: + doc.print_format_builder = 1 + doc.format_data = ( + influxframework.db.get_value("Print Format", based_on, "format_data") if based_on != "Standard" else None + ) + doc.insert() + return doc diff --git a/influxframework/printing/page/print_format_builder/print_format_builder_column_selector.html b/influxframework/printing/page/print_format_builder/print_format_builder_column_selector.html new file mode 100644 index 0000000..495b837 --- /dev/null +++ b/influxframework/printing/page/print_format_builder/print_format_builder_column_selector.html @@ -0,0 +1,36 @@ +

      {{ __("Check columns to select, drag to set order.") }} + {{ __("Widths can be set in px or %.") }}

      +

      + {{ __("Some columns might get cut off when printing to PDF. Try to keep number of columns under 10.") }} +

      +
      +

      {{ __("Column") }}

      +

      {{ __("Width") }}

      +
      +
      + {% for (i=0; i < fields.length; i++) { var f = fields[i]; %} + {% var selected = in_list(column_names, f.fieldname) %} +
      +
      +
      + +
      +
      + +
      +
      +
      + +
      +
      + {% } %} +
      diff --git a/influxframework/printing/page/print_format_builder/print_format_builder_field.html b/influxframework/printing/page/print_format_builder/print_format_builder_field.html new file mode 100644 index 0000000..beb9de2 --- /dev/null +++ b/influxframework/printing/page/print_format_builder/print_format_builder_field.html @@ -0,0 +1,46 @@ + diff --git a/influxframework/printing/page/print_format_builder/print_format_builder_layout.html b/influxframework/printing/page/print_format_builder/print_format_builder_layout.html new file mode 100644 index 0000000..0ca145c --- /dev/null +++ b/influxframework/printing/page/print_format_builder/print_format_builder_layout.html @@ -0,0 +1,30 @@ +
      + + + + +
      diff --git a/influxframework/printing/page/print_format_builder/print_format_builder_section.html b/influxframework/printing/page/print_format_builder/print_format_builder_section.html new file mode 100644 index 0000000..8894cd2 --- /dev/null +++ b/influxframework/printing/page/print_format_builder/print_format_builder_section.html @@ -0,0 +1,23 @@ + diff --git a/influxframework/printing/page/print_format_builder/print_format_builder_sidebar.html b/influxframework/printing/page/print_format_builder/print_format_builder_sidebar.html new file mode 100644 index 0000000..2b5b040 --- /dev/null +++ b/influxframework/printing/page/print_format_builder/print_format_builder_sidebar.html @@ -0,0 +1,21 @@ + + diff --git a/influxframework/printing/page/print_format_builder/print_format_builder_start.html b/influxframework/printing/page/print_format_builder/print_format_builder_start.html new file mode 100644 index 0000000..dc01bba --- /dev/null +++ b/influxframework/printing/page/print_format_builder/print_format_builder_start.html @@ -0,0 +1,18 @@ +
      +

      {%= __("Select an existing format to edit or start a new format.") %}

      +
      +
      + +

      + +

      +
      +
      +
      +
      +

      + +

      +
      diff --git a/influxframework/printing/page/print_format_builder_beta/__init__.py b/influxframework/printing/page/print_format_builder_beta/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/printing/page/print_format_builder_beta/print_format_builder_beta.css b/influxframework/printing/page/print_format_builder_beta/print_format_builder_beta.css new file mode 100644 index 0000000..0bd8d9c --- /dev/null +++ b/influxframework/printing/page/print_format_builder_beta/print_format_builder_beta.css @@ -0,0 +1,3 @@ +.layout-main-section-wrapper { + margin-bottom: 0; +} diff --git a/influxframework/printing/page/print_format_builder_beta/print_format_builder_beta.js b/influxframework/printing/page/print_format_builder_beta/print_format_builder_beta.js new file mode 100644 index 0000000..7703b2e --- /dev/null +++ b/influxframework/printing/page/print_format_builder_beta/print_format_builder_beta.js @@ -0,0 +1,110 @@ +influxframework.pages["print-format-builder-beta"].on_page_load = function (wrapper) { + influxframework.ui.make_app_page({ + parent: wrapper, + title: __("Print Format Builder"), + single_column: true, + }); + + // hot reload in development + if (influxframework.boot.developer_mode) { + influxframework.hot_update = influxframework.hot_update || []; + influxframework.hot_update.push(() => load_print_format_builder_beta(wrapper)); + } +}; + +influxframework.pages["print-format-builder-beta"].on_page_show = function (wrapper) { + load_print_format_builder_beta(wrapper); +}; + +function load_print_format_builder_beta(wrapper) { + let route = influxframework.get_route(); + let $parent = $(wrapper).find(".layout-main-section"); + $parent.empty(); + + if (route.length > 1) { + influxframework.require("print_format_builder.bundle.js").then(() => { + influxframework.print_format_builder = new influxframework.ui.PrintFormatBuilder({ + wrapper: $parent, + page: wrapper.page, + print_format: route[1], + }); + }); + } else { + let d = new influxframework.ui.Dialog({ + title: __("Create or Edit Print Format"), + fields: [ + { + label: __("Action"), + fieldname: "action", + fieldtype: "Select", + options: [ + { label: __("Create New"), value: "Create" }, + { label: __("Edit Existing"), value: "Edit" }, + ], + change() { + let action = d.get_value("action"); + d.get_primary_btn().text(action === "Create" ? __("Create") : __("Edit")); + }, + }, + { + label: __("Select Document Type"), + fieldname: "doctype", + fieldtype: "Link", + options: "DocType", + filters: { + istable: 0, + }, + reqd: 1, + default: influxframework.route_options ? influxframework.route_options.doctype : null, + }, + { + label: __("Print Format Name"), + fieldname: "print_format_name", + fieldtype: "Data", + depends_on: (doc) => doc.action === "Create", + mandatory_depends_on: (doc) => doc.action === "Create", + }, + { + label: __("Select Print Format"), + fieldname: "print_format", + fieldtype: "Link", + options: "Print Format", + only_select: 1, + depends_on: (doc) => doc.action === "Edit", + get_query() { + return { + filters: { + doc_type: d.get_value("doctype"), + print_format_builder_beta: 1, + }, + }; + }, + mandatory_depends_on: (doc) => doc.action === "Edit", + }, + ], + primary_action_label: __("Edit"), + primary_action({ action, doctype, print_format, print_format_name }) { + if (action === "Edit") { + influxframework.set_route("print-format-builder-beta", print_format); + } else if (action === "Create") { + d.get_primary_btn().prop("disabled", true); + influxframework.db + .insert({ + doctype: "Print Format", + name: print_format_name, + doc_type: doctype, + print_format_builder_beta: 1, + }) + .then((doc) => { + influxframework.set_route("print-format-builder-beta", doc.name); + }) + .finally(() => { + d.get_primary_btn().prop("disabled", false); + }); + } + }, + }); + d.set_value("action", "Create"); + d.show(); + } +} diff --git a/influxframework/printing/page/print_format_builder_beta/print_format_builder_beta.json b/influxframework/printing/page/print_format_builder_beta/print_format_builder_beta.json new file mode 100644 index 0000000..a5b1288 --- /dev/null +++ b/influxframework/printing/page/print_format_builder_beta/print_format_builder_beta.json @@ -0,0 +1,22 @@ +{ + "content": null, + "creation": "2021-07-10 12:22:16.138485", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2021-07-10 12:22:16.138485", + "modified_by": "Administrator", + "module": "Printing", + "name": "print-format-builder-beta", + "owner": "Administrator", + "page_name": "Print Format Builder Beta", + "roles": [ + { + "role": "System Manager" + } + ], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 0 +} \ No newline at end of file diff --git a/influxframework/printing/page/print_format_builder_beta/print_format_builder_beta.py b/influxframework/printing/page/print_format_builder_beta/print_format_builder_beta.py new file mode 100644 index 0000000..a6bc688 --- /dev/null +++ b/influxframework/printing/page/print_format_builder_beta/print_format_builder_beta.py @@ -0,0 +1,18 @@ +# Copyright (c) 2021, InfluxFramework LLC +# MIT License. See license.txt + + +import functools + +import influxframework + + +@influxframework.whitelist() +def get_google_fonts(): + return _get_google_fonts() + + +@functools.lru_cache +def _get_google_fonts(): + file_path = influxframework.get_app_path("influxframework", "data", "google_fonts.json") + return influxframework.parse_json(influxframework.read_file(file_path)) diff --git a/influxframework/printing/print_style/__init__.py b/influxframework/printing/print_style/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/printing/print_style/classic/__init__.py b/influxframework/printing/print_style/classic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/printing/print_style/classic/classic.json b/influxframework/printing/print_style/classic/classic.json new file mode 100644 index 0000000..af90e69 --- /dev/null +++ b/influxframework/printing/print_style/classic/classic.json @@ -0,0 +1,15 @@ +{ + "creation": "2017-08-17 02:00:12.502887", + "css": "/*\n\tcommon style for whole page\n\tThis should include:\n\t+ page size related settings\n\t+ font family settings\n\t+ line spacing settings\n*/\n.print-format div,\n.print-format span,\n.print-format td,\n.print-format h1,\n.print-format h2,\n.print-format h3,\n.print-format h4 {\n\tfont-family: Georgia, serif;\n}\n\n/* classic format: for-test */", + "disabled": 0, + "docstatus": 0, + "doctype": "Print Style", + "idx": 1, + "modified": "2017-08-18 00:43:48.675833", + "modified_by": "Administrator", + "name": "Classic", + "owner": "Administrator", + "preview": "/assets/influxframework/images/help/print-style-classic.png", + "print_style_name": "Classic", + "standard": 1 +} diff --git a/influxframework/printing/print_style/modern/__init__.py b/influxframework/printing/print_style/modern/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/printing/print_style/modern/modern.json b/influxframework/printing/print_style/modern/modern.json new file mode 100644 index 0000000..292dfc4 --- /dev/null +++ b/influxframework/printing/print_style/modern/modern.json @@ -0,0 +1,15 @@ +{ + "creation": "2017-08-17 02:16:58.060374", + "css": ".print-heading {\n\ttext-align: right;\n\ttext-transform: uppercase;\n\tcolor: #666;\n\tpadding-bottom: 20px;\n\tmargin-bottom: 20px;\n\tborder-bottom: 1px solid #d1d8dd;\n}\n\n.print-heading h2 {\n\tfont-size: 24px;\n}\n\n.print-format th {\n\tbackground-color: #eee !important;\n\tborder-bottom: 0px !important;\n}\n\n.print-format .primary.compact-item {\n font-weight: bold;\n}\n\n/* modern format: for-test */", + "disabled": 0, + "docstatus": 0, + "doctype": "Print Style", + "idx": 1, + "modified": "2020-11-10 13:59:09.976381", + "modified_by": "Administrator", + "name": "Modern", + "owner": "Administrator", + "preview": "/assets/influxframework/images/help/print-style-modern.png", + "print_style_name": "Modern", + "standard": 1 +} diff --git a/influxframework/printing/print_style/monochrome/__init__.py b/influxframework/printing/print_style/monochrome/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/printing/print_style/monochrome/monochrome.json b/influxframework/printing/print_style/monochrome/monochrome.json new file mode 100644 index 0000000..c7885be --- /dev/null +++ b/influxframework/printing/print_style/monochrome/monochrome.json @@ -0,0 +1,15 @@ +{ + "creation": "2017-08-17 02:16:20.992989", + "css": ".print-format * {\n\tcolor: #000 !important;\n}\n\n.print-format .alert {\n\tbackground-color: inherit;\n\tborder: 1px dashed #333;\n}\n\n.print-format .table-bordered,\n.print-format .table-bordered > thead > tr > th,\n.print-format .table-bordered > tbody > tr > th,\n.print-format .table-bordered > tfoot > tr > th,\n.print-format .table-bordered > thead > tr > td,\n.print-format .table-bordered > tbody > tr > td,\n.print-format .table-bordered > tfoot > tr > td {\n\tborder: 1px solid #333;\n}\n\n.print-format hr {\n\tborder-top: 1px solid #333;\n}\n\n.print-heading {\n\tborder-bottom: 2px solid #333;\n}\n", + "disabled": 0, + "docstatus": 0, + "doctype": "Print Style", + "idx": 0, + "modified": "2017-08-18 00:44:25.023898", + "modified_by": "Administrator", + "name": "Monochrome", + "owner": "Administrator", + "preview": "/assets/influxframework/images/help/print-style-monochrome.png", + "print_style_name": "Monochrome", + "standard": 1 +} diff --git a/influxframework/printing/print_style/redesign/__init__.py b/influxframework/printing/print_style/redesign/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/influxframework/printing/print_style/redesign/redesign.json b/influxframework/printing/print_style/redesign/redesign.json new file mode 100644 index 0000000..952b667 --- /dev/null +++ b/influxframework/printing/print_style/redesign/redesign.json @@ -0,0 +1,14 @@ +{ + "creation": "2020-10-22 00:00:08.161999", + "css": ".print-format {\n font-size: 13px;\n background: white;\n}\n\n.print-heading {\n border-bottom: 1px solid #f4f5f6;\n padding-bottom: 5px;\n margin-bottom: 10px;\n}\n\n.print-heading h2 {\n font-size: 24px;\n}\n\n.print-heading h2 div {\n font-weight: 600;\n}\n\n.print-heading small {\n font-size: 13px !important;\n font-weight: normal;\n line-height: 2.5;\n color: #4c5a67;\n}\n\n.print-format .letter-head {\n margin-bottom: 30px;\n}\n\n.print-format label {\n font-weight: normal;\n font-size: 13px;\n color: #4C5A67;\n margin-bottom: 0;\n}\n\n.print-format .data-field {\n margin-top: 0;\n margin-bottom: 0;\n}\n\n.print-format .value {\n color: #192734;\n line-height: 1.8;\n}\n\n.print-format .section-break:not(:last-child) {\n margin-bottom: 0;\n}\n\n.print-format .row:not(.section-break) {\n line-height: 1.6;\n margin-top: 15px !important;\n}\n\n.print-format .important .value {\n font-size: 13px;\n font-weight: 600;\n}\n\n.print-format th {\n color: #74808b;\n font-weight: normal;\n border-bottom-width: 1px !important;\n}\n\n.print-format .table-bordered td, .print-format .table-bordered th {\n border: 1px solid #f4f5f6;\n}\n\n.print-format .table-bordered {\n border: 1px solid #f4f5f6;\n}\n\n.print-format td, .print-format th {\n padding: 10px !important;\n}\n\n.print-format .primary.compact-item {\n font-weight: normal;\n}\n\n.print-format table td .value {\n font-size: 12px;\n line-height: 1.8;\n}\n", + "disabled": 0, + "docstatus": 0, + "doctype": "Print Style", + "idx": 0, + "modified": "2020-12-14 17:56:37.421390", + "modified_by": "Administrator", + "name": "Redesign", + "owner": "Administrator", + "print_style_name": "Redesign", + "standard": 1 +} \ No newline at end of file diff --git a/influxframework/public/css/bootstrap.css b/influxframework/public/css/bootstrap.css new file mode 100644 index 0000000..d4ce177 --- /dev/null +++ b/influxframework/public/css/bootstrap.css @@ -0,0 +1,5819 @@ +/*! + * Bootstrap v3.3.1 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ + +/*! normalize.css v3.0.2 | MIT License | git.io/normalize */ +html { + font-family: sans-serif; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} +body { + margin: 0; +} +article, +aside, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section { + display: block; +} +audio, +canvas, +progress, +video { + display: inline-block; + vertical-align: baseline; +} +audio:not([controls]) { + display: none; + height: 0; +} +[hidden], +template { + display: none; +} +a { + background-color: transparent; +} +a:active, +a:hover { + outline: 0; +} +abbr[title] { + border-bottom: 1px dotted; +} +b, +strong { + font-weight: bold; +} +dfn { + font-style: italic; +} +h1 { + margin: .67em 0; + font-size: 2em; +} +mark { + color: #000; + background: #ff0; +} +small { + font-size: 80%; +} +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} +sup { + top: -.5em; +} +sub { + bottom: -.25em; +} +img { + border: 0; +} +svg:not(:root) { + overflow: hidden; +} +figure { + margin: 1em 40px; +} +hr { + height: 0; + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} +pre { + overflow: auto; +} +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +button, +input, +optgroup, +select, +textarea { + margin: 0; + font: inherit; + color: inherit; +} +button { + overflow: visible; +} +button, +select { + text-transform: none; +} +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; + cursor: pointer; +} +button[disabled], +html input[disabled] { + cursor: default; +} +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} +input { + line-height: normal; +} +input[type="checkbox"], +input[type="radio"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 0; +} +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} +input[type="search"] { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + -webkit-appearance: textfield; +} +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} +fieldset { + padding: .35em .625em .75em; + margin: 0 2px; + border: 1px solid #c0c0c0; +} +legend { + padding: 0; + border: 0; +} +textarea { + overflow: auto; +} +optgroup { + font-weight: bold; +} +table { + border-spacing: 0; + border-collapse: collapse; +} +td, +th { + padding: 0; +} +/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ +@media print { + *, + *:before, + *:after { + color: #000; + background: transparent; + text-shadow: none !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; + } + a, + a:visited { + text-decoration: underline; + } + abbr[title]:after { + content: " (" attr(title) ")"; + } + a[href^="#"]:after, + a[href^="javascript:"]:after { + content: ""; + } + pre, + blockquote { + border: 1px solid #999; + + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + img { + max-width: 100% !important; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } + select { + background: #fff !important; + } + .navbar { + display: none; + } + .btn > .caret, + .dropup > .btn > .caret { + border-top-color: #000 !important; + } + .label { + border: 1px solid #000; + } + .table { + border-collapse: collapse !important; + } + .table td, + .table th { + background-color: #fff; + } + .table-bordered th, + .table-bordered td { + border: 1px solid #ddd; + } +} +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +*:before, +*:after { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +html { + font-size: 10px; + + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} +body { + font-family: "Helvetica Neue", Helvetica, Arial, "Open Sans", sans-serif; + font-size: 14px; + line-height: 1.42857143; + color: #36414c; + background-color: #fff; +} +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} +a { + color: #36414c; + text-decoration: none; +} +a:hover, +a:focus { + color: #161b1f; + text-decoration: underline; +} +a:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +figure { + margin: 0; +} +img { + vertical-align: middle; +} +.img-responsive, +.thumbnail > img, +.thumbnail a > img, +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + display: block; + max-width: 100%; + height: auto; +} +.img-rounded { + border-radius: 6px; +} +.img-thumbnail { + display: inline-block; + max-width: 100%; + height: auto; + padding: 4px; + line-height: 1.42857143; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + -webkit-transition: all .2s ease-in-out; + -o-transition: all .2s ease-in-out; + transition: all .2s ease-in-out; +} +.img-circle { + border-radius: 50%; +} +hr { + margin-top: 20px; + margin-bottom: 20px; + border: 0; + border-top: 1px solid #d1d8dd; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +.sr-only-focusable:active, +.sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + font-family: inherit; + font-weight: 500; + line-height: 1.3em; + color: inherit; +} +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small, +.h1 small, +.h2 small, +.h3 small, +.h4 small, +.h5 small, +.h6 small, +h1 .small, +h2 .small, +h3 .small, +h4 .small, +h5 .small, +h6 .small, +.h1 .small, +.h2 .small, +.h3 .small, +.h4 .small, +.h5 .small, +.h6 .small { + font-weight: normal; + line-height: 1; + color: #777; +} +h1, +.h1, +h2, +.h2, +h3, +.h3 { + margin-top: 20px; + margin-bottom: 10px; +} +h1 small, +.h1 small, +h2 small, +.h2 small, +h3 small, +.h3 small, +h1 .small, +.h1 .small, +h2 .small, +.h2 .small, +h3 .small, +.h3 .small { + font-size: 65%; +} +h4, +.h4, +h5, +.h5, +h6, +.h6 { + margin-top: 10px; + margin-bottom: 10px; +} +h4 small, +.h4 small, +h5 small, +.h5 small, +h6 small, +.h6 small, +h4 .small, +.h4 .small, +h5 .small, +.h5 .small, +h6 .small, +.h6 .small { + font-size: 75%; +} +h1, +.h1 { + font-size: 24px; +} +h2, +.h2 { + font-size: 20px; +} +h3, +.h3 { + font-size: 18px; +} +h4, +.h4 { + font-size: 16px; +} +h5, +.h5 { + font-size: 14px; +} +h6, +.h6 { + font-size: 12px; +} +p { + margin: 0 0 10px; +} +.lead { + margin-bottom: 20px; + font-size: 16px; + font-weight: 300; + line-height: 1.4; +} +@media (min-width: 768px) { + .lead { + font-size: 21px; + } +} +small, +.small { + font-size: 85%; +} +mark, +.mark { + padding: .2em; + background-color: transparent; +} +.text-left { + text-align: left; +} +.text-right { + text-align: right; +} +.text-center { + text-align: center; +} +.text-justify { + text-align: justify; +} +.text-nowrap { + white-space: nowrap; +} +.text-lowercase { + text-transform: lowercase; +} +.text-uppercase { + text-transform: uppercase; +} +.text-capitalize { + text-transform: capitalize; +} +.text-muted { + color: #8d99a6; +} +.text-primary { + color: #5e64ff; +} +a.text-primary:hover { + color: #2b33ff; +} +.text-success { + color: #98d85b; +} +a.text-success:hover { + color: #7ece32; +} +.text-info { + color: #935eff; +} +a.text-info:hover { + color: #712bff; +} +.text-warning { + color: #ffa00a; +} +a.text-warning:hover { + color: #d68300; +} +.text-danger { + color: #ff5858; +} +a.text-danger:hover { + color: #ff2525; +} +.bg-primary { + color: #fff; + background-color: #5e64ff; +} +a.bg-primary:hover { + background-color: #2b33ff; +} +.bg-success { + background-color: transparent; +} +a.bg-success:hover { + background-color: rgba(0, 0, 0, 0); +} +.bg-info { + background-color: transparent; +} +a.bg-info:hover { + background-color: rgba(0, 0, 0, 0); +} +.bg-warning { + background-color: transparent; +} +a.bg-warning:hover { + background-color: rgba(0, 0, 0, 0); +} +.bg-danger { + background-color: transparent; +} +a.bg-danger:hover { + background-color: rgba(0, 0, 0, 0); +} +.page-header { + padding-bottom: 9px; + margin: 40px 0 20px; + border-bottom: 1px solid #eee; +} +ul, +ol { + margin-top: 0; + margin-bottom: 10px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} +.list-unstyled { + padding-left: 0; + list-style: none; +} +.list-inline { + padding-left: 0; + margin-left: -5px; + list-style: none; +} +.list-inline > li { + display: inline-block; + padding-right: 5px; + padding-left: 5px; +} +dl { + margin-top: 0; + margin-bottom: 20px; +} +dt, +dd { + line-height: 1.42857143; +} +dt { + font-weight: bold; +} +dd { + margin-left: 0; +} +@media (min-width: 768px) { + .dl-horizontal dt { + float: left; + width: 160px; + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; + } + .dl-horizontal dd { + margin-left: 180px; + } +} +abbr[title], +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted #777; +} +.initialism { + font-size: 90%; + text-transform: uppercase; +} +blockquote { + padding: 10px 20px; + margin: 0 0 20px; + font-size: 17.5px; + border-left: 5px solid #eee; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +blockquote footer, +blockquote small, +blockquote .small { + display: block; + font-size: 80%; + line-height: 1.42857143; + color: #777; +} +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + text-align: right; + border-right: 5px solid #eee; + border-left: 0; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} +address { + margin-bottom: 20px; + font-style: normal; + line-height: 1.42857143; +} +code, +kbd, +pre, +samp { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + background-color: #f9f2f4; + border-radius: 4px; +} +kbd { + padding: 2px 4px; + font-size: 90%; + color: #fff; + background-color: #333; + border-radius: 3px; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + -webkit-box-shadow: none; + box-shadow: none; +} +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + color: #333; + word-break: break-all; + word-wrap: break-word; + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 4px; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} +.container { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} +@media (min-width: 768px) { + .container { + width: 750px; + } +} +@media (min-width: 992px) { + .container { + width: 970px; + } +} +@media (min-width: 1200px) { + .container { + width: 1170px; + } +} +.container-fluid { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} +.row { + margin-right: -15px; + margin-left: -15px; +} +.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; +} +.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 { + float: left; +} +.col-xs-12 { + width: 100%; +} +.col-xs-11 { + width: 91.66666667%; +} +.col-xs-10 { + width: 83.33333333%; +} +.col-xs-9 { + width: 75%; +} +.col-xs-8 { + width: 66.66666667%; +} +.col-xs-7 { + width: 58.33333333%; +} +.col-xs-6 { + width: 50%; +} +.col-xs-5 { + width: 41.66666667%; +} +.col-xs-4 { + width: 33.33333333%; +} +.col-xs-3 { + width: 25%; +} +.col-xs-2 { + width: 16.66666667%; +} +.col-xs-1 { + width: 8.33333333%; +} +.col-xs-pull-12 { + right: 100%; +} +.col-xs-pull-11 { + right: 91.66666667%; +} +.col-xs-pull-10 { + right: 83.33333333%; +} +.col-xs-pull-9 { + right: 75%; +} +.col-xs-pull-8 { + right: 66.66666667%; +} +.col-xs-pull-7 { + right: 58.33333333%; +} +.col-xs-pull-6 { + right: 50%; +} +.col-xs-pull-5 { + right: 41.66666667%; +} +.col-xs-pull-4 { + right: 33.33333333%; +} +.col-xs-pull-3 { + right: 25%; +} +.col-xs-pull-2 { + right: 16.66666667%; +} +.col-xs-pull-1 { + right: 8.33333333%; +} +.col-xs-pull-0 { + right: auto; +} +.col-xs-push-12 { + left: 100%; +} +.col-xs-push-11 { + left: 91.66666667%; +} +.col-xs-push-10 { + left: 83.33333333%; +} +.col-xs-push-9 { + left: 75%; +} +.col-xs-push-8 { + left: 66.66666667%; +} +.col-xs-push-7 { + left: 58.33333333%; +} +.col-xs-push-6 { + left: 50%; +} +.col-xs-push-5 { + left: 41.66666667%; +} +.col-xs-push-4 { + left: 33.33333333%; +} +.col-xs-push-3 { + left: 25%; +} +.col-xs-push-2 { + left: 16.66666667%; +} +.col-xs-push-1 { + left: 8.33333333%; +} +.col-xs-push-0 { + left: auto; +} +.col-xs-offset-12 { + margin-left: 100%; +} +.col-xs-offset-11 { + margin-left: 91.66666667%; +} +.col-xs-offset-10 { + margin-left: 83.33333333%; +} +.col-xs-offset-9 { + margin-left: 75%; +} +.col-xs-offset-8 { + margin-left: 66.66666667%; +} +.col-xs-offset-7 { + margin-left: 58.33333333%; +} +.col-xs-offset-6 { + margin-left: 50%; +} +.col-xs-offset-5 { + margin-left: 41.66666667%; +} +.col-xs-offset-4 { + margin-left: 33.33333333%; +} +.col-xs-offset-3 { + margin-left: 25%; +} +.col-xs-offset-2 { + margin-left: 16.66666667%; +} +.col-xs-offset-1 { + margin-left: 8.33333333%; +} +.col-xs-offset-0 { + margin-left: 0; +} +@media (min-width: 768px) { + .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 { + float: left; + } + .col-sm-12 { + width: 100%; + } + .col-sm-11 { + width: 91.66666667%; + } + .col-sm-10 { + width: 83.33333333%; + } + .col-sm-9 { + width: 75%; + } + .col-sm-8 { + width: 66.66666667%; + } + .col-sm-7 { + width: 58.33333333%; + } + .col-sm-6 { + width: 50%; + } + .col-sm-5 { + width: 41.66666667%; + } + .col-sm-4 { + width: 33.33333333%; + } + .col-sm-3 { + width: 25%; + } + .col-sm-2 { + width: 16.66666667%; + } + .col-sm-1 { + width: 8.33333333%; + } + .col-sm-pull-12 { + right: 100%; + } + .col-sm-pull-11 { + right: 91.66666667%; + } + .col-sm-pull-10 { + right: 83.33333333%; + } + .col-sm-pull-9 { + right: 75%; + } + .col-sm-pull-8 { + right: 66.66666667%; + } + .col-sm-pull-7 { + right: 58.33333333%; + } + .col-sm-pull-6 { + right: 50%; + } + .col-sm-pull-5 { + right: 41.66666667%; + } + .col-sm-pull-4 { + right: 33.33333333%; + } + .col-sm-pull-3 { + right: 25%; + } + .col-sm-pull-2 { + right: 16.66666667%; + } + .col-sm-pull-1 { + right: 8.33333333%; + } + .col-sm-pull-0 { + right: auto; + } + .col-sm-push-12 { + left: 100%; + } + .col-sm-push-11 { + left: 91.66666667%; + } + .col-sm-push-10 { + left: 83.33333333%; + } + .col-sm-push-9 { + left: 75%; + } + .col-sm-push-8 { + left: 66.66666667%; + } + .col-sm-push-7 { + left: 58.33333333%; + } + .col-sm-push-6 { + left: 50%; + } + .col-sm-push-5 { + left: 41.66666667%; + } + .col-sm-push-4 { + left: 33.33333333%; + } + .col-sm-push-3 { + left: 25%; + } + .col-sm-push-2 { + left: 16.66666667%; + } + .col-sm-push-1 { + left: 8.33333333%; + } + .col-sm-push-0 { + left: auto; + } + .col-sm-offset-12 { + margin-left: 100%; + } + .col-sm-offset-11 { + margin-left: 91.66666667%; + } + .col-sm-offset-10 { + margin-left: 83.33333333%; + } + .col-sm-offset-9 { + margin-left: 75%; + } + .col-sm-offset-8 { + margin-left: 66.66666667%; + } + .col-sm-offset-7 { + margin-left: 58.33333333%; + } + .col-sm-offset-6 { + margin-left: 50%; + } + .col-sm-offset-5 { + margin-left: 41.66666667%; + } + .col-sm-offset-4 { + margin-left: 33.33333333%; + } + .col-sm-offset-3 { + margin-left: 25%; + } + .col-sm-offset-2 { + margin-left: 16.66666667%; + } + .col-sm-offset-1 { + margin-left: 8.33333333%; + } + .col-sm-offset-0 { + margin-left: 0; + } +} +@media (min-width: 992px) { + .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 { + float: left; + } + .col-md-12 { + width: 100%; + } + .col-md-11 { + width: 91.66666667%; + } + .col-md-10 { + width: 83.33333333%; + } + .col-md-9 { + width: 75%; + } + .col-md-8 { + width: 66.66666667%; + } + .col-md-7 { + width: 58.33333333%; + } + .col-md-6 { + width: 50%; + } + .col-md-5 { + width: 41.66666667%; + } + .col-md-4 { + width: 33.33333333%; + } + .col-md-3 { + width: 25%; + } + .col-md-2 { + width: 16.66666667%; + } + .col-md-1 { + width: 8.33333333%; + } + .col-md-pull-12 { + right: 100%; + } + .col-md-pull-11 { + right: 91.66666667%; + } + .col-md-pull-10 { + right: 83.33333333%; + } + .col-md-pull-9 { + right: 75%; + } + .col-md-pull-8 { + right: 66.66666667%; + } + .col-md-pull-7 { + right: 58.33333333%; + } + .col-md-pull-6 { + right: 50%; + } + .col-md-pull-5 { + right: 41.66666667%; + } + .col-md-pull-4 { + right: 33.33333333%; + } + .col-md-pull-3 { + right: 25%; + } + .col-md-pull-2 { + right: 16.66666667%; + } + .col-md-pull-1 { + right: 8.33333333%; + } + .col-md-pull-0 { + right: auto; + } + .col-md-push-12 { + left: 100%; + } + .col-md-push-11 { + left: 91.66666667%; + } + .col-md-push-10 { + left: 83.33333333%; + } + .col-md-push-9 { + left: 75%; + } + .col-md-push-8 { + left: 66.66666667%; + } + .col-md-push-7 { + left: 58.33333333%; + } + .col-md-push-6 { + left: 50%; + } + .col-md-push-5 { + left: 41.66666667%; + } + .col-md-push-4 { + left: 33.33333333%; + } + .col-md-push-3 { + left: 25%; + } + .col-md-push-2 { + left: 16.66666667%; + } + .col-md-push-1 { + left: 8.33333333%; + } + .col-md-push-0 { + left: auto; + } + .col-md-offset-12 { + margin-left: 100%; + } + .col-md-offset-11 { + margin-left: 91.66666667%; + } + .col-md-offset-10 { + margin-left: 83.33333333%; + } + .col-md-offset-9 { + margin-left: 75%; + } + .col-md-offset-8 { + margin-left: 66.66666667%; + } + .col-md-offset-7 { + margin-left: 58.33333333%; + } + .col-md-offset-6 { + margin-left: 50%; + } + .col-md-offset-5 { + margin-left: 41.66666667%; + } + .col-md-offset-4 { + margin-left: 33.33333333%; + } + .col-md-offset-3 { + margin-left: 25%; + } + .col-md-offset-2 { + margin-left: 16.66666667%; + } + .col-md-offset-1 { + margin-left: 8.33333333%; + } + .col-md-offset-0 { + margin-left: 0; + } +} +@media (min-width: 1200px) { + .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 { + float: left; + } + .col-lg-12 { + width: 100%; + } + .col-lg-11 { + width: 91.66666667%; + } + .col-lg-10 { + width: 83.33333333%; + } + .col-lg-9 { + width: 75%; + } + .col-lg-8 { + width: 66.66666667%; + } + .col-lg-7 { + width: 58.33333333%; + } + .col-lg-6 { + width: 50%; + } + .col-lg-5 { + width: 41.66666667%; + } + .col-lg-4 { + width: 33.33333333%; + } + .col-lg-3 { + width: 25%; + } + .col-lg-2 { + width: 16.66666667%; + } + .col-lg-1 { + width: 8.33333333%; + } + .col-lg-pull-12 { + right: 100%; + } + .col-lg-pull-11 { + right: 91.66666667%; + } + .col-lg-pull-10 { + right: 83.33333333%; + } + .col-lg-pull-9 { + right: 75%; + } + .col-lg-pull-8 { + right: 66.66666667%; + } + .col-lg-pull-7 { + right: 58.33333333%; + } + .col-lg-pull-6 { + right: 50%; + } + .col-lg-pull-5 { + right: 41.66666667%; + } + .col-lg-pull-4 { + right: 33.33333333%; + } + .col-lg-pull-3 { + right: 25%; + } + .col-lg-pull-2 { + right: 16.66666667%; + } + .col-lg-pull-1 { + right: 8.33333333%; + } + .col-lg-pull-0 { + right: auto; + } + .col-lg-push-12 { + left: 100%; + } + .col-lg-push-11 { + left: 91.66666667%; + } + .col-lg-push-10 { + left: 83.33333333%; + } + .col-lg-push-9 { + left: 75%; + } + .col-lg-push-8 { + left: 66.66666667%; + } + .col-lg-push-7 { + left: 58.33333333%; + } + .col-lg-push-6 { + left: 50%; + } + .col-lg-push-5 { + left: 41.66666667%; + } + .col-lg-push-4 { + left: 33.33333333%; + } + .col-lg-push-3 { + left: 25%; + } + .col-lg-push-2 { + left: 16.66666667%; + } + .col-lg-push-1 { + left: 8.33333333%; + } + .col-lg-push-0 { + left: auto; + } + .col-lg-offset-12 { + margin-left: 100%; + } + .col-lg-offset-11 { + margin-left: 91.66666667%; + } + .col-lg-offset-10 { + margin-left: 83.33333333%; + } + .col-lg-offset-9 { + margin-left: 75%; + } + .col-lg-offset-8 { + margin-left: 66.66666667%; + } + .col-lg-offset-7 { + margin-left: 58.33333333%; + } + .col-lg-offset-6 { + margin-left: 50%; + } + .col-lg-offset-5 { + margin-left: 41.66666667%; + } + .col-lg-offset-4 { + margin-left: 33.33333333%; + } + .col-lg-offset-3 { + margin-left: 25%; + } + .col-lg-offset-2 { + margin-left: 16.66666667%; + } + .col-lg-offset-1 { + margin-left: 8.33333333%; + } + .col-lg-offset-0 { + margin-left: 0; + } +} +table { + background-color: transparent; +} +caption { + padding-top: 8px; + padding-bottom: 8px; + color: #8d99a6; + text-align: left; +} +th { + text-align: left; +} +.table { + width: 100%; + max-width: 100%; + margin-bottom: 20px; +} +.table > thead > tr > th, +.table > tbody > tr > th, +.table > tfoot > tr > th, +.table > thead > tr > td, +.table > tbody > tr > td, +.table > tfoot > tr > td { + padding: 8px; + line-height: 1.42857143; + vertical-align: top; + border-top: 1px solid #d1d8dd; +} +.table > thead > tr > th { + vertical-align: bottom; + border-bottom: 2px solid #d1d8dd; +} +.table > caption + thead > tr:first-child > th, +.table > colgroup + thead > tr:first-child > th, +.table > thead:first-child > tr:first-child > th, +.table > caption + thead > tr:first-child > td, +.table > colgroup + thead > tr:first-child > td, +.table > thead:first-child > tr:first-child > td { + border-top: 0; +} +.table > tbody + tbody { + border-top: 2px solid #d1d8dd; +} +.table .table { + background-color: #fff; +} +.table-condensed > thead > tr > th, +.table-condensed > tbody > tr > th, +.table-condensed > tfoot > tr > th, +.table-condensed > thead > tr > td, +.table-condensed > tbody > tr > td, +.table-condensed > tfoot > tr > td { + padding: 5px; +} +.table-bordered { + border: 1px solid #d1d8dd; +} +.table-bordered > thead > tr > th, +.table-bordered > tbody > tr > th, +.table-bordered > tfoot > tr > th, +.table-bordered > thead > tr > td, +.table-bordered > tbody > tr > td, +.table-bordered > tfoot > tr > td { + border: 1px solid #d1d8dd; +} +.table-bordered > thead > tr > th, +.table-bordered > thead > tr > td { + border-bottom-width: 2px; +} +.table-striped > tbody > tr:nth-child(odd) { + background-color: #f9f9f9; +} +.table-hover > tbody > tr:hover { + background-color: #f5f5f5; +} +table col[class*="col-"] { + position: static; + display: table-column; + float: none; +} +table td[class*="col-"], +table th[class*="col-"] { + position: static; + display: table-cell; + float: none; +} +.table > thead > tr > td.active, +.table > tbody > tr > td.active, +.table > tfoot > tr > td.active, +.table > thead > tr > th.active, +.table > tbody > tr > th.active, +.table > tfoot > tr > th.active, +.table > thead > tr.active > td, +.table > tbody > tr.active > td, +.table > tfoot > tr.active > td, +.table > thead > tr.active > th, +.table > tbody > tr.active > th, +.table > tfoot > tr.active > th { + background-color: #f5f5f5; +} +.table-hover > tbody > tr > td.active:hover, +.table-hover > tbody > tr > th.active:hover, +.table-hover > tbody > tr.active:hover > td, +.table-hover > tbody > tr:hover > .active, +.table-hover > tbody > tr.active:hover > th { + background-color: #e8e8e8; +} +.table > thead > tr > td.success, +.table > tbody > tr > td.success, +.table > tfoot > tr > td.success, +.table > thead > tr > th.success, +.table > tbody > tr > th.success, +.table > tfoot > tr > th.success, +.table > thead > tr.success > td, +.table > tbody > tr.success > td, +.table > tfoot > tr.success > td, +.table > thead > tr.success > th, +.table > tbody > tr.success > th, +.table > tfoot > tr.success > th { + background-color: transparent; +} +.table-hover > tbody > tr > td.success:hover, +.table-hover > tbody > tr > th.success:hover, +.table-hover > tbody > tr.success:hover > td, +.table-hover > tbody > tr:hover > .success, +.table-hover > tbody > tr.success:hover > th { + background-color: rgba(0, 0, 0, 0); +} +.table > thead > tr > td.info, +.table > tbody > tr > td.info, +.table > tfoot > tr > td.info, +.table > thead > tr > th.info, +.table > tbody > tr > th.info, +.table > tfoot > tr > th.info, +.table > thead > tr.info > td, +.table > tbody > tr.info > td, +.table > tfoot > tr.info > td, +.table > thead > tr.info > th, +.table > tbody > tr.info > th, +.table > tfoot > tr.info > th { + background-color: transparent; +} +.table-hover > tbody > tr > td.info:hover, +.table-hover > tbody > tr > th.info:hover, +.table-hover > tbody > tr.info:hover > td, +.table-hover > tbody > tr:hover > .info, +.table-hover > tbody > tr.info:hover > th { + background-color: rgba(0, 0, 0, 0); +} +.table > thead > tr > td.warning, +.table > tbody > tr > td.warning, +.table > tfoot > tr > td.warning, +.table > thead > tr > th.warning, +.table > tbody > tr > th.warning, +.table > tfoot > tr > th.warning, +.table > thead > tr.warning > td, +.table > tbody > tr.warning > td, +.table > tfoot > tr.warning > td, +.table > thead > tr.warning > th, +.table > tbody > tr.warning > th, +.table > tfoot > tr.warning > th { + background-color: transparent; +} +.table-hover > tbody > tr > td.warning:hover, +.table-hover > tbody > tr > th.warning:hover, +.table-hover > tbody > tr.warning:hover > td, +.table-hover > tbody > tr:hover > .warning, +.table-hover > tbody > tr.warning:hover > th { + background-color: rgba(0, 0, 0, 0); +} +.table > thead > tr > td.danger, +.table > tbody > tr > td.danger, +.table > tfoot > tr > td.danger, +.table > thead > tr > th.danger, +.table > tbody > tr > th.danger, +.table > tfoot > tr > th.danger, +.table > thead > tr.danger > td, +.table > tbody > tr.danger > td, +.table > tfoot > tr.danger > td, +.table > thead > tr.danger > th, +.table > tbody > tr.danger > th, +.table > tfoot > tr.danger > th { + background-color: transparent; +} +.table-hover > tbody > tr > td.danger:hover, +.table-hover > tbody > tr > th.danger:hover, +.table-hover > tbody > tr.danger:hover > td, +.table-hover > tbody > tr:hover > .danger, +.table-hover > tbody > tr.danger:hover > th { + background-color: rgba(0, 0, 0, 0); +} +.table-responsive { + min-height: .01%; + overflow-x: auto; +} +@media screen and (max-width: 767px) { + .table-responsive { + width: 100%; + margin-bottom: 15px; + overflow-y: hidden; + -ms-overflow-style: -ms-autohiding-scrollbar; + border: 1px solid #d1d8dd; + } + .table-responsive > .table { + margin-bottom: 0; + } + .table-responsive > .table > thead > tr > th, + .table-responsive > .table > tbody > tr > th, + .table-responsive > .table > tfoot > tr > th, + .table-responsive > .table > thead > tr > td, + .table-responsive > .table > tbody > tr > td, + .table-responsive > .table > tfoot > tr > td { + white-space: nowrap; + } + .table-responsive > .table-bordered { + border: 0; + } + .table-responsive > .table-bordered > thead > tr > th:first-child, + .table-responsive > .table-bordered > tbody > tr > th:first-child, + .table-responsive > .table-bordered > tfoot > tr > th:first-child, + .table-responsive > .table-bordered > thead > tr > td:first-child, + .table-responsive > .table-bordered > tbody > tr > td:first-child, + .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; + } + .table-responsive > .table-bordered > thead > tr > th:last-child, + .table-responsive > .table-bordered > tbody > tr > th:last-child, + .table-responsive > .table-bordered > tfoot > tr > th:last-child, + .table-responsive > .table-bordered > thead > tr > td:last-child, + .table-responsive > .table-bordered > tbody > tr > td:last-child, + .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; + } + .table-responsive > .table-bordered > tbody > tr:last-child > th, + .table-responsive > .table-bordered > tfoot > tr:last-child > th, + .table-responsive > .table-bordered > tbody > tr:last-child > td, + .table-responsive > .table-bordered > tfoot > tr:last-child > td { + border-bottom: 0; + } +} +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 20px; + font-size: 21px; + line-height: inherit; + color: #333; + border: 0; + border-bottom: 1px solid #e5e5e5; +} +label { + display: inline-block; + max-width: 100%; + margin-bottom: 5px; + font-weight: bold; +} +input[type="search"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; + line-height: normal; +} +input[type="file"] { + display: block; +} +input[type="range"] { + display: block; + width: 100%; +} +select[multiple], +select[size] { + height: auto; +} +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +output { + display: block; + padding-top: 5px; + font-size: 14px; + line-height: 1.42857143; + color: #555; +} +.form-control { + display: block; + width: 100%; + height: 30px; + padding: 4px 10px; + font-size: 14px; + line-height: 1.42857143; + color: #555; + background-color: #fff; + background-image: none; + border: 1px solid #d1d8dd; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; + -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; +} +.form-control:focus { + border-color: #ced5db; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(206, 213, 219, .6); + box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(206, 213, 219, .6); +} +.form-control:focus { + border-color: #ced5db; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 3px rgba(206, 213, 219, .6); + box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 3px rgba(206, 213, 219, .6); +} +.form-control::-moz-placeholder { + color: #d1d8dd; + opacity: 1; +} +.form-control:-ms-input-placeholder { + color: #d1d8dd; +} +.form-control::-webkit-input-placeholder { + color: #d1d8dd; +} +.form-control[disabled], +.form-control[readonly], +fieldset[disabled] .form-control { + cursor: not-allowed; + background-color: #eee; + opacity: 1; +} +textarea.form-control { + height: auto; +} +input[type="search"] { + -webkit-appearance: none; +} +@media screen and (-webkit-min-device-pixel-ratio: 0) { + input[type="date"], + input[type="time"], + input[type="datetime-local"], + input[type="month"] { + line-height: 30px; + } + input[type="date"].input-sm, + input[type="time"].input-sm, + input[type="datetime-local"].input-sm, + input[type="month"].input-sm, + .input-group-sm input[type="date"], + .input-group-sm input[type="time"], + .input-group-sm input[type="datetime-local"], + .input-group-sm input[type="month"] { + line-height: 30px; + } + input[type="date"].input-lg, + input[type="time"].input-lg, + input[type="datetime-local"].input-lg, + input[type="month"].input-lg, + .input-group-lg input[type="date"], + .input-group-lg input[type="time"], + .input-group-lg input[type="datetime-local"], + .input-group-lg input[type="month"] { + line-height: 46px; + } +} +.form-group { + margin-bottom: 15px; +} +.radio, +.checkbox { + position: relative; + display: block; + margin-top: 10px; + margin-bottom: 10px; +} +.radio label, +.checkbox label { + min-height: 20px; + padding-left: 20px; + margin-bottom: 0; + font-weight: normal; + cursor: pointer; +} +.radio input[type="radio"], +.radio-inline input[type="radio"], +.checkbox input[type="checkbox"], +.checkbox-inline input[type="checkbox"] { + position: absolute; + margin-top: 4px \9; + margin-left: -20px; +} +.radio + .radio, +.checkbox + .checkbox { + margin-top: -5px; +} +.radio-inline, +.checkbox-inline { + display: inline-block; + padding-left: 20px; + margin-bottom: 0; + font-weight: normal; + vertical-align: middle; + cursor: pointer; +} +.radio-inline + .radio-inline, +.checkbox-inline + .checkbox-inline { + margin-top: 0; + margin-left: 10px; +} +input[type="radio"][disabled], +input[type="checkbox"][disabled], +input[type="radio"].disabled, +input[type="checkbox"].disabled, +fieldset[disabled] input[type="radio"], +fieldset[disabled] input[type="checkbox"] { + cursor: not-allowed; +} +.radio-inline.disabled, +.checkbox-inline.disabled, +fieldset[disabled] .radio-inline, +fieldset[disabled] .checkbox-inline { + cursor: not-allowed; +} +.radio.disabled label, +.checkbox.disabled label, +fieldset[disabled] .radio label, +fieldset[disabled] .checkbox label { + cursor: not-allowed; +} +.form-control-static { + padding-top: 5px; + padding-bottom: 5px; + margin-bottom: 0; +} +.form-control-static.input-lg, +.form-control-static.input-sm { + padding-right: 0; + padding-left: 0; +} +.input-sm { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +select.input-sm { + height: 30px; + line-height: 30px; +} +textarea.input-sm, +select[multiple].input-sm { + height: auto; +} +.form-group-sm .form-control { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +select.form-group-sm .form-control { + height: 30px; + line-height: 30px; +} +textarea.form-group-sm .form-control, +select[multiple].form-group-sm .form-control { + height: auto; +} +.input-lg { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; +} +select.input-lg { + height: 46px; + line-height: 46px; +} +textarea.input-lg, +select[multiple].input-lg { + height: auto; +} +.form-group-lg .form-control { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; +} +select.form-group-lg .form-control { + height: 46px; + line-height: 46px; +} +textarea.form-group-lg .form-control, +select[multiple].form-group-lg .form-control { + height: auto; +} +.has-feedback { + position: relative; +} +.has-feedback .form-control { + padding-right: 37.5px; +} +.form-control-feedback { + position: absolute; + top: 0; + right: 0; + z-index: 2; + display: block; + width: 30px; + height: 30px; + line-height: 30px; + text-align: center; + pointer-events: none; +} +.input-lg + .form-control-feedback { + width: 46px; + height: 46px; + line-height: 46px; +} +.input-sm + .form-control-feedback { + width: 30px; + height: 30px; + line-height: 30px; +} +.has-success .help-block, +.has-success .control-label, +.has-success .radio, +.has-success .checkbox, +.has-success .radio-inline, +.has-success .checkbox-inline, +.has-success.radio label, +.has-success.checkbox label, +.has-success.radio-inline label, +.has-success.checkbox-inline label { + color: #98d85b; +} +.has-success .form-control { + border-color: #98d85b; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); +} +.has-success .form-control:focus { + border-color: #7ece32; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ccecad; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ccecad; +} +.has-success .input-group-addon { + color: #98d85b; + background-color: transparent; + border-color: #98d85b; +} +.has-success .form-control-feedback { + color: #98d85b; +} +.has-warning .help-block, +.has-warning .control-label, +.has-warning .radio, +.has-warning .checkbox, +.has-warning .radio-inline, +.has-warning .checkbox-inline, +.has-warning.radio label, +.has-warning.checkbox label, +.has-warning.radio-inline label, +.has-warning.checkbox-inline label { + color: #ffa00a; +} +.has-warning .form-control { + border-color: #ffa00a; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); +} +.has-warning .form-control:focus { + border-color: #d68300; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ffc870; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ffc870; +} +.has-warning .input-group-addon { + color: #ffa00a; + background-color: transparent; + border-color: #ffa00a; +} +.has-warning .form-control-feedback { + color: #ffa00a; +} +.has-error .help-block, +.has-error .control-label, +.has-error .radio, +.has-error .checkbox, +.has-error .radio-inline, +.has-error .checkbox-inline, +.has-error.radio label, +.has-error.checkbox label, +.has-error.radio-inline label, +.has-error.checkbox-inline label { + color: #ff5858; +} +.has-error .form-control { + border-color: #ff5858; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); +} +.has-error .form-control:focus { + border-color: #ff2525; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ffbebe; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ffbebe; +} +.has-error .input-group-addon { + color: #ff5858; + background-color: transparent; + border-color: #ff5858; +} +.has-error .form-control-feedback { + color: #ff5858; +} +.has-feedback label ~ .form-control-feedback { + top: 25px; +} +.has-feedback label.sr-only ~ .form-control-feedback { + top: 0; +} +.help-block { + display: block; + margin-top: 5px; + margin-bottom: 10px; + color: #6b8196; +} +@media (min-width: 768px) { + .form-inline .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .form-inline .form-control-static { + display: inline-block; + } + .form-inline .input-group { + display: inline-table; + vertical-align: middle; + } + .form-inline .input-group .input-group-addon, + .form-inline .input-group .input-group-btn, + .form-inline .input-group .form-control { + width: auto; + } + .form-inline .input-group > .form-control { + width: 100%; + } + .form-inline .control-label { + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .radio, + .form-inline .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .radio label, + .form-inline .checkbox label { + padding-left: 0; + } + .form-inline .radio input[type="radio"], + .form-inline .checkbox input[type="checkbox"] { + position: relative; + margin-left: 0; + } + .form-inline .has-feedback .form-control-feedback { + top: 0; + } +} +.form-horizontal .radio, +.form-horizontal .checkbox, +.form-horizontal .radio-inline, +.form-horizontal .checkbox-inline { + padding-top: 5px; + margin-top: 0; + margin-bottom: 0; +} +.form-horizontal .radio, +.form-horizontal .checkbox { + min-height: 25px; +} +.form-horizontal .form-group { + margin-right: -15px; + margin-left: -15px; +} +@media (min-width: 768px) { + .form-horizontal .control-label { + padding-top: 5px; + margin-bottom: 0; + text-align: right; + } +} +.form-horizontal .has-feedback .form-control-feedback { + right: 15px; +} +@media (min-width: 768px) { + .form-horizontal .form-group-lg .control-label { + padding-top: 14.3px; + } +} +@media (min-width: 768px) { + .form-horizontal .form-group-sm .control-label { + padding-top: 6px; + } +} +.btn { + display: inline-block; + padding: 4px 10px; + margin-bottom: 0; + font-size: 14px; + font-weight: normal; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + vertical-align: middle; + -ms-touch-action: manipulation; + touch-action: manipulation; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} +.btn:focus, +.btn:active:focus, +.btn.active:focus, +.btn.focus, +.btn:active.focus, +.btn.active.focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.btn:hover, +.btn:focus, +.btn.focus { + color: inherit; + text-decoration: none; +} +.btn:active, +.btn.active { + background-image: none; + outline: 0; + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); +} +.btn.disabled, +.btn[disabled], +fieldset[disabled] .btn { + pointer-events: none; + cursor: not-allowed; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + box-shadow: none; + opacity: .65; +} +.btn-default { + color: inherit; + background-color: #f0f4f7; + border-color: transparent; +} +.btn-default:hover, +.btn-default:focus, +.btn-default.focus, +.btn-default:active, +.btn-default.active, +.open > .dropdown-toggle.btn-default { + color: inherit; + background-color: #cfdce5; + border-color: rgba(0, 0, 0, 0); +} +.btn-default:active, +.btn-default.active, +.open > .dropdown-toggle.btn-default { + background-image: none; +} +.btn-default.disabled, +.btn-default[disabled], +fieldset[disabled] .btn-default, +.btn-default.disabled:hover, +.btn-default[disabled]:hover, +fieldset[disabled] .btn-default:hover, +.btn-default.disabled:focus, +.btn-default[disabled]:focus, +fieldset[disabled] .btn-default:focus, +.btn-default.disabled.focus, +.btn-default[disabled].focus, +fieldset[disabled] .btn-default.focus, +.btn-default.disabled:active, +.btn-default[disabled]:active, +fieldset[disabled] .btn-default:active, +.btn-default.disabled.active, +.btn-default[disabled].active, +fieldset[disabled] .btn-default.active { + background-color: #f0f4f7; + border-color: transparent; +} +.btn-default .badge { + color: #f0f4f7; + background-color: inherit; +} +.btn-primary { + color: #fff; + background-color: #5e64ff; + border-color: #444bff; +} +.btn-primary:hover, +.btn-primary:focus, +.btn-primary.focus, +.btn-primary:active, +.btn-primary.active, +.open > .dropdown-toggle.btn-primary { + color: #fff; + background-color: #2b33ff; + border-color: #0711ff; +} +.btn-primary:active, +.btn-primary.active, +.open > .dropdown-toggle.btn-primary { + background-image: none; +} +.btn-primary.disabled, +.btn-primary[disabled], +fieldset[disabled] .btn-primary, +.btn-primary.disabled:hover, +.btn-primary[disabled]:hover, +fieldset[disabled] .btn-primary:hover, +.btn-primary.disabled:focus, +.btn-primary[disabled]:focus, +fieldset[disabled] .btn-primary:focus, +.btn-primary.disabled.focus, +.btn-primary[disabled].focus, +fieldset[disabled] .btn-primary.focus, +.btn-primary.disabled:active, +.btn-primary[disabled]:active, +fieldset[disabled] .btn-primary:active, +.btn-primary.disabled.active, +.btn-primary[disabled].active, +fieldset[disabled] .btn-primary.active { + background-color: #5e64ff; + border-color: #444bff; +} +.btn-primary .badge { + color: #5e64ff; + background-color: #fff; +} +.btn-success { + color: #fff; + background-color: #98d85b; + border-color: #8bd346; +} +.btn-success:hover, +.btn-success:focus, +.btn-success.focus, +.btn-success:active, +.btn-success.active, +.open > .dropdown-toggle.btn-success { + color: #fff; + background-color: #7ece32; + border-color: #6db22a; +} +.btn-success:active, +.btn-success.active, +.open > .dropdown-toggle.btn-success { + background-image: none; +} +.btn-success.disabled, +.btn-success[disabled], +fieldset[disabled] .btn-success, +.btn-success.disabled:hover, +.btn-success[disabled]:hover, +fieldset[disabled] .btn-success:hover, +.btn-success.disabled:focus, +.btn-success[disabled]:focus, +fieldset[disabled] .btn-success:focus, +.btn-success.disabled.focus, +.btn-success[disabled].focus, +fieldset[disabled] .btn-success.focus, +.btn-success.disabled:active, +.btn-success[disabled]:active, +fieldset[disabled] .btn-success:active, +.btn-success.disabled.active, +.btn-success[disabled].active, +fieldset[disabled] .btn-success.active { + background-color: #98d85b; + border-color: #8bd346; +} +.btn-success .badge { + color: #98d85b; + background-color: #fff; +} +.btn-info { + color: #fff; + background-color: #8d99a6; + border-color: #7f8c9b; +} +.btn-info:hover, +.btn-info:focus, +.btn-info.focus, +.btn-info:active, +.btn-info.active, +.open > .dropdown-toggle.btn-info { + color: #fff; + background-color: #707f90; + border-color: #616e7c; +} +.btn-info:active, +.btn-info.active, +.open > .dropdown-toggle.btn-info { + background-image: none; +} +.btn-info.disabled, +.btn-info[disabled], +fieldset[disabled] .btn-info, +.btn-info.disabled:hover, +.btn-info[disabled]:hover, +fieldset[disabled] .btn-info:hover, +.btn-info.disabled:focus, +.btn-info[disabled]:focus, +fieldset[disabled] .btn-info:focus, +.btn-info.disabled.focus, +.btn-info[disabled].focus, +fieldset[disabled] .btn-info.focus, +.btn-info.disabled:active, +.btn-info[disabled]:active, +fieldset[disabled] .btn-info:active, +.btn-info.disabled.active, +.btn-info[disabled].active, +fieldset[disabled] .btn-info.active { + background-color: #8d99a6; + border-color: #7f8c9b; +} +.btn-info .badge { + color: #8d99a6; + background-color: #fff; +} +.btn-warning { + color: #fff; + background-color: #ffa00a; + border-color: #f09300; +} +.btn-warning:hover, +.btn-warning:focus, +.btn-warning.focus, +.btn-warning:active, +.btn-warning.active, +.open > .dropdown-toggle.btn-warning { + color: #fff; + background-color: #d68300; + border-color: #b26d00; +} +.btn-warning:active, +.btn-warning.active, +.open > .dropdown-toggle.btn-warning { + background-image: none; +} +.btn-warning.disabled, +.btn-warning[disabled], +fieldset[disabled] .btn-warning, +.btn-warning.disabled:hover, +.btn-warning[disabled]:hover, +fieldset[disabled] .btn-warning:hover, +.btn-warning.disabled:focus, +.btn-warning[disabled]:focus, +fieldset[disabled] .btn-warning:focus, +.btn-warning.disabled.focus, +.btn-warning[disabled].focus, +fieldset[disabled] .btn-warning.focus, +.btn-warning.disabled:active, +.btn-warning[disabled]:active, +fieldset[disabled] .btn-warning:active, +.btn-warning.disabled.active, +.btn-warning[disabled].active, +fieldset[disabled] .btn-warning.active { + background-color: #ffa00a; + border-color: #f09300; +} +.btn-warning .badge { + color: #ffa00a; + background-color: #fff; +} +.btn-danger { + color: #fff; + background-color: #ff5858; + border-color: #ff3f3f; +} +.btn-danger:hover, +.btn-danger:focus, +.btn-danger.focus, +.btn-danger:active, +.btn-danger.active, +.open > .dropdown-toggle.btn-danger { + color: #fff; + background-color: #ff2525; + border-color: #ff0101; +} +.btn-danger:active, +.btn-danger.active, +.open > .dropdown-toggle.btn-danger { + background-image: none; +} +.btn-danger.disabled, +.btn-danger[disabled], +fieldset[disabled] .btn-danger, +.btn-danger.disabled:hover, +.btn-danger[disabled]:hover, +fieldset[disabled] .btn-danger:hover, +.btn-danger.disabled:focus, +.btn-danger[disabled]:focus, +fieldset[disabled] .btn-danger:focus, +.btn-danger.disabled.focus, +.btn-danger[disabled].focus, +fieldset[disabled] .btn-danger.focus, +.btn-danger.disabled:active, +.btn-danger[disabled]:active, +fieldset[disabled] .btn-danger:active, +.btn-danger.disabled.active, +.btn-danger[disabled].active, +fieldset[disabled] .btn-danger.active { + background-color: #ff5858; + border-color: #ff3f3f; +} +.btn-danger .badge { + color: #ff5858; + background-color: #fff; +} +.btn-link { + font-weight: normal; + color: #36414c; + border-radius: 0; +} +.btn-link, +.btn-link:active, +.btn-link.active, +.btn-link[disabled], +fieldset[disabled] .btn-link { + background-color: transparent; + -webkit-box-shadow: none; + box-shadow: none; +} +.btn-link, +.btn-link:hover, +.btn-link:focus, +.btn-link:active { + border-color: transparent; +} +.btn-link:hover, +.btn-link:focus { + color: #161b1f; + text-decoration: underline; + background-color: transparent; +} +.btn-link[disabled]:hover, +fieldset[disabled] .btn-link:hover, +.btn-link[disabled]:focus, +fieldset[disabled] .btn-link:focus { + color: #777; + text-decoration: none; +} +.btn-lg, +.btn-group-lg > .btn { + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; +} +.btn-sm, +.btn-group-sm > .btn { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.btn-xs, +.btn-group-xs > .btn { + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.btn-block { + display: block; + width: 100%; +} +.btn-block + .btn-block { + margin-top: 5px; +} +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} +.fade { + opacity: 0; + -webkit-transition: opacity .15s linear; + -o-transition: opacity .15s linear; + transition: opacity .15s linear; +} +.fade.in { + opacity: 1; +} +.collapse { + display: none; + visibility: hidden; +} +.collapse.in { + display: block; + visibility: visible; +} +tr.collapse.in { + display: table-row; +} +tbody.collapse.in { + display: table-row-group; +} +.collapsing { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition-timing-function: ease; + -o-transition-timing-function: ease; + transition-timing-function: ease; + -webkit-transition-duration: .35s; + -o-transition-duration: .35s; + transition-duration: .35s; + -webkit-transition-property: height, visibility; + -o-transition-property: height, visibility; + transition-property: height, visibility; +} +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: 4px solid; + border-right: 4px solid transparent; + border-left: 4px solid transparent; +} +.dropdown { + position: relative; +} +.dropdown-toggle:focus { + outline: 0; +} +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + font-size: 14px; + text-align: left; + list-style: none; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, .15); + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175); + box-shadow: 0 6px 12px rgba(0, 0, 0, .175); +} +.dropdown-menu.pull-right { + right: 0; + left: auto; +} +.dropdown-menu .divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #d8dfe5; +} +.dropdown-menu > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 1.42857143; + color: #333; + white-space: nowrap; +} +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus { + color: #262626; + text-decoration: none; + background-color: #f0f4f7; +} +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + color: inherit; + text-decoration: none; + background-color: #f0f4f7; + outline: 0; +} +.dropdown-menu > .disabled > a, +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + color: #777; +} +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + text-decoration: none; + cursor: not-allowed; + background-color: transparent; + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.open > .dropdown-menu { + display: block; +} +.open > a { + outline: 0; +} +.dropdown-menu-right { + right: 0; + left: auto; +} +.dropdown-menu-left { + right: auto; + left: 0; +} +.dropdown-header { + display: block; + padding: 3px 20px; + font-size: 12px; + line-height: 1.42857143; + color: #777; + white-space: nowrap; +} +.dropdown-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 99; +} +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + content: ""; + border-top: 0; + border-bottom: 4px solid; +} +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 2px; +} +@media (min-width: 768px) { + .navbar-right .dropdown-menu { + right: 0; + left: auto; + } + .navbar-right .dropdown-menu-left { + right: auto; + left: 0; + } +} +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; +} +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + float: left; +} +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover, +.btn-group > .btn:focus, +.btn-group-vertical > .btn:focus, +.btn-group > .btn:active, +.btn-group-vertical > .btn:active, +.btn-group > .btn.active, +.btn-group-vertical > .btn.active { + z-index: 2; +} +.btn-group .btn + .btn, +.btn-group .btn + .btn-group, +.btn-group .btn-group + .btn, +.btn-group .btn-group + .btn-group { + margin-left: -1px; +} +.btn-toolbar { + margin-left: -5px; +} +.btn-toolbar .btn-group, +.btn-toolbar .input-group { + float: left; +} +.btn-toolbar > .btn, +.btn-toolbar > .btn-group, +.btn-toolbar > .input-group { + margin-left: 5px; +} +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} +.btn-group > .btn:first-child { + margin-left: 0; +} +.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group > .btn-group { + float: left; +} +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group > .btn-group:first-child > .btn:last-child, +.btn-group > .btn-group:first-child > .dropdown-toggle { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.btn-group > .btn-group:last-child > .btn:first-child { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} +.btn-group > .btn + .dropdown-toggle { + padding-right: 8px; + padding-left: 8px; +} +.btn-group > .btn-lg + .dropdown-toggle { + padding-right: 12px; + padding-left: 12px; +} +.btn-group.open .dropdown-toggle { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); +} +.btn-group.open .dropdown-toggle.btn-link { + -webkit-box-shadow: none; + box-shadow: none; +} +.btn .caret { + margin-left: 0; +} +.btn-lg .caret { + border-width: 5px 5px 0; + border-bottom-width: 0; +} +.dropup .btn-lg .caret { + border-width: 0 5px 5px; +} +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group, +.btn-group-vertical > .btn-group > .btn { + display: block; + float: none; + width: 100%; + max-width: 100%; +} +.btn-group-vertical > .btn-group > .btn { + float: none; +} +.btn-group-vertical > .btn + .btn, +.btn-group-vertical > .btn + .btn-group, +.btn-group-vertical > .btn-group + .btn, +.btn-group-vertical > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; +} +.btn-group-vertical > .btn:not(:first-child):not(:last-child) { + border-radius: 0; +} +.btn-group-vertical > .btn:first-child:not(:last-child) { + border-top-right-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn:last-child:not(:first-child) { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-left-radius: 4px; +} +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.btn-group-justified { + display: table; + width: 100%; + table-layout: fixed; + border-collapse: separate; +} +.btn-group-justified > .btn, +.btn-group-justified > .btn-group { + display: table-cell; + float: none; + width: 1%; +} +.btn-group-justified > .btn-group .btn { + width: 100%; +} +.btn-group-justified > .btn-group .dropdown-menu { + left: auto; +} +[data-toggle="buttons"] > .btn input[type="radio"], +[data-toggle="buttons"] > .btn-group > .btn input[type="radio"], +[data-toggle="buttons"] > .btn input[type="checkbox"], +[data-toggle="buttons"] > .btn-group > .btn input[type="checkbox"] { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; +} +.input-group { + position: relative; + display: table; + border-collapse: separate; +} +.input-group[class*="col-"] { + float: none; + padding-right: 0; + padding-left: 0; +} +.input-group .form-control { + position: relative; + z-index: 2; + float: left; + width: 100%; + margin-bottom: 0; +} +.input-group-lg > .form-control, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .btn { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; +} +select.input-group-lg > .form-control, +select.input-group-lg > .input-group-addon, +select.input-group-lg > .input-group-btn > .btn { + height: 46px; + line-height: 46px; +} +textarea.input-group-lg > .form-control, +textarea.input-group-lg > .input-group-addon, +textarea.input-group-lg > .input-group-btn > .btn, +select[multiple].input-group-lg > .form-control, +select[multiple].input-group-lg > .input-group-addon, +select[multiple].input-group-lg > .input-group-btn > .btn { + height: auto; +} +.input-group-sm > .form-control, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .btn { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +select.input-group-sm > .form-control, +select.input-group-sm > .input-group-addon, +select.input-group-sm > .input-group-btn > .btn { + height: 30px; + line-height: 30px; +} +textarea.input-group-sm > .form-control, +textarea.input-group-sm > .input-group-addon, +textarea.input-group-sm > .input-group-btn > .btn, +select[multiple].input-group-sm > .form-control, +select[multiple].input-group-sm > .input-group-addon, +select[multiple].input-group-sm > .input-group-btn > .btn { + height: auto; +} +.input-group-addon, +.input-group-btn, +.input-group .form-control { + display: table-cell; +} +.input-group-addon:not(:first-child):not(:last-child), +.input-group-btn:not(:first-child):not(:last-child), +.input-group .form-control:not(:first-child):not(:last-child) { + border-radius: 0; +} +.input-group-addon, +.input-group-btn { + width: 1%; + white-space: nowrap; + vertical-align: middle; +} +.input-group-addon { + padding: 4px 10px; + font-size: 14px; + font-weight: normal; + line-height: 1; + color: #555; + text-align: center; + background-color: #f0f4f7; + border: 1px solid #d1d8dd; + border-radius: 4px; +} +.input-group-addon.input-sm { + padding: 5px 10px; + font-size: 12px; + border-radius: 3px; +} +.input-group-addon.input-lg { + padding: 10px 16px; + font-size: 18px; + border-radius: 6px; +} +.input-group-addon input[type="radio"], +.input-group-addon input[type="checkbox"] { + margin-top: 0; +} +.input-group .form-control:first-child, +.input-group-addon:first-child, +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group > .btn, +.input-group-btn:first-child > .dropdown-toggle, +.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), +.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.input-group-addon:first-child { + border-right: 0; +} +.input-group .form-control:last-child, +.input-group-addon:last-child, +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group > .btn, +.input-group-btn:last-child > .dropdown-toggle, +.input-group-btn:first-child > .btn:not(:first-child), +.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.input-group-addon:last-child { + border-left: 0; +} +.input-group-btn { + position: relative; + font-size: 0; + white-space: nowrap; +} +.input-group-btn > .btn { + position: relative; +} +.input-group-btn > .btn + .btn { + margin-left: -1px; +} +.input-group-btn > .btn:hover, +.input-group-btn > .btn:focus, +.input-group-btn > .btn:active { + z-index: 2; +} +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group { + margin-right: -1px; +} +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group { + margin-left: -1px; +} +.nav { + padding-left: 0; + margin-bottom: 0; + list-style: none; +} +.nav > li { + position: relative; + display: block; +} +.nav > li > a { + position: relative; + display: block; + padding: 10px 15px; +} +.nav > li > a:hover, +.nav > li > a:focus { + text-decoration: none; + background-color: #f7fafc; +} +.nav > li.disabled > a { + color: #777; +} +.nav > li.disabled > a:hover, +.nav > li.disabled > a:focus { + color: #777; + text-decoration: none; + cursor: not-allowed; + background-color: transparent; +} +.nav .open > a, +.nav .open > a:hover, +.nav .open > a:focus { + background-color: #f7fafc; + border-color: #36414c; +} +.nav .nav-divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; +} +.nav > li > a > img { + max-width: none; +} +.nav-tabs { + border-bottom: 1px solid #ddd; +} +.nav-tabs > li { + float: left; + margin-bottom: -1px; +} +.nav-tabs > li > a { + margin-right: 2px; + line-height: 1.42857143; + border: 1px solid transparent; + border-radius: 4px 4px 0 0; +} +.nav-tabs > li > a:hover { + border-color: #eee #eee #ddd; +} +.nav-tabs > li.active > a, +.nav-tabs > li.active > a:hover, +.nav-tabs > li.active > a:focus { + color: #555; + cursor: default; + background-color: #fff; + border: 1px solid #ddd; + border-bottom-color: transparent; +} +.nav-tabs.nav-justified { + width: 100%; + border-bottom: 0; +} +.nav-tabs.nav-justified > li { + float: none; +} +.nav-tabs.nav-justified > li > a { + margin-bottom: 5px; + text-align: center; +} +.nav-tabs.nav-justified > .dropdown .dropdown-menu { + top: auto; + left: auto; +} +@media (min-width: 768px) { + .nav-tabs.nav-justified > li { + display: table-cell; + width: 1%; + } + .nav-tabs.nav-justified > li > a { + margin-bottom: 0; + } +} +.nav-tabs.nav-justified > li > a { + margin-right: 0; + border-radius: 4px; +} +.nav-tabs.nav-justified > .active > a, +.nav-tabs.nav-justified > .active > a:hover, +.nav-tabs.nav-justified > .active > a:focus { + border: 1px solid #ddd; +} +@media (min-width: 768px) { + .nav-tabs.nav-justified > li > a { + border-bottom: 1px solid #ddd; + border-radius: 4px 4px 0 0; + } + .nav-tabs.nav-justified > .active > a, + .nav-tabs.nav-justified > .active > a:hover, + .nav-tabs.nav-justified > .active > a:focus { + border-bottom-color: #fff; + } +} +.nav-pills > li { + float: left; +} +.nav-pills > li > a { + border-radius: 4px; +} +.nav-pills > li + li { + margin-left: 2px; +} +.nav-pills > li.active > a, +.nav-pills > li.active > a:hover, +.nav-pills > li.active > a:focus { + color: inherit; + background-color: #f7fafc; +} +.nav-stacked > li { + float: none; +} +.nav-stacked > li + li { + margin-top: 2px; + margin-left: 0; +} +.nav-justified { + width: 100%; +} +.nav-justified > li { + float: none; +} +.nav-justified > li > a { + margin-bottom: 5px; + text-align: center; +} +.nav-justified > .dropdown .dropdown-menu { + top: auto; + left: auto; +} +@media (min-width: 768px) { + .nav-justified > li { + display: table-cell; + width: 1%; + } + .nav-justified > li > a { + margin-bottom: 0; + } +} +.nav-tabs-justified { + border-bottom: 0; +} +.nav-tabs-justified > li > a { + margin-right: 0; + border-radius: 4px; +} +.nav-tabs-justified > .active > a, +.nav-tabs-justified > .active > a:hover, +.nav-tabs-justified > .active > a:focus { + border: 1px solid #ddd; +} +@media (min-width: 768px) { + .nav-tabs-justified > li > a { + border-bottom: 1px solid #ddd; + border-radius: 4px 4px 0 0; + } + .nav-tabs-justified > .active > a, + .nav-tabs-justified > .active > a:hover, + .nav-tabs-justified > .active > a:focus { + border-bottom-color: #fff; + } +} +.tab-content > .tab-pane { + display: none; + visibility: hidden; +} +.tab-content > .active { + display: block; + visibility: visible; +} +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.navbar { + position: relative; + min-height: 40px; + margin-bottom: 20px; + border: 1px solid transparent; +} +@media (min-width: 768px) { + .navbar { + border-radius: 4px; + } +} +@media (min-width: 768px) { + .navbar-header { + float: left; + } +} +.navbar-collapse { + padding-right: 15px; + padding-left: 15px; + overflow-x: visible; + -webkit-overflow-scrolling: touch; + border-top: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); +} +.navbar-collapse.in { + overflow-y: auto; +} +@media (min-width: 768px) { + .navbar-collapse { + width: auto; + border-top: 0; + -webkit-box-shadow: none; + box-shadow: none; + } + .navbar-collapse.collapse { + display: block !important; + height: auto !important; + padding-bottom: 0; + overflow: visible !important; + visibility: visible !important; + } + .navbar-collapse.in { + overflow-y: visible; + } + .navbar-fixed-top .navbar-collapse, + .navbar-static-top .navbar-collapse, + .navbar-fixed-bottom .navbar-collapse { + padding-right: 0; + padding-left: 0; + } +} +.navbar-fixed-top .navbar-collapse, +.navbar-fixed-bottom .navbar-collapse { + max-height: 340px; +} +@media (max-device-width: 480px) and (orientation: landscape) { + .navbar-fixed-top .navbar-collapse, + .navbar-fixed-bottom .navbar-collapse { + max-height: 200px; + } +} +.container > .navbar-header, +.container-fluid > .navbar-header, +.container > .navbar-collapse, +.container-fluid > .navbar-collapse { + margin-right: -15px; + margin-left: -15px; +} +@media (min-width: 768px) { + .container > .navbar-header, + .container-fluid > .navbar-header, + .container > .navbar-collapse, + .container-fluid > .navbar-collapse { + margin-right: 0; + margin-left: 0; + } +} +.navbar-static-top { + z-index: 1000; + border-width: 0 0 1px; +} +@media (min-width: 768px) { + .navbar-static-top { + border-radius: 0; + } +} +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: 1030; +} +@media (min-width: 768px) { + .navbar-fixed-top, + .navbar-fixed-bottom { + border-radius: 0; + } +} +.navbar-fixed-top { + top: 0; + border-width: 0 0 1px; +} +.navbar-fixed-bottom { + bottom: 0; + margin-bottom: 0; + border-width: 1px 0 0; +} +.navbar-brand { + float: left; + height: 40px; + padding: 10px 15px; + font-size: 18px; + line-height: 20px; +} +.navbar-brand:hover, +.navbar-brand:focus { + text-decoration: none; +} +.navbar-brand > img { + display: block; +} +@media (min-width: 768px) { + .navbar > .container .navbar-brand, + .navbar > .container-fluid .navbar-brand { + margin-left: -15px; + } +} +.navbar-toggle { + position: relative; + float: right; + padding: 9px 10px; + margin-top: 3px; + margin-right: 15px; + margin-bottom: 3px; + background-color: transparent; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} +.navbar-toggle:focus { + outline: 0; +} +.navbar-toggle .icon-bar { + display: block; + width: 22px; + height: 2px; + border-radius: 1px; +} +.navbar-toggle .icon-bar + .icon-bar { + margin-top: 4px; +} +@media (min-width: 768px) { + .navbar-toggle { + display: none; + } +} +.navbar-nav { + margin: 5px -15px; +} +.navbar-nav > li > a { + padding-top: 10px; + padding-bottom: 10px; + line-height: 20px; +} +@media (max-width: 767px) { + .navbar-nav .open .dropdown-menu { + position: static; + float: none; + width: auto; + margin-top: 0; + background-color: transparent; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + } + .navbar-nav .open .dropdown-menu > li > a, + .navbar-nav .open .dropdown-menu .dropdown-header { + padding: 5px 15px 5px 25px; + } + .navbar-nav .open .dropdown-menu > li > a { + line-height: 20px; + } + .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-nav .open .dropdown-menu > li > a:focus { + background-image: none; + } +} +@media (min-width: 768px) { + .navbar-nav { + float: left; + margin: 0; + } + .navbar-nav > li { + float: left; + } + .navbar-nav > li > a { + padding-top: 10px; + padding-bottom: 10px; + } +} +.navbar-form { + padding: 10px 15px; + margin-top: 5px; + margin-right: -15px; + margin-bottom: 5px; + margin-left: -15px; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); +} +@media (min-width: 768px) { + .navbar-form .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .navbar-form .form-control-static { + display: inline-block; + } + .navbar-form .input-group { + display: inline-table; + vertical-align: middle; + } + .navbar-form .input-group .input-group-addon, + .navbar-form .input-group .input-group-btn, + .navbar-form .input-group .form-control { + width: auto; + } + .navbar-form .input-group > .form-control { + width: 100%; + } + .navbar-form .control-label { + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .radio, + .navbar-form .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .radio label, + .navbar-form .checkbox label { + padding-left: 0; + } + .navbar-form .radio input[type="radio"], + .navbar-form .checkbox input[type="checkbox"] { + position: relative; + margin-left: 0; + } + .navbar-form .has-feedback .form-control-feedback { + top: 0; + } +} +@media (max-width: 767px) { + .navbar-form .form-group { + margin-bottom: 5px; + } + .navbar-form .form-group:last-child { + margin-bottom: 0; + } +} +@media (min-width: 768px) { + .navbar-form { + width: auto; + padding-top: 0; + padding-bottom: 0; + margin-right: 0; + margin-left: 0; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + } +} +.navbar-nav > li > .dropdown-menu { + margin-top: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { + margin-bottom: 0; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.navbar-btn { + margin-top: 5px; + margin-bottom: 5px; +} +.navbar-btn.btn-sm { + margin-top: 5px; + margin-bottom: 5px; +} +.navbar-btn.btn-xs { + margin-top: 9px; + margin-bottom: 9px; +} +.navbar-text { + margin-top: 10px; + margin-bottom: 10px; +} +@media (min-width: 768px) { + .navbar-text { + float: left; + margin-right: 15px; + margin-left: 15px; + } +} +@media (min-width: 768px) { + .navbar-left { + float: left !important; + } + .navbar-right { + float: right !important; + margin-right: -15px; + } + .navbar-right ~ .navbar-right { + margin-right: 0; + } +} +.navbar-default { + background-color: #f5f7fa; + border-color: #ebeff2; +} +.navbar-default .navbar-brand { + color: #6c7680; +} +.navbar-default .navbar-brand:hover, +.navbar-default .navbar-brand:focus { + color: #36414c; + background-color: transparent; +} +.navbar-default .navbar-text { + color: #6c7680; +} +.navbar-default .navbar-nav > li > a { + color: #6c7680; +} +.navbar-default .navbar-nav > li > a:hover, +.navbar-default .navbar-nav > li > a:focus { + color: #36414c; + background-color: transparent; +} +.navbar-default .navbar-nav > .active > a, +.navbar-default .navbar-nav > .active > a:hover, +.navbar-default .navbar-nav > .active > a:focus { + color: #555; + background-color: #dfe5ef; +} +.navbar-default .navbar-nav > .disabled > a, +.navbar-default .navbar-nav > .disabled > a:hover, +.navbar-default .navbar-nav > .disabled > a:focus { + color: #ccc; + background-color: transparent; +} +.navbar-default .navbar-toggle { + border-color: #ddd; +} +.navbar-default .navbar-toggle:hover, +.navbar-default .navbar-toggle:focus { + background-color: #ddd; +} +.navbar-default .navbar-toggle .icon-bar { + background-color: #888; +} +.navbar-default .navbar-collapse, +.navbar-default .navbar-form { + border-color: #ebeff2; +} +.navbar-default .navbar-nav > .open > a, +.navbar-default .navbar-nav > .open > a:hover, +.navbar-default .navbar-nav > .open > a:focus { + color: #555; + background-color: #dfe5ef; +} +@media (max-width: 767px) { + .navbar-default .navbar-nav .open .dropdown-menu > li > a { + color: #6c7680; + } + .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { + color: #36414c; + background-color: transparent; + } + .navbar-default .navbar-nav .open .dropdown-menu > .active > a, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #555; + background-color: #dfe5ef; + } + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #ccc; + background-color: transparent; + } +} +.navbar-default .navbar-link { + color: #6c7680; +} +.navbar-default .navbar-link:hover { + color: #36414c; +} +.navbar-default .btn-link { + color: #6c7680; +} +.navbar-default .btn-link:hover, +.navbar-default .btn-link:focus { + color: #36414c; +} +.navbar-default .btn-link[disabled]:hover, +fieldset[disabled] .navbar-default .btn-link:hover, +.navbar-default .btn-link[disabled]:focus, +fieldset[disabled] .navbar-default .btn-link:focus { + color: #ccc; +} +.navbar-inverse { + background-color: #35414b; + border-color: #2a343c; +} +.navbar-inverse .navbar-brand { + color: #9d9d9d; +} +.navbar-inverse .navbar-brand:hover, +.navbar-inverse .navbar-brand:focus { + color: #fff; + background-color: transparent; +} +.navbar-inverse .navbar-text { + color: #b8c2cb; +} +.navbar-inverse .navbar-nav > li > a { + color: #9d9d9d; +} +.navbar-inverse .navbar-nav > li > a:hover, +.navbar-inverse .navbar-nav > li > a:focus { + color: #fff; + background-color: transparent; +} +.navbar-inverse .navbar-nav > .active > a, +.navbar-inverse .navbar-nav > .active > a:hover, +.navbar-inverse .navbar-nav > .active > a:focus { + color: #fff; + background-color: #20272d; +} +.navbar-inverse .navbar-nav > .disabled > a, +.navbar-inverse .navbar-nav > .disabled > a:hover, +.navbar-inverse .navbar-nav > .disabled > a:focus { + color: #475765; + background-color: transparent; +} +.navbar-inverse .navbar-toggle { + border-color: #475765; +} +.navbar-inverse .navbar-toggle:hover, +.navbar-inverse .navbar-toggle:focus { + background-color: #475765; +} +.navbar-inverse .navbar-toggle .icon-bar { + background-color: #fff; +} +.navbar-inverse .navbar-collapse, +.navbar-inverse .navbar-form { + border-color: #262f36; +} +.navbar-inverse .navbar-nav > .open > a, +.navbar-inverse .navbar-nav > .open > a:hover, +.navbar-inverse .navbar-nav > .open > a:focus { + color: #fff; + background-color: #20272d; +} +@media (max-width: 767px) { + .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { + border-color: #2a343c; + } + .navbar-inverse .navbar-nav .open .dropdown-menu .divider { + background-color: #2a343c; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { + color: #9d9d9d; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { + color: #fff; + background-color: transparent; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #fff; + background-color: #20272d; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #475765; + background-color: transparent; + } +} +.navbar-inverse .navbar-link { + color: #9d9d9d; +} +.navbar-inverse .navbar-link:hover { + color: #fff; +} +.navbar-inverse .btn-link { + color: #9d9d9d; +} +.navbar-inverse .btn-link:hover, +.navbar-inverse .btn-link:focus { + color: #fff; +} +.navbar-inverse .btn-link[disabled]:hover, +fieldset[disabled] .navbar-inverse .btn-link:hover, +.navbar-inverse .btn-link[disabled]:focus, +fieldset[disabled] .navbar-inverse .btn-link:focus { + color: #475765; +} +.breadcrumb { + padding: 8px 15px; + margin-bottom: 20px; + list-style: none; + background-color: #f5f5f5; + border-radius: 4px; +} +.breadcrumb > li { + display: inline-block; +} +.breadcrumb > li + li:before { + padding: 0 5px; + color: #ccc; + content: "/\00a0"; +} +.breadcrumb > .active { + color: #777; +} +.pagination { + display: inline-block; + padding-left: 0; + margin: 20px 0; + border-radius: 4px; +} +.pagination > li { + display: inline; +} +.pagination > li > a, +.pagination > li > span { + position: relative; + float: left; + padding: 4px 10px; + margin-left: -1px; + line-height: 1.42857143; + color: #36414c; + text-decoration: none; + background-color: #fff; + border: 1px solid #ddd; +} +.pagination > li:first-child > a, +.pagination > li:first-child > span { + margin-left: 0; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} +.pagination > li:last-child > a, +.pagination > li:last-child > span { + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} +.pagination > li > a:hover, +.pagination > li > span:hover, +.pagination > li > a:focus, +.pagination > li > span:focus { + color: #161b1f; + background-color: #eee; + border-color: #ddd; +} +.pagination > .active > a, +.pagination > .active > span, +.pagination > .active > a:hover, +.pagination > .active > span:hover, +.pagination > .active > a:focus, +.pagination > .active > span:focus { + z-index: 2; + color: #fff; + cursor: default; + background-color: #5e64ff; + border-color: #5e64ff; +} +.pagination > .disabled > span, +.pagination > .disabled > span:hover, +.pagination > .disabled > span:focus, +.pagination > .disabled > a, +.pagination > .disabled > a:hover, +.pagination > .disabled > a:focus { + color: #777; + cursor: not-allowed; + background-color: #fff; + border-color: #ddd; +} +.pagination-lg > li > a, +.pagination-lg > li > span { + padding: 10px 16px; + font-size: 18px; +} +.pagination-lg > li:first-child > a, +.pagination-lg > li:first-child > span { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; +} +.pagination-lg > li:last-child > a, +.pagination-lg > li:last-child > span { + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; +} +.pagination-sm > li > a, +.pagination-sm > li > span { + padding: 5px 10px; + font-size: 12px; +} +.pagination-sm > li:first-child > a, +.pagination-sm > li:first-child > span { + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; +} +.pagination-sm > li:last-child > a, +.pagination-sm > li:last-child > span { + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} +.pager { + padding-left: 0; + margin: 20px 0; + text-align: center; + list-style: none; +} +.pager li { + display: inline; +} +.pager li > a, +.pager li > span { + display: inline-block; + padding: 5px 14px; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 15px; +} +.pager li > a:hover, +.pager li > a:focus { + text-decoration: none; + background-color: #eee; +} +.pager .next > a, +.pager .next > span { + float: right; +} +.pager .previous > a, +.pager .previous > span { + float: left; +} +.pager .disabled > a, +.pager .disabled > a:hover, +.pager .disabled > a:focus, +.pager .disabled > span { + color: #777; + cursor: not-allowed; + background-color: #fff; +} +.label { + display: inline; + padding: .2em .6em .3em; + font-size: 75%; + font-weight: bold; + line-height: 1; + color: inherit; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; +} +a.label:hover, +a.label:focus { + color: #fff; + text-decoration: none; + cursor: pointer; +} +.label:empty { + display: none; +} +.btn .label { + position: relative; + top: -1px; +} +.label-default { + background-color: #f0f4f7; +} +.label-default[href]:hover, +.label-default[href]:focus { + background-color: #cfdce5; +} +.label-primary { + background-color: #d9f6ff; +} +.label-primary[href]:hover, +.label-primary[href]:focus { + background-color: #a6eaff; +} +.label-success { + background-color: #e4ffc1; +} +.label-success[href]:hover, +.label-success[href]:focus { + background-color: #ceff8e; +} +.label-info { + background-color: #e8ddff; +} +.label-info[href]:hover, +.label-info[href]:focus { + background-color: #c6aaff; +} +.label-warning { + background-color: #ffe6bf; +} +.label-warning[href]:hover, +.label-warning[href]:focus { + background-color: #ffd28c; +} +.label-danger { + background-color: #ffdcdc; +} +.label-danger[href]:hover, +.label-danger[href]:focus { + background-color: #ffa9a9; +} +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: 12px; + font-weight: bold; + line-height: 1; + color: #36414c; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + background-color: #f0f4f7; + border-radius: 10px; +} +.badge:empty { + display: none; +} +.btn .badge { + position: relative; + top: -1px; +} +.btn-xs .badge { + top: 0; + padding: 1px 5px; +} +a.badge:hover, +a.badge:focus { + color: #fff; + text-decoration: none; + cursor: pointer; +} +.list-group-item.active > .badge, +.nav-pills > .active > a > .badge { + color: #36414c; + background-color: #fff; +} +.list-group-item > .badge { + float: right; +} +.list-group-item > .badge + .badge { + margin-right: 5px; +} +.nav-pills > li > a > .badge { + margin-left: 3px; +} +.jumbotron { + padding: 30px 15px; + margin-bottom: 30px; + color: inherit; + background-color: #eee; +} +.jumbotron h1, +.jumbotron .h1 { + color: inherit; +} +.jumbotron p { + margin-bottom: 15px; + font-size: 21px; + font-weight: 200; +} +.jumbotron > hr { + border-top-color: #d5d5d5; +} +.container .jumbotron, +.container-fluid .jumbotron { + border-radius: 6px; +} +.jumbotron .container { + max-width: 100%; +} +@media screen and (min-width: 768px) { + .jumbotron { + padding: 48px 0; + } + .container .jumbotron, + .container-fluid .jumbotron { + padding-right: 60px; + padding-left: 60px; + } + .jumbotron h1, + .jumbotron .h1 { + font-size: 63px; + } +} +.thumbnail { + display: block; + padding: 4px; + margin-bottom: 20px; + line-height: 1.42857143; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + -webkit-transition: border .2s ease-in-out; + -o-transition: border .2s ease-in-out; + transition: border .2s ease-in-out; +} +.thumbnail > img, +.thumbnail a > img { + margin-right: auto; + margin-left: auto; +} +a.thumbnail:hover, +a.thumbnail:focus, +a.thumbnail.active { + border-color: #36414c; +} +.thumbnail .caption { + padding: 9px; + color: #36414c; +} +.alert { + padding: 10px; + margin-bottom: 20px; + border: 1px solid transparent; + border-radius: 4px; +} +.alert h4 { + margin-top: 0; + color: inherit; +} +.alert .alert-link { + font-weight: bold; +} +.alert > p, +.alert > ul { + margin-bottom: 0; +} +.alert > p + p { + margin-top: 5px; +} +.alert-dismissable, +.alert-dismissible { + padding-right: 30px; +} +.alert-dismissable .close, +.alert-dismissible .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; +} +.alert-success { + color: #98d85b; + background-color: rgba(255, 255, 255, .9); + border-color: #98d85b; +} +.alert-success hr { + border-top-color: #8bd346; +} +.alert-success .alert-link { + color: #7ece32; +} +.alert-info { + color: #935eff; + background-color: rgba(255, 255, 255, .9); + border-color: #935eff; +} +.alert-info hr { + border-top-color: #8244ff; +} +.alert-info .alert-link { + color: #712bff; +} +.alert-warning { + color: #ffa00a; + background-color: rgba(255, 255, 255, .9); + border-color: #ffa00a; +} +.alert-warning hr { + border-top-color: #f09300; +} +.alert-warning .alert-link { + color: #d68300; +} +.alert-danger { + color: #ff5858; + background-color: rgba(255, 255, 255, .9); + border-color: #ff5858; +} +.alert-danger hr { + border-top-color: #ff3f3f; +} +.alert-danger .alert-link { + color: #ff2525; +} +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +@-o-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +.progress { + height: 20px; + margin-bottom: 20px; + overflow: hidden; + background-color: #f5f5f5; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); +} +.progress-bar { + float: left; + width: 0; + height: 100%; + font-size: 12px; + line-height: 20px; + color: #fff; + text-align: center; + background-color: #36414c; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); + -webkit-transition: width .6s ease; + -o-transition: width .6s ease; + transition: width .6s ease; +} +.progress-striped .progress-bar, +.progress-bar-striped { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + background-size: 40px 40px; +} +.progress.active .progress-bar, +.progress-bar.active { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} +.progress-bar-success { + background-color: #98d85b; +} +.progress-striped .progress-bar-success { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-info { + background-color: #935eff; +} +.progress-striped .progress-bar-info { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-warning { + background-color: #ffa00a; +} +.progress-striped .progress-bar-warning { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-danger { + background-color: #ff5858; +} +.progress-striped .progress-bar-danger { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.media { + margin-top: 15px; +} +.media:first-child { + margin-top: 0; +} +.media, +.media-body { + overflow: hidden; + zoom: 1; +} +.media-body { + width: 10000px; +} +.media-object { + display: block; +} +.media-right, +.media > .pull-right { + padding-left: 10px; +} +.media-left, +.media > .pull-left { + padding-right: 10px; +} +.media-left, +.media-right, +.media-body { + display: table-cell; + vertical-align: top; +} +.media-middle { + vertical-align: middle; +} +.media-bottom { + vertical-align: bottom; +} +.media-heading { + margin-top: 0; + margin-bottom: 5px; +} +.media-list { + padding-left: 0; + list-style: none; +} +.list-group { + padding-left: 0; + margin-bottom: 20px; +} +.list-group-item { + position: relative; + display: block; + padding: 10px 15px; + margin-bottom: -1px; + background-color: #fff; + border: 1px solid #ddd; +} +.list-group-item:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} +.list-group-item:last-child { + margin-bottom: 0; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; +} +a.list-group-item { + color: #555; +} +a.list-group-item .list-group-item-heading { + color: #333; +} +a.list-group-item:hover, +a.list-group-item:focus { + color: #555; + text-decoration: none; + background-color: #f5f5f5; +} +.list-group-item.disabled, +.list-group-item.disabled:hover, +.list-group-item.disabled:focus { + color: #777; + cursor: not-allowed; + background-color: #eee; +} +.list-group-item.disabled .list-group-item-heading, +.list-group-item.disabled:hover .list-group-item-heading, +.list-group-item.disabled:focus .list-group-item-heading { + color: inherit; +} +.list-group-item.disabled .list-group-item-text, +.list-group-item.disabled:hover .list-group-item-text, +.list-group-item.disabled:focus .list-group-item-text { + color: #777; +} +.list-group-item.active, +.list-group-item.active:hover, +.list-group-item.active:focus { + z-index: 2; + color: inherit; + background-color: #f0f4f7; + border-color: #f0f4f7; +} +.list-group-item.active .list-group-item-heading, +.list-group-item.active:hover .list-group-item-heading, +.list-group-item.active:focus .list-group-item-heading, +.list-group-item.active .list-group-item-heading > small, +.list-group-item.active:hover .list-group-item-heading > small, +.list-group-item.active:focus .list-group-item-heading > small, +.list-group-item.active .list-group-item-heading > .small, +.list-group-item.active:hover .list-group-item-heading > .small, +.list-group-item.active:focus .list-group-item-heading > .small { + color: inherit; +} +.list-group-item.active .list-group-item-text, +.list-group-item.active:hover .list-group-item-text, +.list-group-item.active:focus .list-group-item-text { + color: #fff; +} +.list-group-item-success { + color: #98d85b; + background-color: transparent; +} +a.list-group-item-success { + color: #98d85b; +} +a.list-group-item-success .list-group-item-heading { + color: inherit; +} +a.list-group-item-success:hover, +a.list-group-item-success:focus { + color: #98d85b; + background-color: rgba(0, 0, 0, 0); +} +a.list-group-item-success.active, +a.list-group-item-success.active:hover, +a.list-group-item-success.active:focus { + color: #fff; + background-color: #98d85b; + border-color: #98d85b; +} +.list-group-item-info { + color: #935eff; + background-color: transparent; +} +a.list-group-item-info { + color: #935eff; +} +a.list-group-item-info .list-group-item-heading { + color: inherit; +} +a.list-group-item-info:hover, +a.list-group-item-info:focus { + color: #935eff; + background-color: rgba(0, 0, 0, 0); +} +a.list-group-item-info.active, +a.list-group-item-info.active:hover, +a.list-group-item-info.active:focus { + color: #fff; + background-color: #935eff; + border-color: #935eff; +} +.list-group-item-warning { + color: #ffa00a; + background-color: transparent; +} +a.list-group-item-warning { + color: #ffa00a; +} +a.list-group-item-warning .list-group-item-heading { + color: inherit; +} +a.list-group-item-warning:hover, +a.list-group-item-warning:focus { + color: #ffa00a; + background-color: rgba(0, 0, 0, 0); +} +a.list-group-item-warning.active, +a.list-group-item-warning.active:hover, +a.list-group-item-warning.active:focus { + color: #fff; + background-color: #ffa00a; + border-color: #ffa00a; +} +.list-group-item-danger { + color: #ff5858; + background-color: transparent; +} +a.list-group-item-danger { + color: #ff5858; +} +a.list-group-item-danger .list-group-item-heading { + color: inherit; +} +a.list-group-item-danger:hover, +a.list-group-item-danger:focus { + color: #ff5858; + background-color: rgba(0, 0, 0, 0); +} +a.list-group-item-danger.active, +a.list-group-item-danger.active:hover, +a.list-group-item-danger.active:focus { + color: #fff; + background-color: #ff5858; + border-color: #ff5858; +} +.list-group-item-heading { + margin-top: 0; + margin-bottom: 5px; +} +.list-group-item-text { + margin-bottom: 0; + line-height: 1.3; +} +.panel { + margin-bottom: 20px; + background-color: #fff; + border: 1px solid transparent; + border-radius: 4px; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05); + box-shadow: 0 1px 1px rgba(0, 0, 0, .05); +} +.panel-body { + padding: 15px; +} +.panel-heading { + padding: 10px 15px; + border-bottom: 1px solid transparent; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel-heading > .dropdown .dropdown-toggle { + color: inherit; +} +.panel-title { + margin-top: 0; + margin-bottom: 0; + font-size: 16px; + color: inherit; +} +.panel-title > a { + color: inherit; +} +.panel-footer { + padding: 10px 15px; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .list-group, +.panel > .panel-collapse > .list-group { + margin-bottom: 0; +} +.panel > .list-group .list-group-item, +.panel > .panel-collapse > .list-group .list-group-item { + border-width: 1px 0; + border-radius: 0; +} +.panel > .list-group:first-child .list-group-item:first-child, +.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child { + border-top: 0; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel > .list-group:last-child .list-group-item:last-child, +.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child { + border-bottom: 0; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel-heading + .list-group .list-group-item:first-child { + border-top-width: 0; +} +.list-group + .panel-footer { + border-top-width: 0; +} +.panel > .table, +.panel > .table-responsive > .table, +.panel > .panel-collapse > .table { + margin-bottom: 0; +} +.panel > .table caption, +.panel > .table-responsive > .table caption, +.panel > .panel-collapse > .table caption { + padding-right: 15px; + padding-left: 15px; +} +.panel > .table:first-child, +.panel > .table-responsive:first-child > .table:first-child { + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel > .table:first-child > thead:first-child > tr:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child { + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel > .table:first-child > thead:first-child > tr:first-child td:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child, +.panel > .table:first-child > thead:first-child > tr:first-child th:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child { + border-top-left-radius: 3px; +} +.panel > .table:first-child > thead:first-child > tr:first-child td:last-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child, +.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child, +.panel > .table:first-child > thead:first-child > tr:first-child th:last-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child, +.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child { + border-top-right-radius: 3px; +} +.panel > .table:last-child, +.panel > .table-responsive:last-child > .table:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .table:last-child > tbody:last-child > tr:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child, +.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child { + border-bottom-left-radius: 3px; +} +.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child, +.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child { + border-bottom-right-radius: 3px; +} +.panel > .panel-body + .table, +.panel > .panel-body + .table-responsive, +.panel > .table + .panel-body, +.panel > .table-responsive + .panel-body { + border-top: 1px solid #d1d8dd; +} +.panel > .table > tbody:first-child > tr:first-child th, +.panel > .table > tbody:first-child > tr:first-child td { + border-top: 0; +} +.panel > .table-bordered, +.panel > .table-responsive > .table-bordered { + border: 0; +} +.panel > .table-bordered > thead > tr > th:first-child, +.panel > .table-responsive > .table-bordered > thead > tr > th:first-child, +.panel > .table-bordered > tbody > tr > th:first-child, +.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child, +.panel > .table-bordered > tfoot > tr > th:first-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child, +.panel > .table-bordered > thead > tr > td:first-child, +.panel > .table-responsive > .table-bordered > thead > tr > td:first-child, +.panel > .table-bordered > tbody > tr > td:first-child, +.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child, +.panel > .table-bordered > tfoot > tr > td:first-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; +} +.panel > .table-bordered > thead > tr > th:last-child, +.panel > .table-responsive > .table-bordered > thead > tr > th:last-child, +.panel > .table-bordered > tbody > tr > th:last-child, +.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child, +.panel > .table-bordered > tfoot > tr > th:last-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child, +.panel > .table-bordered > thead > tr > td:last-child, +.panel > .table-responsive > .table-bordered > thead > tr > td:last-child, +.panel > .table-bordered > tbody > tr > td:last-child, +.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child, +.panel > .table-bordered > tfoot > tr > td:last-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; +} +.panel > .table-bordered > thead > tr:first-child > td, +.panel > .table-responsive > .table-bordered > thead > tr:first-child > td, +.panel > .table-bordered > tbody > tr:first-child > td, +.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td, +.panel > .table-bordered > thead > tr:first-child > th, +.panel > .table-responsive > .table-bordered > thead > tr:first-child > th, +.panel > .table-bordered > tbody > tr:first-child > th, +.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th { + border-bottom: 0; +} +.panel > .table-bordered > tbody > tr:last-child > td, +.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td, +.panel > .table-bordered > tfoot > tr:last-child > td, +.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td, +.panel > .table-bordered > tbody > tr:last-child > th, +.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th, +.panel > .table-bordered > tfoot > tr:last-child > th, +.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th { + border-bottom: 0; +} +.panel > .table-responsive { + margin-bottom: 0; + border: 0; +} +.panel-group { + margin-bottom: 20px; +} +.panel-group .panel { + margin-bottom: 0; + border-radius: 4px; +} +.panel-group .panel + .panel { + margin-top: 5px; +} +.panel-group .panel-heading { + border-bottom: 0; +} +.panel-group .panel-heading + .panel-collapse > .panel-body, +.panel-group .panel-heading + .panel-collapse > .list-group { + border-top: 1px solid #ddd; +} +.panel-group .panel-footer { + border-top: 0; +} +.panel-group .panel-footer + .panel-collapse .panel-body { + border-bottom: 1px solid #ddd; +} +.panel-default { + border-color: #ced5db; +} +.panel-default > .panel-heading { + color: #333; + background-color: #f7fafc; + border-color: #ced5db; +} +.panel-default > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #ced5db; +} +.panel-default > .panel-heading .badge { + color: #f7fafc; + background-color: #333; +} +.panel-default > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #ced5db; +} +.panel-primary { + border-color: #5e64ff; +} +.panel-primary > .panel-heading { + color: #fff; + background-color: #5e64ff; + border-color: #5e64ff; +} +.panel-primary > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #5e64ff; +} +.panel-primary > .panel-heading .badge { + color: #5e64ff; + background-color: #fff; +} +.panel-primary > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #5e64ff; +} +.panel-success { + border-color: rgba(0, 0, 0, 0); +} +.panel-success > .panel-heading { + color: #98d85b; + background-color: transparent; + border-color: rgba(0, 0, 0, 0); +} +.panel-success > .panel-heading + .panel-collapse > .panel-body { + border-top-color: rgba(0, 0, 0, 0); +} +.panel-success > .panel-heading .badge { + color: transparent; + background-color: #98d85b; +} +.panel-success > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: rgba(0, 0, 0, 0); +} +.panel-info { + border-color: rgba(0, 0, 0, 0); +} +.panel-info > .panel-heading { + color: #935eff; + background-color: transparent; + border-color: rgba(0, 0, 0, 0); +} +.panel-info > .panel-heading + .panel-collapse > .panel-body { + border-top-color: rgba(0, 0, 0, 0); +} +.panel-info > .panel-heading .badge { + color: transparent; + background-color: #935eff; +} +.panel-info > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: rgba(0, 0, 0, 0); +} +.panel-warning { + border-color: rgba(0, 0, 0, 0); +} +.panel-warning > .panel-heading { + color: #ffa00a; + background-color: transparent; + border-color: rgba(0, 0, 0, 0); +} +.panel-warning > .panel-heading + .panel-collapse > .panel-body { + border-top-color: rgba(0, 0, 0, 0); +} +.panel-warning > .panel-heading .badge { + color: transparent; + background-color: #ffa00a; +} +.panel-warning > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: rgba(0, 0, 0, 0); +} +.panel-danger { + border-color: rgba(0, 0, 0, 0); +} +.panel-danger > .panel-heading { + color: #ff5858; + background-color: transparent; + border-color: rgba(0, 0, 0, 0); +} +.panel-danger > .panel-heading + .panel-collapse > .panel-body { + border-top-color: rgba(0, 0, 0, 0); +} +.panel-danger > .panel-heading .badge { + color: transparent; + background-color: #ff5858; +} +.panel-danger > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: rgba(0, 0, 0, 0); +} +.embed-responsive { + position: relative; + display: block; + height: 0; + padding: 0; + overflow: hidden; +} +.embed-responsive .embed-responsive-item, +.embed-responsive iframe, +.embed-responsive embed, +.embed-responsive object, +.embed-responsive video { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + border: 0; +} +.embed-responsive.embed-responsive-16by9 { + padding-bottom: 56.25%; +} +.embed-responsive.embed-responsive-4by3 { + padding-bottom: 75%; +} +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #fafbfc; + border: 1px solid #d1d8dd; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); +} +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, .15); +} +.well-lg { + padding: 24px; + border-radius: 6px; +} +.well-sm { + padding: 9px; + border-radius: 3px; +} +.close { + float: right; + font-size: 21px; + font-weight: bold; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + filter: alpha(opacity=20); + opacity: .2; +} +.close:hover, +.close:focus { + color: #000; + text-decoration: none; + cursor: pointer; + filter: alpha(opacity=50); + opacity: .5; +} +button.close { + -webkit-appearance: none; + padding: 0; + cursor: pointer; + background: transparent; + border: 0; +} +.modal-open { + overflow: hidden; +} +.modal { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + display: none; + overflow: hidden; + -webkit-overflow-scrolling: touch; + outline: 0; +} +.modal.fade .modal-dialog { + -webkit-transition: -webkit-transform .3s ease-out; + -o-transition: -o-transform .3s ease-out; + transition: transform .3s ease-out; + -webkit-transform: translate(0, -25%); + -ms-transform: translate(0, -25%); + -o-transform: translate(0, -25%); + transform: translate(0, -25%); +} +.modal.in .modal-dialog { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + -o-transform: translate(0, 0); + transform: translate(0, 0); +} +.modal-open .modal { + overflow-x: hidden; + overflow-y: auto; +} +.modal-dialog { + position: relative; + width: auto; + margin: 10px; +} +.modal-content { + position: relative; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, .2); + border-radius: 6px; + outline: 0; + -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, .5); + box-shadow: 0 3px 9px rgba(0, 0, 0, .5); +} +.modal-backdrop { + position: absolute; + top: 0; + right: 0; + left: 0; + background-color: #334143; +} +.modal-backdrop.fade { + filter: alpha(opacity=0); + opacity: 0; +} +.modal-backdrop.in { + filter: alpha(opacity=50); + opacity: .5; +} +.modal-header { + min-height: 16.42857143px; + padding: 15px; + border-bottom: 1px solid #e5e5e5; +} +.modal-header .close { + margin-top: -2px; +} +.modal-title { + margin: 0; + line-height: 1.42857143; +} +.modal-body { + position: relative; + padding: 15px; +} +.modal-footer { + padding: 15px; + text-align: right; + border-top: 1px solid #e5e5e5; +} +.modal-footer .btn + .btn { + margin-bottom: 0; + margin-left: 5px; +} +.modal-footer .btn-group .btn + .btn { + margin-left: -1px; +} +.modal-footer .btn-block + .btn-block { + margin-left: 0; +} +.modal-scrollbar-measure { + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; +} +@media (min-width: 768px) { + .modal-dialog { + width: 600px; + margin: 30px auto; + } + .modal-content { + -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, .5); + box-shadow: 0 5px 15px rgba(0, 0, 0, .5); + } + .modal-sm { + width: 300px; + } +} +@media (min-width: 992px) { + .modal-lg { + width: 900px; + } +} +.tooltip { + position: absolute; + z-index: 1070; + display: block; + font-family: "Helvetica Neue", Helvetica, Arial, "Open Sans", sans-serif; + font-size: 12px; + font-weight: normal; + line-height: 1.4; + visibility: visible; + filter: alpha(opacity=0); + opacity: 0; +} +.tooltip.in { + filter: alpha(opacity=90); + opacity: .9; +} +.tooltip.top { + padding: 5px 0; + margin-top: -3px; +} +.tooltip.right { + padding: 0 5px; + margin-left: 3px; +} +.tooltip.bottom { + padding: 5px 0; + margin-top: 3px; +} +.tooltip.left { + padding: 0 5px; + margin-left: -3px; +} +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #fff; + text-align: center; + text-decoration: none; + background-color: #000; + border-radius: 4px; +} +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.top-left .tooltip-arrow { + right: 5px; + bottom: 0; + margin-bottom: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.top-right .tooltip-arrow { + bottom: 0; + left: 5px; + margin-bottom: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-width: 5px 5px 5px 0; + border-right-color: #000; +} +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-width: 5px 0 5px 5px; + border-left-color: #000; +} +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.tooltip.bottom-left .tooltip-arrow { + top: 0; + right: 5px; + margin-top: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.tooltip.bottom-right .tooltip-arrow { + top: 0; + left: 5px; + margin-top: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1060; + display: none; + max-width: 276px; + padding: 1px; + font-family: "Helvetica Neue", Helvetica, Arial, "Open Sans", sans-serif; + font-size: 14px; + font-weight: normal; + line-height: 1.42857143; + text-align: left; + white-space: normal; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, .2); + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2); + box-shadow: 0 5px 10px rgba(0, 0, 0, .2); +} +.popover.top { + margin-top: -10px; +} +.popover.right { + margin-left: 10px; +} +.popover.bottom { + margin-top: 10px; +} +.popover.left { + margin-left: -10px; +} +.popover-title { + padding: 8px 14px; + margin: 0; + font-size: 14px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-radius: 5px 5px 0 0; +} +.popover-content { + padding: 9px 14px; +} +.popover > .arrow, +.popover > .arrow:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +.popover > .arrow { + border-width: 11px; +} +.popover > .arrow:after { + content: ""; + border-width: 10px; +} +.popover.top > .arrow { + bottom: -11px; + left: 50%; + margin-left: -11px; + border-top-color: #999; + border-top-color: rgba(0, 0, 0, .25); + border-bottom-width: 0; +} +.popover.top > .arrow:after { + bottom: 1px; + margin-left: -10px; + content: " "; + border-top-color: #fff; + border-bottom-width: 0; +} +.popover.right > .arrow { + top: 50%; + left: -11px; + margin-top: -11px; + border-right-color: #999; + border-right-color: rgba(0, 0, 0, .25); + border-left-width: 0; +} +.popover.right > .arrow:after { + bottom: -10px; + left: 1px; + content: " "; + border-right-color: #fff; + border-left-width: 0; +} +.popover.bottom > .arrow { + top: -11px; + left: 50%; + margin-left: -11px; + border-top-width: 0; + border-bottom-color: #999; + border-bottom-color: rgba(0, 0, 0, .25); +} +.popover.bottom > .arrow:after { + top: 1px; + margin-left: -10px; + content: " "; + border-top-width: 0; + border-bottom-color: #fff; +} +.popover.left > .arrow { + top: 50%; + right: -11px; + margin-top: -11px; + border-right-width: 0; + border-left-color: #999; + border-left-color: rgba(0, 0, 0, .25); +} +.popover.left > .arrow:after { + right: 1px; + bottom: -10px; + content: " "; + border-right-width: 0; + border-left-color: #fff; +} +.carousel { + position: relative; +} +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} +.carousel-inner > .item { + position: relative; + display: none; + -webkit-transition: .6s ease-in-out left; + -o-transition: .6s ease-in-out left; + transition: .6s ease-in-out left; +} +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + line-height: 1; +} +@media all and (transform-3d), (-webkit-transform-3d) { + .carousel-inner > .item { + -webkit-transition: -webkit-transform .6s ease-in-out; + -o-transition: -o-transform .6s ease-in-out; + transition: transform .6s ease-in-out; + + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-perspective: 1000; + perspective: 1000; + } + .carousel-inner > .item.next, + .carousel-inner > .item.active.right { + left: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } + .carousel-inner > .item.prev, + .carousel-inner > .item.active.left { + left: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } + .carousel-inner > .item.next.left, + .carousel-inner > .item.prev.right, + .carousel-inner > .item.active { + left: 0; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} +.carousel-inner > .active, +.carousel-inner > .next, +.carousel-inner > .prev { + display: block; +} +.carousel-inner > .active { + left: 0; +} +.carousel-inner > .next, +.carousel-inner > .prev { + position: absolute; + top: 0; + width: 100%; +} +.carousel-inner > .next { + left: 100%; +} +.carousel-inner > .prev { + left: -100%; +} +.carousel-inner > .next.left, +.carousel-inner > .prev.right { + left: 0; +} +.carousel-inner > .active.left { + left: -100%; +} +.carousel-inner > .active.right { + left: 100%; +} +.carousel-control { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 15%; + font-size: 20px; + color: #fff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, .6); + filter: alpha(opacity=50); + opacity: .5; +} +.carousel-control.left { + background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); + background-image: -o-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); + background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .5)), to(rgba(0, 0, 0, .0001))); + background-image: linear-gradient(to right, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); + background-repeat: repeat-x; +} +.carousel-control.right { + right: 0; + left: auto; + background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); + background-image: -o-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); + background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .0001)), to(rgba(0, 0, 0, .5))); + background-image: linear-gradient(to right, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); + background-repeat: repeat-x; +} +.carousel-control:hover, +.carousel-control:focus { + color: #fff; + text-decoration: none; + filter: alpha(opacity=90); + outline: 0; + opacity: .9; +} +.carousel-control .icon-prev, +.carousel-control .icon-next, +.carousel-control .glyphicon-chevron-left, +.carousel-control .glyphicon-chevron-right { + position: absolute; + top: 50%; + z-index: 5; + display: inline-block; +} +.carousel-control .icon-prev, +.carousel-control .glyphicon-chevron-left { + left: 50%; + margin-left: -10px; +} +.carousel-control .icon-next, +.carousel-control .glyphicon-chevron-right { + right: 50%; + margin-right: -10px; +} +.carousel-control .icon-prev, +.carousel-control .icon-next { + width: 20px; + height: 20px; + margin-top: -10px; + font-family: serif; + line-height: 1; +} +.carousel-control .icon-prev:before { + content: '\2039'; +} +.carousel-control .icon-next:before { + content: '\203a'; +} +.carousel-indicators { + position: absolute; + bottom: 10px; + left: 50%; + z-index: 15; + width: 60%; + padding-left: 0; + margin-left: -30%; + text-align: center; + list-style: none; +} +.carousel-indicators li { + display: inline-block; + width: 10px; + height: 10px; + margin: 1px; + text-indent: -999px; + cursor: pointer; + background-color: #000 \9; + background-color: rgba(0, 0, 0, 0); + border: 1px solid #fff; + border-radius: 10px; +} +.carousel-indicators .active { + width: 12px; + height: 12px; + margin: 0; + background-color: #fff; +} +.carousel-caption { + position: absolute; + right: 15%; + bottom: 20px; + left: 15%; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: #fff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, .6); +} +.carousel-caption .btn { + text-shadow: none; +} +@media screen and (min-width: 768px) { + .carousel-control .glyphicon-chevron-left, + .carousel-control .glyphicon-chevron-right, + .carousel-control .icon-prev, + .carousel-control .icon-next { + width: 30px; + height: 30px; + margin-top: -15px; + font-size: 30px; + } + .carousel-control .glyphicon-chevron-left, + .carousel-control .icon-prev { + margin-left: -15px; + } + .carousel-control .glyphicon-chevron-right, + .carousel-control .icon-next { + margin-right: -15px; + } + .carousel-caption { + right: 20%; + left: 20%; + padding-bottom: 30px; + } + .carousel-indicators { + bottom: 20px; + } +} +.clearfix:before, +.clearfix:after, +.dl-horizontal dd:before, +.dl-horizontal dd:after, +.container:before, +.container:after, +.container-fluid:before, +.container-fluid:after, +.row:before, +.row:after, +.form-horizontal .form-group:before, +.form-horizontal .form-group:after, +.btn-toolbar:before, +.btn-toolbar:after, +.btn-group-vertical > .btn-group:before, +.btn-group-vertical > .btn-group:after, +.nav:before, +.nav:after, +.navbar:before, +.navbar:after, +.navbar-header:before, +.navbar-header:after, +.navbar-collapse:before, +.navbar-collapse:after, +.pager:before, +.pager:after, +.panel-body:before, +.panel-body:after, +.modal-footer:before, +.modal-footer:after { + display: table; + content: " "; +} +.clearfix:after, +.dl-horizontal dd:after, +.container:after, +.container-fluid:after, +.row:after, +.form-horizontal .form-group:after, +.btn-toolbar:after, +.btn-group-vertical > .btn-group:after, +.nav:after, +.navbar:after, +.navbar-header:after, +.navbar-collapse:after, +.pager:after, +.panel-body:after, +.modal-footer:after { + clear: both; +} +.center-block { + display: block; + margin-right: auto; + margin-left: auto; +} +.pull-right { + float: right !important; +} +.pull-left { + float: left !important; +} +.hide { + display: none !important; +} +.show { + display: block !important; +} +.invisible { + visibility: hidden; +} +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} +.hidden { + display: none !important; + visibility: hidden !important; +} +.affix { + position: fixed; +} +@-ms-viewport { + width: device-width; +} +.visible-xs, +.visible-sm, +.visible-md, +.visible-lg { + display: none !important; +} +.visible-xs-block, +.visible-xs-inline, +.visible-xs-inline-block, +.visible-sm-block, +.visible-sm-inline, +.visible-sm-inline-block, +.visible-md-block, +.visible-md-inline, +.visible-md-inline-block, +.visible-lg-block, +.visible-lg-inline, +.visible-lg-inline-block { + display: none !important; +} +@media (max-width: 767px) { + .visible-xs { + display: block !important; + } + table.visible-xs { + display: table; + } + tr.visible-xs { + display: table-row !important; + } + th.visible-xs, + td.visible-xs { + display: table-cell !important; + } +} +@media (max-width: 767px) { + .visible-xs-block { + display: block !important; + } +} +@media (max-width: 767px) { + .visible-xs-inline { + display: inline !important; + } +} +@media (max-width: 767px) { + .visible-xs-inline-block { + display: inline-block !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm { + display: block !important; + } + table.visible-sm { + display: table; + } + tr.visible-sm { + display: table-row !important; + } + th.visible-sm, + td.visible-sm { + display: table-cell !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-block { + display: block !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-inline { + display: inline !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-inline-block { + display: inline-block !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md { + display: block !important; + } + table.visible-md { + display: table; + } + tr.visible-md { + display: table-row !important; + } + th.visible-md, + td.visible-md { + display: table-cell !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-block { + display: block !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-inline { + display: inline !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-inline-block { + display: inline-block !important; + } +} +@media (min-width: 1200px) { + .visible-lg { + display: block !important; + } + table.visible-lg { + display: table; + } + tr.visible-lg { + display: table-row !important; + } + th.visible-lg, + td.visible-lg { + display: table-cell !important; + } +} +@media (min-width: 1200px) { + .visible-lg-block { + display: block !important; + } +} +@media (min-width: 1200px) { + .visible-lg-inline { + display: inline !important; + } +} +@media (min-width: 1200px) { + .visible-lg-inline-block { + display: inline-block !important; + } +} +@media (max-width: 767px) { + .hidden-xs { + display: none !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .hidden-sm { + display: none !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .hidden-md { + display: none !important; + } +} +@media (min-width: 1200px) { + .hidden-lg { + display: none !important; + } +} +.visible-print { + display: none !important; +} +@media print { + .visible-print { + display: block !important; + } + table.visible-print { + display: table; + } + tr.visible-print { + display: table-row !important; + } + th.visible-print, + td.visible-print { + display: table-cell !important; + } +} +.visible-print-block { + display: none !important; +} +@media print { + .visible-print-block { + display: block !important; + } +} +.visible-print-inline { + display: none !important; +} +@media print { + .visible-print-inline { + display: inline !important; + } +} +.visible-print-inline-block { + display: none !important; +} +@media print { + .visible-print-inline-block { + display: inline-block !important; + } +} +@media print { + .hidden-print { + display: none !important; + } +} +body { + text-rendering: optimizeLegibility; +} +.list-group-item { + padding: 8px 15px; +} +h3, +h4 { + font-weight: bold; +} +ul.with-margin li, +ol.with-margin li { + margin: 7px auto; +} +.form-control, +.btn:active, +.btn:focus, +.btn.active, +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + -webkit-box-shadow: none; + box-shadow: none; +} +a { + cursor: pointer; +} +a:hover, +a:focus { + text-decoration: underline; +} +.navbar a:hover, +.navbar a:focus, +.dropdown-menu a:hover, +.dropdown-menu a:focus { + border-bottom: none; +} +h4.modal-title { + font-weight: normal; +} +.modal-content { + border-radius: 3px; +} +.navbar-inverse .form-control { + color: #b8c2cb; + background-color: #475765; + border: 1px solid #475765; +} +.navbar-inverse .form-control::-webkit-input-placeholder { + color: #b8c2cb; +} +.navbar-inverse .form-control::-moz-placeholder { + color: #b8c2cb; +} +.navbar-inverse .form-control:-ms-input-placeholder { + color: #b8c2cb; +} +.navbar-inverse .form-control:focus { + -webkit-box-shadow: none; + box-shadow: none; +} +.navbar-search-icon { + color: #b8c2cb; +} diff --git a/influxframework/public/css/fonts/fontawesome/FontAwesome.otf b/influxframework/public/css/fonts/fontawesome/FontAwesome.otf new file mode 100644 index 0000000000000000000000000000000000000000..401ec0f36e4f73b8efa40bd6f604fe80d286db70 GIT binary patch literal 134808 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 literal 0 HcmV?d00001 diff --git a/influxframework/public/css/fonts/fontawesome/LICENSE b/influxframework/public/css/fonts/fontawesome/LICENSE new file mode 100644 index 0000000..3042281 --- /dev/null +++ b/influxframework/public/css/fonts/fontawesome/LICENSE @@ -0,0 +1,4 @@ +The Font Awesome font is licensed under the SIL OFL 1.1: +http://scripts.sil.org/OFL +Font Awesome CSS, LESS, and Sass files are licensed under the MIT License: +https://opensource.org/licenses/mit-license.html diff --git a/influxframework/public/css/fonts/fontawesome/font-awesome.min.css b/influxframework/public/css/fonts/fontawesome/font-awesome.min.css new file mode 100644 index 0000000..28906d2 --- /dev/null +++ b/influxframework/public/css/fonts/fontawesome/font-awesome.min.css @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url('/assets/influxframework/css/fonts/fontawesome/fontawesome-webfont.eot?v=4.7.0');src:url('/assets/influxframework/css/fonts/fontawesome/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('/assets/influxframework/css/fonts/fontawesome/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('/assets/influxframework/css/fonts/fontawesome/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('/assets/influxframework/css/fonts/fontawesome/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('/assets/influxframework/css/fonts/fontawesome/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} diff --git a/influxframework/public/css/fonts/fontawesome/fontawesome-webfont.eot b/influxframework/public/css/fonts/fontawesome/fontawesome-webfont.eot new file mode 100644 index 0000000000000000000000000000000000000000..e9f60ca953f93e35eab4108bd414bc02ddcf3928 GIT binary patch literal 165742 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 G*yFsbY~cLNXLP73aA74Mq6M9f^&YV`isWW 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

      5q!RY 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 + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/influxframework/public/css/fonts/fontawesome/fontawesome-webfont.ttf b/influxframework/public/css/fonts/fontawesome/fontawesome-webfont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..35acda2fa1196aad98c2adf4378a7611dd713aa3 GIT binary patch literal 165548 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 z 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 d0mRO}xo^G_%I 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 SdSQM28?+M|D(2r_;{O0|uQ74} literal 0 HcmV?d00001 diff --git a/influxframework/public/css/fonts/fontawesome/fontawesome-webfont.woff2 b/influxframework/public/css/fonts/fontawesome/fontawesome-webfont.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..4d13fc60404b91e398a37200c4a77b645cfd9586 GIT binary patch literal 77160 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 literal 0 HcmV?d00001 diff --git a/influxframework/public/css/fonts/inter/LICENSE.txt b/influxframework/public/css/fonts/inter/LICENSE.txt new file mode 100644 index 0000000..65ec0f9 --- /dev/null +++ b/influxframework/public/css/fonts/inter/LICENSE.txt @@ -0,0 +1,94 @@ +Copyright (c) 2016-2020 The Inter Project Authors. +"Inter" is trademark of Rasmus Andersson. +https://github.com/rsms/inter + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/influxframework/public/css/fonts/inter/inter.css b/influxframework/public/css/fonts/inter/inter.css new file mode 100644 index 0000000..25f4636 --- /dev/null +++ b/influxframework/public/css/fonts/inter/inter.css @@ -0,0 +1,152 @@ +@font-face { + font-family: 'Inter'; + font-display: swap; + font-style: normal; + font-weight: 100; + src: url("/assets/influxframework/css/fonts/inter/inter_thin.woff2") format("woff2"), + url("/assets/influxframework/css/fonts/inter/inter_thin.woff") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-display: swap; + font-style: italic; + font-weight: 100; + src: url("/assets/influxframework/css/fonts/inter/inter_thinitalic.woff2") format("woff2"), + url("/assets/influxframework/css/fonts/inter/inter_thinitalic.woff") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-display: swap; + font-style: normal; + font-weight: 200; + src: url("/assets/influxframework/css/fonts/inter/inter_extralight.woff2") format("woff2"), + url("/assets/influxframework/css/fonts/inter/inter_extralight.woff") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-display: swap; + font-style: italic; + font-weight: 200; + src: url("/assets/influxframework/css/fonts/inter/inter_extralightitalic.woff2") format("woff2"), + url("/assets/influxframework/css/fonts/inter/inter_extralightitalic.woff") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-display: swap; + font-style: normal; + font-weight: 300; + src: url("/assets/influxframework/css/fonts/inter/inter_light.woff2") format("woff2"), + url("/assets/influxframework/css/fonts/inter/inter_light.woff") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-display: swap; + font-style: italic; + font-weight: 300; + src: url("/assets/influxframework/css/fonts/inter/inter_lightitalic.woff2") format("woff2"), + url("/assets/influxframework/css/fonts/inter/inter_lightitalic.woff") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-display: swap; + font-style: normal; + font-weight: 400; + src: url("/assets/influxframework/css/fonts/inter/inter_regular.woff2") format("woff2"), + url("/assets/influxframework/css/fonts/inter/inter_regular.woff") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-display: swap; + font-style: italic; + font-weight: 400; + src: url("/assets/influxframework/css/fonts/inter/inter_italic.woff2") format("woff2"), + url("/assets/influxframework/css/fonts/inter/inter_italic.woff") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-display: swap; + font-style: normal; + font-weight: 500; + src: url("/assets/influxframework/css/fonts/inter/inter_medium.woff2") format("woff2"), + url("/assets/influxframework/css/fonts/inter/inter_medium.woff") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-display: swap; + font-style: italic; + font-weight: 500; + src: url("/assets/influxframework/css/fonts/inter/inter_mediumitalic.woff2") format("woff2"), + url("/assets/influxframework/css/fonts/inter/inter_mediumitalic.woff") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-display: swap; + font-style: normal; + font-weight: 600; + src: url("/assets/influxframework/css/fonts/inter/inter_semibold.woff2") format("woff2"), + url("/assets/influxframework/css/fonts/inter/inter_semibold.woff") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-display: swap; + font-style: italic; + font-weight: 600; + src: url("/assets/influxframework/css/fonts/inter/inter_semibolditalic.woff2") format("woff2"), + url("/assets/influxframework/css/fonts/inter/inter_semibolditalic.woff") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-display: swap; + font-style: normal; + font-weight: 700; + src: url("/assets/influxframework/css/fonts/inter/inter_bold.woff2") format("woff2"), + url("/assets/influxframework/css/fonts/inter/inter_bold.woff") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-display: swap; + font-style: italic; + font-weight: 700; + src: url("/assets/influxframework/css/fonts/inter/inter_bolditalic.woff2") format("woff2"), + url("/assets/influxframework/css/fonts/inter/inter_bolditalic.woff") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-display: swap; + font-style: normal; + font-weight: 800; + src: url("/assets/influxframework/css/fonts/inter/inter_extrabold.woff2") format("woff2"), + url("/assets/influxframework/css/fonts/inter/inter_extrabold.woff") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-display: swap; + font-style: italic; + font-weight: 800; + src: url("/assets/influxframework/css/fonts/inter/inter_extrabolditalic.woff2") format("woff2"), + url("/assets/influxframework/css/fonts/inter/inter_extrabolditalic.woff") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-display: swap; + font-style: normal; + font-weight: 900; + src: url("/assets/influxframework/css/fonts/inter/inter_black.woff2") format("woff2"), + url("/assets/influxframework/css/fonts/inter/inter_black.woff") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-display: swap; + font-style: italic; + font-weight: 900; + src: url("/assets/influxframework/css/fonts/inter/inter_blackitalic.woff2") format("woff2"), + url("/assets/influxframework/css/fonts/inter/inter_blackitalic.woff") format("woff"); +} diff --git a/influxframework/public/css/fonts/inter/inter_black.woff b/influxframework/public/css/fonts/inter/inter_black.woff new file mode 100644 index 0000000000000000000000000000000000000000..52af3fe46a9ea28684cd604ee63030d7c4a68727 GIT binary patch literal 138628 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

      AXu#;V#1_y~?; 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

      ocn;583j=;TT%)q03K=q; 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 literal 0 HcmV?d00001 diff --git a/influxframework/public/css/fonts/inter/inter_black.woff2 b/influxframework/public/css/fonts/inter/inter_black.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..89c204c80c11c60b529ed808db45f39ea0459d20 GIT binary patch literal 102832 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 z zyl9oq9Cr`te;XraR$)G16Ed+MHF=KJmG7_lzdgfL&Ai9vB}k(Z3F<@Wd7+d$mbDGk zJmk=r%ji5l2>s4KXE2d9|CAxAF#7^2Bj)_j-Tlp{DOi|5D%W|zyyaL}9lpe){*|-+ zn3m@5_)^#;PG-myb(mXz@bKl?toT7S{c=I`_k_Ih?4X)*b7Ouh`(B~+w|^xHJD|

      !MYR(_3!qo^0BXxJ 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 A>i_@% literal 0 HcmV?d00001 diff --git a/influxframework/public/css/fonts/inter/inter_blackitalic.woff b/influxframework/public/css/fonts/inter/inter_blackitalic.woff new file mode 100644 index 0000000000000000000000000000000000000000..0ad6c7d2c13869af70ab1d529284a02ee3da5456 GIT binary patch literal 145612 zcmZsBcT^M47cYo_bm_f^F1>?vLhnVSg9M255?W|Vmrf$mqy!=g2q+*(7Zf4V5@B-dmIeky#6(0yw^YW6Xl~luFW%nN|KDP$ z54@4>yXn7`eIrOF`ALf3(8|)5h0x(Ji`rL_}g0L_`nF zDofT0mbTJzL`1}IZYFlQArIM(wluFm&k!Oa#$6&JDQhAk@B3`>n&iF#F+M~@w#7um zy75G}?lKScP_BW!JwZ2PWp2hP{YRp?JQd&@<_!;ga}D^9xPNio0tZ5(Z}xVZh=~3- z5s}D*U!h8DK(N=1oUZl_@8Jz^hJv9)H_$WsW?kZ&dm!SvAwEiaqN^a!KyM=A7i>4< z;Y37a)Qx@H86m;p5F+B@w?srjS42dhW;q7%!T-*Rgk-MucVE6c-TfmXPO6(Jh=`Uz z-v906zgF_-;=+UL1vLlJEh4fT`tNjamLnpfd~a-_ydx^K$Do>6_7}#xfe3(!2KEF{`1#|?UJ=|S!raTF?XB0kZ|57p4u~oZZ<)a z%W^SA`%-nhlFZ^IF?}NwL-gW!32^|(FlP9=RsEnhCV0*DK^=v&-&CNt4ZO60N=oJ3 zC#A5gEHh9lWAP6gQ$;ZI3rP6eL73D#eySA%(>4Ul2OH&(~_ZqsKeo&gF+)bVS^PEKF{q!}P;1s({Fwces|z)K!`6ErdW_>j z4g~dP2jF_FqjUi?r8xVDzYm(Fey+BuY}gOw&)rr2Xf4~Q7QLYwIE{>d2Yl7&U`O1+ zxm=Htu+lLY=leDBOUX-1+SsUYM&3X=eJSs)wIxPM#~_VAK3C|E3KMmO{d{ET$tYUO-1;>M`_P+h=#7`UywcKK*=dqAG`n|D0H;i-Hs z&1+Vzh+p6arG%g20e?EL@;F#8>-or}zA)=A8?Pfix0FwF_Ge6|^>DdSzqV($@t!E! z!$=?+j=2bLS~tp}DAQ*kDM@v8Y3f3ihkcJO1!}XQ{%vI{q4ZIm6gARxK|ec-Wp77V zV_kSjl?B~)0;_(E|8xt!;~h+W(kb|mmLU3Tee{U4Ze7Wj?cXg&{gsF@wxvDB#7B>C zaB1px`qiWp--p}1-?~1{ZhiVqs4vj$V|Z~@AM#`PB#;?ms0f_o?_>CP zW;G4nYeA+!hV`2uV{B>Tj+_Nul;@NB|ZR>e$Yb&1USLTH`xzM?fR&^+N_JkVu4r8 zy0m>U6f4kfZSRHs?;mZe5k_0QZLXB0*0@4Dm1*|A2|fDDLX1 z=*reA)^ht^|8LTy8yx)^y-zZGt0-9+Z2reCeF=E z!!oDu{>%U5U~r!=x!6_^UK}K{+@1;mo045mG!@bze@yce0pi^@QhAo z@>RJAyGBEM#MiZ1e=?CA<+|HQxwTAlzw6ZZWG8!KabvIcexn~q2rT@j@gt3q&V5m- zzo3;XH~jsWKp(L|f5a-goA>TgDd=x|o#U|>|79Sxdf92!(wD{{692ZfiJgA85Bhn| zpGz-A-Mpm3-Dtf|e}8hDrd#TLBbs{suJzq54kJFom^Voi_DC05RUWqc{tepL-Q2am|Lr{S zSMx3DH0CJEg9!LXMY)utA_iafwa`bWdaX)M$$(%y-){xTH`^}1cg!9gckN~MV=AqE zGa|JV^~!-2XC3uWqtr2h?I5|_`dW&KrQE-guu$v@W$r@q<{YmtN1v<6;py#PS~{26 z-PvG$x%PqMP=)3DOL;YZf3|Ud4IU~J{m#YNt6TbIKtE^L1c!P>>*gR3FG{=~Uq0_m z%?#%Xojv-;lm1feYom8B){f2gYgKE8YAjmfe;AA*8#ueA^GYABb`pjHa#dioS5HMT&D@TsJ|4# ztHY!FHjM*G6R7NEh% z>+aA@0*{>jzL3_V+^e8y#u#7bnEC_e{LR?MyzKgAkxLcd*Su7%76xGfTgKaW_@SkD zzvK`oJEq-UyCQ4fcyGV4wjDm_urtLM;h&EQh`r7)654(EIa;RbQ}D0a1hreb4=eY> z$m11UlX4=``lQZS<8taD}>T=W5G1E2{j;hN*{s&7@eX&qOLp z5OihZoM!(%jwQ`oVm6AU`BlKb{3RxAD#z32gH?edcQ^<5kf33~C9s_lNJk~q*LqmOWkNDplVXAZZ~*@G|t-R zeURlNy;>QQ*vvUmN8>o53sQb>n$|GR@L3`stgwY6!)p+&d1taTIw*xvh0nT0x2b>| zLd}AP!3hW6wv#BaOXkk%eqs6L{*fQfZTE^GicHhth}C3!$C*Q}!=B!zhW{=6xIG?l zzkc&~MuKQtgL9ZpqN`6tx>u^%*5kz(w;Dg8qmIw^U%YcpdK-)061x_a5sXc@MZ7IK ze&;5nL}vYpwO2uqkC4S^471BWM637gTc|u!{+967_lORZqt>Y2Vgz_@c1RFfp%|W! zlsUHkn)IFyb#K_I#$sm}RPd)XJKpLAc=N8JjE_j$(+YGN4?N=jr}mu|-pWG9IE&dg z)6qsO&%OxCjlUGdOn9pEWhu6=@RJ0b$F#e8Evawyo8E|gbZ2=6DpT0^SUT@z<5+$C zw%gK1Im7`(86L^&)7!DG6MQRY1h66SczLga#o^rR=XOIbWh-5RB{p=KRTY@KE?B_% zmtKMuigx-c7oi)JiY#PQUR?Ug)4&I_uoU$y96Cp(e{L0!Ec;2lGrOxQE6{mdqDqBR z3EAg=)2rc;cy~Vb9vab`j_Hp|3*djhrhBkj{x-Jsp1{Ii*_lp?_u<~zyF&T%5+vPP zde;kd@Aq=pR$_^stuX#k@2K{T$_lhdpKsM+dh!S0u}PH3{_bJ!lRl1}vL_EoxcpSY zHCfDWeWlgoVP{N0ipIUZJUsCH+TN4&@d|9~a_zT`Fz+pa9yZjqor6zeQqd9mu*U=z z^W|V6_BDHgMS-duF45h^a#c?qMF5<~lo^6KFYg}d;a`F2rj-B-hX1Tq)E0wWzX&u4 z@H!41t%}c%Y*yOlZq>35CBk{(Hbt?t&lKK=i49WyP#jqzp%e)GQwHW4qGMF^(<^jG{azhSNZ?g&$SBJCDSey&;OQz@QK`7o-(ze?}#It~jKL?B+?o*TW* z(#$b*;G$R`uD$7=q42xt&Kxe7MeT$^_7PtB*^i$7ryo9REZJvmUTdhCj;JWc$=PW3 z&xN!6p*>ui+BHrluwOpZF86I`f3b7gBJsD_Ir%v$wd0NBceBj4a~C?nbE2T9yZ0G)N5|dbiv9NcvwvKyo|Y>;zCD-y$Rjbu{}UMk!Q}o?rEosx`kl7VPZzANTlP@$`-*t}n#E;q&8`Owo{uIB6O~B2nH^ z+{%t`w=LJ_?_8!?Uf&*h9Hh5tS-9VscW&FntL-sj?ORg(6jC$QQ#GUP9b%+U=9#HW zT0hpx>>`IC> z6#{9`)U2>MHd{4O(p#<}2srRw+I6g>%a3xj$NW4@kUskSmE1#{MNe#Gg7To3G%*o= zLa_GNyGT&UN>uVYPD&rSwVHxmJsHy+&0eY_UAtN`7fw>?V;UVc{_fG@ z=DD~BC@R%!SJ8LqliT;4Y7VIyG2izdbXDUC7NZQAKJF9mjY=3P76v=fX?NY+df;ae z&hS(CdP)<@=Zz)H5ceZs$`IwMDUXoG?nwjosv%r}MYU<^SQPZ<;2!1H3BTWlJ?OTz zd|Jatk-zEsV`H+?6sVWEJd*Ft)lL(fsb1%M2JMz!-%XG1J-xSS0y{j$2wuLCVHIbK zj?C@z{P0G(VE*vzDwz1VRwk|;nDcklVZ3j7cX&mXKBC^pxvYA)&I=VdNUmo7FK}W< zOHFI?7z(Bfk&>$N4W_8!{nB?K=C}VX ziglUPH6%74!V;2nu*uV^<-7mVL-pW;mC_##jiIjK`X8|mH+PrMs94=$99GE&A&JCVj(@$vV{&ZDV1%YQe+hf&E` zTl^(sOjMD8st!xn4pG-6G{5gL>{W0E6joaEpledblx)Ozs}dPhrueyschOs3$`2No z#Dvm9cWBFH6OU?q+E9P44UchCtpQ6$P=xchupuQ96Yp}-sGS$bkq}3qYg6*yH z{cd?*Kc7ab&2`QX-ySWBKU@1nC~e&U4msVWS(Gecw3U;d3OFAQ)9x`j&f0l+9gI~^ zkErdhlBC!Kei-#47W$hL;utr%pMmTZ-&lcFUwWE1I!g1K2>={h0(SD1X$YVk&;^>;_=xZMNO_nKoG5$Mg zG^^qA-ru#c-Q9_w`}w!;w7nwTre&J@x&Qx4g~zMR?v+ElOQLsW0zu>25l})Y@bl@@ zv1Jr=9nKJE@ay7y!SsQ-{`Ub-z6fNx_}k-cazZl7!=$9Nw*9fcT4{w(2D^_>*~8$j zlYtGRO6m9lywFsUiMw$8E4=;moTZlJURoWqMKp6MehvH(b_= z)lmJDjHmPxg0sNgEEUdB#h*G~rZY|oLIqn5X0rph7gSq(u|G!W3#J943|2FGxMQ^b zc|OmSvre#MD^w8owyS+}&w8P{#W0@!$jk288<@3sO_u>pW+~SPfM3K6;Z39U>*@*p z@~kfQ2<7dLH(GMr*$H1$*x?{ExUAmS0CpW_q4q0%3meCaCC(Gn3sIeorOA* zd&lP=MJQ=GPX%mg?j1j#oYVwae(=~Bq|0g@HZof-4v+*)FpxAj}T?cBJ9@Fqbqc;JWu0lQnzD zaOH(ol#5Wk^URw!L)Hz|vj%}@l~Hkm+Y{G4URmlE7l|vOy*Kk?uFrEVf8_dmuRc+) zVD1%$>fvADTrua>c^3!j?bqwMvFo{&>$&g?k*K#`KZoKtZ12_DnH?1`e*9G{>*cjE z{5ETWK3sakI3R64;QiXrMrPyL0k`45+_QOyq~WHt!uv5%CCq0se-nZW&Q}nu_$QY8 ziE&!LDm8%n3B1nN`$YmB*Ik9{`*a;?%N%k3`6K1_PyThbnqyZ(_PZrLWI9GJO;09Q zv=TZ9E6-H851okz!u>~Al%ky1-iE#YymETtyc(0tTSyoDYh`Gu+kIIzc5|@YKWci# zJaTide6FM8XOtT$Rq*6TjjPDNOAEoiz47PAjZ4F!?I)2nOA*0yg9lpp*;!{`@bBzP z-K%Di2P*;WoLU=BW8aHQWLF-k4P!rcZftxjiw}zIRaHFh8u{+;aN?HJySx`S+#n)l zFf(p_Zr7;l*rcDE*TZt85BMb-T_85nbMLTH07FyFX>dY60A;?EGBqi9?#d{Y)vY$+ zWnG^;;o%hhLH|PWN=rb;`e~J&6=@?Yb!b9HltAc|wi<$Zki2p}TI>CwrJ@JJ@-ykb z(_bdYe~R zF9`(qx31l&7IM!c@dJ0DHRs&d*1v(eWh27v#({EW%eC)Z1ESRDX*=|GS(Wm@XTVG& z)jF|c?~h|IF2B{Ciog2yM$@{}KK8qLgoEA;K+$r~mi#+KgvF(I;ya;T%Sc4|=G+5m zh?J{#(QHJ7yM|F7=I|&e{$b_pHO}{I>4A9=d~!x9&@5Pf_@0c!xLWwkM$1q@koqd` zqi|?qEL{No0k89N(9zD~tH@q%@a7^c^4Z#_rdF@O?3^S2(yPeUyJG1Xw28cjh&rkzm$uyKoeH^dX^+vgEY>opDuWaY}vp*@GjQAv;T3 zmm@i$87o^$8u_E|A=BD9GBn>o(~jn{u1PVqaHmIv`d2;~PCjY%zkQ0GJkuHqeNxmw zEDc#ctzF0~rPniB8*NaJiI0?O9Zb|}xcdXQqRhS-S-Tq58=qBmOC8+bVi==^qV66b zvK5-~NRKyMHmD=|_}Rj+h#3b$j$h-y5}9ILY5l~;ut!~~hZ9-63&-?>@vyS*0g7vQ z`N<@q?Vg3Iic&9~evc$X@rL%-S!9y>Yfw)~}qs;oL}f4^WUR(FsvwN2HIaXxIqN4BN@VA2KT& zCtXSD9?*3eMYy_t-LY;NEVos~lT$?4pcu?UDOkbVSc?gN$ujle#pnj+R~=|o^HNFJav?{LrBrG_t6)wBBa{$+O9(J z$FK$);Z*TJn|e9n!mfaJTo$45adXE= zm?bl}!z_*f$~R>`H5TkIsV#QgT{vAI7JdKiLa96{iKmPv?HzRE@3p6kTu9PcUHtL3 z;;jho8MI`Sm3n#;Pa3~!KTCHcc&Xh#E1mRrqroG|f1|57DX3ixBwqTjHbacdjLxT6 zO{!1=H`O&$Tl3hLEJgWqtWT38i1zmAxN-P^cSwO$_xaa&`ko`-k8IM3GV?>*y#za% zS!}54@@7|BQ=@+;V^Gaj6?vE0{)_HCxg9ZRy~cLJyWzbURVY%!)wLh7 zHXr=6nL z2S1_?m(I&C_7nX3vm`dZ5dxFT5{7?7`~gI>-;Y=$Us~blP*TC3gb1#vbu3=u68@bN zE-#4_wEBzIrVHtUZl@A z$fLmLCd!UNMRspHe=8P>iUmlOj855Wzza7rUF^iLB8B#s0ZsMLjneH$O%0vmUze?X z$ITf9i@r-rXBACK{8Bt5gJRr*r1MU)ismGK7anuOn!E5fmo;p1M6ZmCxY0Ggo}1g* zJUStZhL1S~XDWa9-=~pV=owgTlkpE32$Y)~r#hS}+l8j^rt+p&-VR@>uPQUWn;kNg zu}iiMBRuir@uAY9(V}WKbv&{z{=CDuL9IpCpHD1@9%b7-oT~d9W|?67w+HJ+ zDed{rJ)V>6PaNv8n<>I0Jx1hmJl1kXd0X8wrh@RWz5d0(gondB_4!|<6;#Ji1nZnB zR_GWOZQKiTGm$PVa(ex5HfE=nX4Plq^U}}2!}f0Xp7K@AgQ}?cxV>_jPKek6a$&Dr z&cCO1HRb>owa~rode{{GepT|IeZMYt>+in&LB(T46t0|56*ZlO`Q?K7W7;~b&T1Am zo}9dzQr)TJnN;zv(`{Rq zQIC3m&TPog3QNaY1mxQhZ9io{ZNIe^RDCeGcc&hHHqi4|iHv0BwHwDl?%9t(n%NXO zJGtPF*S#9zbsY{bpoXB%s2S6pm7}THe$rmoZytTly!R-*l84qMBNo+v4gKAZ{o6z3 z7I#oxq&;~k7u`?Jovh{Z+Qap9UVgQy>UD?vi|AocNz{ED*Tt)r=yd$rzfKEDcbo#u z&^KSFtAllYgFJjW$s_(0@Q_mzFVL$sb0XDD82D}7IHp{mv{Crio4`5K;<3$4f1y&(6~pPXw{OX=K$ zxt=1%wQy6na)*CgaWMDIA{&B3-5UvGAy-H79dR3=Lt%Yasls&QtIi-nNr>~~x+P7iZAgd+1gWqCzqp|IiV+IpW!?PrD5iFVJja*kKfZrdc<&?Jr*5*EF@m zDW2R{C$}{$-kMNdePmlWok3U@JxaxFn|j$f!kr4`AKS&iAK1%m*^wW@x4{Vy;?2dr zPdA8K4nU~_Cg0+{Kt>UbL?Ju)%#c_!3w0V@`XS|iXx#b6pnN``C{)YuK z8|N|5hwof_WOM#t>B$M1a@ z9(&+09vLydYc)4+5%GDKvO8^MaNjfUo$vhU=yp6demDN7_o3#I=3$GeFzOx#25k9e z`0st3K!bPA+ro$zH>DAS1?-1EPSD1lSvSt+#<>Z9X$Ne*3w$R`FUTbOOMUDrBsfiZ zy6pG(@kVrbZ_Gw}r`LXI?1pmq+C|XSpRknPUYI9uv)}yO&fQ~q_ms$4e1Qn~k z=gnCEGlRau18nTsK*NUpiC?s0uajF~bFPC!J1TO4U){TC_k~8wob!(xH#H`!S7_BH zxTItG!uv|;x|nflR^nse)EOtB~X*6CV%E%M~4(UOg0=S{^5Y;Ae-^< z@fP55>gUe|>)_^^In)kU?on5?>+cuKFVw@7xmYdkh{SmHhOVB5WX_H5?3w=&u3wvp zKk6pvMbNxXn;zU+^$X7Nt92^YGJhMxF+uIyUFjO+y!fKUJ7D2Yu6p3k0-?QR$Cq)2e{+en{beK0XdAG*iYrO4Q-6b+X?);PH`-3OxDk~pumr*}T0*>1Tfe?HKW@IzgzvvP2S zdag^oDYIey=3TV@GDKfRlZ)AcdN>9CnSK535A%0Jdk~!vk&m3qR2HNMt^%xULlLi5 zcpO#&!#)2@&`?~3P7F~6k+UX`4+wP#M&ES_?=!{Kcd7(M{gL^|bw6r&<@m?pkE|b~ zKO|zMPMv>JRM;$G*M`^b2^3)I3~EMv4CP^gDLw+uY~yCD?qG#8X3Ra1mR>6?FeBkM zuUBGvf^H;zY0j<@-nNAsJvnlf=_9g4Kd#d*TFTh>DK{&D!reUabW;A2}-Z&9HG?-hhN?r*JHu5GSI{rCp?V<0Qt0xRYkRJ7%5@YX}W;V2pLp> zsBQ~yCM*mrYx5WL6KPW|WaO=%D0D>O1hWfjW2!p_KNd}1p^-H8yW^BQ18BQn7Xv1> zIY(zAX{!s3%C#lnYUvO7k64`wcj>^2EkfvRP@=AXzH7!TK%K4;m=O3}zgK%Feg?|gyPu(4G0R0FY#Jns;Noz3(&pA z%&`-&zg#Cq3G@#Uh?+52DtMgSE;7xzr|m<#STJ_0jj^N<>?^l}mICEgyP`I9y9*@K zB1OtL_jDrft7TpA;{sY#F{;?hoC%==Vzs;gdWdGsbrsbJiyb?Sp(;bT<-Sbz<}ym?-FayO%D}Ls|P8 z;7WH%bv59YtI5}UH{Qri7*D^oiSvpUfAq6za^yBHTuzgaw9AGUVo&c&Ql2Nlbbts| z2O|NxYw#lWOKqRt3}JaYn813bnc3)E#rvF4fXp5L0Od-b#T41y)GH%P7D=)Eq#lN@)S;UgHDqWRc{F<< zEfa0^#-x_V$sEjPCt}On$gjZV2YdWvmVtx#14==E=Z<+=7f7w6 zNiD~P8`6b#kV(dKLTau2i-n%k`M@bKs!V43n=t>(3C3;eL)1lvRF#KVmlgQdJ%MQk zS2G=62t}s?&PFZ}b`=VXRE*21x)P6*_2i-(>bu8+Y;R$?3 zn=ZfU84uD!0cs>lNekCZ;X!zCLG46YxdeY9hK(7jL{c`r(>sSNwL=R{%KMrRf`$3v*!# z0v+ikh8!z^PDh^9vG==MOHThk+<4xpv;tilogqI*vLejD-{*@JNG>||-C&!;?YQuNj1u-(GU)2Pp{?s zKx}U8bH6lm#z;Z2!~o1;V~c46byaUgukGJ2eG1~?;8!3KK)LGIq=;)9jHDczf_-c( z(O|h@%sqfH=Isq?^{1FFAHHFZ)2#=T=9q=yZEarN7(}FYD_lHhgAdLyZd^}IP%W{; zu_C2v++hnz2|^J1J{BnHTK?m_1c^4%zT^YoGBchWwu{RHyuwtbP!3Ka1u!nijjB%6 z4Dc{t_372E7~ry^0vEGm^e%sodhVy=;|IJWa?DF^oT&K=VoqgY$fjKpGdMA zp%EtSM4<>qNn;;DClgVj=e0ioPH9QgU`K>|6<4PcI0i9jMJ%rC_(orf)Tw0oej80Det;uwtMRzMSTW`RCZ`mx z;zlXzsTb@dMGD*A-O~+HS}Zi1Nz*(w%l- ztuRu|6?UqcWaO0rIczI~R$!Wz+subZEoHC_XGM8{_&hQOOouzB@lmmt7)XH3FA+(*7m`+_qj|!{6Vre@nmVEW6Qh*M6SN&R&u7_a0d;CQd zu9Tzif)S%AZv%%t+^f4$@j3g5AEI5X$DX`~C$=HCHMA2n z@O)?H7(MK_s&tgMPF}uqHjmJWEHrzt8y6b(0nLc*)3daB$E*Py@WLyQ=Aq@W235W& z8ofztw&V(tBDP-bCJ|_Xiz}Ytj0Q6rLfS10ExcgCDgN8J2{W2gSNW9h^cpwOap>(c zD!kY=#cq>ac3q@ z@AOGdrB}*@(i?$(=6cjYHpjp2YM!$_E7K;DZTK27&B#HT+P|e5{P;27|bmOY!yg54=~S}0t{_+ z{9ZQeOk0KA80Q2!1@6uQFf?ojO^tm}W1UZbZm7U`7LOUbo3p|@uA88xAaT}<&Ko&+ zmSEO&pFlyXj1FMpGSGVUgN48jV*MLiqbm9_a|4igm!R6Yh-1al>GuPo$SyON-SGaD z6E$mAA+(n-mcwo=@+kTMm7|+j^+Syy*jb3P#Ihi?Y@>O{fkC{bWiGJ_be+K%chUK@ z4WT*qal?HF1ufMq7QP~0Nuzycrwo^{Hks#N5^`Qt|6p_C@XJi$XaokaI+Z0!fVsgE z48jIWIYOVCnGS5G5T_vcG)FmaC1FdhP)7r2w}StiIsh>Q&&d<%Uz zK3EXW17?rA3p2&Z*c@T#k&iG1x*Z-&G3FQyz#7K}8^k?iBQNr}Z|nfMRp*P-hZW)g z2nm@Itbkgo9VJ8@WrNMb1w&ZwQVAVCMZ6^ka~uH>G5oCU(tgLU_73>}1ScX&UGhcB1`Y6X_$ zO8Tr)nZ(22tO|YJql1%01cbco8NgBet3`;2jb&C;|F{J$T^7buzf9f?spvuBNW(PH zSs{*qi#xr#11@;+PJ5gpWfx)&C8Ql}C3EXWpV>I{2JR`mSa!klcMjqN!$>g0NYiRd zBVO%Fm|w1Da*=?i5}w)E5wBR6hgN3V&|ZVhk;jwshEy1B|h_?xVBGYkh36g2CCF}Inxv>7SS@-qM;H|EA)fx?(yL5x%# z4FatqP{xFsn=fOAwv-e|fg_sI(wQhQ2S4fKXKPw@c^Evv8AFY9s;2Mh_Yf(ikkcc= zFzo_c@1*A2i%P}auGAGcr4G|J88KF4CaA_y?EH{GjQ>&G;=w8FI{$D!rEMQL!6xV= z-d&{7qTEZaOraf|)J^ASEosIq>x%HvmAK%fk*0{4R0~~Axo*sb?&~U|5=F2MqSeX> zcb_RuAcP`QSN0?2P->Glg($^)D@v0rTm_hY>KCR3tEo3iv6B1`V%FYr7+?HO3`q~D&%gX1cYSU8cot@DJ8b} zB*`AEM|o|)Y5k5P&}>}`%t-m5%3SP%G0bD6}bdx`8?Q zsRB`P&)OL?la6&0b;&8yYPeC%+UnNt?(UgwsJBs(!ZA%)r>Ygv;-pBCIfgx*0lPMl z&UUagr9pKsirMfye<|r$sDpRUut{*S@}(|${!R`pwi3ARfV*p>RjrpA`ALSdADB+% znof^YLkOZSp5zJ~l5tD6(SNm)xyx<3Zwq;8#la>GAs5a+dt$+~NC_8xm~2t|hxi6R za*%p-P_Z!5vkT2r3=|hra|TO~@jGTtFst1jP=twmmTD!PM)$8r z4VrWwz{IOW<(eX?OW`506@ybkP``7-W!N<-9U`=24b@{b7FBxt5qJ7 zQ21avINJ*Fh4PZP!zey)Rk(e)RBEdDn0t$WH3dGAsPm< zta>$4p*xzdk^F!!mZkAd;XCPSdz72OPJaB;Ss`~c)QWehYI=l6+qXa?J4h&&dxhd- zhYTx$Mbo2$6~zZYSpn5l>y7_ar$O9i1X1ZuQ)gC{04eTVXia9Wp3O>Q<{k+ zEiHvOE6^C5DoAccg4JLTz|g5Z3^hOxb7zVKZ3LqLGi_nO2Cy@*@RU4e3_yhjPqm`& z0jMxDWIEd44w;b3;T@oMHW>p7}Hj(8cFMh!%R{VEC?zD(z-9W zBBW)Ndx1$(f;j;|ps8!abs(D&YXv6h36=yk0!bYnmxm0l(kl4&tRHCR!(rc?6<%dTHm_ zcDrN-*Npsz5JDk!Obf*GUI@ckB=zcVIlVAbYvZb;1;@x~A()LK^d(?t?2Y>M(oQ92 zVr`Ul31EAGo-D9w=+zQ$USz9xXf*3mImp*L=Op^ox{e%}w%`Dq!>Tc6eu1*l_E@z5KK;~9oqVDOF=c}dB0$QoL^|w{jipRnrck19cE4c==C(;7( z8THn{FuyA)7tqSg4cz8eD;;x$P}dpa9?ET^LyD!%wyy^Vp?oBdxB4qzTDW86~ba122t8l!EwQua4u7SF);u!%o&oo>j{9l zE5A*LwE$_r3O_~*0Uo1YAtk%A0g_!W+YB7?k$Mg=Xik%WabA<~5sRxHj5x7T=!n+U zkl|^Q$Pv$qHp8J6QmcwJjsl((K=e=G`)NFUTPZw%6>bgvFdhvvs30^2dkgv;FN|la%fyAsZDAOZ z;s_BGNZYcYKD$Dg@o5FWJ*kGNFoT9+y*RYOj0yVGP7yCql8qCx`HmR{$srt!zH2wb zP}y#R_Kdw2!W_Li^(;`h8H+-iU7*4Xvj=#VIyGFN94{iuC{XVJR+70Y_<{Q}XG{UF zS;vh#0M*tc<}W_GGWH=Ou-3Wa)<8bh*ZGS%ntbw9@kV}hgp9EOylhk*Fu7r2x_VKkc#v5;w5i-Z5@QQJFDY`TD@7xw}doZJ;%LUS9)CqgZs$;7IeHmpa zNUSw0k(F>;HKdPYoMP=~4=I5g`}K@jfsW~}Y-M#SA|=D`$%~{+fz`Wha&=2!dz&cq zHgdmO(RfAAUq_EVSZ(o+Q)clJSmIw;RioH$PCI`yVDZJECRqJnef6H$ZdN28oq(Fn zJl&rD234DU>Lo@Bo1@chqe8T2uo~BFLG zb5^JSc$a|(=6$l>9mmPS;&Gvr=`|vvT+b?yAqr5+iHvA&Oe<3^Q2G<_Wt5KefIWR@ zs@v-jE@<)~1|w@*M?dZ5h&QfhCuG`D;T21?Fv_4uH@Uuw>I2c2&-z}6v>PF{UFZ{4N;8%x z!H>c^F#wQhjWOzlE?g&UR}M@`c~B*WDmS?!f)C59JuWSA&WMsiuQs|=P3+e0s7C#qvaPq{BslO8Hallr3LKoV7FsL5;= zzH>pR{M|_~zi9TQkWGgP*!CS58!4_!(QOW*f zk(n!SPQ-0~Ore&xs}8w$rm2=Y7=ep!nZ!`Yu%9!lrS8T;#LXE0GOJ~|s6musc+sjL zX0@V#j0wR5MYYPe=~8?L++F!Sn>c|M;;!Nz2OL8Si>pZ&ZC7@W2rlfOTeXD8tBmhB zamu-x22||RCjJABE|yu(ew{fu^NkuYQD8>Qu^M{H@DrvL-Bm$|L~0p25EKtlkIBPu zgSflk0XHs!3nPVc&D?rg#x+l``T7IAX6h0;ohgX*8H1HTLc~nI7U>79gI@f9p5ui5L{pe3sVNyIF5X&s?=7ZPvj^qzbL`6 z0zRX8QB4M>`R=5W#_P0g_s;sO?MI|@9tibwIpthXf+PNo)+mlhXUzioshzTxL2XK% zj$rOEd2|<6xaw#mM{fsSmZ`+2Od4nuq_CK=3}}<>d<tv9PN0k?)4;@TDh_{6*Az zCi{Rz8nS08>KJS6q^=>npNw1YD(>cC`C~${&Dz88e%f*(`1_)&EaIb^Q}$3imWN25 zSz;ov{n~Tz>)R{0oZnyTS{7)rYUo`F;JMBk(3~NR=ZaAB{YU-!iGtTk1oE@!N^AH^ zu2V~_Rppqs0{=oe_)&{G8UQjxJRaFMkk4O9^AYT(k~E9ta?0Ah)5bG1%p(p%e{JmnqpOrdu`H6R2O|N7jBxi=`9Bid_2fBaLd28)35X-CI0Z(LF z0bY4Rl+T7S@%V$Y1j5M$(HScy8S8G4m%oz5BXq3*g++G;D0W(U=jC&W_>&68`3u0e1hVPs?SkZ(6K>{3fEuL$JTB+3nYPTc}pkhuh@f>Zp zx5jTgn~oYG;{zR29mQR?HnoxqFpV4#QwsWq_%LFp?XTyxYXWAJYsA!|O~2IsAFBR2 zx{~LO9)%|pnQk#vJ+*7^ zr*?M>4fTSZ;~jgd`gR~|K}A%5{l@51I1f2yRE_>X)8c;wZH@DcsPtc025E&@)F%^N z>Ty12K1OJcz>V4@>Oy`ET$jG52(p+l*}u%58d2ugi7E5zBvt`<30IQ7ruA|hW5bAM z+fd;A?aZ@9d(G?(JBEZA3hjppikJUR_9`i|!?K{Y^Yg$oW+$!7%#&ecg9H-x;4&kC z7fm;#wTte>N(K&+LDoPI4+x{iQALpwHbF{DRdS{UuCp>zv>-`~^l&kAv;e=s_LE;R z&cT$@{$_d}ZC&I$|AH(n*<;L%3!eO)dvVT2M&tfXM9~~95_>ljxsQ^YI;;%Kfg7VE)MwMP)|PR zsK*2MiFnT&c1CsUOny%yydv6RioIrj_3IVQ#XERTUN$n7Atyw64`oH}eo?P4`SFM) zPbc{y`Oabb3*dH#Q%@``2fn52Uz`t3J0f!4M+GK!7aS}_-zD1z#Hn9u}A z>Wva+2;e^nmk{RVjg40=rY*;ztyIbFjiZh%hb)J~j8yoquP$B?Z6iLFm&h-H(Aqn+ z8b=efyX9W_%WEwt2HBV_%KB|L(&|Xuj02vGRAMe!WJwoN`oA>o6Vjb9KrL#dSi_XGVQF`Oi*DMLrV{F4bx^czw#w{BTrzO=NSdCYG@Z1!>Kb|S6+r>ppQ z?rygeCdIhd{@DMTBaaxFjX1}aN8EUKtB@Ft3z@%ff|*Heh`%xCLmoma4QM5GN+JGWHv-lSR1_Ni7gPlzn{lAaWdx03k5Vmb!Q58 z5g1m^Fs}z@mXzz!sk{&dt|N0$h@$Rd_F-)vBD*un@}JJe&y?$u9liYch+!idD3Qd0 zCa#SR&>Z5r6W-6qaS9{K9QnRB`Gq?0{rlRXx_d+Bh~;iQZ-kuRlrZv%N%wn3Nq6W+ zu31}xCi~$WeyXZP0a^xJ~#S)UQH;hy(eH3`Xa_n@8rXng3HHnJcLY~62 za)BUHyR2Eka?F6BVi^kv`iKXd-7KVVqXK54)G_1D9oDQ{3#czi86Hli47KtjB(`V9 zKr(~j0YlF!SCG=#IPdmx_03&O7eVrwG?361Ly$-NR9PgAlJbP_EgVLF4$EDu&4Y9# z2{NMpI>sNY3AD;MMD7ZdBc&;D3$BdRMM!r|AM*_E^rR#Mm=4}a87e~=Imjh(*5^YC zrutnaT1@;P)#FXR8j7HX%MsuotWBxN*ae=dGQbO){>JEp&r#rZZ@c|LS`(+Gxh?n! zmm@(cA@^6iSbl-I{E!>}S`=!5Q6Zz%Q6JM>m4{;DB=~inT*yhC$ydDG5Ch{G>8c_% z3}rr#sV{ZF9l;K?7r1*a0R-Xk&76q&;AuJH%3f;PVo^e&WbWQ88Jn_<{WCl z5D18yFM{9oDP-ty4GTKo{fV#thgnJ#*?cy2XI!5KvN@P*5W-$ADcI3~xNc<_*;=oX zSwaGj(mqx6_{}6fCku;=4BzN`=&kjySj4}Bdph`2O81vX%Q)GxhV=i5=QH+UC*Hj- zq%@-=3jLEw^7S$|T}vn$d-=@4`(1q~nl|0&`m}xtXS?VphIn<8Jj_HlRIZjNuLV|c zW9hd4`gT|u=UNxRTb{3g9{}<}Q?IYrsJL9ynG0C&s0U1sczq#Ypg6i5%RxZIyL|`*bevT~0d{r_>`dvgB9zYiN0HA7_6pG^ExkqW*uC4-3ArFs3`ql)aC&0=>sF=)mDd!VOw`yn zQiEL~Yl6^5E4|*8x4$0OU{fi?y>`V2VzUG=W7B$zZ-!^*$)fe7j+7rAQf1}#6`xljW^6=%ROdzde)ILlJb-@k-U)x&}-}yMBHx*IwFiw1(u>!ktNYbgU;Wm ziS*SxejIyqA(#@AQJB$v?%hI9<;oWr79_4qT2-4><{A7dJ1PTE*PWD+5z&5SObTl6 zfxa}LlM)w(KGpjp-LO`)kjn#_+WS4Yeuy=H3r%vlA7Z9{T{1I)^VAHPDDPTAg-Je> z2Cmo<#|%RtIG{SE#0Rd!89^L=uEX8$8MNDIlV;|lU;G+p*4FzDV9Yj4Z*++nU)uI= zOIG_pjb&umxVVU0=0c1+j&bS!#%k^;wf^*seGqpCd2^V#5IgD~YORbQy!Oyc)o*Nw zG-1%-81S0cHT#|;-_fT_&g6>mqLCEYF11JQBB1(7d`B;;l7HmGEsedKI^G2=Kksqe zH&eD6ti!$Yd0^kSnjk2=rF?6Ne(Q>hd{@eY4@j9rpEO`=PYcZLpUo1QceVP}Xb<6F zuSL~p56Mt<)sJiq42BPKgXUOM4Nr^QRfS4_twm=XiMLN(0ozkSKB^nQcUKpaW1F8IcT)_wGIt2^^$Ob)Q{%pe7z|~lTMuu$tfXZXkHhSc$HhYMtdMFOeW*8$nXIt?LAo!W06Wr@ z-KCxnzLYqB=}h>ilp#1*OL!+|LfUTsY{*FY9{E-76k<^!JLEHOAQ8p?C+s_(ojJqh zBOc+5v2;SNC#l~v_e@np=pkMB{$$V`$L?K(nh;X`w`z5F$QtIeJDv2?7uE%LXbQp0 zruh^p>}1USzeCdfO9}->F>6!G7?jNJaD^DH5W`c!%0 zcjPtB(}$J>T#EDpguTNaOAD$R5zuYwX--Kac zwlnCk8RHO<`cz;UB)cwsKfeya$7^i3^znVz&>4JmGIBhTdlA3k01dq^mM3EuJ&Q1z zkCY5sRv}TuQ3%hwbod*LX;0toz>;c+0(DO9zblVA_`0K}+( z@Ha)SX&^XzBB2VC@baa|K#2_&Av61JZL>SZSU$l7YxYT5oh%37s9Z1*r+12plTNV! z$%uAB?-NEoDQ>)*NzJ30Q)p$hE?Vc~wGF>XAR!M-R3L$gMcOTZP9Wh7nw0RUMY5Jr z$W383feJXPkpQ-MYZiJ7&;wf&OyEOpa3v7}4@a5u08%dhB>_?@1trLtI}ZELF6R9_>bhmzT(Y^^$PggqW?-3XV_>u7VIh|E@hB2$ZK(A55jWy< z@RxuRB~eg`5cR)M6P!EMA!`|j5W1MI*+kPm;(L=+rTrW@C({XsNJ9v!l~FEk5$()T zQJPkUxPtRN*4Lu*2c*O|pb&sFq*$M}bS^bnbp&{CS%9{$OHelbl^8DBtDbC`5IvmuD{@urxN)U+J0e8tG8Zdj^t%HkY@Lp{hg`8k#qlMNhw5_!Fkdj@; zpMa(Kg6R4uLfm^PL!8bISRD)5%u&Y!u7B*qs|4?*oKG1MU(DIV2Qr}KEg87i$Z?HQ z{M&igsD+}a@|M!2j)0Tq>VYGcdqqeribIEG98sUvHR+GRm#$v2=Zr)DY#Z~ z3elhC`#+5>*BI}20hd9=S^)H#un(X)U%ZXrJPoSTb!qr zKJKNxhXU`;Bh=k67Y+WTN0Qo@>*yJYP;=s*}l^jbDS}gb6AW2IJ zezU@)mf@}ZpHe17r@S1&F!lOBueR?f7?kn>CW6c?92Y4g=(K^RX?2Avd%QX!rXtu7 zIkLu$qMmiPiczHFhbZpc4B$o_-7NQa=hl%zJclWN9TaZ~jaS?6_lr<6v+yI{sWGEd zFwhHNzU2;-PF`h1t=!L!>hFgsZ9_;_#JfL|(DhlXuF+rJ)xbsUV#2ttFssAc>}z8bq#IBaTrHO37^68ILP_pgxSc{_iBr$QG1K^ zw+2*uOo+%`*SljA^zJMQBc_ebfgud^II+S{3N43m19OBqsW#Q6xwj~VW35&`Se@B~ z{+qp;09Uo+z{?HDUOk2+Gmh|l0*@;qxrH4>a;{vGia-L?Cb2WR$F~mNHvqOzJRHRm z7US_9vJO;2Z=>#?hz|!6=mxdXU*mpd1(+f2+LGcp_Rbrcov~;wov8^zpKvo3kLTOuDuVac z5XRpM^7*`xbL#|<+jcS{3rL&!W@r2#R{%p!=iv`C%LA;(*MReRf=m`}sF^;TdK1#7 z*m%*3n5!XW+eMFu>yM|27Q~+)&x}I&2t&W{jf%K0RJ%8jyfWgk%X?%5Q(KRVtwzUt z;x~x?Nk6VP)QrtPS5%&74=$0K==9<>{1rDoA)i6H@WGTQAL#+Fuf?)2&F-hKiNRm{ zS6!z-QaD5EI<<*KB-DnTA*6U>qA(z-8QdHPx_+D$B7>}yp-HH?eBglRFMWkS#`M@L zLma%jT}bhi7!@fa1xWFndyz zWlEVB`dT)0B`J-+4Y1m&@1FB-~!yt7q2L*F`c-+g;03 zro&|morBIK4~CgsZpy{xWm1%CzVPKR=r3eOEGz2wz4(;uF|p=g>kpN2VGta|;e=>@W*ZW^M%Bl@TPi06I}xHLtk=vVP*UuF{CK>!Mv~+^SP0&&Y+T zVMWc$+5H&AoEpr^w6_F_pPW$6Ir#azXF312VQ~4QwjyMzBQnx859Z5mz&ntFtb;tD zpqu8W5GwkFGZ_I}H2n-wl-k9+*eN<_92!~&Xsg?z#CzZ!`(2`34-h&oG6PIOjK>Lf zS|u8t>d{>c6mk`}9e!WY3fT%hbgdjp?}(-$oQa+M18YD$ERTKp2XykpD{1?dg|ZV~ zvIE@?@Xh1!w&c=hjA1hGGAfK4gtc1IX_pnaa{$_5BGI#YT4b43YcDnW0 zWjCf$E)D)Y<9mUI*0+IlGr4y?P0AhcukJhHmPy=a$j?w!?|^7pwSm7}7vw?%ic0Uq zmqL0?6=z=^R?UZ}jzRMwx*@s?cExlTxsGrr+}#k~)F%>=y2Ob7fTaLGN209Vh-UD? zb~y?sn&iiD#m!pm>o!iAb*~TYVE|JKcLoK)cI&wulskjCky>`R_BmXO*UdUB6$de1 z8FPS8GBw!grNQcgew$XYx@Qxd6UE?KVA()PFrHboHM}+Tw(_j3s;?F!A{PY>Rw6W==Ar1wim1cHP zVPXxWB><1|dktP@K)~89SYV!A&AXpw#s~yO6@q|rsW$4t>A)|{{M{AG&bT&D(B#py zUFu!2?LUOde9S*y2+?3zslq1A8h9gvtKdI z-fs5ca}-R;uNxE+U1xm+;Su0U99As#;A}oioKR1cC zLHv}NMY|M8m;M-~&k%=g8iQNJL&-0t_n=0dJf27a&blGh>O--EXn{zWpoTnAH__EK z+L`4v+(_-##WQSj7GRmh=(SVK3$k*K@d?$q)I)UM#wggY<9%LHA}TbOtJNN`o-fFq zjD(n61VGeJU-&rx^{-xh;@zK~7x(R#bo?_qKCw|oRj%7E*DUW;KkH-n&093HB6sWu z^nUfPcwT@=1x^SF-7&i}=94!(%+Moj{&cy$G8OIzdna}tnvj0pEvwr|jq%}mIDCjD z(y{q|oyi;l97WYf=+K_Mraw(|`&G~0wx4JZ{Hs5kl-pxUuB7^(_IxIh(t|G8e6!q7 z`=*jm5Fw`ei`c=TRosb~Zo*LiS&ZJk>$YaT$B*jkze5+DHTdW{Xh?(n&n%uGvQ`0u z()x-YPm)kOo&tAZKuZBZF$gmv6$f$_uL^Y4L{aZ)^4a2|T4xo!hCmv8+o#1&V`r^` z(Z5vj>9wQrh^;KwM&&Tti?DpzT0(XvK!M8UM(ia{4I{T#Qe*%H`&+E;Z?PVWAZ|dHkR4du zJ37b=hjj6nWJCa%v~*Q+;u);ONRzNR8Px#?%&6`(&2?gT+qN5JroVC9HV04hBbic= zzcNnA%~Fn7@-shWCmx69wE$r2hf1QoY4(fa4UJ{vDynGfa2@nSXTdvHy+5pW;ci<) zm{gt+Uk%2`#IWD|^Q#|EbG3h8Q-8Ag>b$*kfy3{!vTZ=ewWf57KUwQBFy{860q7uJ zU*ZW*xfvaoT_Y;ffb+A!J!e!Bl72a-A?9;2w0-7xpl?IV`yG=6kqI{T=&RXFhSoG2 zf(>1;$!`Btcq-i<&XIG&dG8z0ucIEeE37Nhv(7qBT9kaVcw`v_2UjFQ{{95zrwO`% zr{6k(FVfY^5XN?GWifjr{j5Cl(4a8q>~21XHw&>OzB1YDnit6eix4oM3ME{gKz+n; zj0tFxkERa<8iULr*$xMsX3O}?60cs$U5l6zxG$zs7gQ7O7Y0@{2QF=7$=<|*(J5bZ zL;I*^yJGfe0NQ0TkvZatCH71=6JpTauFs_s1r$P+pzWbhDW=p*p`*RYToFvZkmatF8VvMNo;?QvL;Kt?s0{t?eBFkjWr!J$ zWcqBR;DlstJn zkN$_Bh2nz)n(8=|orx`hfg00W^AX;S;Z=dG7&s-*oY%2fN}N4x@w<phM}aG&TU9(>+&>=cNnI20M5s=umq+;&qW?-QZhN4#Dgc0VT5N6|aNWU=qLZ1V<$ zh3R%(KI_b|#lKwsVzoJ>GdXAmLM5GMjE`JB&K3h?Ls=$Nb8Ovjx?md*G}$|emv|_3 za7x_tad*5mUIv3ko#;?NRt3yAE%FbeT8t(X`#ZcgG=PitACu=DQQ_{epCfg5rU&Wu zJe-R@?!KBY)MHEci|RHlCdOGV74EgH1*JEk77L2k1OS!N%<^ig#Ye?55=-&WX(j#aP|$E!!dfP zEWmi`j9f8khQ9gRDYi$r`N&F0w3AqSC|A8hctu`nZY;@kkzXh1so**T)+B6Pv1*GX>54} z7P3*e%3L9*@$&ard~yiP6;S}CJ48(Pt2{Dk)#8?bGn|)D^4Aht;R%Fdy|SFW`^f(^ z8l=LYPk3@2s8C^u*JnM1mNW#56{Q>}=(IQwYK^>X<9Mx6FLN;%BB>Q)>3!Usu@osBrdq*hc-;$P6#q?m*cTxi)TMMiaaK;QaZb{lDu**kbXm!jvi0t@Ay8p%Kf%8ReXz0XYo{& z++JcQDtJMSO=26#1I?4)pl4w1x5Kr2c4R4!I9nX*IW_0SDz$TTVI|$tY@DMYxsQcv zRPd|kOAP=xR(faq#{~~K$v^T~&M$A^R-gf>l$5kJXSPBwk}7t~G9Jb&t`kig!l#_a z6mQUuH~>v^!hi;OlD6q@wlJxdy=%x>zI!I08yNxLvA^DH*fOm4t=x1wC5pO*Go?$p zI}Wybiki2LC_B@ubg?{a|5I-bq{j*S6vre}JZUER0@gr8`$X#?tQNca_~&CA|73F4 z&UN;L8coy<&Sb%aQSUA$8JHFU0fmGZ*A};V3U0?2n9KrZ#EyhrSrmoJKs1VpJ_$iz zCGi}2bG!o|q3NWqkK$qc(JkImS1KT$HJZVj&gm)*g3|X!M6oI2*!8SR9 zAm$NxFt(@CSndKRMK4l#umUmo{DGC;C5g9isC>Vetg3&&H&{dC$wd0cKU*@M#RU*P z@485+=Xa~vecJ49` zeI3<}xoXq>JsN=={!(}Qon@i7nyM*E(ic|JTuUPVI~t31GA)ziO2dT_+%V~r^o z$0TOJp<{>ZL@9Qh85#B)lvuEgDh>1`_yRDx%+*@i(9=^rbF&TCaU_}NNBcWC$skRl zQs|+{oK%upRwVMwPxyo zEtpiE4Mu!|{HN{JE;ao8@hJC!F44OK`1J}%#Qf&lT(P7VT@h(A8=K)Z0ExaDojc_7 zokHe$ppWI$JwKJxCsrQcKfFQ+pZ2)@^0I3nH3Xk?+alx7%i zusY+N;6|#ZHQ8=>+gwZUrI4D7wf)3>qSHkCAOYaNrKp^9D?d{C9nbG1Rb3pm7N*h( z#?ZWAI(95;`|Cr^Duv;6v_W$SI8WZc&4@#|ct&+RjfRy<0D3|qF4Nn3CRTaMF^f8C zc9ISgOfmMAZ=!f(3DAKib3a8rDA?6F!4{9oD%wq-PHxJtgJH*Lmm6TC4{>)~i1_4r zKghgb0wilZ&1PQ2Yu_N6>SS~Fu;@3$kjZ8Ul2|PhPZU%2?c~gk+G_sQPua#Zie1=& zO`Z$p!$Qh?Ix|5rK57{mjRRoZ+V{O;pY@Hx{iF2po$VkWFFx@b538L~UC@_nv1_cl zSwVk`>rNTKZp3@a?)Sh{^B&kJf#BIsk3&tk{a2b$71bechr!T8hqatH9Xb^Of8Z{P zgf`?(=eaM0%@njVGBPZ!WsIXsD0BQt!g))`pQ{uf zFWx%$ZHMlSpI1mWzoaj?FWq>Ply*v4S84#C6}ff3J>{fQje*o!f^<7kX$buxTWI2k z09_K>{1bAUs`(dLClfEu?lvQ4H)N)}1~6@F{fxg+c&DLea|y5L)}RT*ioJ+<>|Fo0cjEKuXf&|cxyl-Mb}9{dav9(z zdegU=WXs>Dw`A@EE2?jJoAH0geO>;A;p6jf(x*N+qcx`CB8xFT2)mMWX4k$8fl~X+ z8G;lrJ}h&FFohIWUwMkt7rQX74O0=aosBr=1$I|~_J*L#F8x#4VYe`hd-40w_%h#uL-cC z7R^#5*e#?ru;DJK{~3TRea3$0$9Nr}{49(aS++s;2|665IiWsCH`1h*d&pA{D!u0b zsFWK~&}xh+Xf`A%@0K;2(@5H_s3&cgHk3NKV6ZBZKo%i?#c;!aqhP}lTq4P|>#%CpGhu&g;#(py zTvt6gE__Y~+M1Mp^s{eX+%Y?>gnm7sI7_uD%U(8r#Xo(1AkRSsWpzsSbIPXZpu-@Q zz{uaOmw|2w9GKfo2|8Zc$~e54ygCz1ovwi}n8FHNI>v9tN3+daZdR-O$cwr}yD@CZ ztmt04uEZGn8=!Vgas1r4nUo`1dSjxo#d&zew==#-S`6gS1ZhZCZUZ+%Htbk_*E%vE z*re1-i+xBA=5Qp7isrJkq^p~ZL=PQrSgYFcSiK%_I@;p65&8NQ#VbXY1NXid8+)!e z>eru?ONm$9b{r)x7;RN7Der_+xcOH4-L4YGb`ZBQBm}oS%fet4O-Tn~nAlKO$sz_lmGMbAXnPNFC?OaBC@x;=O*vsaheWp>3zc&{$Q7t!j2C?bZ zp@1yUYrA|kW-2!?=@vdq%H&yfWY2?Vi;SW3-jJIfY~If+@*V2F&xe-+x>`DoA6z5-o7$2x31J-tr&GMySmiF1|6eADNs7hO z{=fs#2fd_O0})Tm&d4%-k;y@MZiERIzXX&i6yX9{tAx4sP86y*wWypEDgpAf|9Otj zU+F@QNS;vctnatBSlj25m~a$Jb1`N#{xT`69Jt3rmj7N}mzzqSo4Kkf{IUu0<3r>; z_@2w)_RvjLxPM2h53_BMew^=iRKI^E5yRyeGoZF&ZiKe5PYke!bS(t zF{4!WB1E|~iUg~FJwIVOm%Qd?K-QQ z`!~{MgPZiO?Y}o?Cc5a^28b1DOkM>%>@ZRnEZblG9P8KL4-3JPF3b{1SY`DP0H(gF ztfSE2fKVDTU<*#Zn9RFS@?F~e{apK|iBazu;^V)G5lD3d`%O_o_;yhEiXfLdODBAD z@ihM`y?a=}#?J|g7T}AW+#eEQK=n!P_^RG!%NViIQ-#p91pvuGSTIXd<8EYTqRp)^SWVmV^f2Q zC4prbRSvj=I6(5P;)%ITq*6{8AIxtZu%B@LZF@cRQ}w6IVQ9tBSyGzUs@>okczhNu}Yi#nJNC?eX)r3b8Qia1OAT~ zCfmhGNUJ$gHbfv;yr+LZxlOd7AkG#t8TzhF4K$dW!Ja#dZ1NRWzYta`nqZECmcE(Y zsL=YYk|!$XkXUod2G!>u(m@jMoNyc6!~gRalMM|;Qqh(xl*`Ty)c(hDo+JjcWRHtx zmO5(BuG}vL#1!w)>m3D))4@d{L%R7>Xb3p6TrR_CkHM(=Tnu(IGctomrt2 zQv7uQaCQ5Eos&j(O-lMa_T!x?^KaFj#q)DKhz?^C3qi~Zh5DGrYWUu|vxO9JrSj6^s=tCyo=7ux&ZF0)?$Ou(1EWge6)z<+H z>1RA(DH<`&h5N{H)Gg+<0>ua%is=~rSz_3w=T3S`qo97w5$(6;M@gx}z4FlVBrT7`06nsPCQ@Zl@ELK9N zsw2>P8&o;tD8PFAa)6Q+s#7XI9I~uAeY;?{@^w9j=Q6J9dwZ_pBibxspYD?6tHU2- zR?^jpjypLke><_`8g$j(e~bDHyM0CY6AmDYf@#_czUA>;i0$~FT1G3J9jVkbDDdPt zQJ2-UYtSxX&k?*yKglm?R+sJ9UnNA-jD=*7fs74-lZZ)XUd;KkDIpkkY@|)n^eczA zeDh93(XE=eyN>022vzDDnl$SZFcp4cZ9leCYwM*h>mVM0#5xo!%nKG3w?>|VW?)LQ zW08Qa;Wtr1PY*5?EQkcIDYNPvS181KA7dc-QYw!DN~}qpt%Rnj2KW01j=(FU8>+ut z;k^-FIX=Dn`bho&7A9=H@vrPeafkj0D`AKl;olpil@)8<4K9SVbzDm)2`6F??_Mqc zX7{8iGrz^M>IcnD;<*_Hw`h-en=$J5aA1_)u-QJ1-3XAK=24vrHKkrBoa z{}W0A6?^1uQhq|jkfK|-pjIOJJTA~ zR2o)eMXbo3cOrU-qE%x{6e3|BL1$qj=zM!Kxz#0Mh!WTD#ENgxfBaq;m4jS$AQsC9 zmmv8$h_chqO>sc$rP9AR^R@s>{>GzTTP*5ovc$D82L2!AKW2aqtkZ1k@95kWZ%4DA zs%ka)m1D-)KQ?pn7a;aLZ1GF=*2C%oxWZHG`>v5(8O)vyh6;7(6nOEt`o&6}3(?uh zhi>)-tJ~Z0CKc3J#dx$S1TINtZ`D8*i>b2`2~_^wOq27xNs8Ep4k}4I>lYwtGaL{B zc?vlu5?D{tU!h^u8|wT&8rMu1xc(hI8zf@mT!9uj}>2LW7!d(Z3Dmxhh3 z2))ei#e+^-xQ=hCGK9(@tOZ!sCNbYp@C;$x0QpgwAu`mISeKef8@vrnSMFi zm#n$7!-2_-MSO?FC%wtPIs(%`hnC~&vt(y#tPL< z`5A^9w=N#r;)Bo2q(M*(pxIEc<&>ZM-yr$;BCdb6nm;RkETO>`?Ln2e#)Maiw$QqI zAy$55Z2$S*>!rN`HS;X!_Oec`l$RmfYgGVz{WRj@Qu>~SZdQta5Y>tHAF|&Ir`%1i zocI;d5~6qSlt;UOA$96W^CZqc!o1=$XiL0@5KIapEta4W0AHX7oM%cL@TD0S6;Vyo zrwECB1X`bX3Ju@T%AmC6GDsQ=QXI815u}?OD{VZvCfTB!-zL zj+O35;sNZfSQ6~dVIe7OEWF9tzJYu%36qEx&Jyt5M{F7-oW-XbNj~o|Y;jn_=5%!e zpE1&5H8IqF5;iveG-SNWkEg;SCA`cTlnjz7npV!)g1{UK@+4=<$KUR{2drKZ7%SI_ z=vMWt7g0EZRndc_B-`ZaQ$Z&4(L+x^$3H$klY^~V-0J~^2wJV)=)7~WR-3QektG9N zc)o@h8}w!tm#t$b2}}}axJ+Vh4y3^U%Tw^n8MFu{(H0_zeXX7}mwfh3fn_GjT~?fE zURUtKCecWgjT>W}DOIK*8P8l>%*Ej_=OuFxW?o(pxvp>7XqS403vf3!91n!S7 zwk!YaiiwMVCoG3EFH8YT!BpqRW8GBeh2y9c$xbji?wQN~9codut{wjI(TV@BP#dvn zF=`dz_!oI5q5KZ3bagIwg9ES;aALCs%JzsaVfkA772anHbjDQ8KEv;3Eb!Ep)_{tg zrxQDgKeRDTs+DZh!G4EXfFr9@X+8uvnU;m&9?gAgfQ?0HbUshijmmJbnYk;2G4Iy=|G@^^k!13TDyAErcU zG1X8F%YCFasKRB$qKI;g;m_YR<;p)SpCX~oGk%otC+ZgR?Lu7(iJj9BnY8zFN}Lw* zb>~NZjyYXWp{&FM=dq6z^UK1l?zeMTW=pyCB<2;E2{cljua-LvxI_Wzc3vYZO%T8Q z86hiHJ3q#=`Nx`Ud<1?+NB^7393Xkd@C0PhcsLdX2p6!_!_SE1(-ZqkDXez~GPu$D zyUOFUb#DqFVf#jqd#WYm0S}Gk`$@%om$h%Ve`dTRhJnp*5TR}$0o$v@$f!ht`SK~! z`dYFOt>K`wTCi&Z&8Gp-Duz5aGf!p~wIV`mgD;UQt-qn10SIi5!%U{rl&G=3OI?p@ z;RT2hifU0N!(!{Fl)wauTpkVeJo9q7#T3uS3KKS|g?!BllVNsaCC$by&+jZ2Br4Ii zz^sXSJ_al*fAV`pXaj|syjLc@l~(hqu3Wbh`#LzRt~QiNYocTm@6KKwr_~DsJ_PuC zXax0D-)bYP>KiX@KYZ#+RoBhn*-ENYa}F4TX|d`AK5|U$3Pit5*sBtBatUe(rEb8F z)DKJ>qUomuwk%&RN!e%p{*&vcuqTA1)Ow-79qV-%ZyR*PGgm792WgksnU|Vf8)ak_}hr`w#ud~)9H>hx1WC+74@;Sh0`<=M!I7v zg~!r5Z)RL2VYim>WP%;L4xe6&X$11%zdhk|kLjnhRrcT+XdvpbR2DRNK3?9BhM!;k?SbYUp7og)Q?T(L5bBgW znKg3(AI`8_-XKZL-@P!zRKqG6(TNGvqxR@vEGNu2*Ht5y^!_@U>zR&oQ@TA z!NO(pY)+g;xYsgS^$+70>)NCQ5o9^&d?mDB_xNIV)u2}#O;1SP8xD~#(;tj3TA4tO zw-QiUu|FWJXn8}gEK9ts8%n2JXGtsFcZ=HJ2j0kC zg^Trf88!x^eq(S}!uH)1=QE+_*L!>w8QG!$^Zl8#a_TZ3Hw*VNuVrm;Y+2!@HxTzh zX`<^z@brvNS!I}<-7Ha(|JxZ(S@unGPV7C$i<{pO%#!UG+Em^vq;$aP&Vul#2k);J zOi(K*zdbCD)Cux-R4|1@Il$h*&C=qN!K@@tO{EF4z-nii`VI-~OPbYDfr&%dRXGbX|3I76Tu#+WGI!dhmMA$yGfE9{%@Wv@ z9fJ9pYf})P$c`oVhe`XgRFarpy1h1&-K$8V8f$d)O|FtFgz-yz&sWJe9G-mE-iq9~ zy=nd8RU7LjC;rAVc#|6Apo6K_qdIkYt#3ySRfZQIwD?7TSp0&bPG~O|&hIx_|GD_i z2LRec12kO>{(s^8)|f-Frk+F`KwECGzj}{$9cVXv7*JIYL22V z!I;e=VMQAGPuU90Yg7AP*vmDlZg{pi_zl zMVg&{ga9oS7f^TwvvScFT4tDU;}uS7pg==mE z`cI0j%~d2=_ky+%d>=tt+@jsoRd$o2w61Rj6anM03tJ=5=|9)PcU1moZ>!tK#eP!a z*OE0d4&^80sLSHF>j_52_~{~wN1@aR>G$XdsoIHebnxdBCrL$oLH61rVt=--8UHjP zn?W_nTTnAFkqv(d=e04Ov4Aj5s(qKfYiJ?je+Ar_KL(zwt zpKU(N*nTFYx=7hM93hh)QMNb^BL??uTP^It&CT1IE29bzla3ii4MG{v`G1xNUx$U4 zAf5?Z5rFk?4>H!=crSvH#OsyAjtnJj?;CAwod3-2pq5Cx1T)h~(ulSEL3R|lJI({c zt1viV-sBNR4m0UkTcETyIi_E;IG&ylL|t$Wd}nMvC{9KLJL60kA0m1FHG5LW6+b8A z0f9{#AxyH=1o780C^l%SQ^a^)DhBoCJ?aWe)?|FtQkxngYs`VvnjLO^vJF>AyG{fy;8}xE~aQZU#BBniCK)_ z43F^q9Pp95UGIym{ntQX4+k zm}*Lre+KdWr_H-+ z9m*>Q{p1N+J12|aNu|@{wlL`D&6vEwBMf&$-H?>uK$XOgo*Vr=7Wv3Yt)#O;n^ zJ`RHN@^B;0?-4%ayYJ%quu#ZqhEG=%^R+Y^LvWK)wCqi5P?iwGEEw?26OBs;Q~em1 zZ(N&iq{4ANd2cqRrDO$1TgUs3+IXkHBdlsn703K?{hC4Yij#iWNXlQVmiUJf{x@;c z-jL`Yict&gR-8E(83q53@YheT+A}7`Z~TziN8Jl0iP_|vWHIBW8!GyKj;x~K7&f0s zPUR#=VDysXA{2U~`gkzB8O!+q|SVMVy5~GFp^7HX%@i4A}rQlhvC=daP6RSy=Cs$?3 z36{J)bHnJ5OU`G4aIx5yZW|!;7ZvBN3oo}WJ1(I`cXgH z)zgI^A4aR15W4b^a+e$va@U&upBGLV6eZWrn;6FH4v@71E{!O$PvF-2XK)df-<3Bs zlGOp3vk*l%hu)Gi@71$VAsKYK%Ap8yV9ox|PE2$aq*IubGG@>;pP10Q*X{o#fBc}x z8)6CLR)*Y4uIu$9Lp(LKE)e+Rry-$7ND8Y>T5alN$vBe9COD$FTb|klq~KlB`^xYS zd{ZyHP{lVOUl|t*RQmGRWEZ*-KS#yaIK!0q&As9u)FvU0;+Ro!dD47Er9*L^;EH7h zdqC=g&q=-HhD!*6gog3tk5tJ)4~V`3dDNvO;Z6%VN)jRpZb3bGwOe)%E@(9-XQzUf z2x)5kwX(+wTp?B{te;ez5Vfj!B#+ei3hvA#@-(aQiRy7F?ohH2Ca8S*RXARiC)EXC ziNBNRloR|zAy36qaDW;=ML;UP4x3cGNC;B#^|%o!*OE{rsjA!3PSIk5uA*m;nHQQR zg}y-1E`&5D7oMABp3Y|oZ8sMgcW^g!nJ1K~_-+l^$WibiI!?tiHPXh9vsNtVrOSJ` zLkP*ZDx-=c!0(9F6i0m}V>78sRnC-yo(aNNee3aCAB`OwGsL!LzB`Yu*M0EA3Jwan zNmHFpDRH|*^fjd*DxFC^xFp0IJ$32!vDKQ#6Omo370)~+Ka&^2+eDE{mYBEMXr$0n zvih1@V;Sc&VlkgWGGvgBc3a6NwcRGK5YT;S_QoEB2M#2;);;ho6Z9=S8HpG8 zKZd`ba_7ip?ryOoXZpE`#b3R2{!2}4S0ygr`=7g^)_ZK|fa#N+PMM9w{2gWgV^WV$ zN}YO5ty2oFF}dKIZs1Dh-@f@-$-G>PK9)1jG^;jPLH~4?!fIJP-3)uy2z5-Fco^;z zbDL9Q1=Q_eIB|vJEMwV#?@&`q?%Zi2Iz4cG&uX=yGDWtnEk88Cm zMx8ihW`3Xk5N3i3CPPeya%j!P#at`csp|sy?KymUHm-&WG2M|bE)buXM&qS@~J4mLxGr#~FIR0wci2gbDI{%oY=}f1ULD6KF*U>Gci^Ap{sY z@8Ib#lYB}*>qm>9wi7yl@un@s7Ou1sjRSu&2$qSByH4Y-cPyqy!FVAkU_KM{6byHi zSrWtMGTLDinc5PuiK%Ta&Ahmug+Cmof)RrTjYbSMleTatHFcA>+(NbC(kf(MhPi(ee_YJVwj0GpOUqym**9oj;dZS zsE=JMm@`Jqrdn2Tp5JO$s-xySUA|Vz>Tl+p+NG+odv~?OM_vyeNO9#gl191UCd{~| z6IN*$MDcjBtQWllnx#@xAL;y=;;4_@MTd?dHJI0O0TGZ)U&fvrgUzPEwaK{Q$}pmp zj${Gn=)HK4MklSoByCI2EJw?_B<`uJEg@%@x{`>k3BGWg3Cg6kc7*u#4QlX81ehY& zo#Jr%a&!L736iYMO8u>Owi28p_|qJY6~hi17tSdkoCsSeG(szZ%h9h#Nua8Sn8>Ok>833sgyjhJywC#>QyqIjHGrs8sS zSah=kJbAw67EE_-|^O2dG7y|<{!d_eEZ!U@|11FGz4@J&W-f($gaE#WHF>K*TYfj z54Tt+5<7?#*ztJrMaD1@|M(o0f(h!8lQG;-Qx^ie%o{9ioZgY0l97?ZX7_wk{$jPn zpZ0{t|IS{S=QJh%Z``GcRy7m-UnRIkmQLlGB8@SBSM1pKBBbJlD!zf;h?FS^EnF49 zNAN-T^9aFj)!-y3g|C?qDB;pwX}Xv@b>OOyKTu9EevM98dZK5x4! z6A^LyYyKOT!R1JT=$UobB#57 zwx-IG&F`ljIxH|urwb1p+*?esdi#a!cg66#)1Pao*>eZ8g~;hRo-%6OxKSbD41)k? zlBKqOlJz+Px*Mlz=vI})Q7{^B$Db>zDKZ0hy9$$wzs6t5>dE8Hx2QXGIgPH@bMp{V z>r%E_msXu;B8(ur3or5}*=3b#Dn)k#|G6qmET!H!7#P?I zhq|xBw0i;ryYRl3H=Xeryd*8{%wV4-s+H9H_|djD{OJ2o_@9?v`Vaj$oiZOfsb@9H z4qydBDxRj|Yf-th`|Ry3;A{D0(isd!{<4EAWfze8r($K}DkI3kN!!g4;02c2y{}A^ zM11?fy|ueviK0Swz4KrT9192|QVQinU@eO?{nHAMehyoxxRZ|-=6;SJq7qML4#I^2 z|9L3{m;L9Z;ej~XOV*IKnvX@^$WJ={%4@x}uBKG7FDlnv`!X;Me(Ye7RbqZaYafB% z{E$dTfs4-g*ypcLd+Z&`yGMJud&fN1{pd}a%T_c*>WdZKgsQ933ZAFn$_lxHA{AZN zasv%gP<7p0%5lCBuj0v&hzv^n78lx^WZGf_twCz+Es&2CT;bVI<$1H*J9yW6P^RL$ zwH#{f!QEuZW&78u&IT3NtN1(GFeIpS3Kd*k^Otd5hZ^6YjY4LXPPU3yX+qTa?QV%G z{+2cp3Af<8lHiX6`2@jp^7g@l@_WhPz|SpPoxEd^B!;DN#cA14S^I5I_O%tVSr^=#c^i-=LV4|r*ch@d zs$fHIp$D-Mk^^A`L@v(_jPghx7d!QS7tQeKX1xf}f4%Lk!)3gYWk)&cRkL$hGO0z4 zlUo#ZQ>o&GD!zdkcmtOz2E`dPhK!B-_Y^GHZneejVw9+PRct(>|A$ecegtC|*UA=& z1CxkFB3WpP`iD`XUQ|sIC7wbS(WWomyoAtOarJZ6r>g|yt$<5zDMX*%8Wq!AB;wJv zTgarS)WlGgD&i^TuCcsHwk4(Ju9KNtSnv^XZfWk?M$BEK2AO>L>3^Rs`FoSPlkPT> zv3Hnyme__p=y1AjpjXqywe+fgO4t>ht}egzvUklENp~z*6q8)wZ?U0eXK0^;6oFw3 zQeArKI9%d>*X$vaePc%6w@RAfL>gA$`iqsg<&`OR|94;j%zLr^_i#gq7=ICTBSt-a zu^wL=HG(Y5T&tfx++|BEe*c>#V1xZ%dMwYBmbJ@Wl)WU z=+sj^jN!6W%+fs}bBJMj{XY9NTYO-~h|DT;4xKcdJ>jmyK^}dIbLdTK&Q`HnqT;KV zA>T~NrBoQh-Hj(ESU|X$H0Kt%LSE@SAZwlv>CMX=J)s|~qq!taCRLW5G^J_ELRBHo zBhr*Q+qgy<(~VrjMVE>&>bQ1*ayH!YkFeBS2WJ;d1060n*52 z1*)5+Nn#tg>8GZJ4Vf_@e8SyJw|r50hmTmp$HvB4G%UpF+y|#Lyh4O`AjBP%$=Gbh z-dpXAL2P@*=b;ap)0StCi}V@i7oNGS4=23RkTRI)x`NeM;$mt3m0~p^#2RldQ#LW> zR`ny9u2}rDl8DvHYHNy+trQr~ue_|R%&NuMYh{)<2zd%huOlF(pwO_WH0UiK%_^QN zl&Sb`J_I$Y@e5Ua1G?C=lB;t4#|p0|SwIx;y~67>AZ3MDO{cE#B9QY&bk)zLA^o!* z&G`3Sdl$7@XANKb!T_ZiMdB!r0Yz#nl1SM8(TZ-&)SHG<IRwEq) zkLa--x6^`(@DJ}9h`ksBZxN+WgsA~T@Z~GGb@^;TziG+5y@MF@Bpit!zKB057&rt5 zUV*7g!&&#u3uf#wa{9lZ|L;FmUmF_I&(}8o4wjiS=NEi=&v%c#^!2dFenH+7X2r)U z_fjVd7Boz9eDoqv1 zoS&4we@j=s>{3um2ZCdy^JB zO(9Hz8TiuGRXMxkhQ{`D1Xy(jucg{nAQk_j) zu{OQOoS60q>h_)NFN-UV!Phq#Qq40w4a1Ht$6wf7 z)A#pw=4Hr-bevBKi!~BqUzKnu>1xo8@e^?ya5EEz{dfFO{Qi~k%g2Tdo)8qWceS3w zj&FzmgX1u4cHnls9^zSzdG_o9@Oz63>rnBf|0PfFd2V#c)4yFfbSKJr^5H$&{^FVx z!i=-Y*{r!l2oIs>B$PXYrGt}wS$X4|)F+ZJ$^^7GW}8eDp$obg+!YZ&<=y}IxW(pu z{rKXyo(P_>cT&m9n&cIPHSFefqlVeX@cdBQP%UAH)A7rFe>^{}=)Oeznw7&=MSwLS zFOms^{O%6}Mob8?+X4|oim$~jgh&*MCaIZ?2>9pmPOW;pld7M-o>WbMQU^&qs+7)j zeafBd{BLpI6ClsIr_^@iyH46mGjTY@4DtrqjMKBz7#Toik#$R7y$7 z5s$v zURru(H&ZJ%PQ=$@K)b2m2Bq}CPp*-ekb`{YASpY@(OWrEAH~@?9Rac{mD~p*V??Op z2af#H(to|GhuvOdQ!+#g?79sEJMIkVRr^OV4UILj>^Qxq@kf{X4+EV}Qu7ljH`0-A zSGs%odzY-31KI+y68(Z+@$boQZL~?Vp*^F2l${K`kL{&Iu&2z%m&(z-r;QfAB;|WHc@{nd%SV!AZm2lZ+Yr^ z$>@~MrFt2iI-N36Y4hQL%#6MRRXk0_do~A`kduX0K3Va1i8SoZV$dJq;er7S!|*ly z^9#3>rymh@`0>qrGREHx?EdT@f^BCBKHvoe&I++vTSPMLO^3WH1Nl&|vZNIUiIi&I zU3XXZQxss=k9mG`>Txf9me?#<9mQ$6xoI^24GAeJ2_p4I*h%u&N$8rTaXss&=A*Yi z!U$ytRNEQy?LOCx_CtBM99>{whs^i#A(Se7R!y>&jPk ziKfQqR6J9gOZ6bPB`IGyu8w3}amDe7ib_;+Tpf=nvC|Y>ap^&Ev4FNziYwB*)8p`$ zNGW^61lePy*6eVI+1SB^#2cWPF1ql@A+VhYoFq1{#P(CXTBfrclMS3$A8)|PG&0-i z5S&h#uq4P?MUVo7D6XpItRhG_6@>P_ZX7Q=&SZN#&IC&~6J*j$6IwmQFhOFrSVq_p z*&K;%sUw*!CTt@ewN9r@M(V*7n`p*F$>Umtgb2a0V z$Ye7KB+Yw~g|$4Wv=FLZyG1)_jO!>|zUqh|Lwag5)X;xAF+vp-Ds{ zf>vy15lbk`6eLOhB})Q((G5ID#n+&`8#s}WmvC@KlO^9c9?|zU3+zZn8o85cpf{0& z4kAP4VOJnjGgmKq4G9^&ed_inKh6nR3db2=(fuRah#3_5&wg(uJAmE-#PeNazT(M{+$V) zVSw{YqN6JME&PjE9S*s6?PLod)7{7gUC+Mzqg_s?OiRjQg_6Y+2q`?SYBSJ)fAEm3 zQv9^d%RvKJ^hNzQovg50GD$U8;N)C_V@9p(TOt2mJFPTLAr+C*R zMg!Z@=Ex;kQy8;RZHz^1p`SL~tmZdc&F@OuC{;X9!O2^t@dk=iblrs;XrW4D1LPnT z*I8A3BOM^!#FO-2jD)J)K!sx46z@WXQ}Img3}>fGB~8WGqT@H>)5=J3HC~V!Z#_DK z{!e+KJ9{swv9=5GYU*Sdj|@t`_qz~H>m1MuK&Goo2SN-dHP6r@d1$C7c!~`(40yg+ z%Em-jLw_MU9B0yUxL(DPTPAtA(g8)k+J9~8~oapr)* z2cRlOgTHK` z1c@qwbgR6-o-72T719!3U{iG zD<1CgN})KOP|DKLD!ZeA;vUBnf^x>9FB%}Lul_boY+zpeZOhSFZi0RLmfr=0OV8mitE+KS2~J~!XKfCT+%`vEjN&^92Tjj) z2YhFoBM*1VcfHeF4FuX#S0f})Y1p2D4{iL>Y$wwR@m+09cAkMANq}ljkJw&!8xHyJ zw;Qsgi~rwmIE)MvOT+$`YYyRYTn*8Pl`bh>s>n?nFD*6Yb1qg>72ADew#GiY>z5f^ z*bM4Ujnbgf`ULY{l}@@fnry6 zb?B2p-e(Uo`_|`tIAHK80sXG8#b^Hf9+y|^fBVh-4Ve4GUy$;F`)}g7K1HPu$QzP9*MJpd8&2H_|T_1!$`AY=QmN3?o$=CzuC5tV|_#T`;C517k z_^div##o-qF7y|WSc6IrsEtEpd>Xe>l+Z-!KmDy9`0__!}bw|E_@KI7d;^ zL!bEAFI+fyxzOyh?LZP#8`eSGFaLq1j-yxbBHZ>1u2^Tl(M(7<46OJThTQYhi3XbJ*og*%+7(j??Jg8xWx zk^({$2iyjEH3K2{{c;FXtK3^BPv3TtkTFU~mU3@hycCj$klRHi#N7MdIufGrVM2P{ zTUSm(p3x=K5ar_T`*`GZeafa#x!it^z*-jWI)t*2s%G*|L|Sh(Ei@q zs0IDS50jZ_wq{s300RZOLC;(k^O?lP+9CaM76TAvcT5#uQzmE)LON05RVGAfCWTJY zuw=RDz>dAD4D3o7gcw*M7V8$>I0;5)Z=Uidh}BMZ8a$=-PP$Ya4l9`K>(I^QI3ki4 zcp9I_t>vVv|7-{!xZ=f!-Uu42^B5OyFbc@=9qZF|9KV7WZB2i9<2P>)UH2~t{8{{N z=CMgDlJ7!{`odH0i=(Tn@>>;gHrD07I88Bm4+b>=1M8pk^(DMkVw_ulqSDhd_e4E? zGq#_@m+I>9hv%W8b*Em4DWH6VGO+tl$Bnv>q8p+N!(S;4984)YdW|;iK{5v1Wnbe>u#T|`> zVrzVNWjy3DS=BftC8h+M*uvmq7}mw6TrYE^LK_9R0-S`2yGHpsi$2Lc=dzT;n>OX{ z0{L?7#Ctzw^h}v(f>JTL1^>8LY^8UOnZy(}2!}D{V1D;rqVQ(c>WJ;GWXLKX{czE2 zHO1t+D<}(+Ni5%8;Z`O!#t5?M6isj3(s-!1dpFRNr0NCJvOwGw<@WjnCb;+2^#ba- zxL!$KC7(=$(4MULv3^E{6r1me(nCFyFD5{tm{>P{jAP8wEc)(d>^Vt%JT7F-j^yrQ zrUf@ZuxKH~TPazxR2nS3{M7TU9Fi_OR$P|Jx73?+;+Zs3Ew8``7&tl@|Be^o_UMye z*#nQA#d%qTSR)(cm>h<8;#%B}_k0WmKYj3@-$W|{X;hU-uaKZGz6esrMoIm&a5-Ca zY~;o>cZHX0>TsFZ3VIxu7xz7W0z05gFgyqo!IMquM0~uMg-_u{RlV;1+$`%A8-yf5 zp0y~W7h1xS)VPDG13hCUwDaP{mUsbOFXIzB5jH?O0(r(I--4x)b|E9=YC)SO1jVN0 zWQ177RwfecM6n@`$oLxUD7QfQjCE0(9D0|?_3Hg!+>2PW+gYajTCk%5$S#}fMab<< zx)#ch3~OkI7(qsc#-8g#=@_lYcN$l!3uJ^qBe0NmcEnKJjN`HBF;wgv+6Rj`|J=|K z5C|{5xc-g91NzS|S)VT^u387oe004wtiAmb-j6%G@bOxQ;{v2(5FooAN6sxj zeyDf`;}E&h>fMY5(qX#@11P*g5D;;+~BQrHzJab!Y^tg!1PBy|(ExCv5Y zrKwpdtxUzp=f~hV@2e^2D)cGEhh zDfXW@NFycviKbKLSCThxkP-$_@%LFM3R2vS4`Bj z;k$s>^e|=c1(@m&i(x#IBW9)U9|&}3D~s?|eBIc3oz9!YNWHH%R&4Fbm)Jt|ZISW? zeIe_75R*f5<=9E`H-_d4*Zz)x?#t-HdZbqgv98pO+PE08W7G{wX}!4U#ZFoZ@%TH1 zs+iToijRkK1a#n~7kcicclp;$)X@PQpr`c$TBYx@zZ>N`AvYwN8e8=@>V?sjfO|_h zHpO4f)2dSgfb7FXa#(?TrFB6RjrbJ)Vw~7>e`Go;!HK zg{zJ2Vaxj5#FO;dul6w2s0;yvEhR6)M!`aD4|*>C zB#fl`OI@`e62vgj@KbFAqheXjr;80+tMO2=gz#~#N{!JTda89z39}b`hkpjByPv6EWg9+5E=*5;8Hi~V+-!sOD0LEYO4G0Xx`R*SE z!{IFuARpG>F|s>i&?USD#vI~eE??h@-@_|j3>H3ri~W=Nms|hb2cP&;^Gn_2cG&f) z*o8-uk@{(*`CEB+76^E#mVf& z-g6HgHx=UG)i**yhT%(iEdCk)h;vVl4%P(ff5(5!`k;OhH|-}7;3>y7VZb|&S9~v6 zT5u9>#w|{SxBbup&%r1#y!N5TU=!34dAgc{4je5ZwQY=aj14iohP+Lb8}ibLTz$!d zr`<;PZ?y7WnWjA#(tyj2k4m2^4x^XlF&PyGO$Q8hVoOd}9KAXZ0nzgpIy&_7kzY>A zg(Q6$&R-rps&6R46epgXGUIe&3#@zD-hb!!_(3v!eD+08&%7h!9w4LNCeVqG5VANL z@6>zj!GAXI`|XX5TFBqGVf%4+cgLIbIw-^-e`hoQl(0_mI!J8ESoP$ga_WHWKtY5* z`D^4f{=E(zT&$`8_t;?$yzkl%xaNqz^&(7ocjv#tz5E|sDz*l|lo|I=35JEx2Tq6~ znJ@SZ-gr;WlY=Zb;94Vq{Iw-JxmT=%;0 z!c(uj^3?P1D+ggnr`U5`D}2Q0Xo3#~~Ay-vU2|ie2KDsdxbdp|V@Ve*id@j%)lApuZ7c*QnwTLcU7h z6)%J!7rq55{um^p0*Z@?Dqm?m1!`RKcl&TTpSKTJ@~ecG^$u6_ef#uM{%@aN)dLAv z^Q(rd`nY{~Z@mO5;dCRe7$Fr$3QjiSnvg0@fh)C}waJ@QEz|J-mhKFc-r%OYk?tre zR@14tl1>dr;5<*7>LtZ8lpg7zhnbbr%z^6VCv@{6f^K%Jma||3(zdwS*?+yZ%xua{ z-MMz);70u6bG60#)P9@xZhXX}&$K<$6Lk<4w`5rWZ%^_Aju1)P+ z0|ShTbtJWY_2rpZU&sRM`N~N<-`KR4=c4BVG3pz}*}Cz;AAiOdjtt>Ur&@BDpT6Pj z+xF}}=g$q?mAd;yzT|^HB5_`r-_Rjr2JOMyaFgXH{8aTzFtf>W>{_{1cuc(0^wQ^_ zzGAxf6VnF>kcL<+FX2tn*(_>8u3VUR!!J#!HdIS~s@HY&m4v1-;e!*Wjov@N`Y&Du zzmo&RPPfeqH*M4k+g7#@WUTFDcQ0|*EYHZ=tznSy#AWO_@C7bE@9q69YZzX!?f!RC zwtieN&hL)E(24mq#&MA&7LOUW=lLBGn?A^UfSe$f1(SNPsY(xKmwf9)5qvT2U+ENI zLnZO_?-ATfTKMESqa?CwZK`I4Y5EWTsK`7DmgU}S^!^Xb`Ol#%&(HR=SNx5y=vk{_ z!8U^?boIukhYk);9knD{D@<8nun>X3ch2yNbRzPiSMZk~e}O-GcNrT~HDTU}1#kRy z{^_wK57QNo$Qy`cJU7*V6xA+JR2v~l()&_ErIz!YbP~?yTFsepUhOLn34Hvfg?n^j z=YX$aR^^v4qr~j_<&)1mVfOwLAN%6qpOwq{Gf1~CD`T&lFf)B@yNwOXpBNf=hu^q@ zkGH10bN{xA;r^8mupm+31D1^1vt@A5rj4uT zy16e;FbuIQdl$xj@i7Fvvgn&n(<_G0!Iz0lY)tNSBJ0?v&;Rwtf)Vp3R3Sip$PlfB z2sZ5&8nbfV{)#N#USrf#5ZeEu#3?r`5jPgX;Y|H5H6D_X6?lE}m-uBW zUVt2^>=yAKK#eLL*Z3z;vrGH}6@LtZQ2{k96`T2TIysgGN`SPyw>QT!nn2ps1_s*6 zSvk5U7p%z%7Blb{{saDug6Bq$#tU2L&DIULD?$8wn)ke{vUK1?y2#NcX5ug2J3rO1 z2mgS7`n7U$+KA;tOnc$@;kxoywu4@AHq&$~r#f)6Q=Qo`;OQqCmsqevY-~5K*s$(_ z-i}(Xw`q!9Jflc`#yq9Z$V3(uFIR9{4mGIs7Wm^(jLL6;A49b+@yk@a0OC>EE#f~w z^jdz4_$MIjMtohPia!XIDt%YH5aM0<7O41Rkc|o?Jkf=3fbPC9HFviSmGXGoP$j2I zXqjuMn%~={m2!Q%v@Us1l=G;Dsv5X$sG;ehn7K8KKl;tmg-;Nc&;XUh6J@S9N9r@v*V=B{=4Tz}NN2mYQ z8ptDuecoi8XEwJus3%uvyJniC?2Xc(F%8QW$b=r3BvdIlFZ{cLHlq0i{ZPnJW3Q1n zTV?J-c@M731%yg(fj{nSLgfl|!396&xotdgqJN{Mffn&0(APNdMu7m$jt!_g8xj^N+x zic~s}I?XEIHl0buk1BL#5&SQ=7!}WRlA%1cx+k4P!2aO6ddf0vLwmfhzw2D?uN%W>+L!l_- z;%nWdnXw$scJD_wl0|wd&mxUX>7Z0ARh!XTWq9f>d9Kt3hO37QJ*7X$HiqAl|H|P% z`uAD4yE(*M_f|{)etXK!);;ds@5sg7mnSS2__3pL$;S^_r^2&$tuP0}zu?YBZ7t3S zJ9OtX++1+)zO+4o_K_E|liz<~T4nHE!vkk}9=!9r_uy#G=vT>f34#znkh5JV=9LY@ zUR7E?9AFfRqw&>seM47fM$ddO?n+z0PV9JFYQNu||^#OJV;#-{4;gEcj8pd`7gmR1?;BX>rQdC+1ofKf3xI zr&H!GRpYx|XQr;W>x>lF$kO)U+HSF+$gYzQ>N9(W2d=osn$(lqtRNTo2!%d@PFRGFSBjlZl* zRh8v}UkhYhXQN~A+jEnQob79T=I879!%QYr@bFl^CTp9w;NcdxFz*o4Xsw4?Z{d_# z?`^cyK_(r;+c+`AU^B}=%_8aya4umL}UJ8=`fG=@ixpf%I)dL^+M?t6AW-uBb)xZolT zfKg7TMEy~z+c_l7z|$HG3hFE5sOXwhq@aU@Dg~wE7d^75c)5bpx|4`XZ-GA!VW|8T z_%W2^62DBv3s4IxQ*f(G{0C6AN+--E{s~m8#*b9WTd6xlANk!zm_sV3Z_f8j|qc zxO8kdw8D`@=uCucSP>q92jJ~VF#ObgZwwx?$?@l@!>{=aP9?$&3xX$M7EFgH0vJ#6 z_W-;czm8wR%L2sDw0#{(ex0ws`j;b{E%v|i(yP}Hpp9a>&WlHqrHqRKUN|E+3fVf3 z8VQ1)5wZsDx%+j7d%gT=pMgd{sF^a+*Y`7=j^E!TIyU`*EAJo1Bhga&k{_4S{pj%K zmrugZ6Wj44AN&VDfN`LB7n5Us8y3(Fmu7NulNuHWQMNpz?d>O2VXySpbUTFFKrtoI zeB=d-B`k6Fz5yvraLRybaf|j^EFm$_ykwg1WV{T2R`x~ca(wkU+=v@z1ia6qPjKc_ zWfz|~Nv#7=yAKU?WvJuJKQ>(Kq7j8ddy3md*ig#tYdb z7rq55{urBw3Meins(fY3wP7y#yM4Hv&)bJ9`BlQpdWWm|zI}Q*|F=)C>Vbr-`BlRe zecV1=){9ENz%~44{qRWDn-uuRtik~c6Xwdcv80h1s z(}=~jpCw#MhZXEa(aC_rg6r;TTKLKP%%k}$M-2B3AG?om9!}ozW+7PdBWCO?Tg1|7 zx^kx%X?@aoa7?IIZ$FcDaWe`&D}D3Y$uauHps9ng5D*%44d2y;@55)O8`dz<@Ubbt z^U(W_*EbXnGOygQ^`v=FvJi~(gGU<^FTH%^96o^G`Vl`<1uw(t6lNAP2;8r4FDp9p z+;eA&$`C7A3D~9MWoG5hYaExDOY%uJw-kAZ?FT?~MX0LwY;@;6 ziAlwUOnQr%?2ILwp@c~nOCeFrZZai_jY%eI+0QhHbxfG_33fCvA!0rK&_iq33a7&W2+;pr>AO# zIn$@DaT7ZSegYFJAPgq@n=U-Vt$r-tXl}q2brcCd zm)8gK$tG?P3gF(59D}S!CDJI}sflzJ$z-*Oyn11Us#sbTgOZYoHZ#QnNEfpkv*LLq zHZgWbrzk)Zw$sGeE+zK5k(j=9kw&?Qx;L4#pS)`yf?vR5ZPt^<0WZFsuMiZqvG|kE z-olUVg2Yj!YjcaRoeGXk+EN}7!QYZghI1gK2;509>#-gNM9+gCZkPE^=Q*smdHE1+i3; zY5*xIWo%_sCT}V+o2G~xj~~WAPfeei7!npTkIdnE6J|IQZo#p$G}k)kB>%JqrQm!ldU!@>^slz#yi|w>E(L)z6pdR#*RCgpe#V7C3j?KJ>3RJc@MHakAJ9+ z-ZUj~V3==wvc-Gg*f`wAl04%AND~pR^&ILwrR0rNav20Ei2}awsBt0N_Eq3KxliU> zc|;EwD=j}tcGHHv_!hNR%Jq``?eRG3M%pgP-d9a~7eXYh>XMMm-W4-Yy1pfm5FImSjlhTTq0l4-CAkmY zy)Sa|PD%&Pxq%)c8LQYlBX*!0^Cqcccd6~;(_Zc4UWJV}&0UY#sg`tUH>^%M^LYQD zoeU>&9O@U~2*Pdn`f$qB%@`cP72(HNo_Uk<<(XgpvVsS=QQnhg7$9D*#kyKW3pD$R zBEUd%9$(e-*NLvhlH3<_$@J4DHC`O+R=~R$ZK}yxw}LWkj=fe3s5h0@p~z7Nl{W}= z7b#|w>j@z@X;Fk4Susd9npDO@%q8GqCd3X8#tZQ)jt)xp$Qa-j?Q?feUce>ekMT9^ z#9#2pQG*-jO`f)pP^}`0{D@bKF;%LxH&^L$;l1g;j*rE#S+}B0`}ZE&vH_?rT`I3H zX?Zc{cv>47`WofCgd9>W2FdkN&5WOs+HaQ1iv+y z5e9!Y8o0%K^t$B<+6LSzmf(8ai820?uL5&dozM z!fZMtOh2zO<6CemF6d#VsRJ_<97;+>C!H z*_jmeA?`S>xv1({e(_;d+v*FDQHxVj`5!!z@~T>$-Y8X8Qs zX0ggu?%;25rJcUY5RgC?VHjSrp9E@Ts$S@=Odo8d^@S^DsBG~1q<+|9`wXVOdkuoO z25xr;`1JSYAMoi9AO@nA?g6-O zDHzt>hZtu)v7Yo)T?MnDp6ap=;Y-K2;5uVrI#(QC1+PKa6&SlOAmA^2_zEe*iatk{ z(=AA@`Mw7%gX8Zznm>3*0?Bb5@G$j5e7faFe4?Ik=EMfl8VlYzfBv;so`3!=${is- zn^&w@Wg}L_Lcs>A6)T&)@CE!d=HS7*x0}tM!?Mdvy%XWQkopbVc>R*C;J_R+f6B)> zMOTiH@kK0Hl0!n5NS#KercE9lf_k9HWo)QSO@_Dwf*sEVnelu0`??Jkg9g1)DK;@9EyE5i>+nqcM?3y7D^1Lt;a0>~fDF&-5K5EMWtm1NKx&(^8)hQ~tJO z@%q!=As!*2;dgq3^e<}(u#c>SNguYn27%Aw#Qs~8TQ>e{VD7UJUFbadFZ_8WZruCn ze2>mWM<6&MIXMA0(Ce(LaIChe=d~a!oi72(!h_o5cns4>A@hz zVA#EP(iz;1uLkuKYwFC{HQq=zxfOP7Qfl=7v>d@#7W`v zt&AQf_n9|t3u36z1^=*gw#15%jgCuPr4D3CIZ>HeWt5sW6u*QoMcRqo)lWl9hC%>X zM_|-<``vk?7!;1z;SYw6+<)gbv-jv<2f=f2M>8zB=-sW-%q$VQ@*H(_gkK*?6 zTVfIiP8*Usp-*K~RFh#89E8xBo5m*%nL6Rlt$huedYs=~ioeI7(&-NGwqh=q2`$3-ThmHiFgC4@=J7k>aEsULd`bE`ofFT(sR0*=_6!b8N2u zei#!n23O+{eB}jzeLf0TjNCl}SB(_6YhVhv;fpvFJNtXEFAjlN zpPl576{y)^2tva9LXUejo1|7+iu6PNDH9;*Sta9I@y8zUW-OjMP;4BCuf~F#2mUJ- z`aH@eim>vGpC9hPsn`yK9)HSU+W7#kXG)WBd{GgUCqX$|`U1XC*MvVk3FW)Cr_nw> zj^w*put-PnNNJKHCH$4GRU2-nzo-4xbUJvd} zd|<6P$AN3`3wYH4{8`qE8bPPW*RmRpGJ|05Szy=THm?N-mhO6SvL>!|bVTTg_I+zd zhU;;BDQtx$?W0DI4Q+*GuygS|94|zS-gKAj`ik#RjGb6N`T_tgUFeVx#E{BB`&$iJ zTP@7`I=bsWeW8XEtHq8mh+cUpP%Oy;Y)+Yv3raNcIrvvUM$>`oo^M&gi?JrA6G9TB zA#CU_cEIl*hkw{WcK#6EILh^xC@{W{dvRY}Sj5eMgqf+84*hji_jtp}Gy=4nI$;CcL zQcABmn@K4MdR-RT*dIw=yBY=X^JAK)ZI+I}ra>a?Gb{^dg2KP^+X4Ijgm{QHV}XlH z3yytACnSt8yF=>iNAI54-~1fL_&-H>PYG*&@63^R7n^r)n&CwFam|Ovx9Y-2@eE_R zV{2H*raL%&@+f_O0XOWb-)`1lKR9*8k@;@=9mk@X0x?ZM_`B~Q7FT|nvD1M&g_tSE zv7sXdX;~&pKVr#acRli-eG%t2>W1(6B$sJ?_~%0mCnk-z!Pp5i;(Sa21E*OzV3;WL z((+TW!it>na}Oa7bs(gxl1kbS1<6Mh{AhIyCAE^X(W3Apo-fJANOcM}sZRQn3ZSK} zW{j@N-p(*A_lF%{-iasRCR~#$Ha-8f{cHCods94{0)en|ME4%%P7T9?_&od_IUiSR zAr$WchzjDZO?~@%dN#fHmzd85HAOc;Gydxn++raAH3bD>K|p{q5OO1VN5&qPD70$a zSTGZ+;&2pW7SnJtZi*Ef3vf#uSTdNPj7-+dG&+9K#NbFr4)Y3j;5*nXsAa0e82UjP zREdy2hmamZavz9>qft`+Y35~TO0*RQlFU;&pXtgeDopqijf!X_&UB* ztlOT5!^OIl_?o@m?FIXu5zsaoUiu6g!r|q5XuIgh2F*zPHd!s;M&WlgvG)%&bbpZN zZQw0UX-(9j9sQ7>(1I;JA6%K1mWC@^TU%(ifsi(rbQ?h~uL!w#Xh-d0>R^-q^{20j z#Oocgg;V_9_#cD5_53R6(?PNi>o^Dt79IT-%$|5hUvF3zgr7IS%phFjxrhyeGLMC^ z!Hyh94VWMXKac;|Fz3$YH3Z;t;t?wylrE#oN3us5V+36?ApE?EQL!=-qcUl1fi{0K zI*YTyQnV1-h_!$r%qj3hw)x8wTxO41kp_4sBsX`h5vdtd_@qhfH?2T!vaUh`!7PefFuR^ z^;9d`H}z^$Kt3#xI!3FKkZ!#%FxPtDRvdy$a1%YkcaJqEkbJVB5e~HNV}i4~u0NWl zX9`oI4H{EzYps~V6wwd)$+U?k`oZz_<}B%?Hxd$cQ~+NT*yFp#77$2Z^NI%>M126E zhzQv|hPQ^QPtzLsZy52IUc9J39m{%+98PNX?%xA(`OshS`};?Yh5_*KFGI6;KJH_i zwjSo(ebR(=sLlKa{`|FP1MnQ&f}QyAH|Age9ts}t0t|*gr~tR2R~=nboam+Z@Rca* zz~h^Ue0#7%NG9?PLm~2YhGb$v#&QrY7pHWt!7qZyt2G8tIqA^=G8UkIGr-Fm0syGg z+PY^>7ZWdM8aEtsxH5nVv6f&5E^eAQerrzVhGm7hue@9`HWFqN1-gwKu_zL-^DhT>jJKv5qDE9^Q5k|M`6$|BGl2#&21{lZQ9puXc|L0Ug}+&Co;to)CFjhdM&> zR$RH^5nK1aXZ~yV)y9+N-+`nz5#`#+zbCqbt~x$Hg`dEW?LmOHkY-|)PDUc=8M~25 zB!h1FeL_YJWAkNX!v(E;@JpNA)@MQRs(br-q)ub>r^FU~0bjxUo(+$L>F^o!gNaZ+ zTx<@-zrJvNtdHOLZBTE~k9~UMgk4-EPSzPWuUR_f-XvUwD>|S7;>o|pPW%*pH3>{k zglk`GoyfdcmKo!PfO_i{Oo}Tb4<_6-fo9_I^`mJBH z^}G3EVI1EPCl-#-|HIBw@NJDQ(_sAk8;fKbe8E7#b z6=-C4bn@lz{r_S9rUjSFl**6Q z$It)$Dcsa%84N>#zjMaXrSGK0ta)!$D8mINFK&alccc9D3q=vKz8eo8!J5y2*=HQc zOu#w#r)`goN$+B!4kkSL_{<^tLB17l$IN_*=WBJk>GPJPMtU%PA$IxZU4yUnn>gNg zzMzd8K5nAr&T*4xHC3GdOwUg37hr^9Q(!*``GtQR|A>!WiWC3BHnB;Q25aEndz$fg zkLs!AT}-r?$OQ*S9RqD;dT@_IMZ|RYs~eiY*MXft~Aw(LV&xC3a!>5;46>Y-b8f z8AA$_Kl_e8C5)jY2)~N|!z29lZ}e3g71>?Hc8I0Hv22sW{u};U_A{vcP1*OcNz-A; zA%m9u_C%DC7b3N#SZucs96cpNY~QsqXAs$nun)Zg>pq*v7$&^~eRm9PD*khpIGcGd z_Ihbkg9t~cf0wQJ{xB@!%x?MAcb{l_wi@n|ZfRS^w@d5tF$f8Z2p&vFwMMB1`WcnO zn36}@hyd!U!=KE9srr@auxFsAtYeYIXUL#M9W$dB4Hz6$`6`lW)(* zWAQl%$RsNk7KCJnIp{v+3Ya*NlSbo6o2En3wo*usBXHschI}}re~h*8$uS)sps@ zhsU0EZBDwJNchK^3FJi}dN>^+#EaS;$;Z}S+Z~S-|4sU=LJPt<_BJrnq%7^(sM1&v8&;NC7 z_>o8e_zvd5d}zEXHen8*$38@s8W;i7U;_B#ukd@g3RCZp^@LmO4L^2%U<6GpE)yH* zevv7T36{LrA>09t5JuwDsDVUIi{$T8wYgJQi+LVt=-PlVm0fqR$kVic`;OB7q*h*o zyMDh4@sHR{WGxe4{gPp=vmu|!TyZvS_q|6CG(u$QW4Pt5U9I?scZ>Bn#_5z;Wm2zN znm%&DGSSF@pvOrN8ljxny^U#tDiNoZTx=nZ*aYBHPurNFemR>DRCpLp-rW>kll^+X zzDXN*yxUi7AJPeXnty@VQx+e5wf4*3t%K;{aLZE=@Ya$Yk1Sug1hVg2AD_H?c@2H>G4hEM%Mz3(uGrWL2wY`2il-m&cIZE{%h>7W5u@c zgx&qbdJdwYTw@^n8}YcB{$}dMXqv%gB!jd}d|fiA=F6XuRSw{Aa;_qO#pjC$GnT%& zTMoYN?REI>C1*$DAkb@)V;!j(`e=YhUjZh=X2Te7v6Mosi9QkTAF3k~vw4`6v6_Te5%Rt5`ni|Hz*ygQ|Z>z@QW&!4AP9 z-%&wi=oxd-K_m5}NnXw2x4d1L&8Wm5Hp~r)fB+~5Z3|2*aTi;5&3~zNlwkpcJpC4M zTWo$9XMKLC-qSO4*TH%(!yd~n5st$km;s+Tp2gpkFPVeizHT}8P&vLx1gbd?B`+Vz zD0u1Uo(B-16jH(#xwV8UE>2UpV9pUAt7(tzuHz&1G(Xp?5Q~CIF19GSh){G%PL7Us zIkrh_rep=)D;MCJqnK;>p$d!F6YYo3y*g;%->{ENyJw#rXu(%sKJa#u7x~6(Ufosh z>3Pp?`u7lU8y+?E+m^b%xSwH%~GWa;2dAvLS!S3R+E+2#G=%u@##Me$i z_Wc_--FIZ;rekc{ft9QFWhEzPBZiLHv{^ms2RaIoJTItq!81b3tL_7gE(bTcv`Pr4 z>c?N=dAPF$zwv|{V}afCUv3>~n2+n9c@sOfSnU{cKD+l)KaZWe?)li%Z;xQv)s45~ zpYY2#P%Hy`MRFWWanb^Skm?4Q@ElHh>F|yRUpl<=0mPDSvW|301hU&AX~$Wu^qO?BP=DiwJ|i8B zX@Y6j*=h=He3CfZ$(LN8YO=1sWCT6lB#^b2MU~A6W2L&(pRNZXNxAV%i2mr# z%+sD8#}8mfG&DO(V>N9tV(#-_Z5!OzivPsx+a37HGfeu@?faLm+XCs3Nh@DEwivuJ zPQS3|HT>&I($)~D-ICHN0hs_OJ(DpgmMv#YjuZ|)TLc}8uf<*)D|Uv<{ZcnFFa}ZS zoMay-T3+_hAiFapq!jNJip>5lT}8gnD~FjR#wu0?CXA>CVY{y#JMi8gKf@&z%Hb&KH=H^ALTOsT1RQw^DrR!)E--i;uy$-}j$AyPtdriEf5jFFuOjZ#$2x&x&D8 zIS~Lxey1>YI?+^Tt+X`CYuu^NZ-(CQmP1>=Mh{VlAZRIh*dxX0GoL=Mp+_yGXOCd| z974jA1Vo+<0)4)Rgr`f-{etLo4tah^if^>i=Qa|*MB-pVxe|vM>1t*gqraIxAEY%% zdiF9QNya)Nqm?{+OEOa8DG~)UL`oDEkrWjY&jEJ&97Y0@rRRZq`g}hLA0|BylEQyM zNa1K9nPkxrXETx6iUQCm6s2@zblyQbGS40_$69F;W0MLc(04S?8Cm7{uaQPp_il@t z@a4M)SX#^3uu0y7sv+RT=e}Px!26|w^v@&a8lYmxqxXPLY+MY7aO)G-N5r_huUoq~ zc=;XCTDJ*z#-s>j;#36u>GMCPG0T3uw+NV1_qW}je{J~;{LP%_$xdSa{rJ)Jxl^G5 zrVN=MwRjbq6&9Rq4~d&KEp$C%P?ocqZ4zj%406k5eMv6l3y>JL3Af;=ir}3O>GiMd z-1@LySbkVQUD=xKYt2vW+OO#+Zn;4(jKGGCigb+uMpfv>Xhk=G`8q%#64)OWo@yuM4fGb^PVzNU@YIcvFyfb0W z^398zz6i77-T=d4}3WFBynZM!TzH$y2k^_9MrU73#ImGA7VwwYpbsr;zv(oZ;~;s^nyr2Gt7p&8?Mrm}CoFyUQy72JI=Jb~ zp2I|^4ZU^h&3sTi;{H44EZ?$7QR&OAi=vP1c2%jQqDoDrrb7`(OSXXiHx(st94_t= z=)OJ-5ePp;RPEHKQE*LxoQYdB@WT#C~po=>5vCqo%8gz zF-%7kgn+YJ(%T^pIjKloWnIEWm%~z)AR~tqs0`lR5Zrv4GA+iBMVc7|aN= z$zu!@vxPA@8@EC>3D(u)^y~3BU5%`o%hBlcFIN|h4{Rop9jQvv&1@si7E^ILO)RDo zkyD7sMe58*nmuwwIN6aJ1KD84*)amT8Up!zIpn*v!@!H90N@~ncs-84ul_uS66LSjx@X1P>*?Y_LTs(Ka#LH}Z zt`h=IF)^80;eGA;rSCl_oINvKZ}01wmWf09uludB=%M?d`M!sW8h=~YAE$?JNobmL z{|?(o+m8F^G$m{aS8`FUh(hZ>JtHVzQX=u^{N!N{?SCRhGV4gWZt9ubIO&p#hzW3|2Fz|$DnFvUP$Cy#S zO`nDr;pZRNJplB!%C@bW|9vXj>ig(NU#z_AazP)10kTT9J_BZ+3<{Wb?)&T8u!y(g zAIhw=;5le~5oT_T*@zRDK6iRRpMKl!d7^HxhyP(QAI33rpa{ai2|MfXXJQRrhm-Ik zpFU$&MYw4uC5;q>r4=yf{aP@;yxQPpyCXQ=fue%0fund|rAbe&m(Hq~HDy98>1iWT5Nah0A~{Yo7cU{>WCW#>IK>h&9@(Wh zc_dDaghVNEVo97537JF@B~Gh^OjhD-!zJ#&=^`1eCK@3y$-CFuG-YoA$<~nSKUVrE z|A0PJEX36dar#1hEe`ld+&LSx3n70YL`#2lvvFr6xW(b?3&o2N9f_-<0S7|^^N6~yB|T|OvZJZBq15G@_LIVsk;#eldduOYMPGrZqZ^Icf_rrC!9MX zUJ_fFk!}T78v6v%5e&{|JA1BMkh(vjX0SkOF*dVVxk54JBXn>AxE~m*&3N0 zEfTUA?!dv683{YfBy;CDM8hZ%?p%x`I)+1Zxy|Y931xWAT*>Rxk4&=WnH#Y+&%|_P zE3xHJ?JqY%wZFu4M)wMJA9f?ueOS!jNadu2Qcm+JCzJ-69LKdv?+UslK8cYAnSB{( z%Y1R6$@Iuqd-ql>GMP?(4Vk-Y_wKFTCI3Z?OVUCAOzb+vzKPJ>Id|#F_+ri;y?$YD zqQGsKGiL++AV9mAWVY)@KG?1pN41(uR08SjC9WMY(mboAfH_5gY#dF1Cs8ZERFjEx z)biioij49hY@jxzrA~y!;e%fV^E1qqYWzY}XH+Xp1^!>4J>H$(b8{!kCnRPlBZ`a)?~ z#^N{kwbN<2M$ghw294k25FvsM3NoY@#5Rmg@g`yItKWSpG9&iQBkhD^BZ(g(O?}pq zzktfQ8m3rK%a?aXkT}9o2*- z3FfU5>CF=9%}h2zGA*?cv|`lkV`lvD(q?RZN~Xlkk}I< zBv8{R5!ogs+NLlkxR9vgfih0RCE@^PE}M-~^TdKV7&} zgR_$8CIQUSz)H(AbZk!d!I%V#ZC8d3mwB&P(cc8-^n85%-;rSrpwFe_z9?rilf&qx ztJGb_?hxZT4(>JcilehSMo;+fHc;tzGo_LoyXm`54%Y;$;aVC_amtAe;Y|#|<@l(K z6QtWbn%E@1nNmW*$RUL^at${^7+$lFQlL3x!*~;+yi1`_PANF6NX`>U&Uwu)PLY5q zAQJFI0{R?JILGiN^1RnX>C_WCankc$W*Ytsvyr*NoscMQVU7@_m2IW6-jLE+L()Xc zY4q|{(4 zIr0-U=}f#>Mns!tgsIRemLUdZplF4IyUatQ)v$Er(izWl(T*t$dv8>HR|q-0#3n;C zkeR}BeV@bG*bc=?YO|7>a`cbzHv+BPeC>7e27Z@u?2gtHT~8v(W7uR|1vE8es0IP#mCPi*g|}1r{beE& zQ6_t#*d52P3!!=;qlpv6g-Hwt)eI*(aJ1;437KTRg|qhbm3&KFS7aP##nBSkEEKGb zS7Zzmf8eB-=W`nlJnZH9^s+?G*qUI)ij{~bOIION$pkX`6wGBT^cT3x&pUE)7HXA zYaW`L690fd#5e3O3 ztvW?(CCE`^Mnwe4CrFrtIw27@ePm41%f<;EK;0mNYfVa1q3NT z2~wOIrbrv6Ng>F?G!-3$Fr221OOW~$*@P=X)8@&<F*qU(-iyKGSBRY}@c0;bKMhZeA=I=Dq64Q;gG8~0iGpIL z0E!rn%EKjV(zMM|$Ew^Uo5w^$E|_t_@qYMqE}6cT>G^O3bQH@6$(NCUQN#NC8#SbD zkiX9`bv>Yd2KWa4)gh(isux(XJ_QBerpGy}T%H#W>u-Y@g)Z(?{Zkj&r4hB0Fcp31)5wEk+te`5}{@i{mdEQ)#Q!Lce&dioQQwCdKK

      {Dmh)3kT)wNCoFX48QwGXux{w=ToPJQ_mtobv=&+mKu9A5X;gW%O#WVWRJR{9|R<>$u+ST_GrGoT7QPTYHO ztAJl%7tCH&(9f`J&8$6MFxU6Rj(bmhv(Z`tesDLTb2s>wl;F$wz*k@51NiF_$lqQI z-0?DSKe_z_?0D#D{P!s)|HQS2c71gBgz1^&m^Dn}> z@!`bx1p@&ZtF(utm##fxSwGTC7;WTBvi7tQ%TLYhnd6%Gf-v_x5cW2Yd$qfH**88+ zvCp^5G*(!O&oRw-61*eIzk@3(#0JEm%Nnz`KyV{uLkU@xrXS1F0d^>_9)^}zU&6h# ze#&%Pa!<$e?3XSqvuBPIlOB28vz-eH517QU14qn1y41aIUH-#`{o4iGP@hShcH-Q9 zclR~CrLnroLq9@JnkC5W<89Hn^}BEW+P%QswQtnmezr)#&2V7xn!6E8l^~Q#{jCuV zkmS{>{}e4yV<4NB{bes>^}*E!Ko%$RglM1c;vQ{qXm4{8SbeAj6{}0PRLII;n#Bfs zCF7SGtB!7@c_O-5j(|+Xfej_bPeA%2tu2tUA|GmOtA)@&hFA_am4T@qZ+;fz*M7tY z>u|me*Ub^b_d=%+ZXI~V=Zc+qq%@TB6ZgPSSW#Jt&*J@GeU+U}Bq*$ChM-!RvGxI} zguRgUrZT2=kXFJ#S|IX4f$_b|8^}ZSnIdnK*QaNuCQq0pcnKpWC*Pv%o1Z_Narn84 z*pQsiS%@KgE!s~7p76E(gR3&fva)`}98bEIEtKc~$(Eh_UeOS1V+|}AdQp4eQQMG5 z5Bgpl^3KhCxtFWodI>jVR=>0Vv4;@@dQPv=3;G+YI%C76v7c9e6VH=%ow3sGs{Kt~ ztp3=~M~?h_?EcmxM_TWnxpwVL^5OId@28Lk9xC-v z+CZsurbi6DZ2`s`{WJSU*s1l;K4vz9$Mt-@e#+u^2Xvaj_`w?=_{8emJe-IhnZGYb zr<<{O+}*)kz{u!lkDYuXcHYWd!?9Q1dIQIYADA8j1xZoiQ^)ubA?$30CWQ#$Ass!E z7OaRRAAJWfpm0>K+lV!o&-`8Z)39tI*F`-AA7@cit*} zpFB@}CX=uZ7gRu^k0GQi^UN7*1}@k* z=)URe_*Aiwi>_R{vA$Z2VX{h=5mFyY*Wb(DM!=6%ucEL47Y3eP`I$fBZ+7@tKY_(R zvIv*)>5kl|!0)w6@O!!wDL2#B%jrQnud5yxCI|65@$Wbt5?J&jShxEO`2?5#$aJ!q z*YgGRH2%8sHT*U8i5QPlc;ZqXO-6BaDy6#4rblw<%8}}(TDE$LP&{jnWa%>04HF)q zV1S&^i;u@aLVUbs&IGJM@2ehW0ZC2t(N%}v-e z6c-KMu_nmRvT=)b)^x9dwGj5&o1oo;`F>FqO14kcR6MZim|15 z^zCn+?UULcv1l%;bLV*h4M!8nZv^x%dh$QXo?=oZJB+8Njd-3`LfLG|uK08#@kr~F z1}hq+cjvXrtb)xy^2uZ4^&UEp0s5W9z_=O)-{<31@HPG`{{#GIf@eRSr^z$+?ca{P ziN8PU<6UqW`n;*%vUKhPbL^vZx`@!b=PrGOf7N5-^0|+V(@*v6KPq(J+~p7P!rWa^ z9)0wBqi*>VmH6VzN3K++KDOAiuim4tNBFioUV>4T_g#5?qnMnKF=5=G5qghFZ0M|g z^XBb~3(<}B^dA*Ii5~2Fh8Q_am)x`lNY|80vo4l)SxCA?Ny=xBL6epVwUT>oIOfM3 z-|A-$KRNQ@OWPnH7T=){riQYAUB(~X8^jO&8sb|HogB=K`VG){`$SDi_Uq5bLejLc z;=eH)kH5qVf2xGoM(Y<4{V$wy?z|bdKtcmG14_bqLY-_rRHTwR*#d#kW^7Rl8e)X#D82qoQPA!4oDPRaVhyOB)g1h0(Z?p zHx-;_u~c(hNJ3`9&*flcgi$$DNtMBT7KZb17#&J|5n>U`>4qF@-apVSkGKOsvpXb+f zA|?*#l-}g5(&q6oq%iE%h|@dJzsILN5yszJqudKXr>H=;8>vZGAOvUwXcJ9K1nso3 z0|q|hgs>;?|2>iByfY@k+w!c4FP?qyuS6zzVPTqwhcE6MQv`obS+MxAO?^CK(!N^< z@IVO+{$lUSgzrL*3-$MX@igYPkifXbqO4e~nqMqI{*L9BoH^mq?k5e0+a&G`3LGnrNygusV< z5?1gA{T=f>2gT|5iELX@(V@emc{h`f*&GraWw)MrF6Qk0VrqK$Tt|#O+0Db94qUyb_=A&={=`zU%KVRg+3$j7ZnM~A$pT3 z!E^e&nq|AkxwAnzPBX|Li%NM?pd${)TNM3lqPA-)9%Hckatc z&#Gu8c{ok-kV*33C7o>{3Z#9My3a$8T0;1ZxfEA`i%bU7^c{&A<#T;}iW;^jtr?*g z;vo$iJSQw(n&#;lxpZk7hcZo5Lbv}?QPG+nG2H}>j`FR0?%uHV-dw`*ev*yP1Ul9l z?2-|$8wc|>JV)LjlqZvZwQX1+#Qk=tA%&P1Ze%=?=7c8NAbuRajIY2r<0G-_c#EFsk-B8k6-g?S9lIGr<+rqY@R=CPR&TFxH!z`bVK@zlwdc52~x|v$}l!R z>+dmTY4UOp&xuS%S!PuKnNhKYo)bs^>8$v1%Y^BMobHb7XBRRRtxFHyyLc--6_iV= zPJdF0CS@?ex(p^LZzeto7qmcfq9NqTeP_b$L+mQUYAolP_Q34sRG zN4ZU(=L7wev*+MK3W6c6%;s#24kquosX;ts&E3HAnRDlD=J{O$>hApDc^<4RgpS~n z=&)(y+_~Ekpd6Chc#>N^6;Ac7re$+T6H7NiG!)_4;ENNUo*6=$l$Zz!I7Y03jnnWK za&o4gB=6aks&6K`(Is@1X%b{rYG(*~$!&x>D)at_K3#s~mO-M`5dKW^nnND?5ijgK z{HzZdtEKAa1xC%$rQo78-JD?~R_c?fma5jRPEMG!;rw|qo=KZNYR)*ZgvksYd*^hq zLMg<4f?h2|?X3z?;udm#`|M`p@qmHGA8`+0xiSXBwB{Fw{Q|_?k@d> zm712iZrm~%j-LXD!HjkgNWX!rK22Wj=@GD;{vAr%mF+JF81SdRwc}PW7o@E_^5E7r z#|Z6MCR;O1(5oGv_MaUe>(cSXwn2*7)OX6xr1TdOq>gV<@<_cQK96*KOH-wekC4mG zF-)JZ<;M!8-@^i%O*_6cYC(O1gh$dILq5`P{AV8(;o3)W@wgyCuXa&qKHWmQDB8Ag zEa{?>H+Yc0&oSB1DR)sXw{9U_R1W60?m4=A`$4&WaDre!{gk}9b{A|;86;Wsi=MI1 z_9^llVVk_mBaxPDMNR)fxKZbklpr<}5=V&g&kB0!bfzXuk;2R*U>q355c?ibiAY?08>)ALEiA-j4bXW|uSQboaUPSyDi z>OV}U_gOaiY_-pvqrrJWkT#U|d^jWAQ^4&y1Iy=~hZ-<+bP%#nkX>iHas#x{Mqjw9 zV|s}No193RG%m5~L!RIF&}my5q$cSjqeAY|Q0IoIdg=CmzsqEaHFM_8Bh;EnjgMBx zU33KjG|^%kPOpOOC_R`2+OHsbS1gI#LK>#^#vQMrM(HCvRg6o8bd$abQZ4#=NXIca zH^fBZleCpgtXRdwI1po%N+y=Dv?~k8q;zQ}sLcnHmf8rJIJ+L_rPt$J$Rxu_dgfQ` zzJ zymjO^^wmV@IWs-h+g+MdF1LBES{BC`A)SDr{utO?I}^m=kPtRE2y+}%k$$WZ90J(e zuvx%Oh(XV>&6<~mCZ%~>kx@P+&h&2HHTY*I3JryAV1mA22oJ%J{I%vGAKS8Z$;W+c zYuIL(4`ax`8Mp>l;bvR{Va27doqGJ)S56`Tjzes{P=~bAyijBGx3N%qg2#1ZU@1h` zkx(v!0eWih9raM z%Pn~vw31AM02!eRrbb0gomto(9jd2jdLL1m#3;

      !9VOX-Tu|4Lc6QLopX$tPY1o=LoydJk;jEx1=yJm(SRE(Du6UCTuiIc%Oo?Ago>|XRAV#sIum4LmA+q| zG&98~zTMFmQddo$w_C4YynMuV8?G&5^Mb|FXd&>CrMWM}d78*&1#J83vMqsQXHA~E z)NYStN^wKYut^!OZQTFCPQ*w&(}Wf@7!9RdNA}#z*k4&IkLY2@^{PR2>Hm2o+9%*6 z7<~3C=!1)NbH|YjRQ0iy$=Rj){Hw3ELjQeVo(&vu zY^!PRq%GBZKll}_tL8+-uAVw#B?44UTcBL4jLMmxYNnDCbA#EAU@?Z&ssh^N>K)XZ z_Muh-2=Se$F~Tq3kgddNZiLSM=F z;!I%wBiqff(N|x~sZPaBtLB8>v1HD$6$lV!8n`fBiquCEi{Kju#sLfj4f~9&C&fQ; zz~a+y&Ujq5KaRJ60EMN|_%Hl?$qV>AnfH}ScS0DL26hV}Ll=Yy1sCgL)-vL==W!{e zV)2+21qoMpEB+g&JUJEmLOe0fn-7K{o9mWP z{9Fhu427_8 zTvCt}(aOz`kWUok1%f0PP zP`2V?rj1P_$Tv!y3PL$biqothm847)CFENLDJOBVrBvT3$R!ddK|;QFL2}s~g0v_| z6O~zt^FxmgNt|Sf@{c|GAxOTI>L*3Qfno+5&v684RpJDT8O#w0`B_0)#B6phXCcTh z3Nl8_W{M=_SA~ul*K<~eQf})(kb$#8G(mn-Qgt{(xLC$Ykl!hS7=pBN+c;|I(VuQy zGLl7vr!)eQ<||}C2o2YN3K<5Iomc-D5rSX7`2JX*#HH_q4_a)7K_N~uIYXzL7fZ}9aP1)QyYy*8e~%|F<#13D)CzIK;={wogvFk})R>=En;fOb z9fb1t&UVg?P!5*%WL;j+ei71-z*`SEpr#RD>kqLBQFAhM{H*1r_M)ll^sL6PG-{s? z*LoIS&dRG^+SlX$H3znPnDOU^7f;#-jSjU1c8&}6aQ7WzOFlk7<<$J;YxIKdk>rP- zMhwj(7sf@)BaXm-j5zE;ObQfjIc+~TkYsIiIFBoffn$oqRNPry3<5;sIz7pbgJcI0 zA~up8dE@vt0}g7_SAMW|qaLEzW#I{Hbo%&&kkvl8t%wb>i!EV-&*3?*R1ESw`Kw{k ziDaK)F(Kpbw3tJf1l&<<3;pu!ma^4|k=R9X2}p8hq>;uN@?xaxSmU2V4QNV!^=OpE zUI{4wjZPNV=D5 zn;N@Dr;B;)ke4AlmTX4PHoS6d!%cf0dk-yok1Uv^q``1l)t^AN3@7PaA2fBob~mzG}g z83e%}Q&mo$o3P)f|3WTd%w1nSL>q$9SbLiPG1yGbt#q-rIZS?B@WogGv5Hg z*Ckg5q-%H+7jY9L9}okbAL0tBQ?6i+GaUpOBqOA|uVCMj;#d^qIb6avNXTH9I9N*M zqaYVZoDU^r$PI+!S%Dx{1-V30y(z`9Dacup>MMz|uS+U-DV3ihp}`r=mNBUWndG!~ zNeGH#$=seGlL=BOb$NMgEn_A~v=YaJ!^l`AgCJ7~Qbpp#5yUJZQ)PrKsJ3bb3Gq}L z=oxBfN@uz3b|IQ-LqX_%;29~7ML~|S?OcU~40efgSxV)jAm>P&XC!2Z3-Y6oK((PD zFOXEHr8qVPIY{EXE>ZS%N%gCg%8w!lP&=2$Q+ki98tiO_-BBCFrrWtj zN(!<;FA?itGudjPQX0L6)6uJJ?jns|&Gt9WKu}EncCt%7GaNT=UC5iif|>gR8u5?0 zzw*;#d^q09($LO2}ZBIG3bU zJ_>S*#5pY?LtKzA1rseo1vy7j9hc(R6l52P^Dl|AuS=?Lq*Q(iQiYK22a+m~x3eaC zX(vDSYBiNk)2>{>)9$ufh^9D_V?`rGW#tnQQqnKIMS34ifE0c}a@A*`;aq@3!H18O zE+~uWp!|!R?e1ndJ!h8EkCW0{`6elSAwoi-9L}^$>Be57>CQUab>Ay^my~XTl+IUk zNtTO*i#N+f!rKs1_`)st`towV?;+v+6uE<>@CZ3vOCFF0Je@^4Pay^Mll%mZl`mgb zUh7(Uo$;T~EZCs8B|+~euE}J6xb4w17vctxAHP5LSj`+qnZfi(`^hu!%rO~^C$2p9 zSoQ2Vav#d|HMs@exnPF7$=$Z-&e@Lp3_Aa~1+hAVZpflLXF+z_!KOnwO*v@?zCLv5 zYfb;Od%w!b`Rd-ZoG%aF^HtXt+5329O3>0Dx3R9adT`MCq}Bx^11u_SBM z7wH%jjpm{xHFIQ#+9gYY^d)l3d>a2tJ~h2_{iVt4`(_TtdHMrr-h}j?RR`pnZlOFGo^;ptNk3w;8 zJ&yg~)H-+i^tp;^@0>pU&i_@tl4j|jTPd1Di=2&awX}_*Q79CJ|IcgU{?z0PY&ymU zWYFvO=KsDze%&|*K@5{s)cMI|rsDswbpC2}wY>XGbEzo?j`Shq4qf*m zfgV-4faIPgS?W7kZ4x9);cf1_Zw}XpO891Ho10S#uTjF2r0_qrf*OAH=5SJhA|V+8 zbC$2x91^mXGmPXJ^w5ZN`D*DD1A}V#Qa)0MR$j9tMkx8zKpK?d@Ur~Gjn?Pj0 zDHP&V`XTfZ`D*f}%2tovMYTX3bm?1YG6Db&&%2#M!6LRF0X4l<2o6Bz| zYJ0Ay#{)~Z-s7o1#`wED@D)XA2^p>*1aO3}P$Q%vFB+Y)^kL*f{JhqeI!|Weal8GeV{_NVM%l+r)`LE<_1N4s zF;Ri3e05{PnCLMHv7a`w zN-91d6FzPs&rj!z7eAPo_~2ssZ_TXG&{_0@02+w4e^<2a9YOv3LIiudxD{#{@fVzp z_cKCXQBm1P41_cE!pL6cyvi( z7F*%|=!!)<-3_M}t=OS0XUt-2(!3oz=EmJm1bAA2rD&B zDXg(>PbYJyv-n3l3>{*w5|TU*C7*q*A#1x2y%tMsU}8`%GRa#o0n|&7>ej)V=fQfk zw4Z)h(j+KP$q?Lz1q~c-G!H)@HXPZ%=!GK#bA4t+g^umRcJ>b>#H?aAv^c4~IbumR zn;W1VR}kzn_H{bNb9^;ALfVOzU)e3)THV#fSK~2m*`$rG>`bI>lzf*l2tjuUpNp-u z4ay1=;~H=>E*GLDSJq6|1-{xDKuD>_jOj=8#0AaJ$mHVFyWmZ^0RkBmb%$s(#>^MY=vZB{B(Dh6Zt}7pF7gGY!H$zT)ldM z)K!wMjW;_JwGC1_$wp19coyzzv%;NXH)yQ5M3}v0^Hza3zkB`qyOYs0iV2e_vB6P}atVPgU9zI{#1v7FOf=&}Lb;|I z$3lKCSbl7m#>Fm5JmSKZ4n`Zo4+SLvO z*Vlz ze@1C_ofh?uQYT#=LK^jmuA{WCVfpUJ=Ex=eP1Ol2UOX9|I<;_r1`%dEA@C_75Xj#n zhDgKkd*Z3yHsUyEUsNaQE&o~>iP0o3-qvU(cq)l1yW2J7)Wsn54=TV0|)DZ>;2~fsZ42@8Z%W#s|WM~vZ3O`ur zrT049S$OJ$1?0!+PBA5z%?id9^D;g3o^uoCZS&N7%ob2ro5n=m(59|7wwahBN9O%} z^ytrd^b4nUb~5F6Z-`9G%uI{iaJSP5dg6z3yyQfug0@;Ti)%!P;v@qGL9gQXY0MO- z858I&_dN8W&yrVc~Mf zh@SQe81knt{uov^oB?ygo_BEhXY2qwj+(w;9{1w%IXD~gViOR+Fy{o#C)!4Mppyi*SPVV311@Jf3jF4C+Smonh@E^ zb>B?A_S`K=%y*{gvbC>Cf=j+-bPV^V6EW%C_8FRB%)l5nw*W>mUQo=W0N;H<9NqFWmCf@UsGcVyr%**uYsDLHskXH}B0 zAbTrex~_6|ab1jN?ss@gI=l%1Y#!uyy}%Tb1Sur$d48&#KoZa>od2%tg=prKsE0+& z)illAt`{JmBnfb2&?HbnlGH0njLHpO+Aptm6W={CT`znG$J6<#_)8*Gx@ImBidL3t zqKas_vXE?ONIi*jT;`I26Fc}rauIYF(=xff7fwgB)oeAc$I)_eIHbbC2APm&^z8__ z^R9$Cx^7R1q19I@9HU%3I!;5fuAzmbl}o6*6#CV5W0^pNW^-xz(%{SK`diG&~*#7Pi_Eja%Tet}X;n1v_^xn7<1F`h%Q|-Azx%-uhH5 zm-7WK>PYdoe-8`b@D~_UTZA@U%2}#*8{6x;RE<`31VE&+y1YoZT~M1 z)c#)`m!%efqyS_Wn$qYw7ZL=7%joJ8Vq5~Xaxi_fDp9UCaLpIHtu!X&bIm075fb}L z61xxWgltx$k<8R>R+c4Gb<*ugclz1rT?bH$-wA6SSY4#ahDl+q1Kuv`4&o|}Y%Rm} z?T+ow*0K<-$u26v)m`K-1E1(M7MQBFP@xrN%MIf>mGZ(wY!iH;!sz$CR!7XIG1p(?CL`2p-C0vu!$+>lEp-9U@Dkq z`I}1=6R?FT<≈#51v7)zm7LiDwJtZ}KeRi0MTY>9jq3nPK9SjtNXk5C5(|m@gb3 zh=EABNe*Mr%{}COqvKif@{b7A(hraiOFm2ml@Zz_sc6uG6XfqmDj~F9D{n4t;&Ymc znsIPbQBzW5Q4>hQiIrW|R9LZ`EtbE@vxL{wR8`Rr6GpzSCskEdAvbVyn%v!lHdUY- z*Dy`E$qN}o=$Hu51cEn^4>18_=~U51K0trFbRe2H_g51CVnyHW-}~B9FR#bGW#VyR zJ*0Nk;=(#eV>9!iqu^R|K6K{iq;#fqCj>FcK^imt+ifNQ?woUg`X!sY*Wj^)iQ0QH zE9>9;_Whgw72Bb_9LIBo(pPyo`MNI9zhWelEEaR=o#a1OpuZheX>oHmZn|U6W~94) zM*lCf`TuFw{@>0GJ+@0+%r*Cx{@MF5kDFy9Cxknz@{5Y{E@1|7ct zINS-LsToD`_qxG{^9h?Qm`>Pa&09d(ESQ%?*}P7cj(UhPJbxKU3$$d#y-kM@{hy^q z;9%4gI$nS(aUEL>FWk=-JI)s}eZ|XcCSzwa9l2tYBNs6)>5Mm0f;a0=t;oIEn`wwe zO=BPcUl>z>FZ_5DMK8iJHdJBaA{Z;t?{+2JzMW{oD&;baq#CHj6aH zUCAQtkrb;IqCVMW77k?LhV0pWIGD`VE{ncgB{VR5 z#Y0_r=L!9noejF{q>4>IGtfMggw~@?2x*iCLT=1*X)4;#8;yo|eo%f%F(UlPFHj7z zsO3$l+BMizdnS32Nk)N?X4d6?u+q!>>Aw%3x12xxK{EMyH8=OeWG~A(i`O&c=LgB& z^t;SbmiyrX7q8N92M3=q{MbLtebIub=rstX^!vMkrj?$ zwjr$d0Zhn8$lat~FDc(357M9f7&eO;#^lX#j;VYy6kjKqCEv1pschHe zp-OT(X3d_#+|LmTF=9q{CBx5P-<$!(d@@eHUMwetnF#5QDVHk}&g$^dPa0C%$gnX~ z@rz2t(y_$FRRb*fb(NoQ6MhA6|MV05)UT`kpxCnedTA+yd_2gqtFznl2pf&xhi?1~ z=BSKk=v_|@4fq)Tk$5N>2s;`|aa&0Vm`a}pV(*_!Vt0te_)BIb0$Ct8Mtbuztk>=3 z3DlerbJ>jxFTx$)Ex)ZigEs$ihMU1$CM_9jpP;us)Za>w66pJ+pm5(PbGNC3>Imw( zjprC`4|kh1aIbE_PZ3S{eFhWZp8AE%UPr-_Cvfuxi)GjKrzPf4?E5tpOXgJ%o55Vd zGpy(<`Z5EAbNI>=J^W>v5DR96`h*E|O&F5^W|KI`7v6 zTd1}V(h^2K$ zUvz_EC=gbZrOM3+Y&IllGuim=q^>kg0Mj6Lb$tmzuDpID+y(ipb3T(!79f-V$w`&~ zv4jtDJE6=3jOtu~vQrsykf57{%Q{c+J{PZl$EN=yW54yx6bKjBJgJ-{f3IfYir|A7 z=@q17NL%=hdzoE&ZEu{w#}U=|bblwBp$>;3fM9gi-7I?t^#@>k?8ZuH^;*o8B#M8> zuqEGJfA6eRjjk{TDDEnENhhf3Tv9y-g*TG0iMSJ|zT2I|W#vP66-qE%A3KU)eUs*bw zEOuxryJ8Uo0!f>rq*aXcfWGlHQH#5G#&E}og!7f8eW92NDcYr^009W`-wE@P7iDVG z-gLcn1O%{4nbqQzfiQ3q{*nBR#y^GP{;&PN~W21h&usf^K)bpcZ07iJDjhjC0eG{UksFW^G# zCBB4T$M1_D;voFwn-EsZW(vH}QG!3koA4m~H-1s9#&>j(9C40n{qrP` z^5JRn9tKYvbwUIT^~^FB!ljwDNsKNnut3LnNOZ3+eP}S&kH^n+B)KRu0t<)s}VxD=ud?#%{6ht=Ss+CZX>NaUuf*fnGd7lqI*LuRpww+Tt$!fz9@ zlACm!ko&m!+k_lrSKKD#aW>~RA@A{Sw+;b;n|hm&K5XG_LcF=!+l2Tsw~+vbvCrHl zWHK9bn~y^_A%*OZw+Sg>D{d1~!_Mdxq9Y;cQpo#Itc0xo zN1BTy?yadc+%}|*y>#mkjXPU%n-D#B;x-|DIQ?xxNYl7Yh=EPIO-MiX%56f(y4h_) zOx$cIzGX;%w(&M;ylyL-_ic4x=FZqqO%|dKc$SAyYVXn-J3b-zFr1 zy>y!p()-^gh6@Cd9;T zzfB0~{cjWE#dh2##QU~7Fmn~R2_e1zZ9)d#Hm8GbE1QKg+$LnC=62M^aGAFWiM?I# zLVN$)gq*mo4j#E(ji^Kk~A%-{BMW0>aVl=kTSvI{e{zsCaPehNJYLlo+OaLm0>e$U{>Bv29MPw40BF zVz(TEyG!D)VTc?Q$0l8#fO>EN@y$I3cph}iI{+n69NGkfl&M1U@H>gmqa=a@N+-`G zJzWJ?RuVv-dGcjhIBpzHyH{+01nT#H-2_&r<4VOKAa*2Fcw8JwWY|v#GzkV-7dD%f zT5gbOVkn)$LXVSDK1bv*#*aSXYXvtb2Bsz0kr}Hgb~GlmoP68U^TCtl5dPtZFtQZ% zP;tEo97i{7J&Azc|LvY5Sdg8Hr<~Z-Fwz=XkF0d}S-G;G*9ws_c#Nj>*ny8?8GXL^ z8-B4J7rxKp>KvL4Cio~!m^+i%FXFRrK|1Ih zEdp{>fe8+NsNB;3%tdO(DCP?-RNHcM(K`7&XAuH4)7hduK*S7^#FX=FmG8(SqDf9F zBRr9OW3YVaHPogt48t8K3O$Vna>3WaSO$0Dy?@{|eAno~-@cM})I18lf&JgY%rk?; zRu9T{78B^Q<9&H{)6`?G(HpZwheFaRc;z+I&U$*HyUyqlOTtftnL zXqHr2HbWy42%bhB3IzkhvtQq;GB_`Y#oBiHHrPPd+hCRBzgjQZtEsdoYGy)=kK`7A zKE$u&4DN_#A{j?y9G@~(9K(GChHZ8m@v4a*d(VlFKUek0tJmNDvJnq z_#Eb4@-i_N$j6Ze9FMa*zkK`ptB**!QEqW(8wt`JU^=Weik+He>3a_w?fur~4H<2k z96rQ^JHyymv1ok8+^1k@O>+$dKG}gA=0)7QwIwYNfa{wMF$mBFXRA=oS4e)ZZ^hBp zTP}JC%kJv2PY^q`%eG`4?q@fyz2Aqh9&4klnU0hgB4|r15!Bai{R?(Br@cC70k}Oi zVM$ozkZ`}mP5s!q-qQ3IcEF&0WP2EwvAR3f$d~u)Pr@Hs*^hpOZ^5I|iIiFd?K%cgrSi*Cti59RkNtX?`K5$n-Yf z;iQTMLeCzUQ8}ccWx*SGUb&}&?eRksVT6Gp5TXNa=|*2`-+0@`K|g=V!hi!B){Hyc zG}?Y!69vCM2|g*7mftzh>r%?&xZiz!W=FCtY|*gyK09}d_!bNC-33D@n!|=M3@*@U z?|a^N_At1J03E==+T%nhv)nD)kx}ta)#DkMWo#PK!L`l7mvQ^p_{pTR$cBTE7Adxx zw;o%U6>FY9kw~8JD1!GwpcrOC8yteO_&)q>LEe;-hp++XBF6clSg1YX7LP_qr^;-k z3NsqAM@2g>*;P7A`*VA!>bya97iCjV!Z0Ba2amw=;EDLm+vP(Be~(Z6l012i;933* z#$N^mz<5W-fwF#{Pd!yX079Dg-0$JB=Qyrl=AXX|HH`QjF2ze30sW4z;3G%I|M5~8 z?tU;TBrOg%`a!_YEkABd-L#+(EL&Eryn{o<#d@3pd3w2e^)b-u1>SP?;$x*(p8+9F zjg0FF&TnA!BE9m{fe_a~s*R@SRmU~Nbf<{+L~!~6Ut*8m6@lvt)JS7KiX{<>_0-r)oB~jAKl5u#)xbBGd?lm zw)UWJa~^uF!WiLGHNx=jGz}Tz@kQLBP_Z=e&dvEoRXy{HpjY)N35$)B??5U zxni}ug;cQtC|J&umer1?JqK-x5u+n$edE0SL6(UK<)?@h;cmfmF@hO?h6c@_dL2Jo zhbQ3Wag6y6CJx>MH`u%kD!bd0hCevFyngdwGr%C|fD*{Nzk%Ay%17VjUcT24#7U<~>1ZIU zi3x5pFL@{@-`vF5o6IYY96M^JSP;I7ceS2%&LQ1e8-C_A)2IpRYAh;y`V9R(^R)bZ z1)ne9wY{|*=F^)#oxuY2F4kXiKdALW(uMO{Vrmi~-})I5VbsbFxCzEH^cZG@ro!hN zn0P2G6#s;`{A|O`w(szpxuIcTffH>$mLWSzf`-mpI|Mfm*;O_)XzBVPLfxxFumexU zACiC5okQM!dk6r$24sC2-UN2YYuD>u3cB8T%{i>9YPb{q-E}?iWyGROL@O`r3eiY3 zO>&q`4?^6e8`3_Fbg&_*$SP}ypvVrP+7No+j}~Wyd=L<%6EC6E(Y7P@KpPYdEx~5Ay&tYTOX2>7ruE3Vx)Re?ZzQr$(P+f z>w`T|dH1Q>0BRyc{%MHeNWEM!>dCJlOa@$}VxYKm{LSOEY6R6Y!S!+-f;_R^s)@FU z^;YcA)^wX_Md8}IO|+uy-mzmhsVP_JGp$H`VnahY={sITNLNFLQ|vbgDGrqFfJ-VrmPL>p=`4$K3rulZon#}*?2LklRd!MMoth zjDC*Od=J_9vxK<5L-4EkWqWhJNtiL4&EXQLb1=Ls>bd2mO$^tZ9ukcJ3dB-fh+vT& zk%r4-cTei^97$)wfQ1O@uhi$TS=by{0vv2^he>eqWQ=(VuExYJV$q8~&lK+{WUD!& zw)@KyT;9?v`Sp9blviLO^gWF)iPd;r0nQRX#h)|i)#vePNQEBvxgH-0uFYY z=Hvfv-3klfi6@7L5674Am`=wvzTXp-Z+>O@70SUM3>(Wgntb{~oo=v?=?AWg4NpDLlQE z7dD$U3Ho?z!{^`=xaqIy+;A8IFM;kVtgkeRZRSUwdiUJ`F6i#(q2a!r1ARL1UaxyiiwsdJ~ynf*qtk4%c4wmA$! zpwH;c0PMhhiSN_Yi_bH6cK?Yd!)5jg5kHgA+9lnr>?8F#p2(l}Ih?2B zU)hg*NsWi(gUJ_LW5+--Jc{qq)`{WoLqM*7EW!G1E*ZB;Y}>ST;jxt->#^yZE zJ<+Fc5Bs3pK+4{fuCTwmasYky8i4~g)0F|iMnw^jBODY#e*KvWvTYL;Brc!+KCo8Y zPGY=GC31EmByf`7*}e!#J-U4H2LQ`z>Xyz=!pTWdxLvH_f|*3f6(bTb6D&5+4>coU zvlPetRzf0Ga)|3C<&4r$sVUZ2Y;iSn2eV9~Q|m$+eDSa(X?f-gn4ch^?kp~+6De$@ z55*Wd^N$b{wb67%fJ4fPKr%uUC(R(*El0)IKs-ZmX9H1qvRe=npzNlt=QtNUN5$80 z2C28oLP(P=BvWOCf_$?dE87xgfiM!5b#HtpIDg8_JEpkd4n7zLgSVB8<4se;uUC=O z$DB=UgEpO}2Ae!1q@I?!fCwmKf_^cinrC<#h80SGi`>;4mKSa~__jWqo zXA?9pY4m6&zZPdZisMlY(brOmxJ&tJc9MK3UaXYPT>p-(&P&c_igd+Y zOD1uY1xAKv8>M0*g(Z|r&rLn6d)jpR3_GRgUCtJ1!H1^{J}$jA4~0l6Hla>>zKBo{ zeL5GW^eJE98THsM#i5Ua@25C$guYZAIIUbAAeB+3TD((Q4>QTVtCQe}sF!bX(i`73 zJzlQ6W5U?RL|hl3t%e}c(S+c79OlF}XDaOsHOg&Wp(d^J!^K<7^^`JwTSyACV@3WN@o(J;K~|~t0bq%gZbmg;C7y5{NLA`*u4lKo)8LC2>EI0{i}igM&R|} z(VY;<6bu+3mPK-5Tw@Y0g7`{~tASWd7ZA@9fqw51I8Y1pJ;|O<19#kjOPLTztYP&` zqOY%5%Efe37h93eW^DpbFMT6DqWmnhlIH;Jf28+tuBOPDLl0Dwcq4felGZmscB@OK zEs95cKj}T^P5*e3fmay8=#mF51HPT}e+&Qg?7h#7j=_;<`wp7GY22+N=7qqd_ytW5 zL7yMQmpPBwtM*Q1bxTI>-krmATAJ`v@9$Xi8rv{$83#G;eY$t#;`w-&CG}q$qKIFG z_LvKZMRlk`Tcb52Z!`+cq-r9!;FHM+xh9{uh9r07lc@-~AU{-d8~k?hIg}bzA7ya# zTT(E4MNn!~U9`brNGM#ep3S&T04^X8llygguE?9UiUib7>(^;WELgA}s$)uHQZ~#g zkLrjaM#af7WicH$Axv^i>CE+MaTF?#OI>C)?~SL=S7th%J-&8?cy3OB z-a4d@KZ4G9CeB&Qrbr$;NYjW6?yLxAQ|SE!#6v*{Q-BNED%z5-C*xWY>1mRo7$FG_ zLBpgaqA}84IPw~SCqg|gwx!le=ESVB980}3yST{Y%Im~cK0aULGc7m+zdA=B4R^u^ zo$q|Jz+ie7e^Zn2_O|in%&Su~Pk8pv-1+x#&+W%UwueP+;dOUTote$GJc+-pJdeM6 z9zKPSV&Nbo0Ke_Ulr3*&Sj>kXOo+zGDVf`MweDQcPVw;c95yUqJYpb`PuEll<){zp z_fHQW{t-3}mjjU&{9<<(?y^9LTk^qs_TeLKKqf8PhBNQIcV^t0HMWipLT4GDd0RSx zHg8I20Dc8`;p+n=I@$a2*%nAB)Ha@c|NWDM%wUPkOhTqcS3$^FWHL5u51Bw)psP0! zE*GK(;ODUei}=a_h}5n6ae zr%j)sL(omGL*nAJ4%DBr8OhRPc+wr^c51svuD6WCB?D(173(kIKhLcV!X@D);07lk z0PL{s(ppWOb@}p1la?>Hu72;yl$0mmLvTG(tP@L|xw5Ae)k7>9y^1-wM3}WHJzc;h zR)`1G4ItK0e<#H631T3D&(Kuq@<~F!f2z*tEp_e^u|TPTP*g6IS4g2;u|SyHs(bIc zYkaNY^y;{@h&sTJ(0gaOv8Ph`ADn0jmYPE!BWn;6xR*MvBSkQ#x6DihaHF^USUd{ zF>WT*7Y*#?D4)6+VO?AiIU29w>xzewv_6p3|0#TJ<^6jNeJc)TAM9&-J42I)RYcxqDzgDpwo1MA6{K9lq-|!2= zi49^qWLZi}O*o6>xscByUK6rNeA5jFM`Qh62RjKxHBiZ6E6&3?Q00DfMbaksK1Y*Q zY|=L6=bLdQ#2Y%K(Nl6VA(+LNI*Z)WdlQTRSAtQM9ZO_@Ngo@jdvdoPu1qA3psPbnrgRxr2`%w{YZ$1(Bs`#fgc< zX-+3vA|?`szzC&n~QvywA1App? zDi2a=I$Q%)VrMOO;AU+bO$eqooC8@>v#sMV>q1apxerk~C)tm$zf4Kf;Y9kUwYtRV z!C!sisoy_U>?A@|l{VrksL`fp^OHsls@S=8>Rf#YZp+F76CvA4$O^hhB9TRv2yWdC zltUt}Dr>+MPz71VeKy5Mtz8yk8Kfz8-!wmD{rZvKK1AkbBC{3ZqURkRHf(HQ$eeqF zf~d1D3m44BlDREQWd}d7Fu}OOuBpd#Y0Hj~94Ma$4mHv!c8IfA;W{ZEl;9*OE_Bst z(#0sDT+$z_cm^_xQ6Yx(6SJEmmkkIJ%5ihbe_kA)KKD{SSP@{mCLI;XDU520g4-t` zg|w6vgmlH}6$?>`ka3n@%6N&tkkD970h=xaODB=&V8NTN!b7Zm(1U4hwk`dGGzGQK z|2Qsr*e8c85Wqf70y{=XmgXt}fy8ArX(c?pP0h4kBgVrFyLih&`T z46vkLEGOCd;3RHA(A}v?$Ep8J(cKx+Fa5+UNdcO4u=XebHzPpxY@H^O>p-l$#$O-C zg@v(o5QG~^Ac)2MOx`3p-5w+FrpxQtp0YO}buVl*&wDZN)L5Wpa1(y*jlb~coBh-L zzk&6Cz6aiS`D2S=#>TblhxbYA;}%m>_A>6udlwd8W^$Tf@q2l=t#0w2^s}?Ytl4MN z5jgE)DH{SU%4F2_c0#Z)rnpd?g}{jGKLsMi>9^C;0_|{fY5; zkgr?xf?ySxoX>WDYyYV2SNj?(IZYwpZL3`AF7`VoQRJ87F2oO-LLN%oYb5w-?N88oSIAEF|NOb^m#OeA?3YAEG{qY3)dx z_u)y<)2X7E?T*~Cm6O)SUT1wnQ3RTRCZlO|O3=^N&j#wJKlwM-=C6Dc>^2{7n~{Eq zqZUv;48ll<=wG^P>_pv&p-(D`$=_?}MyzX6EH~-zRhQq66iBz=M2q~_0;S}IbK@_K zK|dU!0NKiatO9l^-)_qH)nY4CY}GVdtqdZcY^#-Ut`QyDY0?~Rq`cl9K&nZPQ2}D< zcw6h|!+Nt3;p+aNydka0?T(6Jf~{*7mcKQG^ZgkzufcYhIg0c75dU#B%x_KV!v5u4 zUw-k{!h5_quY}os%xwKVYKHMl3z-%K@%heYb+t&#r*%^MN7nJOkPQj?fvSr`>bvzeo-OeVm<=^}UF4>_ z`}IAPkh-^@dycrAKHR%-{XX*GRtN|2ez7%gbzG|JhrGu<$y>k7Ef)n6uYGj2lCF>h zFalz`8p$}VT$d^hF|4;4Wb_)r6c*Of5sZb7U^phxJ$KcTE$)5pU$QDyeB1r-ij}E! zJhN=0E;TRDM8`9Fv32^I0Vm`?%$P>yz9$1;Ui5ZpeM2*_`*yJ_SV^dbmQE~)- zw6+o!dq}Pl-2_*EmA7LfhBV@2T*hSRPcL1%UGMS8(&f8E5B;g7%XjEK3YRY>nV}9F z#W*z+@`scqBUCs~=@BW*ohX5!gP#nvh(nTJ!e(cjTQJfJdglu`7=g1{tl?95#VXCu z#V$>g76YSj@+gcS6FVVE6BOfUj$lnV1-F335hp~EU*(AD-Y&f8JfuhR(jBX{^}022 zFZEaoo7V0#7)sWz-Q(d=qDKygjzFJlQ9R zd;_u*vw$?-)Z4a=r&b*+hU z9^69jazV(gpCs?UWchfK12U01;z$-RJE`OQ0oPB6A&8-dJNz9)VzYb?hCL8usco{c zfTvdS24z$>n3>FG{KZ7DM3moo?;E2Wci1PuJ1`Fr3{SHOUWI?-*Zu~-r3pzpjeRX= zmP63o@bu(HSOtea$EQ9IJBF_|+{KnfZknDHA4Rt^#6-T3j;NXG$_h4;Y}QVT#hO+j zNm@n(B8H2dQz$5JZ~F3%fbrlK40l8S*;|K4c?}J1%+Y z1U?4bp}$Fwvv_{m^vLT~xD&UU^pK;^jYN!dt9Y94q}*gvKLEPhYz>H@(_wFy+l%Q) z$>rA9!f~)3cRCVqYXkoLmmxmj3&PwfzXrp-;2pIkJjx!oX0Q9d#S_CuOkSkb#f^vw zg%n(dzr;14ZMdts;@M9*&HYraejYeWKW%z_M$ zCDp`-?)>#K@FM0z5`T4n?PnZ1c<8{`$bA!G01O;v4fqG#P1vnPNT?N}k)J%=2~dWJl)hpeM|9zB>@l(t z?6F-fi+;s5M}`FZ{SFzw!ksXGu-KuUv*qAHQ?Oy-!Fd`>jN^(MlOH2)G0+JCBfzsT z;$0AGVCYl$GkkIR%G@;Ex#bu@D^7~Si7BLlW;vVLCc4J187Zx~Ulv=mQ9OC(e!1~1 z5EDoGjCg+2ctk4H+r+>zlBYXc!P0mWv~+<@>&?=NC0+btr*%p;!z9BWnjxuDkz^N_^|!)m;2-W>r9E^2w?sj^qmtPfdw$^_cm9#1Yk7nV z#7=`1mvy8rdk#CEx-)Ui<`?d+Zk#r0(GEz5Y(sayG2>$Vh9Ypb5`q1B)%H^M#2J1| z+e`j&n-k&9j&^+N#lfcIFzl)Z2Tjqm&2f}+ux31V9C+EN-Z@sqYVx^&zh>l8?g5CR->Gz4(VK=BfTRNaH#~ z@T`9ona#EMa`ExIwZgjD3s0J$+4Mes{yY3Oe!Fe=RTj>%;iU<%HOAlcz_RLq7lX0=^nLex87? z|Gp@GIAW;7Fk-0DfV9X%HQzS&d~3>caNa^3vk>RyuuV69Ya1L*3@18Rdv`qfC5WA@ z9RbRKCeEO3L_YNW=H+Tn6v2{7mh3)6`p$^5O!u3oYQhow>~KK}_RpAX%ri2Qii1by2B$+{E)!Unnh}JzwK=GF4iRua`YDqwJoGEwol1A)Y-e z&}TjMf<|9Q%#+g2{#p5O3=_>)bNNCx;`vpr2r=LdVVY(luov=^$1L8eo0D+)PEl`` z)EZNNWo?~}Zv!$p3!CQAFYEmBWlt{eCrlc-?;9u$6OsnmHH>zU)xZfFvfI+_D13Ta z>Rk~Lcct3XV=LJXu|8XT#WuP>!ww!3V)vCAbFtP;w_liC`K$m!nsjOX=4HiNxYWrh zwum`Gwu1BgN{kQyM}FQTnVuk!FELP7ReO4GQkm#xHj`I&_ojgs5AC!yD|fqL%iu3y z`Wt_P|7Jf+ApYa4Ss3N8rk9?rSYEU;A)yl3``*FNeqGUxkIu()SrffoMq%VDRS&Iv zvivq28LcKR1pho3qrv#s5HLm7-Hx|^S1r`w*0M4%L;Owb|G5zqUaQ>3bGkp9K^7rLj+I!~L?aOC|(R+kQ-izs$ zw-ByatRP1|0(1u%#4HvN@=s3{OfJp zn(YTIhB+J7tskx@EAhG*O)%4d&&*!Ekq;-hcoa*&I!g zt`eCQf8m}it(L4@4$A8g8V7^VzyJ7maOB{3ibvpr#LPSP8KO+O1+$1M!rpxwJMrJR z_}SKB_-{rZn+z%Yj>Xv#z+yr>UfV3$*r+!+hm!r)kv-SjbExmk32~XGK9i=EMB|D9 zt9A|VHxxpfzWmI}hs4I+yEQor=RYvuftn3hfw}q`0@y@omJ)df+*s{rWvWKfw2YE# zAx$nWb{{b*aYDZ+!@BwDiMU|I16v124m|Md%e5^+*obNQnYbu9R_p+M@Q=?xa#35( z+X$2*QR+RVM+}<&xtK$$g%`Wgkt24_XnZEvC*T`MC962*yX@G~m#pHX2KVvlBTQ$_ zj#hYb4rBpW-V96LJ%ZaVF5a{4iMaB(Gn?sXk*{X5q)ql!qGSlfq~aj5XWL2OsuU^@FPYZCQ=eJ;YWdI%T$=11zINJb z+g@B&GkD+YcMV!Kj|~}M_fA=%ZU5JtgbSC&SSBndd(=?T$`wBILNv$WMjg8rL3gWI z?~H?DWRMLXke{;FMC%~tAU19oe(SgJgzzxic-xr$DS9Zz_IB*hct9+x@rs#&K-?kL zqga^)>#Nu?^^E)~+I|}vN2s{Z**(@b+!_{{B-R7h4o%n|Gu>Om;z|$FCxb7Otf2-V zTFqSACnG?QF{#XTE>*f!*=6=u-tVP@cjfdmR@`-WE}Odb!3Vc)zyJQ52`GjEb<#Tb z8_GI&xeFeo;_Fd~3yu_==hrnO1k^f(t>O{#BoD6nk_JYA7@1 zo|-(EN8KH>XPbE*m+0K>X_w9;==xd=b`&~`WpDAyuvjbE@pp%SIZ88RjB!X`>x}u^ zj(aELd>sQ_BJ=q#Hs%TtE$yj%Hh;m%HhkbJHNDT`qI`>5noL_<$u=jrCcr-QySX`uYPcD3B zmIl^2E+FWUjJ{i;=emD7$7CZH6~IBJNX&yIF}18LLF@#?Be9F&MT4Uqx-icx#iLD> zH1U>f7lNLVa(IP2tP>@>Io_c31M2w(#RQ%s9;`a!aPyDh!m!J*;Tnv9@NwAOfBdH8E#o}}gJ#is z$aa)K-AH%@0)u~qmG9#!T)y+3wNIr4C!JYEB=6wOOpZW`K_|VayGyt{oXT$`dD!a6 z(drT7EEB8wE`i=uIaWRbq2)E?gKTu5O(t{l`3I|f{9$wi`QP@>6A+LMvG5?Q3>b<< z7&p|4SK;SlpYrg`J^j%~J{IWrbuzuvA@^v9o8GN22ubVQ+|u)KCA0ecck~3rW$6S2 zk6!oySwN=0vSaHq{BL|cebX!_8VlC9UU;Wu-n8iHljG-3jSXQddJaDzKqlw+{AWUv zWTDP#!G{SArPV0Fm)4{}S`y^iPnR0!?X9UDvmXSVPuTE~8H1w(`VKZ6saS1TiT8+& z%=h?#5C|Bv&N$33B<7Wf)2|QduT8>Dc8A3`VG>T`0MuP}(vPjhz zf*}h+ChUw(@f|nzWiLL8&bbXl=RBW7Tan1IVa_5X(CwX2qj!Wc6zYoF;~BTQHk>DN zOd&2DJQz~NRC>`l8D?D4)}TSs2#BSVU!G_sLS7{BqVG9Qs^x6BdZh!`n1E;E2SfO~ z4g?+EJ9!)&1#9t7n+6K~UdHcz0euJUS&+KZAn>DY_NkuGj5(MO1O58|>H~MfBG`M^ zPsLGdfBQB;7k}4GW>IX}(x^cb7c57B%;H%gNj^#;Eu(>!jK36fd2l`FM*<;{K?^lY zqR8Cx_wGBrZ1_6faQ%H;w;XCGKDy_Oq2Iy#q?;tcKbSr zNjkDCW3M!ubXXDN$P$`>(+^axS0bZOx`UZ7EyB&0*M@bC4o5Xz1tzh>_$E~A7Tr5` ztQ0#2GJ&k5YDw%?X`7QJx3SbaXX*-j)-G8R-*~&l*kFe#ILQ6TvZSr%(>;yaA!&X1Z$%MgsWO1@Y*WAMZ2!q?L_To+|Wu(h$DC!eHfXqy}k zi0)YwO-N5qAm4})(}{X=dej4QhK-OT<#;>(5o$hkQBPM3-;5n@f)bl{KqPioOAy;0 zBo^W_Vx`PC6F%wG5Yg#mtT>DhbB56Qmb2wr2wB$q5;NlO7*pih>8lt4x`(W3HwlO? zMEcWp8s^4v8<#^Rv~!myfZWoR&LaFr-PjHWb@Z8)d}qc`PN2U%<|NPA76bw#;4&P> zB()wo)$l>u9x%cbO>I}?y!E4ojvOQZ8Xmxp>O@a0U7JSDks+1sWD01Lr&HHjHYChZ z&lE^^g;$8p+9LW=W$#5j(?he%^zdKOJ&b@V=qpNo#nHD=D7|vs#vlL(u4KZB4cktn zk7Y~?@C^JCj~s>*V1a~RaFXLY{EpKJh|6Z}L`eL?)?I>S|>>Br9+h?R$9NC&Sv@p=@ddE z0&Wa{bpkFCJApZ5f_YQl#J_xa4S%`Op9{JSsaMba5eL`0mu5x9LqFpkF)P-G8M!`g zGkJ8anz#7x!{*hc>*(gDxgAn#vl6PhbE2UwZN#W`PGLtemj-{X?4AK|m#UY+AJ1~xKwK;Pic zp782FC2qk!OE41>Y>AtHN2<5iVh3-k#ar;Dm+^;H;Dvvwf^g!o6^_^9ln1`we`N3d z=Rv@o=kMQpWdHXMAVA4PxL6(`>ut|`G3tS)+#E*8F^}50Z||TShRYc=ZhzW_K@mG?491Kl_(3{|lpG!2Bd{v){~t zHG`7Y4KNR1Fp$sL>3s=5|49RWrqO(=(D*qlZTu7#d~D3{=VEYYjLUN`3D|+ zY491uqQhis;2v$0bja%t#XqK2o}~uzH);>tbMluS`@L}?B7|xcn+n%cUdkaaJ}>4g znDR)V+5cDk^aowrUUj^WFX#B112OFVIwqsoV)l+*khp#@T{{~Xv}j?Lx7o5CW&~M- z&vez?IcZfNu2Qhn6fP)w;uUdn+AcSnDlhf9RQv-m==1JhhNfBuv?=8&R%a-W>o5SNjZlQ(8d5@|+p&Ss{7(UV5F z+n_Xc_JB~x@KRF;G@CTD5Z**rGmOeieKK>4g$${EZG@J7OINO08)D#~kNdQB>ofXc zi~Vbu`OXyxNDJTsn(@Pbzkz?bhY4OhZ`G4z+jIWY+wa-g4E_&Xz{NkBTk+%X9R&?y z5GHl9S=)fD(vsw@>!NENRZg45GF;Y>30Yg#56NPLtYN?6r!L`-5B&!>T*6=6HH>1G z(L3(S98!p*?Ocfkn=RO(sTU2jf->!*fmYB-9j+!xpB}=aHAhSj)qYT2DM8`;BHz z+M1pVB3Toz#2pZ;t?7)?){xwlBE+SU@Bm?7D;;z01NqY5CWJHv1P>%gD&6GMb|XE+ zNvlmsl0(osa#%5hN5~|-0$4vUt-L42%bp>~q8RY}KihD4pnFJU$56k?cq#t8?2FLl z`08``bNqR1(6+xKnK#GXVX%x{JZJBKASO6yK+K%Qdn`i&c!bNFe#Ghc(1Y3855iu^ z|LH4;CzGMCR+n9T;$(aK$tNzBz0-~W<)8$%f-RO!NGF=it`Vv>-4D2JA@A>y*r! ztX!imjdCJUh^@B2w}x16LrbaimA2PGQ;Hpx?UlT}?$fX8E`M43<(G9$gp;Vza}@$2 z%Fc06W#@o{as<%mnM&DXC{Rs#Yw0RLBxDDf2KaT1T@m6Tp!R)lF3t!my|5Z-U^YU! zG;Ol9bM8kS=PM4Kb=>Xt&|QAJ4kFpuPWT=_4vDZFzVE<3umYFivvAe$%3WD`hQ2Rn z-F2*M26@=?fc(JhCo3dbM~(F1K5TfA#_IP<)?K-Pzyc4!!(hRi;UV0DH{(sXg&+fx zv$K=e+Akdre%risI}K1y&j59 zCg?A)khC?wf8RAnXJ_PkdRAk-i|N-Sqo8SeVrSf4bIxwx`2_XmBBt|B!oyE?Ewqo6k~7t=$-{O2+ej6cij?eI73`1nQq&L?4Y_|4ZQ z&wu3A!L|*#XSksinPIWHiO=l|n|t3v1kN%@WaGG6I{9Rc{wBF4d3%u&mAs}Bm=ML} z0I=2#LchL)`syQ(&U!F6#Dl4W)B;PG`Stez7+mc$Y}-e(Ws%DN3ojzksNGxJu z7jM$U=_;hNNP~J`Y0A>>#)UeQns&(3^My2W^{eN1MxF-DAWG5>tiXXpeDnFH%%BK(O33snj!j=5o z?nstfzVz(d!{3cl`4hUVNv6wZ_voW^%B4{nA8bUCm1GP-bD;G%Dsw~mpVclUDc6S*;ukA49cbC;cPP)AU+DJfVHOf8#X|= z`@V+J5*~O3-ZI zbLTi*9kc(l9CvrOkUD(!GdKVX;+oRO&xPRe+v6N>Z#FHRyXFXj>(SVX|K;2-Z87;t z=a>EHPLsFP$H+j4BeALPoeRn>O@n}W`d=&emzTabNJg-A!QyAmR;^l9b#}qxr=3oQ z+@z~HEA6zdv>#0qAp%8Q} zK`5iAN}OTNNWMT0wLxvSfIGPkzM!*Mn@p__ut{5ls_B3>!X|HNCqh98XAXhF^v82{BYhRO?{O+ zX#GdB5iYw>>dVZV9@Of~G6$oVY~>iQVfeeWs(VKG@&TX2>c8Iw?|p&T;u({AaK%8q z_v~}q+SBu_gC6_FDb_+xP?)E$K6U8Evz;&bz7LaMdjNmcy#3JeL(zAgO!AEleCy@M z-dt@Xy=E$U`D%iy5OS{$o zQ4Z}#AtYOZ@r?xMB}*^{3}pSHNJv7#C|q7RjJTbFm1JSXbJFfR9ZiBdA!1?~6b+X> zZ7lH<^do8Mp9o26YwykrUwrufR+Blq( z*4rwpsmCgd=W`PWj=%4(VkQ~VyT4h>#EHezBZg2<{N&);B*}lm2-#DYIx}>cE8CH> z@!-jl2JCMnzS&4L?WFNR(|vR{CqC5pC9co420(Lwaof60k9zXB1OGI_ZOD5t>}3$H zjtL$VCEgQmh?!;7Te)H!qw~;?t;HYxs{+E`X>HwcEI-|EBJ2V8sMo;zQCx}}@KsL7 z!tCU`-Z5vZI|5XRkTzHwCQSiBqxA*@Io@qO&uc+cw1vqy}b;NE5bB4<`o zbfkS%s=Tx;Jo#2t?5x?*Z@k3@j~@=3rDasMdlaAToTkenq;0f<-1;2K?G}2)_B!Jk zVxhcZlJ9t-rtc%`CvDy|-a2uW%W|^#wz|-~Y@@b8I-M315|vy?z93jxUUpe#F5zpO zm2UCMR0V;iFViEB|0G6{2W}$|4g<3xqbwycW8>^$vvj!Cu)TEk?0tJ?44dt!@;J44 z=^hXLqsy0WXANX+dg(5`$D>P^?i3RBDKBn|j2u1LkX)V~9zGg&C~MLS=WX3e*QXJ5 znfPozM2M2~FI`}xXJs>0(vpH%B3-x1iS7XR`aC!Zm*4}~ziaOkLx3BJo8EZ^&%+b( z0(?As{quZw!t+NBX0st;di?nlo&yJZjzvs&Fe!u>T|8nbeY$LM>td*)3-?S>ow0cB zKXpDu7w>T39I97Uy@=^5CE84MZFNc5;YcuhWR$I4oUw7v&{=M{)g%3xHFNgu znLd0r45#bwE;>qjeh1ajuMP+4Qpt+4Gm34KRyQ(9r3Hsx(}9CXRtD0JTYj-fH!{g1 zaQV=@SX+o5=xMR~wuZ0>%o5hJDS(6EnR$m6O1+uvhM^~ye%HcvI*NiJf z2Wat<7;QsmFdqiR^wsJbjZgy`&5;lTPlJ|5gJM36E{Z78v`RE%553Ru(inJZG>xj! zLoo!=9j4nw9*q~r!jqp%bc3KsV<3^!ZWlQRC&oZ&4voxOpoq1QaD%f|tJl<{A+!n_ z{UqB%>A<>daTgHbPs>x9K(VPf=>A`#HHFb)aO~Kp(!ovi^T!AA;adF0(-uZxwxql> zP*}V25hmRLwbT(+O@BWpYH7r+Yd$SAgxa#hr%Eg2ZQ6UKn`i^*k{EfnhDz1OlV#+| zuOBIcKqGkCq`PSedn=P3=ML|0=o7d$6gUu^pGw_q-4JnmY% zd!k1_&BFCShTqX*R?HORSzLmD!5#RCXG51~Y0kd}LU%r-rJh@n^wb6f498bAYG}sv*vMssbf0MBkYCT+nJ5J*{zd8dWi-c0HdZ$Er7Ts-K%z3T?nGZU zwlRzw!$mM9YtmpM#0?c4L!O_qRqqyaU>;}y`FZ~vzw(JA|B=RJzNwXyUeO%ZteiI> zVlWo}vg-P`r@94iWSU+fujdLAZgtc|#ZH=P^BDPMUGDpvSN&SgWRN#2M*Gh0j)YMk=PIjfx< z*5Yhq>~?Z`jpNuByjZM1T6ZtB!?IuD1VpdTUXSm`USi^VXG*5-ny@gaB<1myh(Xa} zwU8y4<<%M4dWe%8ThaX^qcqS^R!TUeomH9E4ZBOG8WxN3V)|sU4a)w66v!QCF!Z(W z+_CDy0%ovXVZ70*H`veQEri6t( zyk{A~rWjN3jF$F02yq%B0a;e$z%muAkp9Uk!+_oQ?>OQvwp!X@_YbfK4vro8NZyt; zW;VjXSqZ`TC(x@dNmq zDjSsEN$j*9^PzheMV$e}~Od~eujag$g&2?QAXfWgfjUn_z1g#1U>oXhzG`ds__+UpxJ{tC}J z0vYR{I=doj#D2m#UaZmXCHgSZ_5f5Dk|zzNFtofNc0huA?7ao2aHrS_35*~tdNcJ6 z@IK@t)(EDl8{@B&Dft-tu=}y&1;n83WDNO&;D`LBiD0PQtrJbMM6c57zcifmj0iA# zl46$nf*y~@a%66p<-Wsx1$%f5M1pJ2B3E8h`Q$e!uKli2N8ro;eIG`RB*u zWVfjCYbN6O6Z`j`IDT;dam5Kmsjw0aMG*+W4XJsaRtHsX*)6duc_`ak^r#aL_Sa7$ zqm{uhJY?VlX$did>|HqLM@uVyiQb@Rd*14AGetTDqD>HDvHD9Dm?*3)xKj^#+9 zr)P>LWXH~g#X87maV!4%bo0zPKRr&y(x*>D$d`}*7&o)|G@EVxRqam^Q-mvQI2nJ=^?1n{HZ z1D}eV=fkH!v1Z;!cUAnnXgE)@+4REh(ufQ(nD5WdPkw0Of{_pGzgshzA2e}4c^Gwn z^2Tvu4fE#g1St!xu=2h2%g&#gpHzb%|JmAtOKKL(5@R$mmNnzB)r~1NZRQ0M#!j8SBOh>lX87$NF7Z2 z;1c-X>*I&7*TspQyUO<*^BR0+@uGfgDV=RSh_Ai;F}`lckxXqycGc=F2%H@VY3X7e zkC1Gw2Abqk;vun=b7K-mdJMX8hqA0@m$pG(QkU6^q~{N$ zJxaagZ%*~64m}6gKuC`1stIAWj?Z6{t^F5jz6f!2)9Vny!p3wLOO`avj%qppAw(O< z_^7Y!DUP0LH5w@$N~rfE6K8l5K<_?j4)Nnc zqk;#9`A*s2N88$6`t5(TIxw)B_*evVp_Oamnq3_2HW8;q^^9V$NWra57HensE||8z+`&(wwnKAZb*u!-fjQgG2>R;3`W?^*9~! z^@xFV-l8cK3VXl)HOV24=n%Fu*byD9jg(&h$;#`$M&y`U*{lm#KOnB8*+9suZHJ7G zqP7IU>9~L?6jLE>Fp1nGa%_yVLb7=DkBIQsQb$otgD9BwY>XX;*zHg%axMAsZB3ax zpFvVok`xw_g475$g8GAs&}c_fZ@RH|sPp&7Np_|~;Pl`wa!naLpV`zFX^xLZ3@Q>^ zncti>bpNWi8EyoB!?VtLkJt9?Jmlf|($0NHKjcS@2qf$C!vjZ5#Er>$dC40NA6YkS z%$Q;J5MuemBti^V%5stkH0sJBNFl^%qBR@}BuI2ujucYppds@~$Pp=IrL$ZLS&2AD zj+CDUF4>tvh0@$|X2r(RS=BRZ7Vq3NYv>(hR@E=#nN@T4?VdJ#c9CIq`IfM-QRDQ< zFKj2%D!K>@%}lY7i~=>gk%gf)#JipCd;$Wf;q_du5Un~9W1%LQNeRK9jN7`BB0|z9}IHk@kT_N(m zy->zmqSy}RzUj}TB<@O^J#4xTxAaX{R%Gs&^^XvBt!9>B&?lZ*D>aWrr&Gp^83S+X zm!4U30~m#%GhY<2)fp^1V*(%(p}b^4=51ejnazo_MF=Hf|yxS&)`ilsj`=_za@94nESkT-PEyQDRk>QFQ%?b1lSb zTrAAqvU#h3OO2)A_Ea}8j;&;;eDBeI(yqA>L|rHytlfSck01p?cgIN%I5EUg=PXyP z3TPc9Rs~%-+3!A3_2B#2HThNfVn|VONpWdO(c@1QmJp-I4v`a+NGGfne(5qJM35Yj zbF@mxuh;7+(wQxCxSAk=!mpjR(z^K1-JLYV!Du?f6@r-{$GT%Eg63=5n1{73NXy@& zF$jr7s%Q~88l44_+Yl<0F_e|%(sm`)w!}7?ilf`iomqLwC1O%-bU_Lr+?i4kU6a^Y zkJ#(e#YFK_GUzZPJ2ln^xpBFuDo|eZbLsgtF2AoQ>Nh;TV$Kf#G55~SJK4|pao+3< z|1mjp?thGV$}MI_%u+WYI%dWSY<7!Q9;PkFDYN&-jUGC5^pjiDibn+njVewXcjuks z#w}Pdj($hQ$0O*@;S+I=Ko+{>g%DcL966ntkc}OkSvXs0tH)9Gq>pJ8qq#I^I@0R; zIxDXQA#jB7X0Aerm0bw&JV*VVk;Mm_4@+%~!r4SfiX=X>@K*da{uCemc$F6<`OG-n zed!YPD_twZuDMXMX5CZotr)U$<>bjLS0WJ2d@Ea}ZAPqOg$}gVrTYiwhr~mJIV5a0VjLNK2DZ4BDIQH(KVyXJs&;Eu zXO=-&I)2hr!7Xvp_!wQ9C@`J1;ZvuFhR>Kmq};7Y*{8>fVXU0IKhIMOiLsJ=(t-@Eob6qyY=;rkDJ@qEr3tHeAaOGyYJ2#&U{;w5o?Ie%#1b6*+y#G zy?iRJ(xpjfedPU*&>NO^eqz$*ZG_9sD z`Q$I2)t5Gv<%dq6uq0SNGI9Lq&?Bdb1|B3*I^4>U0a;uacvBHc3ymPtHdO^hCw30< zsXCD|VG5w~CN4NNX!@`*=IPVpJoHvR^{XfK=P$)WUUyCM=m|BWVc6&$^YoG;(n%Se zB`o^U5^_C*rX}SinqyM3wrBaoI8ltpuyM18F6=CKE62+=JpKxGQJfGpT>OiRLeQ1Q zXE-y69_ZQF8%`)G3B}Sx{Yig>cGs@CclKuU=$!Z+cUg#*-9`-`HQB^)agI2*kwK%U z^q=R*YS9f?HevF(DG#2(Ay6MW);6fWH~kep#x@x9bmHcSBeE9~uC#V(161l-!W$O` z>+SZyY38|v5<7oMfG*X+>O4I0V5W1{Jd*EbK1FPB3q}KIT0d5~&?Z9Lvl|u>O6*s~ z2Fu;!IG>Sv18;P<_}=NaW4G71<(3sb4j)}c87{F60$?ii`u#^BsjRY+kgX>o)$1~m zpk8oq^cKw(z~O8s$_CEDYOA2i;b1u&vzf$?Ch^Nje0RycODGs6KV__=F22x#+l!ni zso06YlCR6Gt;M-Gqp1nqtd6x+e0f&9)TG?H-|6kIRn$u~51y#RhN7~KD#v;0jVY-1${WFr(e@wYs#D!V0MIM?k$rj2+Zqs+dA4OeQwoE3mA z{bp=hyMAO}uCLql^()d1&@%Wke(g_>V@WCN`u2HX^|FnY_h4nK`A0~;a6fMOY|Vk~ z&m@=^m2O$It_J^glob|l-n^Lj)ggk#5Wzxw1T(6qmT#vKe;0$ZWN2aL$Z~k-h#b3o zuqE&ZnDzQ!5VR$b3-}HfUa7%9<}f8GYx_u2c^LATpz!UAFl!(B4g&Ju!;cd|aq)%w zfITZoHE%I#Jt!XwI=<&>CnLSnpY;dQfdSi9@Ff$#6gvvwcQLLFtoZxC;>A{&Pa4F2 z=*wg{61vN<08w>J+2?o=ZLR2{*raVFxs0Io=#~d4d4_DP$#xp*8ilg=Mz`OX09>}` z(LS3e4T%~ue$>gRM@uGg(s{~+&xd&QH;o%KEQ}oTI`YS>BUVg^>N`k#I>$}04KR=C zs~7rYv!%TbS1w`?e*YT&eAm|VbGFqV_I=>dG=_(C(h8$O)22inUlI38CSuSou|}X) z2cFVVD~6F4t>jx{?9@XJ$W2{2%Nj0lBinGw)M4WEKL2yn1WJZ=v= zvAxVW)_2@v>DymB#5(XVcn$815MN`ag@X>xfHy3H&;R-h55>=X*153@zth-}lF~uM z2p6mQqr6FeP5B2f>MEKf5V*R5`Qx{`k%3oW@1K9c*nNQ`@XKNfMo@G#Z}5=#qxa(}7Y_`USj zCTF82Ow%c?fzX4Ol6y2i@>i|kwrD6_mTAWWBH<_Q&QzQf10@|WVuzgz_yIqB1t&u0 z`c;s4=T0d42tUzezKoyv!0AM^s}7QUs(((0^wK%dN9Z$WM@U-ds1Zt(GqYfm*Yaqu z&ILmolZVC5d+{<{_TU%P4;7QaM6oqphz)dn!Tj9yl@NshT_YBScL@WfjY}J2lUk^{ zW{PCjZVaO1Vg>}pJQ8L+Qn7lF=V0BzQw1Ln5!*(AH$35lXCZT@=P3LIo`!$J*YMe5 zrWV)jyY|}NxBtqx>r69D|Cjg|9+a zS}Cp^zRp$;U$3HLo31P^az+bDsJ}e)_x;DUK{$aJ+M`(;*5+=Qevh83(f7ad#HpfXBMLrGdC^0OtL)koA&P5k!b$G3+sS`@B)cg@{?{Nb(S z2c9Y;3*gSJ4@*JJ33{qNbm4rqOZm}#iv9?nKOYI#GR0bG*R~@L=|~T2L}oM?`O+B+ z%SMD6sksgv!bWJwwAkMpjG^QcNQTi62;N!}#i)I)vBZ4*dNz(SLyJ|&!4|6-YQ*E2 zJai^p|E{98FUGBXab9KDkBi{5y@F5Pmd3NV5{hzhG#gZ!n_O)m|Ccg`TvJ2xwI*mW z;SeFl1ohZ*JtzrGxhAdg{ytse= zF%OSd_V2%s`^)3yef#n}JS+C^J1Rs|KYO^6ELbIzPph&#LbyCgxSVp~0ygl!4VUg5 zNM$V83PIBPSYNW}a`&EP87(o>%!O1oHCU{#r(_&7POL&kC(RwaB}&?Eb3GI%7c)`mxGb}b#%*Z8 zu{Cr}ma^?o;scEjBz~6x1ST@E42p4~=*X<@m7+@2JKCIyl0%|@%*?zTZ9yJEw!o2uolazhMECej=^Na+ z9BuN_byqJ;UAJy3Ni2}WB{Z?TfNWEf@*EC%^t%X0UNDy>r-WoHewr^{x70z?Ubsqq z3iHuimJlR6GuLYASt9_B&Ey=821j}=$2JQ=xZIKjA-F!v3UMS!kVX(6(^g5=95+ON;Bq{%owB|~r<7Bq0U(LDTsw(7|KMK2r~nCmkmDs*fgwzGdA zQJkGr^bTi=vd+)?k+SJ-+yY0cJ2v4xRYlD>yBV@cE{T;b&LFFYX35LQ4O2x|i?(hK z4CONx_(del9qyYjusYz`qjw%YFl+3d85ME3)~+8pG9WxUza%Vd$OxK`3A_oH6UF(_ ze9%#C1fi)c2g(%r2(2=-CoNAboNmCLcMGk-5}&xb04IQNiJy{MLD+2+M6`vES+zE3uS3Y1x&gP5ch4 zV@4%gj@ngc6O03(&&!|v5;Wl9e}A1EyP^4+{sY#}zvHl>U-HT))B5^Y%sCA`sj4A)oAgjV$E_p)eFB zSGilQ=aB`G-s@{{BXa@%%32%N?|OWw|DkPkk?&&lbgjC`_wd}&yJnQ0^)X;`|9e6< z1SX9r$H5ci^}Y$Sbvz9ElhZ|nPngapC(a1jcw_BvrjLy-%lQtY%W`RS{V#Ix$dhDw zZVnOsED`+-5#5SLqVfMl^jj~w`MYF5tljpIpZ~pE<<-e4Hv})ZV|`Bbqk{(hbZX8W zdVRoL`qvNtK1CLN{0vW)PfnN_vi^p|(&9*s)IXlT}U2H}v6+2?c2*h;V zO$smV|FCW<+2FXYn>r>1Zi?NqyPxqt?@ZfEPDSZASd}N(RNPBQzy?M#obiElKNO~wZ{cvC}rou?y zXM*?me$T(p9qpE*%#Xn$#Bm@zQdZb}{ zmPCmhj)K_719Qx?CeK}=X9Jc;o;)~+8&r34`r-kDnlcAG{RK8mEN^Dss(qj`j z9L@iF%gZwG`qL#Zl7;}`V<#>a>|9Lrlt`l?-we5tVK4HjuRc`v(5qQR;l6i{s0p3N z>QV}DWeDwwOUY`g$*oHEpG3|N&^=c&6*7v6E4`Z_2)}k2H)qA#9Tjd>;@jSh_6xDx>?kKI$lWz02$7&vT`AH?I~ru9j#I5-Wp3wsZDb7_ zCgvmP>L8q>NEBrj>SrVia^$Z~M1UYFw==G_I4-s`7s9j!jLA_4AK(ah9^c7^k+dOv zhPYmrDJ`k@Uj1bWv+n$VEU7yYS>hc`mei@23%aB(USsGOCxuAaPDId2wkV;p0FKso zW;=2n^pCC-PlY!L@3-+}!k6i$y9|$z1cRm9`uwQ4gt=sVrVS^})4RVdfV_^!(l#nv z?)o{}7Y%ddLTvGiM(?N7hPeq0!+;h$SdQfcx3N3srt7)U5pmiq+{uO=-TBZ|^Um>h z7+?x_6NFLTR^A>uu_k`dIHJ=q!u*&{GEacOo+GhZV}C!kX9(cPPYPeOZtlL*{q?I> z?C#7>-p=#G?17Vnv|@9RIWKcoq80yOfEY^)u9!Y|oE!YwNRfwKm3Ml&5*! zy5*1Fw>2Tar~e4g{@d0qe;__HDw*UWljNeotyWq`^HV%HX{F{YW;k;DzmjjC+~1-f z(tm3oK7)7Z^6Se@J&mxnpYJ^6*o8h75e`kx2_Jwz63< z?uH?=;)aei@8L7JD7-aj#HI84C%=jsv$`@PRzG*w1l&6AC9PnfClEBT_gOECSBPj>knKzW4^D#k-Ztj%Ob%9AC&+g1FjWuwFZEuYFlQZrcHm(KtM=i4TFnn?#iOf&kxU_K-6zyof470(Q} zF(4OH zxwR5K4Vk%x?!Ts52QeB^h))9ile~rIOdbEIzreFio;KI;JU_f?>0v1 z((rV69nXfBMFC22BGZYu?z{LDlJhcTk;dBk4A&D^}p z*zcp7sl5A|`EHJWUp|)itanJCKArkBVcEn9(*?I>6DCf_>s+?JVRiJZ85%CgGWu4V z_fr#x4Pc)Nojx_3?2Csh`{Uxp*~7?gz8A?v4r$9RBolPSGZ2OT_r3Dl-dNH6=4s2o z);Ft84Yafr-7&k5yYC%$&UNn-07fEpWb92+Pn>d_-F4OwKDC#qq^Es|^t3+_5j~L) z8g|Pq_uDQ_rLI1-WA6Y`AG9O=M@%*)(ye%0<-U0Qr0MS3#3|#az_ad4Bc@EH4-ql^ zJ4<^u=?9w`cTr%g|DiailbqMe5gT-7>4<8KhOSqni|I^x3yfnCF-dAr!5zG zn7wB=vE`Zomfj&-N>gN8F1;b~u{FeEYn&nB^g8m5pVLdg$Z-Aq)2ZZ}7|odYL}w?x zflT|+^;|X@F`Zrxj7-jR%Ef6wwyLSyHBE#+UD^%R61!1H6K;dB`~jgLKp3`O|1OBp zV^a_tqXT!1n`mW{arvS}5HA+{wx^`FwM9l-BbEDkgM=hyOO`DBgxl4t3g`w6lOuLE zH-&sX4t9YMvvYWq|D5=p1Hmr@H)vwP-r=BUZ}r1R;3lUNj**qzRzgh6O+nHPwkQ`N zot3&F0B@8-*IB`vO+_>d2_z(mgcy5nHn|8On^ zqPM-We1Jd3N51+2mllsO1cMRogSWr3-0ei)o(nLU?tFOTWBmfydPt7>Ks%qiKu;T>uVPXsUsdC2!B0X&HSMxNtt2oOLe*jTp6 z9B>^!e5nb)TsYDY?1T(>qc!O;{sMQrZL^r?p4jl1r{~lj5t{M8A5%ndhd0S?r5$Gs zEBFq-|DU4#(*4`ll_JJToXhIg35YgepU^b79Z@C;abwIyx5&qT*g zkCSN|_Ih{}ZP>7vc~N~x+pQ~#T=Q^H(2QYG%O42}iWaM=`^&}Kwk@W|jmZZwkja}h zoRlFC*DQh7kGdp{G?>bBQ)VReH82faE_QGbA_(c37p5jmemFT5$C#kPLh_l<8#ESy zmh3TeCm~uzo|DP5Mj}ddtOZsj%|>%=5W?ZK46|41>g4z_WR4OX1Z5T+3k~@>mG(m0 zo^YzYMnWY^qWXXZ5vs|&nQPO95Zw*Ao#t+E;eu-V6|SF^nmjom(yhku=-P>!H;?n5 zkZ&?Qu_`exRAbVGM6Y=`I5;*WYRyS&9txTJZ=UO0)!>PDv_GWK$yIJs@)T2C$ z?S*6cxzjiM5TYLwBDoUL6aTB~Zb*L{)!__Nn@Q6rCg+ZQ|I}S%W?IOm{#oWDy;N7I z?`h4Ux+1kT7opS}Ey3k>Ef+%>SECsq7yU9Eti9h*VtUVJx}uoGHZ)2utLH zN%7*O5T~;z3d8FjLp0U}zM4xD3Xn!AUxElf(i_q?^3@?wLW+_kQX-K=XQV`L^9EK= zYnp34g9cIsw4M_S?@YN-u}I>tN#r1@_#d~>#L;36pQ@=x8pRTiATeAEE)zS!z=c84 zwGKX=BrC@Gd?IT=T1_bm={6w7nIt9(CDP57LvB8y3-oeUExoSQD&5w~*4*aAF8&4v z!!NL%K33z6zv3sc;oiEVOi1seyzauv$5Tpz7EahTwdBluM4k?8;w$LbMpG(}ZOC_C z7xi0H>rCL|wGFO2t#BK6a(sMuGcM#TxYo0L|NcBra%yM)z2d*<*L@!3Z{`EqhHQNa zIFxPsccqd&lqE_Ml5EMoOr=P+vJ=KqcE-?PW-v*}UfCH`M3yjQXT~yy$rdKYI@$M; zWh{g7&GWqP^Zwue_kE5z9LIglJ@?FYUcd8q{?7Y4@9XjyxxYhUu5YiOzsqj%XxHXT zleuY^r!s~a~`43Q?>lv-`=y1Q!KlB`8ZI0y;zAvtXTf_(5gXrn-I?dggcBZ zr`B28BqJN_yI!ZyTy^L%q%wrcTj`&FjKqJ?3wnr9vMeN}X8F6B*V=DNl?kmo7xU+i|#3S1+8Dz@vK{>yzoh4T@Y(a1bkb{@VODw-a{P zJUUKtBflq;?a0t`vZ5+yfyq0zODmv%{#*8}uvc-Ui!kr}X7hvlni|4y2HUMSCt$1vQa@xeAsY8 zQp|dx+gfK$9bda6FwheFF`S$Ee9L6*Oq2`XvUpG4T-~HtgnHQ-9-HciO(to+ovRJ? zIUJ-uf6mnehEwZP^9QAk7vwuvz1vAN_}Yk-T;+iFgbI?Ay30}PccT~NSSDksp}jtj z@xiZZ1|LwIGQpNWWv&BT3+rD%wGGg)(R81Wn|z4J`rxj9@NUzmu#e3hE=5Mo8Z6o+ zd~CY48C#RPL6h5s&vp!ehx3h0i{U79y^FVz}-SbD{8ZH`5 zA&jVA^NLBQpVhhe8$3N`=J>Vmz4hZuSK&pod0WTNUu2M<_G5n3r^m#>0&6447vFq% zaA|WxL@a|!F&df~?zIY*o&Da*Lln4H4VzyjS>*XW3;OUH1ke)zYX7-(bEv+w8Omq-o@@Vj;F^ECWOKRJqNk9N{YA;Tf|MQ{9{w5|AI zyXLw*b=qyI7bCTQ_DzBvduMKr#~~_bNga!jcA?!$__Vp=j!$=1!1Zpcm)p;C+?=S; z506)8202i&CxmNwtHN)bhR5@RGUl@F^AOWngxh>o|17ZhSIic?UGS!5htgd}Io?tgnz}^@o|Z_2;VsC+}ezjPXix{4c+6 zuorBB7)fuU=lBp7U2>{wbL-^mDq_{|K>5$G>4#RfT$iredb6mPiOa}r_PL8+JUvSh z>}C1taO()~5xV;UxIPuc?p;ZklKHc>7k8xIRX2nA<9oPe2#|f>B!pvNZaxcYD-WBF zwBJm#j#=*j&ba&;8gBB*T==t9&J`rneDe=ARdEkCZ<4brIOAAC=!eZkw|&Yt7=xW}PZh-C|KKzRGT4 zuClqMCJ8<>eZJ1Lx-Zy9BjZs<->L6h!XV$LZ%pAO18A#71l%!rjgVR11^ftkTx8fd zF9cwiCGE>G$psJ(TP`dAG@~BO>!+v627A-fU69%xNVcHX3FE6}dUv};_zK7^mglkcJsV1cET0^Zk*^c6nHqhpFdYJ*I{u+I!0R3R}tk#O4bW16H#?Muq? z7o(gxZ|binxT)Fl{@ga0W_Q}}HPm}^ztx?omN6G?_A7V%4$7y-oHM@74lA}R1WI4- zxh*7FD)?-1?c*%8ep~RN&EE5$C~@DC@6v<)z1bby9{03v zX`Y(f5ioVS57@wnB0T-@PU`9hTY#0mPqLwh$-@daQN0*6t6s>XG@-HC&d55?o2(HB zK*7RYQC{^Kt-XU(&+?6yzO542R&xQV)g`zObBMdzyOo_`#K3&&tzW>do7^ei-+bfx z*VRJRgzRsx3>{CLKPFOU*iw5l27eeg_pADps;HiG`^rer)cn8`CaOb;0Tt_33>7R7 z@CfrW={wSlRDM3#UY%iqH|T*`CU;uT6>ZRWHgl3jS)c_4ZU=9CIn@3jvmR=MVM14X z$u#M1CY!WG#vEDfPL1aq_}#r|nS9FEEKIDyA-JPp zb&m{cW zu4aC_k63PG{iyAEKA$NiJJWLMYnb`jxz(_t`9jlEFU0kpS<9$$IVax|iCZG{pEo(T z*OvKH==(d%)q5I|8vBdID@Wb_T2HOUqttXzHZ|D}jopZMig&~WaIGs|ps7^pdxVgU zIOHytK1FM<_3F|ilfIvdoz?I2oQC8S?U_I3hThAOE4ybI5Je<^9b9bRRrpC!l4mPW z^CjwIKRti*CMcAh5ZtePCoKMSA<>tAKT+v}cwus^jmZn*yi1aY6eZCp#K3X3zPsZ2 znIXNpVR8QXN)=fyGTbq3LDpDX@sMAWF|zkHMXR{L$aLl*a)zcWDm!*ajc;Y#=pY3V zTt)?W8SB`ZZF6voxhccQzGYIt&r$joU(VK$d``WB`Z}nmjr2#uAV!)5cg=LIp$`88 zr58s6^TB+Hae($-(;yz;^zt<6=o;;4VOo6#D6MxUm6u5s|8YH0sTO z{}%P~mYgg9XcYP0zWc$KhyEg4_FKv3-!+nPdR|{NbfuUn0Z+qsA|igLSfBi0szSVU z>7LDb%8S_|`#VKPo~Ny9{eN8+lB)iFwlT7Zz4hZ6oa_0gb5k9KH;?0v{yCctTcP4fiF_@z-?Qx8ob~jJ!&vd;NQn zc8}k#*LKwt<@yA4W}X0y1*8q)p_en$qxvi{)Jo)i>navxu| ztd38AP$X}hD!P0AQ)xByIlRlLswY=o%3X;t!M|zw)5S;#@WMS{mtR2M3^DXFg_3#4 znQK_{cVcUYQHxWqYUQ~>T$l`?65uoa;i;7alb<#+XgWQtHhn2c2EF1jr1*0eu*R6D z7JDL3$c9TWJSvvoS2$WO=9{BHnEXbjqrhZ1iSrqMV)ZAu-0sU=E^=Y7y;J}i&c5F* z{{4j6?I>#&i(GHRfg;aL>`(9H#lF{FM4Dfh;YD=Tl^58H!a9J^pM8LE&2ag&2N(iH z-nqnxGDf4YWGXiOQRshkT_}9|jq6g;!xYx=WZT|a`@=JKF=|(YLM6qNT2yhj9;iL- zjy`k6&+dj@!U?lKPlIAV^gfFGIa&Qaw&@xl`8CFY;D)eqP}>vlC1^7|4nEu;xx!+O zRnD?dgBDxL@VztN{W5s-!UvUxQ1J$a`|9`5zKTrY=N@pA-sq2NV}A^T(qCggUWLu^ zyk1!gO3c+_KgBh>c#dmL+M!ofC05kI;hrs6!{=M6gDa4eh6TAyk;<9KmIwVg zT)bzPuxB3LWC!QzcDnTHpJDRDDQlhERJq4hSOdA8Bh;7q)Ad*4vsZWaT+F&E(F4zr zA2*Fp7qXnb=CE2g5P47wT|BtN#>2&yL4W1hhHkN+@N*ua+HK&5t zZMc#`ym7+{G}MsuAz>kBhGWZXSHATqc;WsI75eTy@+`jko<*6X76TgX!r>}Fv}UYh zXOp`TZCdBQ1WO3ryb@{_a^P;6aqN>Pc!f|AvtYV*>e!>j+y79Fz2%p%qxggSD}Ki( z*IT?{N8{M(!i+UPp2W!+Ab5{i^LlR^i2124htaDk5>5(G6?E}6*%5tGe8us2NvqwV z4|-r}W~Z?K3<@Aw22wqzW)k`_MxA~)Z0lb>Dc03&Zuc>ky9rNcEo(^l&8jfVd+p2M zM+Q+32BQa{KO&PRqJXmlnc>5)`*D`-z3=SP*k1|kxnO|^4|aCW%feTldQ8hj8;!d? ze=ReuXMZYS(dora?1=w(Xv1Ri9Z8Enfj90{_yx$0IY>Jq+24%UaHoX5H@gIz>`tKN z8)oDQe+m{52*-R>M;!ZKpBn*v=$3E0aR2!xbIY;yRmD@&K#@~_9_c@SEU}C62>_9| zB_#O;9wlj+vOl=V1JwKRGwWuTSWZ@I-bpXlt6gUCw#rumVueg@R#&#@Uq2ml|FD2z z{3>Va00*dk5;v;%`0mxbMGYPU_0I)|Hjg-`m{iiQe|j|Z=NiwG54qVF$AIl);!g_G zKSoa74C;MwuY9H_p>e{3@-y z+^Fq1GJS-uVY<6(g|h323gnovX84=LnojOn{)*n42frnfi^>>&eJM1m@WgNOpBQem zJ^iPZVRrv_WkqD=m84+7onA@CMfQpx!rr1P$DicBn$m=nJ|KhBIMkANw+=oBoj5C$ z#PJ>=A>_aJ)}>MWhtAV=ogW=w*+TfKg{HRi&a1b0k~M7D?IaFdDcZ>ltYRDIc~PGa zg$$=_H;=F1Sa)g!LIGFyWs*34^Tcl6BTz|69G?b`zXtK8Zs_lyBSUMF5~gq3*)wzc zT84!_WEzXl=}=XCBIX4a)qbD!zEe!TV%ssVwxXgY(){xr@=?nB*Q+&)4&L2?>5>w{ zVGz6!Hu}L$*Oj;Sd3O9NA%4EjCnaZ@E%CpZ{q5hi-s*8o4z{9dbay{K!y@Uz%efV^ zSoJv?XjLI~b3Zc|@LFLF|Iwnp(>`6j)ipXKDnv-)GjJfS<@|V|<%8S20h+7YROgz! zitZ0h_*mcDiPr{vxnrQdy8f(>`{dl>lf=D*MQao?gfHVg>D!{6-R@PbLattEQP$sN z@5RS9h^sq3e=2VNK``u+X*B=ZYFWLml7wf`S>s1tEOmamx)WEr&vm$lhoU3V5f3Ah z^|qa$+>$P4KWvz7a;9lu*s}qLGnFRRi1BYpH^3)3KVy9LeEj$mGw2!^{OHgu&$KD1 zX4ZM9O!wC(u3tzqhEdbGi>~*xLn1a{)>4-)$C)W#^1C<`Z9PBm2(iAN>Q+EBvreiM zo^a>D_Yeyk%aWOS9{%bmHMP?bl4@nQB8bQkZ$C=@d~>+R z=A+9yuoQ+f0;~ zz8D^x7UeZo{r4*fWK0SfYV7LtHb50Gi(M z9^$y8!m|ry(^dIKRO*FWR}HQTSBa|;D;N7NRv}g`h7zk1s}!rrFVFv;Uy)y(kIJvg zugtG;E_eR!T;W{pjB>7Wu5_-MFQ5NDUol@jkD9NVubi*pDd+iqO7iH?J_SHbnZ8^CROruC}qfeXlO{0 zexm=-&Jg=h{E*?0t4C~AWmQa7aaCqjLseu|K~-v1O;tiwSyfII2J#B>5%LaF4tWdt z3Q2?1Lf$~~AW4w#kXT3w1PN(`yoP*+e1KFyq9H|)3`jjB0+J6&fmB1{A*GOP2pSR! z`2=|nK|!J*g^+Ye9V8s`1(FP@g2X|-L9!rCkT6ItBoR^xiGdVDG9e9+NJs%B6;cC9 zfRsUUAQ;Lk%16pON;%~%bC52K=iKmoOvMFdvDCHC7Jq1OHq7+inDRq=^$`?v9rHT?q`9{g2G*QARxs*go zB_)PZOv$7)P$DS>lvGL$B|#(JYovOOSh_aSno?a+wKg_0Ix{je#!ciNo>XXnNLIxa}y*2+LRuF=jMoBxX#Hm~1;1H!+$uF{U@FH=;MzMeG{w z8tEF7Bub7-j!2Fb6N^WSM~cThh#sRJBOYTb#Ff#Nk(Dt4qQI!Yh`?ATQDoI%?fs7P z&ioF~&JDO8ToUd97l1#5tHI%LMz{`K9R6JcH#sudKKX01XL4e)ZE|q3YjSL|V{&-1 zZ<3_htoc*3QYpoX%1=jYEEjlX!dL3HAghtHGgULXijLh zX%1?3X^v@jXbx-kX_C;*=%46LG!czO6VTo0adazs0R00!ipHUb(7otMbPKv4jYp55 z+tI(!J?IH^8+s7kg&sq9poh_YXi`XX$j^|@5Ml^6gb>mlG9JiVxpmo#6X|1#Y z+7H?&4M!WI_0lG3Ewp|bo;E^jr~RV!&?abYv_V=IZH(5z-+`hRrU(vi2u?lHQ5oIH z0%vU?H?o+JOj%LLs4QKiZWbPi&yqk&WECO{v*;V=W-TF?viOnwS?S31EGwi{)(CPW zO9iQtRfnw0f+C?=`^f#QGsrVp%}iOHQCV2stZsZ(t3*~fGCb=c@?lmxvOP-_`J*rk z=a$vGlqH(=1^FfG1@cAK9C9w}BJyHZGBP>K3~8423;8Qc0jZExg{;b=yImF?`dMs9 zwyZd0T$TZnvp1jq1gyM^p9_miQstM*AmwV;`xm7s;7^~2wXi-)s^tB2Ev%ZKxa8xAmK;dlBcCR}AwM8v$=Atw|E>qF0peRutL!Ml)Mzuf>1AC8wCz8wA>U=9d} zA4fofPr}Ot-vs{zZ~`R3FCoCl$LOVzuaUnI*a%|eXB5!q)AzE^x6i*1+z09N>kE+a zk$EZOE8{N%mVwCl$pn=7l)WtTE%Pq}mqE(>$^t+>pqC(DkUt0vf`I%$0qZ{NFV}t7 z{nx?kkafTH03jctmqNZm{z70Oh>)L9K#ot&%N*Yv{~T}*B*!l&0O$jJ3G@Z}1HnKD z&<_|u@*%w>`I7ueU=oDnM+yM=0A2!o0sa6m00Qs>1YmqHFEPFte+(D{!T4bU5I%^P z2w#Lh0*rtl{15@hKF2SQeUJT*H*g!b`gR2SDC$Om118x!K_DBDIw%Nq3UnWI4de*o z0vUsD(|lMtjo4wAYG6I$PL5~vI41q zprA9LhageV3(!T78AyR{D>je;NDAZyx(u=fX@G)33?MC#2*?S<4SG{ks5CIvJvKhp zDlq4@@7s?Ay4NQflLQ~-uffg_es0DmDa2PfW9fm6fD#4VXO7NP%8dwdq z1|AR?01JQyz;^?8VY|>>I7c7{j04I6PY6taB|sD4MuA2!Bd8I)FR%~R2knE)1j@i< zpfd2Xz%p1Fv0r0FDX7z%Wn@91)0sA)pBO@mug4QO~J!7J>KANhJMj<(-M%!0tJolKYhQ zNdJ}?XJRxSJL||S_a*JIzEnQvVDu7p+wrViZklfQEoaWk=n?FyBfDHdnqIfmJm*&Q zKK9V@Bt~w&pdIi55-}qJj=(oq*6G$ zqUW%ij%QGxEpJ#yi=PgA}5p2F=9#P*@ zy83)BHpNjARYlbHl&t0+i@A)AcNCYij^$K~vBj1;Dxn&PTB{N}T=1A+EZPx(!YJPr zyur@J7^8*#G%X9RRlYAM5znO)D%DC+WR&VrCb&<9@wgBRd7@7z405~TuL#|u-~Q?!40)q z;}T@9fEYNoX<8GE3B0R*;|do?j1D$;T5h8@@V>f4HkVP13$}7vWur0hUgHfrE}0k$ zZ1J?hMtz`Gqr@Z^C?){gFs-qH-n$!gLz7D=#t2(5Ex%E>cRxr1%>|4BVQZ$l8ZWz$L<4SQOL5;QJE3}RM!;6wGxJV_0SyrjqOOI_f(K;IyhP~;QC>*6Vi zow{?+eSKBJk;aCi1u27_ZgVevVJczBr@hx*QYt&G<~)7#Tb zS2j=1-cEWS`{J5OI^Kq-B8debK6lOcXkMPieUv9xGvN>}HLIbUCB z-D{htrPnP|iaX8b+eM9)N_BK*%}M)O)Mbv_l7)AZ;P|FFO|7u#?bUe8~QR{Nf!9xIfYGYXjY@$>v7?T9Vm(3|{KHJYaw4+@`BmwX_ zb26J2`n_s?f{;G9XS3qanHTWuK%w4QY$aBA~ zP?vGvpfKOqq}Yw{-Cb3oDdS$xS7&4SVk<(4yP`rvhE`AEys=*~l+fs|serM*EA{o% z#iB>;H=h>%FfC;^D+(vHx^q^2wbqv^O?i@BWR`3FtI$rWFvVD}_ywWDU8Sng`rfy% z=Ejo6W`rVlg{pdMt#5@x#va8GLcP026?*ip*H=Yjfno!~xvIiTg^DJb#ZrVDRbNIQ zdwr{ZI##qs*l}m9${p2>Gj}5Ny7N{Qj~c9%?mU4PH4!G=g{sO%jn>N8pD-4M5n7hd zL%yo&UoMS*qEnPh=wIf8l&BhBE;D>0UX)0{FJFKZsTyRJ_C9ebsw9jo3qnd&jk3z5 zpYRsN5ZafyAcgh%wxy*{EQ*Q=^!4)~-|7u*%e^#&8A&-jneEVMnwgL!DW6*3CvKV%T5h_X`bS!gI3aR?RrRXQXqB6qpvH;~9)iAg$o8-D^lTfW zPXz>%r9#Rbn6*Y$wq4&x7lQm!I^`D3Tq85vw(rvd!EdRKq7Jju$jJuEeB>pBvP1Hm zvKCEURENW*DfeLxO>gZ?W%7B4^%frw*Y|%Tzbs27=q-Jr+=f{-A?@tSK3WhYmy#(r zVP;JkcD7}oh6x@^RTNd2ZBw=#5cE-rAg~li5re&%G)*96F7;4&U`dn4pf5E;V~buC ze;B+eZ1U-Pp3aIK%%v%2(ri6HU>LrrL9v5@ni3{0)(dur85bES_o(*4Z#7MY@;Qce z7DXsesjk7Xn&v_U3B%%x+!S5v^WbPrvz&aRVV6aDiWSu}I9}5tr=V|`caeqikm?v5 zg*FA|%M4pAN>a?I?!j?rb6`Q)u+pLc#enJ@9D_C^<%5O;7S$-WRPW#fv<0bPeVAjB zk)lO)IC>jm3dk24Hd+*?m{8r0VnfUU1v$eqi@X#)>Wic35Hn0Za2T|xM0rN_I*Jdm zz!Z>%g%&v|I#j2lD4HoEA21ADl%ZHqJ&xjN=7<8!Fkn%LVnlU0ilLbu=Ocy@ivS9c z3OY&v#Dn&$*Y`@-_ghoZn^o%vGt`;=nFH=UZYuXa_d()bA~kV8@xWxygle*Haxl0z zNFCfCJdod$r^@fkA5`vDQY-f>59o`lQ~mb+4z~8Ts9XD62P}Im)KDSnd!d7<9BO*b zK{$}gvd?l5vll~+*^fEU+e-!>#F403qys&w-oD;J*IpO3YrpG2a!-;fxi5K8yjM&u z-Y-7z*z=%z?0X!n?5$8&_E!!B_5`Q``vM1*A&Kxn2q2yz z)DUn4BSHruj`$A1kw!@Eq+g^S(gdlEG)U?qjgdM?!=yeE3D6Aq3Frh60ayS5&0jz-lanen#4n$NAJxDA|4L$7U|c5>yM1+G-pmRMgxV2!3dD%q#9-@5v}K2)tnF z7_Oj5bc}4HvKP!2KI`G>lm^B097IxrsJWSNw(kbj4N}_xgT7bLS{FCU66kkad zK3JwHFweXVA#0_|N-IXN`QvUhzulN!8!1~_e|@!;k6T}^O_1dlsx^bkyvbYAx5cV9 zDxxpj7QK<*n}?4xdtk#QzA7@aKZ~0>if5*ucS*n#)iy~qwcKjtv*7us^IDS;TM}^w zeQ338C&Tfd0OGZo&!;3k-_ew16*bnMhF;ulY>wWNj5`BvM^B&9D=EjFeYRSzRDCAJ zQh4FgbLH{kzmFqMM({brGUcZ^#M9q#h-0fJ3a{w=ug0lAlYi8mCR6kF`jppI3SNU1 zW^OxmY{fFf%1#9{#ft9hZE`W{YN5G2Jn@MRp%-t)Gx7{0WwnD918;c$O>doY}oDJP!@eLUjf9YJfv8F8tU(1|2gwtGo2(^rhA^_o&*o zHJ%sQ6istzS+IGuDKzX!RFLr+i2zJNN~I5tQBi z$#dMB7TrRg+JuEjy(g>ju49;)*?-Fm2YLA}{4XWQxt!#f1E*n(_tKHi^K6*sHORQ*V}!}MhU0Gr(wcrB_{PpV;a(895-YzT_DTosV=nLDl$+0s#LBf? zkncwQEYRQ@`!-)H##CS|r5^>DQ9eB^@B(M50~XF=Mb#|2Xb3hTi%SzHB7=LW)Mr)ZUw zN7+}~$N7>8>3><7cxiRvua@OrTwre5{Yz9|^uuZlY( zF3zM`RBspHG=De6r33$5N$z$LdVyEp3?T4F@;Uuby}UIN!1cah`q14IC5L00VHhed zZ;6Na(k;`g#wLqB=cF&&&gysTFGSC(84gw&?Yxh~m58%{Emjf}#&xg626yhI9rR7vI5$Ly39IYkie&mKc{KR+H<%i(EQ%5;QWSp$U(FVKP^`P> z`<+1L0=g+KIeL0p=V937a2Nb;p>RCuq;NjJ>_hVs6n_ZISw6=%a$;$h_6;-t1m&tL zfXDe8kXteQ9S5n5^i`WkH(wlg9%NrXIa8-&X0PvFvM4kemg3*7SAyz@-?&Wqk|wg% zdfsXICjGkbO}2d~u<}){FuUA8ggAQ`u_b-YY1||rPHG^V?cgKr7M5=L_kER9Ha9hS zN3&HU{EQaM3JX4FcUyAusz6xjPq$n2)Q8c*kA!t0Atp|wcLjHb##1vIAL46Um_6p{ zTqdw}zOd5ZZ*Cy!M5{nL{`NzhG#&IMsP;)>BvK2fDX>{Z95g~-dm(rrxGZr$R!(O~ zhGp}@_hI5@NdDnbC%+O>e!)__F+pRkjjFQy+_J_+V`$AuU#{Z=#K~!tzkz-ozDm+3 zp@S15bz?+K@K!WTrSHZUa9)?*K$I3FRq`s>( zKTa+AkH>`c?G^oVAh>x4>EycXArkw3_v;-o2iIGX3&#D~_S%3574z>(II;XD#rgMg zi*Zk5nLd7TEBQC+@d2lQk{lxmW)&gHav^UcDfnTofKWLjKA{b}Go2cj zdJ%zk_g)EW^Ethia848Oo97Mt_rarV)qlAytKSayUy%VlVl6-`T{*!20)q z#qF1eL=Y6lk8k|P#qmZ6e*3qaxHS7{{tpFFvbfXog1wc^1D0oTMM{c|yWoGmO?fq@ z_Af-s3oaL-a^p{=4~usCtqV};{BaNW>m)dCJnYUtR~6i`cxpudV46d3eLBh@isjb( zgQPS60-|GtmTer0`5)iF59-i6!@sGLy|JsnE_Z)aZ}cEz=d?W4Z@Q(`l7LM{>GA=c zJW3)UZVSl4uLVt?nnbS5x2FvN?c0^-#W^cN`VLj|HRSU(z=p^6y6{j@MN44{?`)sr zW{PsG2TE&5%ET#IUQ^gPBmfq zqnM85xGea=4+Oaiu$=xxr)aHUHt@Z1&JJvlOdQ>c({OzQLAvpe*uW7gJeHgA!hgl* z`TrGq{~eEcky=T@3s)-pqLn2^fJR7>gHpGIvE$YIqq+Cy3lz4Dr&H1>TbG_WamDIn zpV9Vg9DMZpL-TV%HCHD6QWOKMlM1}+hC)4l3xDkD+IJ&a+1;&RTNZbYTR-l-n^8mT zH{5rp0sAiU*Q8k|#(v_x+vex{+wPxsu0lm}mXnF0R!KitW`!OuJ1nSZZ!6K2SF#*E zE@RW?v2^fM+C^PlQFuhVp(Xv)CNA=L?7{FE;jBic7j*1&&Rj-MT}-RAw7J%9sxTwx z{WCV9;&kM+_4F$Pf*899cTF8ihvuu@mYE#j8Q;#LS4H{0%6%aEZ_+*&2~pj|(h22fw*8O2Y#I>@c@$axj{Q<7xju*h_{Su946ygG%7{#N zv&Qf#-brQclI=L9r_X1EP{Uq4q6h9v#azW3`f8-G8%G#>8xzcu> zWx%+_M7$O{(m~YNC;BZqJUc$)oZt}ayzVw(|J?J`H1o6AC61_`!xCoKb(Es{$q3iY z;i=#6WaKG%{Bj8Xx?xHj z;a5r%p2@tThAZH6dL=AIQ0m;P*6OX2A8M2NQmt@Xy70f`&T5>&KMa@VL>DIX+(62| zlX`ik@~^B2ciN;|R%eTeQ~U4CrAdzcAKvnFdYpZ$`Fy1MzvcJw(TOMqE!VfKC}Fw0 zi0*d+%vx?=S*t$DmEU(+JQiVM{Um%F8-aay>9&^l4c4|&)zvuN|KAt8{lATe06*=+ zTsN0hZYPsWn&zE|Ad;)Vd^+vA0IndviB;dmOsw>A2$fN+)X!;{lh--OZTao(TULPh z&dh#b`UX^6gJ4_ktvxB`5@LkT-uZ!rzUGH$cXM|%1D&{W@T_4jP2+fj7IzH4cFg-OR_hzwDd&>F2N$N8=gJql zo)ID*Z8*d(<%v{}IcLTxU6FH~t8velq63(SCeR#bv25}sGpBX!%KC9vI>y78rM?(&gD!6Tq zV*l^F*jcw{I5smSgUm_TtKdO~F*~oHdV}b7R>>IgH88$%P28=sIMWyQ6Vk z^fu;_XB@=c^YQXy+59Y6eRC(opfe}0jmiYo8d`ZPLY3cqU!eC^i)zvvM$4b!kuAGY zdT_N2$j6t9OrzEl_)@WQl++R}K%eN%z~Gf1L_ajRMN=T#udj+(e9XP!a?O}O!EoLi zq~R-Hx`V;1AY(bAD&~#1J-zpyPB?hP23o4j>@f>QXl)!iJ<1VIm5q*bcm$6ZU3m{+ zFLbFzs$_ZxE}oH_**dLKQi{#c)3rTr82=n5&)ddAAF zTckLJvxyNMVx@Ta>?0&nh)!=5Dv}#N8(#6!f&a+$V)Pi5TweHbhmhq$kNa0K_dloA zDpvFP(>YZNacsTm3n@nKt4~LRA7X3YE`&aPIL^2~Z z_~Ca2J*d0OOLT_mI7Lr}`XAjy$NazVpp=mQ zt?C{B5+VO-{Ex!=??%Rdf1FNhuA7h-G1%aL-Gy-u%Auz)^d!uZuC5r>t^?_@N(y1* zF&$qkN{&h=;gq{Q-m1}$7Pew3^?c7(efV@Vg7pMBIiQm0WmbROf7IUJ*$usSpV&8( zEg9)|Bxc+I2ABIHkf6K_NWJ zD1*I_UjiOG0^pFTKL;fac0IcSG+IX+X<`5ieKB2vMA2>r)*EW)@AmtTt?6U_s`xqp{JZjtUjJ2(e}){t;8Y?vFXnfotB;^@SRD{q zai4aioz^s}rH5*~%Cpv@C)RG-I2rwxfM4np%i+%qdLI-+5=TsvbQQbbe{VO$pGJPr zZ{x@7BU8IO!tw4l3$-igm!xTq(iGkE)E z`edqwyNs(e&Pq>R1jSJ%3vSr`(MCv4P6=Uk4s-Wv5rR28Ilo63AIhy=2)@(;gpkI(}ou=XFl>;L%IuQtC7_hDT}7p!OH2eJwg-E4FBLo$3C^8-8?bmeFm*CKU}a);vUs;YVUcbT&G@FN5GdwffjDrdp6DKdXhSw+j`XC*|j$uzb98AQ^1 zcr8#<)6phK1MTJ+B#C~A5dh=^;^r2kQP*rWhT2n5qW*6$`vf7I$$85B1={l+?O>*+ z+okPnU>5HLHA|Av?o59ms}VL4XU8B`RTUdJx;Ak#$neGS-o^^j&vrMM9O_{IosLLd z*OM%FUcECfd}nE#jJ=7vk#hZwOTvXatPCGa&tcEqjlIkD^+BPF5&aM9zHnSu2Cwuo z2vbj3IQCee(6ZM{{m6d6T@`u}PF-KHfWtZA!w2&H&(gk)zFfWacsJPoF5s!*=>fZ^ zmQtAk?3@=t4NW2&y<;E|<|*Y?uGcyltuWb*r3ajU_Oy&B+@VJ5e+VL?G+l zc%|tCyk{Z);eNa)UncSS_ot>eY1kwQ+@p;QxzWARzSt05kb%#DDR zkF4?`Pv%Tj7q}pVc$g0~OZ;62KV8oqz;CBiWpnMlX%-FxH9JiD&AP zO#_Nlh*|S?&)LEBf)aV1y5ahoGzov1hE$bbh82n3N~^&;A@aX8U4?EJjOnb3IA;VC zG~Ii*Ps3mLSkb4lzN(CIuSF>iY7E0ml@bP@5 z`K5e=v;(<|4>y8QsxmDz*8PG@s1#~ksL?9i2K&=K5q7}-WdysznSrtRx!ss6kB8y1 zw3fYriN}D2BZGy^juDjZi$kGU88XTY({=gBE}vRcknjF1YLId_erYb@^y#xN+4

      J%cM77#8>a;etD##tCg4W<0oK79FZnDx%jz4N&f=Wyq zHecO@;Jz^8PKPK-Hx>CuwY}YH3($a@9euHmac3!xQw7`-6nf(2Qk8{HRvaqv->yHs zE<41!+!+Xe((y6gsfUH_UPJUH+%9eH{7bArn+}h{H`|!km;~EIwH-F6Yawc}m3~g( zw%QHY*OL(}Lj3&&#T=JifI07KI9|#(wNXTxf)}?LHm*giaKzb~G~A=5fEDTuciF4d zsy+I5p0C+?8Sw~i4&86yak}PyocD~Gt?AOX`_HR8xUT{U1d?9%Xz{)7lFZf2{Nbh5 zEaQchdhQiU5zyAispv{t4l>l!y-g>0glM~ewwhbVE+!(fNypc7j>v=3r9%Af;0(P+ zYaV(X2@JjsKMTqkjqh2L*k7_M^lT?>!^Zdg3)hUT)_FC)t0{ROdf7^c&`=)rhBD*+ z&oG7us%u+2ufSlfbC>RRkN|Lh>S&F(+R@!^rSZunX*&d&HuNGeRU}phw&+fckmH=m ztsfE)de~U``{P(XKsE^+`pM%yI*7V=17!PW6NeOjGgas<(2%gy@2($qMdRIgrA5Me z<3dWdE$r~OMnHU}3PZX^CS~<=jpV^;ezOy-`kDHAW!W&v9UI7!*8Sq_XYlM*X!XJf z%H!=rHj?=F$BQpHweGj#-&EE;QxQH0l`g@=sM3TMx9$#zvTMQj<1}thZnHN%mjUd| z6X=y7B;RabJUW7pWn6 zMg@#Q!9IIog~6A$-3AIwa>&8dZT_DwKBV~I^`OtbnDUUe12#VRo3w#-h(;bIa}3n4 z+<-|Jd?zT24)DF}_`q?!U?wMiH=a9FR8d6Q(B?3Y5AM5)7hiw=xTXN^5thgh+$JxO z$dEnKK7(g8?Dm=qI15ztp*C+jG(}Kg-m-097ZT##{v7r@G-V&ZR@CshEa$vk>JS`h zAm|SxnxrXv_LLVY+u9R9&U|vvi5h`+^>sD=l36dAR^t%YnYCk)lM3FJUbo8LpT>Pz z$!Mc|s4+${4l?fayae^ysZUxc1U~OIpHVtHv?p!N2d_Dupn{iQvX-3`RB{gj;p`>E z^x_DBoe{j*zU^);+%>t5$u5-E^rQhWY=)+gkv|*=$?Gd6N)=1cEyR z3kfbEI3#EY9^4&*2X~0zHn>ZG;4p*xKybIXlAa?|SC zax>;3RSKu*B*4Jh45g4Huo|x)-=dJ6St2=y`{=QhySHQjCn%+8lR{TLpzA4Pd(z54 zh6SmVAwo<_48bSmpH>lM|9xqY!%2%-18e?WlBT7lxu%h3ypqQE(&rgvdZqRD1!fvi z_WAq8!qpX}p#32aR;`{veND#cclFvz8btGN`PD}(3~1{b$e*4JC_P%cYU~3@ycur5T5%K4qa60vBQXY%OJXrR|3Wk4l*k-zm!<4TVmh z?Rz7_D1kKsCE5N<{Ml=yFIN z-7{8}GNS#U%sjMgK2QRDqy0T|8{0j2a1Z!NyD@XEbjOx;(!jyMVN+7uR~r*icwjUO zhH+@8vl()FfT4OEE3TH!c>N?mJ|JjISUZ_b0qAMjQoZ#EZUQ$!SxDEwdf<-MG z>h*{CJbYWgHw(ufJrh^v43B`Y^4rSW%A1*h;bX@3v$lhH-OG2rKW0^+D28$+RKquauiVN|^d`W?t!~Y}ews#izyR#gp^NG(7*6zHS%XFwIyW zW>KZYlnxDl=t>b2VR4rFu9EN}fC!6SE$rtyy52_T(ty2}m%R_ELr-XUAzfW2PWheQ z%$biemnZ%EOX0B1$UT6N&YW4= zEkhO4U5)O^_|vLJis*52rE%hs6Wm8;PvcM6ADKVS?o-8;;x+pDsWpti3yblTSJw2^ z^D6jjDeClm481&(=c}5E-F99y3EUJVsUdr*UtSG&vk%J(C!4a>n~Hdtu!whZ9`@4i zGEyNc=`fui=d5yz_YnAn`RL1tIG z%-2^YonLWi6GJ2h^F;5f5+o6Xm727$3EYhTao7%giJsLee5CU|-ORcW28@I%(u3G9 z(I$cQEeu%Y`uNC*N~IWfe`UH+ihx?8A}f(_+dt||qJa-28(p{GaoXak^$H&TppZME zi21b;UUHiyxE-SnQlqo7E-FSufdM-EOf|#oNEw!S+BGIJ|?&Xl18sHlemPo4B2Xt1&S#Q_fhU!sCX^ic?MfVXA8?FB#71 zb4aIIYqaMnZL%2!ARy-yyS!V^qVm;#{$}F{Se)>~bY43G=qX*h=kZ}UV)uwCJxw`% z%Y~3bAY?;^r6;}b%3&d8m8Lb)a&Dm3a4!0Ra)d!ovvIW8+z`4hrq}Crwnz|ve~qMZ zk|}q{s*osPZJ3X8Px&j%F)~g$z364%1`URfK#x2hUtDne&cPneX4ff$>?(6q_YisT z1o<~@Zqf~I+Q7HicM(`#Bt)l_Mtw|*y~K(=RPTCTDfZ$1Fn!KhoBUr^Ap6@^#e2mM zF@5hebt45x8=^sbkd+T!bd6RGfdq0rNfloh>J9xS7Go}%y)dDYP9lXz-Es(@#Bi~{ z%=H*u`>6~6r3r}0SbFSs`WcfcH^}~RcGuaJ!ltd;qBRkzf{MxULp?$n-P_j8GH;+oa#C+Kp{9Rf@1CWHc0>3+B4q&fLB}ZS znr&lVSyTZ_s6DZpO)p}E5L;Xr9@k}K?kFa=4jrXG1LmI}D3Q{K9Q`1ty~=&~{?}~1 z^4EDh&o$a5z(di`q1@DwS!zBbi7tfY+sa>~EW+FBXmYecKS@6@ex%Cs&zwy-Z1Ks@ zDig@4&MxYVAt!!d5ZAm)-FO zSCTNh=V{bW1@iC|9_;*6Y%4QTD@GbuZn86OI-JUwLqn{JqdF=04+B5oA<8S*&`HHunuY2Y4VDrY<-_lm6jFH1CO=ZlaX;!|(T5^9Wh< zvG1#qOn;*J`ZVfk5CiU7*c+z2r_uY3wEUkQ*yfSiZpe18KA(DPI3@Ssl{CAfEaNL# zqE|9}qTjxd2Qi68c94JF;UVsdjUwZw<}!O|)cLM8N{u{PK6S5~yrwco*kliW8E*Ao zaP)}J;r@m;4*_V+U9r>RhZ$v|`J-RlL^a%0dyRQ^-u9Lhi;eH^nPEvu84m_!8&=&#zWd*NnoN3`c)ZN8J}trd||v6f13xB!u(|4Sojxf@B70dTpF(W!-RLn zjE1+8fSS!io9T-~xBvaiQ&d4qOd!Pl<_u_l$2YCn`XJ3uY57Yl^u8y((2&8ICj;Zf zym=ISlHkW4N38aP?YObN`!|KO!zIei>r?%R}-eYiUf99EA zyD(>kykRXg6}EczgpUWOCd*xrCP5UHOYZt;DD1q>W{; zz`{IYL!}Qugl!(SJ;zLG!>nYQ)2G5M0e_>jSx*nV@ct#&8-C|et0xCexQXAj_4lT= zTuHRd#a^2cy*9@g4X^My*1J}m|7MQVRh8+H?2VKu1QtFa7my{2>O|~EH0gv1oRX4m zIAjL;N3C>yxH6(6Az`Z|++kzot)!(B#*)#T%A8FpzK`26GfFH#a>(y(1R{z!s)I=C ze9kj|D@g!)rsS0C2YTj&6L-MToyyT&)_9^dh+-|>)sB+~Oje>tn22tVcIQ}Xpv^r3 zJ_=F*XO9LZyVDWQiOerdX`2@ZjZV~@A)~y8|ui2^~bIHEA@fjbB*g^om%^0p; zvp_Rg!rN!d%BokGjW2FFc>~cqG@>Lf-wm|qY+Y!;tHW-KdxPQmWi>lHD;qdRq2W6X zf?_u#TA+hPvRrr%&ILKfXkp{(JaM~F#yamxYb$J%_%3*>r-a823Kv z)Lx?)*UWM@(rl;9*<~_5W)W^$8Q|Pk&yv_CqOsUQYV26So;PC9TxIc%)7`yP8AheR zH+v1b;9IcKk0>O3d;d1@TI{TKESD$}L4AE6s>yjHzQww9FqxxfJPK~h4KeE)Ko^_&Fr3XXIm0@5ir&A4Bw4 zY0}Tw|8J=NpNK4X`2;qO=DsK%b{HM@{XyORGLv*V`u{In&ugB?b^aJb`Y~K9_4B`n z@^R=wWs)CNy*Z}GI^$udBf6h{QT}s)Z-PN8pU898AB%UXQFl3K{*?EN;c~`C2V<6JG}VxjDxS{R65FaYs)XLY-OKOB%Jc4-yzwI83B)`AY6{ z7IG3@k31d>_0vZmc%Dr1Jg`X(*G-MSjk&uc9#ToPRK+t@W*Aa&D>BD>Eg0@IfG(N( z`*_L9^ffG*nsMkmb!R4dGWCXXAGQ4_GwFh%y&4Fcgj;;%0X!!s7KOkon)Y6v^D}B+uA!%avKjj0&@v=Gf^ey`z%eiTFre; zIeR<9FY{a_CTG<_HwnevY&Qluo)X?*k(Hh9`~I&romiB`w!8NtNX!&FG+MuCD0aCG zzo9(8?6N=Mz+~0J=4X~pek&9tL_d}I)KhrdGo9Q)PRhC zM4*E|6kfX0B+srIDJ!CIx9fAhleRa;M|3C3$850I?%RAhs@NC-PXt}2g_l*8c*8Nz zH5XY7s_Hy{H5w^s0O)tHl~G6K3P=&nYcAE?alT3iSX;Pi_0Op($=S+lAO017R*-Dt zR_N%9CaRouJL^Q6AQN_1S*%FQo|@gYP~iKE zO$aHBkEImupgnU$D&KF{Y0vx@&&d+Kqu~16G%96v%si6+*^IKXm~T}dZ9PrS+7eF4 z+WKuA{iAsispnJwA!K^G^Mm+;YiZsJ>tD?DwTpy3(d!L%0E%>F&8fe02t>-I)&DMY!E=CrlO z7uUa^PR>~LOTaEbS~R@q!oH>Wfnf)fooH=X8#v)m9Rx=RtP%*li;F38R-NIY%pbdZNKaLe-+Ue=F561KwZ z{X^3rJn)U$OAKpmQ@!^6F_V#X1tuH)rfAh?Dmwb4~zoz zr6Rd~dX}g@rriRJ^Nn5ip;!K4*AgI~EBlSaxg0tlSNEcP@Ha{z3{D{((pEz_=1j|F zk!!6&L~nolFs7gQ^0VM&z$O0Z`V=AJk@w#Pg>f-Lsgd5P01R|wR`uA)ln2sF*!se5=(!t8ZMU@3 zUDl#`k_VGYy6`UOMVk4B`t??ol}P^7hG-twBVihJO8M4F?Q#is#wMxk)H6%?$OL6paTAo|jECywx+0nXbhRn@^D`C>AvR~DY1adZ#5eMZ zk~WE7(5wpeAmcp}G_j(F@ufJzwh`~`<85l1uc|@x)C6JD#Cd8ov-yu4-zZHV*2gIVUtd7S+K<9_dUDXn%$g*dr>HdRBE$;RG+)JnJpi zh-ZP#zkeN73eBfsrNto!PlHT?o)McqmV3xR5d2Z2@1pn%l`2u4DwP1kBY|AJ{XE+J z{68oeNbx`MFX7_)PTZiA-KIiKzb)pa_2j$Q7V;kmufKNdJPPYG-~HA%L8kJZ?wXN6W&NdS z^dDZ29`HPt_agnq^RVlLLDqz~aP`SQWUQ&*no>g%I-i5Rh$2sZr!?#xB3@()S^m5U zrvbozywzvoJI!mv4#sQx`wm#vIjkENtbst{Az|ZU*bVJ7Gfj-0*wTJz(iJ!&mNnL{ zvh;eNSGR5w+SEwd^9JJiM3N9272KZUH9T};$k?X_h*>-w@{mKbmgEDP0m2(XtNcEK zcR?MQSdw~uITWG-Bi)~eyizEg>^rUO*JyUNev^_v zmHmSJA<)!SG%fj5yu;Klg7FGh&Kb849Y&mzC~r`)^sdK*B>f2se|x+Cm1dwhkMx{A z{dLq$>}Rc{Pi~&A5|9tl4fY8-&G(ABk?A$yDQnW%wJ2_=O!cEk-taG9XoGVtGDWIZ|*$z&k&BI_fKM$9Y?ZHD-*Vbz9iV2USjhpE_HiE^Zhf9bR4`! zv%i<8$iFds&JT0eUc{^kDz7r3X-mJ2Ze7L66&{x}U>E*xM|Ta>pgxGcD1a*WG@)?@ zZLL(9f)kRdtvOZCa#z1ny%s%@!b+5WnJ7zfmCrtyi>5XIAW_5my9FrwSdt**lk_t} zwJ=$C0{~^?AN*=Kba6 zu*p<8igA$XPu$ut4aKJWbs!`T=c4nSt*dmo1E;ikRs<@Dqo|PMAPFq>DD<&qDCRae z{P2Ynae@rSH+Ltm)lkA0SZzwyzbE&vUwRTPeT#Qvt!4-t8NDCq+_+o-QM2@S6ZlP& zoBWmYb!pbuc+5uQdh(mM{5>5%-_%tSu1#5mLS-twq2mA-vP7#<9Lyr^Lsb09s2mAPpTtq!o1XY(dT z)|Uofe;muw2DnEdUD_jv2WGzG4>KqCFE{5EoCd^i@oEblpEA|p9s_x1&8dUO%*lm@ zen6Z-!AQX+Ps@4dQt@>!pvSz8C??!6^^%f5nl|Dy15~pp_hh9X@6!ieGfEPdl+qEQ zO65z+*$49jz=>;vUgO!(ZK%053o2KE*eARzXrSpol@jTCWMit( zI)$NVfByHmEgfrvk$$$a=wN=2EGSrr)tNg`nWNg8JhlbI1_!JIE}w{Vmc#n+PEh+BD#Rt z2hPaz<(5fHCsy@S#}leQiOxLY_xZRtYNT7Lw0{(TjwlWqEdKgNLx$fV^w~qkhq;fX z1R0+Gzro}uob}APp*a?_sNc%VT!h1149#3PpfXagmg+a1ctwt+HvKdA{1JcOk6b&!_Y{N{ z*}pmED467I;1xlnYGj<8CoK)scWCU8g1|qulz$IeLQpSBNS$5O=BWHAPts32B{nz1 zv_qrMPav~h`ICO0^4^*7l^w`nQ@fb(cSc7kmZcc4r7t4RWWD5fX4D&}S2!O|v=x7r zrH0L(2tPk9J)pSj<%rt&A3X+5Bavq=-*K;4Bbnhi$?_+<=8b^!H!4i4f1G!I+l>xA z;im1#_rmo*5xicB@tT1|YPEK)S}k-n*#t)$@cC z()EG8%cT`Sq7$yE6U~2h&#nrOLI?2V>M51bf`Dlak(n0Z0GVfd{Yit|lR#JgQPkoa zSoB=b$lTZb+Q2>-Dg=6pfAcwTUG4@)V$C|!{4S914x^oc3J+e0ftyTcK1v6YQ(}iu5E!i-v83S zDy&84RL0vEF_zZOQ=y-jQ=gN_H9mpHje++v^Ci@@@+v4Lv!41tGV$*h?Dgbs`+pcc z@(_#G&uf1n!Fe8mOxSx(Men{6-;fZUus&bsZ;g>RF?B>iWn#^HkpN3)g8hkT{$-!+r152>Z{Zf|z0*I1#ZWf0qColW4JBAXlr}6zB0=P#@2Z$o`%g_#(4tsmmAJk+EyCwhef?0R zMGNWrZ$nwYs#@d=NIk`hJ1mLp6p&Ey>VtPnwN0}-W&5AEjW4!ta7 zr-oEg#^IMg0<6L=cR$B8KFRtdvhIv|+-*o6`cLL4MA6F;CgocDMxj>HuJ)~dZCAxd z0N!rc;y6|VBdzDE@>1|!5qqSrc1-+A+4r-8S|bT$NxO z+f})xGpik|i)5dgBw~w20lHJUz-jwjx&;bDsYYKYGxg0P2|QQi zSPuVO`abO&F3~WE*JJz*0?~-Ram;_0n6-A*kbnKFK3l89YGf)J(+21>j{VwhcHgK+ zx;12$2a7NGX^q#g{r`~m{%_<^f{>2Ac`J=b>~A6NR`ByT!Qe@Tfyy4wzlc>Yn1j^P zZT^7^jkDCSGWhSL;&)85eMPkv#rWUuL2a_}i}T+n7XM6%W>(1lm!|WtnSbL0&vnj! zmEXhB;+v7zQ}F3i+FnL4Vn8lF?LU$OpKs^M!`{B@tRau0_liN&Nwg8B?|N~_W5{^! zbD1(3^}dMeC2##&^)IW#w0hU!?+L9x)l*W&MUH=*=8;8m?0=(@ zV;7x18I2h2ZMebKfIGv^*2H3wMt>ZI3(3ObU4y|D64Wb(tHFhlqrH#*)-Ai_g%n zs3Go4YXj1){vkfRH!>P#kin=2XyxxpY)Y4*E0yc(N=n2&jD5p?fku3pmlMW3}cINTQ^E zUagb{0R0IQrXZR+3KO8!M8i`X)SIk%!h^2ev+wpsJafgE;MDd{G-vQBBVJ%x)eOi9 zo9$=e%l=y8^`N9ysSEeVDN=EmAtzeE+hK}qmUx)wdE;+7DAH^8emOk~?H6NK_+t0$ z-H$qqXwUv6fLEh-j|K_lk8`~`=J%CNMJa0fCuN-A)WeK9(3?%8&jq8paNcsu?B5}7 zp_54)zRgNMG($RmnMB^C{c=i{Xs5jRr0*6~;Ef_Gi48{G262y}@Uu6Us%rj2$-yUR zXJt*wUsP%|MrVJ7a#WfVH^8Z|!iXzKeLiJh6u}>H)CWZ6HT?6FGza#l-;5uUnkgcFMtOI3=#R zbl2U0jJ%PcFSzS zC?JH4cHxRQWC58KkW_G|Jg3K~l<@O?V>gcuA;iPrR2b$tr&6Eda^DVTtF(1^O~Mt1 z|0WOOdhW+KTmKM1?UOPr@vAmZSH+ zdr^{l%A@|$W{Q_w*8**9>x*qd>Tx1;^z-4)H(8mCEJ)SS#Mr~LKCSxf?QW|{scQs= z&h<2^=~0Xw_;oc43Uaq=u~vT8BSSZPi0a%yt>^;g72FieJb3h%bzIi` z)5%k`sm_J+uEbvQK$i;onm}A_#SZ?KzJ+`%@Q>CQ`GX9~D6RAQb#-bn{LzLz1dizQ zN6lkBP=NBpLB$Cqzl<-5IfPFcQl|Lm;I&9(o&HT()GSYhD%<(hIiki})Il4`g2>yx z9(moExQ|*9cDlW6-Wj{tWfGwX9A6L%kO6EfdWY8qwV_7cz48*S`k)d$@QH!JmUMOx z^cf#2oq;#l&v=~hM@Fx~h|Jo&7#)^lPuep~aC#;q{f{fe9o50ay!Lt3r=wd{A<*bN zj5Vt$vsMAhi2OMX-6Mfo&Qk;jj18x-7#)jmZWg3yfz9qVe9f@q-IHaFyvkUfw!eU} z(jPFS1g>JUd9I0-2%l3N*=7ZUst$7Okk$v90bG8k61u3|oe66vrrS&5Q@}!CW{tMr zmT`An*Qpp>QZo(l@j=xmi*pgu@sua55~yB1@E zMhu^hQS2PZ)sJS{Dw?k}#d{i$j%J?XBS2Yu2R+aYZB(jI_SyBU!Ng>Z7S~M-ClVVV zX1F&uNT!ajgWqH9z(UfNFGK4hb5Qlk>BBSJEYjg~CxmZ;&g!YIcFk*{G@+f0s7^VN ztEO=^ryzqXN9nb6sKGoU4zlOfGpny%8)TNe?`A>s{OqM{-^!&Ch$^Nu(nkf6!KdCP zdIo68hGhY2q^kJ>c4`{YYk^qQ_bd#jO~l0v)M%^}n+fi=(d-6iT}3t(j=NSt0<6WX z3_5&T2Xof8kDkmP`>x|04UAfkHedvxVC{2#DxkX}3GLd%b3fz-$xNrjenRnK>I!HD z0q9XUBJ-pYsP494`NH}gH7IpFF&L|QbCKx8nH;Ni(p0m!v(;|p4OhB4KoD`=1-M)W z?q}ali>IF@w_l_Sn-il3OTNp{G0#)XCqxP>SfMtV?mQbtq0qWfI@hr`+P4yy!lEJw ze4PhaLVfNL;_HIP=Q|ZCU=6hKK-k!u>TMw;H*Rmj^Y-&=9_pg65jzts;<_0gotJ!) z;)St|+j!#?R4HJ1EPOtuD=C@FTu733uyOGIx(D%=PC~J%5wS65-OfiQX5j>W1XaUk zsT-f)3A|-FW*A%WK9x)|SV14U|1HxFwSLLNLUD%{@$9_7=?40&Mi30Bgt59n@ zL%&WV?E=6*3?BGj-2|GT5QnVnXGIcT-YZ7F2S;OKp+?!)AaP_HYJm^M$5Qf12mpRm zufNS-jk3S)0(rc@u{O>i3RHq@yd#`Op^wrnQ$MRwF*e_+xg|?gCAl#uTVWi5AHlAM z+FJ&E>+B9#Hd(n)sBX>hvj#hO0N~8>v~O)1@p2o1+Pm2R=-lz6XfC@Mcy z<>q(x*?bMD?K(BX4qDCpWAAH8buudcnDIFH%-0Had>LKyW7FY)kx_a083Kl}TJw7U03Df1%ywa~5X8N_NliW2f96T6GF-1-#P% zO&T;Bm|LtD;n#{ltsAk|X}QWJ>M$v7er^7y36f3h_zVLA;Nzxw&9P|>M`!m>?yHe|E#Tdf6|`tGYQu$XMc|NPnJ#9n=j$2a8*PS5(AC{t3}GdAA9%y zt|3N5i}YkMj5Qm(wm)xo|6N18(q!FaJ<#4Xx$J6@?Azx19R2WF3-+3!go%8LS;z+)Aoun@>s{!mE=?S_In6eyFFhZcq*IuxqJfwQOiuEc@jl z*OJd_?pfEu8i+JSnri;8AkH70zgd1tVgjg@E5*Ki<(ppmv1rj%li4>7tcbC|a!6EK zT2x$`-BqVfZAa33m_jW1wF&B$NZAPITOPlWm|i^ z^xm0&=6TZ?WgSJ&u;4Z76_dOZJ;5+eZr=L5lpI6_ip1_^GABe>nAg8B{XwIXdi1EFXB01?_7x=9s1l2>2VbMU^c2amw)1_a@RL=Ehe>Am$D!V+lP~@f-L1 z(uXGy%C{4_{Z6gRZ{9D>GKLpjos>p2EWepBGQjQ-A(WIXFf5>>T$-kgq;>_K@a?4G zbYE;so+-g(p4-j&4f$pGg(L8epy@;XDH%fpD+1bapw|*6*>&4`F!ycY1-{{m&Bftm zhW_DKe7djzSBWu~pQ1lAF-N{_8mAh^23p@mm*`rN-XdBMp4Vq!1S1EE7u6#wks-l1 zCUi#4(XNxs&2A&VX8m%Av~a6RlmP;(p^O!%rNnL^u{%xatH`Jj)JxarkglBmWy|qi z*P;BiXnn82WL3#1YE83F#GX!L#43}hz^J*zSY1?>A^de&JsIYIWi6hneH)dw~N=o-hy(G z+@lkPjmp&w(ikN_2;NGa`YIn$}OgW3aq(IQv*8;}()j zLPJP%iE||uiXDhP&{8FF^RQhpF=c2Ao8ave9ge(B$|l*`WV(L*-D$lhtvQ!01D3BA zA;TtE2U}=U+mbDGjL%4GmT{zCVQq+aN?7b~7&_V%*~Ck3Qa}E&#fTu_XxZe1ExQOD zrST>d9L4#1_X4)i;V}!G@8YxQn*dc>29$JwWPnZptF2mA%wviDNoN=l><4#=Mh?z5 zy$fcJqRp2C&)VaSPZv~%COZqiX3?W$1G?-)*~TJRHENvFSoEzTKkdDsXZ0>if1#pz zMns~4r71b3#;hst9LZYnJ{)hViFaLg3v?sHR%(a=izp>L)6hyvD~c$URQQfA?J1J+~9qn=V)plDdA6$|=MCjDwUrM;)*b%_!~2xY6Lo7&>k zYiG~`us(9sHgut5Dr=vSpva@da9x@8oL29$)PK0$_=qg4bz0Buhh889Wm(sL<;NoQ znT-+k`(eV8L)QCz6Ctbt21}*N#YB0f!X?TPEnpHujSDTe35fngj~FC^Q_qJ@_(;Qz zfg~&W6&Yxhe=&)Z^~0=oJjkWu`Iy@g!7u&g;35548D=L#mbTphwRiBx@W*N$`pYs{ zPVOv4%{b@})7~2vWi@lYSD#%W7`z1{4W08Fnj^VOk$QzzjeOFzzF=Rmp`}&Go1xHS z{Iuo?aGYO)Z$jg!FUB)P;Jr3dN4UUoP3MUbX4pNb`@Oh>djxA>yk{;nKQ(|4q~)RR zpC7}FFuCz(|EP7Pc_T9WN9VcGA7MI9M=V8`XF9g++^u!SsgMN~BZfMm)b4L*Yn)PX&Tf&eTwH|E8$%@r!Y!#4-w!Tn|W~01^6JWh#VI>eGg&tNXnQ< z+F408r8QekMT38zYNX$3P590J!97N(@Mk|gGb)@FH`SO!qC`4FT@bvR?0OYPi9;Mw|vp4qBVp zS(`H=?)Zc>GnsD|H?!m!PbDQi3W0Vq3Qj%*kJ%{Z* zi9BazOLzGcZa069ICW#OUs91d=S0jUbp-=M{k)rZ)#ae$_>`1CHc2cnlGWoV?Kh`Q zU~hDjb}PC+%&xw({X`DHPFb7EeY{_ldr(z!P*rf?>}v?18X%2#1-D9xzZcO=D^<~a z9;nvVKSkgoO+eCI02~i!mRxixF<{LrnS}C1J=GWX$x842SC@bu2{SDPK_R@V4fF%NDuu@TV zwvzYKwybBW>S^zlELr)Jnq`y`u!C1)=-$kqy@~)6)$hz_!_geSK~170Tx8 znaMx@P&aL|@7M@zDR5f`8_h237M`r!47v-h$mzgixh-gRW}+10`*>>c5|x z8pg0aTj4X+Zv!PAwPPP@2EhD+Id$%SvDjP9BFCyf_PfGPJ?%rgZen0VM z)?8|KL%Eq&TRWqqkD!@$WxJwpbIN?dvouR%5>PzoP%_wJGhc=#&OZx3)7$m&iv;%Z zG}8)9Uo?g{&`!E2(hya+KRNzBTFjT1R90G6`Z3?`lboSHn|B$PbA(+4w}Gp{ne@w9 zzuEh1P`wR3g|^Y8x5s5|zJ{1VZ=Z-WzV}%%#gWoBL$?w(=T=~wtn$Sc7+t9SgmW)@ zCtL2gqMO<6qC)aw#DsRA^JvO&JNNikTU^9J87LVDDx|T%j65g?@dx(-Ws>UL{%r2Q zpZsTw_3k+P z%I&^fhi=6=Uxhz@+wVg({J5a@#?lU)odo^??C`*7$?(rv?mFHp*)35Frv2J0TKEz= zP8FWr3O#-fc|918AuF?CS-SHPLV=%8JIdWl!B-LpqQH!TMmzN4_ew;oPk{wDYUexc zhi*&sy-}1y;Id;Yg&IsLOujF<`SU~6HW1m03=44?OZ^_?T-{V|+V-O}DZZ@hnvqPq zMT1egTWgBBn$eEEni$hDr z?Tp63(=J6PrSH|$4FRinvw#_u8SDeHkZG;g%(hP~-{_*Rw+OHkgLe++2%<+!PLK?Ku{8jE6j69tY)cdxzUqG5w)44m#~;Kb;&N%&Csuud>*;#A%npt7rD*mZ1Hr_-gK|SfJI2)mI%1!VeMnM<;D?GayY4G0 z80GB|>%T2G*ri>kWms{+cg0VLytplF5TZ0nR#-A$GT-Ed3&I7Pme4o0K15fa=lSLt zlt2Y4e!(-e4vzP`u!nx%HBU?>hrhWA|JIRqYvSEzF7n@m!!R*C4!~GAIJ6`u^h2%$wQr=Ni>8m zNNtf_mR;Jx6nx{w-mW$tK>l?|RKUMOl7T4z~*t3F?T(Xiu`&2H)22EUO)>^a@I5QqDG3=#}*>S|;> zmL@x272J&$H@L+Lh%!cR>*RUg=K_>Ay)naq9a;bcv^{A{2tR=Mm`hMhyQSF3*Ff-lkpc|Wo{ zOO9KtyFnN5bV=ghwsC*Na`qmlTo3tP(QWt^i7jCcIg_Q1IhF}vWxenVc)jZjGbBKA zay%N!Tlf}$J=m7m_Xr<1;5a_dxH7QnOam7vD@xWBro)AyVk_tpsbcu3Onkq^X_zVd^5GsHj&WSNjsVsJNGz zBkeZXh!F2Na2;qzM2(yoV zyJ844UmAUqW#zk^m6E4l( zP-IN5d88dy)>C1GpSakotVV7(v@N{QejMk@IEg*dPVdI%ti9W|lzifNDA7IM;%tail?41z@`|zv_MU~OHnD8ZQW+>pK&R}cq;MewWhD39VqZ2V{ z(HKI?HP4ZLC<5{x@_b=XfLF$%`ff(uq7@P8 zlru7Yw5DKxj-H-MaTy6;9kaPN#-b~b+XfzchmPdRoz4*CHWzu9Y;d$hjtZ{++Q@~u z%}dS@hbd?D4V9y3dfOo!3~fWdKAPGzgBy2SAVAJCC_k)p9fqDLcTbwG zY;AVGYi~8^b0uJOWmsfxHRvGc>@;jk1%ZEXwRe-debJduF@BE=c#Jv~;2BCW_9!Qy z?P{`+h?GTl7ee2lHi-W;*7I}k#CjSY`NV_UU36K z*{F*xZFZwy&ocG$AIe#z!dQ z42WWEn>}T*kbA!(*89~nmf@C>jj&861gpbKq*QL^IUayjuAIy}UNGCk-#A@|7gn2H zLr^GNNU*`MNSwJpl=f>PC1IIjKDbehE6~dV-^?l5*yOI?RoDs^Rm;cU09LVBN3=bO z@;mlhk(EL7d~HgiD-BI?k(}cVo?~2rGMfuaZKsErVRCMCsW~?-{d9i*L;5kt8RD+I zc)xfUc3kQFtvIg+2pmcfX2XbxE+N#7u7IkwQr;M${%)(``Ra{l1NL&l9%)jnK2Dhb znPw;xP3Tv(8qh1y6BP-N16%ig;swTR|sUt%jvkq>2kW&i`ZD&|H!u? z)$^gu*33+2Urb59;qG9Xr%@ABRgm?;=*ox{*NA??xr8U}b>N-&YpCB@k5Q9cj*-=x z%DL>mpibYWABXuES3)0YxuCbT&G-&_pXf4B|7XkVtUy3cUd>p04h&?(16P#99rAQ6 zQQ|%vU=GtNd{#P2>tv97CIj)9rN^JpvmhEt$5X57v5`2SADk2p)YzeNhYW5+3pe;9 zu(SI1Mb&psuXP#DZ}x=Yv)&O-Ld;>?x^HV~N$$EUj@k8S-TiKB(^eXSnwZ1l`{ED! znptu3e5VOW&(Ejz111N+1nZT?w>Fq}gjBt`LkC$m_;bT8TVdOB(9$o!&A zFz;-jxj~c+K_(2Xa1|>W60j!90!@q+xNZ$Gx@9Ym>P>_4vI2mygwpN?4Vg2@2UMBl zxwAj-iSZltjry>HQkLL8sN9r#m4+ptzE0?-!o?j?u4@UBPm+s69*$tinw>&0h>dAw zHB2I$vsiWuXnp~X@?61I8l@21W_(&>x^t~heY=*|GbsG#9Oq6dk^SNvZ7i^s^?|hw z$XJa@J4-pZ#bIPll{-lQoM;H!PJh z_KBRv9z_?pXO{qs!Ox?=G|NCASnY$T!gYPOqN=lkp^xefn`{Q*(z{jBP(z*=N9W|DxtSmv>cweG~+X$GN869_w^5}rk7w}d<4Psw#)-yYsUJ|ag4{%8(uald&BU&92`icg*@ zFxcs+T+r-=y5SRn)x=77bo0%{md9k;^5>Fkf9#8EOb#uTjqUr0rAq9%wgq_XiO>Om z@pMt$o>xh&r1m8grxXGnds?o^AuaGHMi!opO&!5^Z%nfq&cp|~6nC5@>1KUw+L+JB zU4hdak?sl(YVo_EMRyg0m4l@zb-P_=H`2Rqa?_g4OP_4xAdBoB7nfxL@NmJk+FKti zDRDJ}D!Q!(EY7vpY_i9w z5dbXOfA6r*mwJ&IgC)#44frLOV>syvywu+mgZL-O*1pqm&<@sS9h1zZngpwd@g$}4y--vtSz`zXJ37mwp3 zK^7TFe$Hl*?t`F>4GT!>Gs={^GpjJ;F3r3vD$T_q3vW%&X4%=Dxy71j8dn$x#A{Hy z_)oF9n32$k?9Z-?a9$I7NBiI#cY3V#qkCC5Jormt@<8kQz`JeSKW6$!w;5b*6!0Co zAm4z0l4TB_7}uXyJfq=n&Jqfjv(Nkg8v6>MIGSi(oDBpE1eX9m7M#Uh0wiG}NN~3$ zxVyUq4Gf)gATd3#^Ici(@j?yL8zx@)?+=bSm+b9!c~r_c9Y zI5uoL8FPB(7a}2gGM`nhy;4NHHGVxZc+oQp{|ElySO9u%g7)3hx#X_YZs0^7KV>{W zC8;I&ZMIb;mV!k*Th02EDz973H4iWa6ennRKu2B7$#>ThvFv2&G>P4JpWz#Tz+%UxnzzEVP_x!`r3P!cFIp zm7ZvXB;gfd!N-7Sw1Rana|O>dNBQc6goV%87W)T<_3XB+MHdGf^~|j#a>W9sS^An6 z2UT3gV)#W?Vsez@JGQ|A)>eI!>&=0q+&u$pBwq*m)!JJypq<4Qy?T;T-HaZkrQIAFbzb4}t_NGYSuScuyE$#$Lc6&uN~NOP z*tz-Q^)hjfc11UVkdaaZX3wP=YGs1NIa0Gxsz3Kz1+X9H_0qJ&Rjq^BKNVf(Rb z6!I=?RI?}XeCwZ$IQz-*Ql0Uo2GCB8LKD9=#$dYdBPc_ivo+0N=0h2Qv6ACYx`xt` zpP0sS34bufWf`WGFNnn7GR!dEMG(%Edb}~>yo)_}6U;CBoa`O{+aJAVt{s$2O0kB0 zWWL3RJk^@F0vxzQX|!C`846qKVj#kdnXSAt9awWUxO@6B%8HXIfDsiwqLj41y$7# zM`YQ3&Bi{T6Szciz|5D){C2*lg-S~vid?v_lgLa=gd^@$?^$cQq4OuWNp}wq=%<+l z7pbSsGK&n)E94?L@f2AvqO7BQ@#ed67N1xD_FBaDUq_=2D{{e5D~#UDjD=-JV=rPD znj0R*tkgUosg+6W|0#-_v7y!c%zFb}QmN<@m#<(tcv31&4Hc@bI6GXK>_k=?B$x?j zi#R&}#Q*DuQ2kfoNw8mJ<7W@jfe!EyEyb&}x422vL%Un#x7f)6@92fxD(*{Dctt?XWG8Yg9}^G_yFU4G{%Aq_?E8hS@oGcbJK{k>}1ewl;ktAVY)LY2N`AZ zzpZxr%N+5gG+~(qiph944CO$>+jfdn>iji0DD|H5Ht{SzK=&OnSn<)A<5i39tx{(x z2EfV~rhcroR5@8v-mq_%>5#>V(B+3%;Lm>eC~|7e++tdsb!k|(RK}^AuMLr=E@?f`RuFEI&T7n_r|6meLSex0TnnD)xG(Mnn)$+FxL6yq$P+1iP|IS$ z*_++l@#Bz*=%+z>`o4ur?ERZLos9AqMN``JI-Z~v)u8ga5C`5@aKc{h-u7h7ZJXCL zZ`D#}W&l_(}9( zv~n8zg1zHy^>j{sfplr`GmfQLweJ;wIHTh5Tr>1|bmG|hARC^fP!H1TeO7rPRghf{ zyVX!D>&YdE{vfAWB&~&Y*O#?4L3RF26^sY!{S30o$pJJbJ6cP?OuYCs*!h9Stearz zbhoN|)ZRae94g7)g<+4uNMYjW6g~p-Zt6um7Rv3IKiG{lwBek#uUve7^;N@^=%(p7JW+(31ptM(`${9xw%5 z>6=J9Z&lBVdRCOHZ_Z%}X{e%5>xTBdw11YA{oE%M8Vl}1tU))N&kr;m zq6w!6=k8MJ66yN624AaZ<@Wg2ptg|J`nBrU!5=PTU7G_xapi>t0uP0}3c~}nRtV3O zv8PC2@o`o4w3Nrs$!#fpgZvuQ=&;sPfem}4kgYd(ewp-D z%717s-Ub%Y7duZEU4FQv41~~EI8SX}{A!g?S|Gh}^y_#`eXLlmLd;5B8v12C=B)lg z%tw!R&V(4f6|p%#3A^62^$UHpI`vy}FgdschLjv>7^r@j1lA`CPbxuV9_0=9tVu0- z>qCU%zL72`Y@$G1*4{76S*hLRF&TT1<@5==c5|M+Zx?e5%VpIax;#sJt?iB6MzPD~ zLx&JlTeuG%wNBL|qeeRiW3@kPQ8Xa6skRL>ti>y-R~}0#9_jgI^UWyC(ueYd^2XMs zzwyFUrn=5X9iEr9bEUe$O-gFlV*=p#}{3lmdh!VQ$vux+idtC%dBQC@^!kvXWlq~IW4_5j!*4S1&O{`k8Qin*Cif;Of?wTSfRErf z3dYAk-u*(4u)>T$692%Q6a*-e;EzB4!PnXpIn@E1B?TcVt2oWrkrLTO{+P<%Wd4_0 zSAmpB>911lv6hm#mI0hdu`ds_-TYN+C=9%}D}(&>6C@pdgZ`pb(5=;Y(oa%X{Z{)FY~z+b)J;`p%rmRdvWW~!Pnp`a1!q*@zJ7(`jWI*O{eCDxSY z4;l+=nCl;W%!%0bk@oriu7krqp2TjN!x6Q{_5^Nd9}qEosK|8Ki`SwpqsD|E-my4NV5ooK!#Kf)yF__( zOdK7=x7h2#1ri|1eRd97CGQa+y=7ao)x)JR{~S9?xaf#@+CW>ojKOTF`u)#Bsw?q= z)~VgvO5*nKHaaAF^@sMa2YBuMvs$Bwz?S@&XH{%#>5|d;N~>|MIUc5RpBDqo|U)B%zCLt$l4 zDYn{%bd&2crZ-arCvmCk!SgFmaerSn~idH(7^a@2lJr1G5jfXiIwYaIh@5^pJI zN~Mq%r<;$`T0jQyI06Oka2Tj+qmOvO8Uz+@4~a-7LdPY zB;6-nwL#VKTUvUc)-M;>8Y&gII#{szetfV%lo`4iFFi!{&>=PA+Au~^wfR6fwx>kz z4TRqU4HtXKP2#saIGZ|qM_Z6*Li0h99sYSzcbwNN{k8scOZbk5Yx5$aM75vDK}He^ z-t0Xq4hN2n4blE5)`ll_zn~E3lUnDK(?jWP*pWn%Zb5A^uc(_cY*hv}z3Z??X;UwK zDrP2-9TLR%2QkQDy71S&G4RD;FwF<57F?*%8Qt(&ZeWj?gkxsMO&(prJJsUKsho!sTW{P;i-M8P;D?ff=7m>n z0vxv9Li^^Fg=Qu%1uJK{tL5sB(r#22hD^u zXuCNPmDXa^EP{iEALF<5R{Lu!(eQAlRcWUT3FtwVqE)GmWVYQ|oD^e{2$l@JJN&;Y`+ZT~fQ0K%9mLUZY6MS^_~tX8kqvD8g-8^bSgdesSFc>? z(p%Fsb&l&J#-%&Ef^_r#N-QH!mXU$u(`}VK$egRw`0G62re&pTE*yGaXfK+#VN&|b zm~rvAX}(Iff-U4I$Yz&q(4ds`nEKJVS}{nbged)C~;Ou{9yZ=kX_YbUoIjb#tHW zxcyuY<|GZJ3Eof*h>V}_^?W5gd=Sd*INy33Z$<;YzU7Uff%Q{{%aAc2nUIUPq$$d~5{~nu?0_T&xGUScIri#y?8ezYi;dMij~9Kw1|sX#d%9)!AKy z7)TDbJW%akg_i5u{v}3U0pLc%^Ymc*5vG=aG(%lJEomEy zWAo0TkivZ^QnIh*?Pn{dI`>}j##;<{KqAuFDkm6~EOLUuybI^KrkgOQHJH=gqqgCp znZJbCl5_2xvBQaKG64LVU8igmmIvZl3>W;*Y6lKVJ2^@noA!r!KsJ2Y*l$X3};Q)(Ur z(CRkip@`SUo5_KPE6_=!(Nbd@eA<*i8)7eM8v=8USqa&v10V1&b?F_JJ}0 zn?S7><{02sCCV{S6M)omXBd#$54r}Vj*n`Slu!eum_Rr{DQ1u-_&qa-0PLrjnLV1O zmLN=zec ze~)^SlrRDxm_a;1OePRB7)L2n68wc^2Lp_wnAt!2g9$_m#!=1;09PgPsb*2VFvrWJ zqBh6Nq9Qks%cP<=&*ld*bqm_;L;>6xfy~Sxe6UkI9|b?~b$7khP6@!BA6Uc;;s?qk z@MZ7;S-a~kc6I>n96(EEkUiKbmM?=}i^P0M04UvkACcusQbGW1Qp)ri1;y|kWn~SH zZp%C-;(B${FXA$Et)Cv#nTJs6(YTi{Lpl4fu6pD|pDVBH<%Xn-(%Ed#8ijmz;i&pO zcSD9^IB2Fat!4W~Br7XT4eJvD_}z66c6QoqU3Ln(eXWkqJwPf+Z{R9*T20_Rzt2pW zyBB0WSaL_Bfp;X09R&@jc_nSWX{&ZEUm5XSaYuZLZ*Z5_2)|4>y%3Kh4Rr_&Nt(Y_V4<| z!bc8Ye}NoQC!ag6Zg+Tj4|26RS!1>lS`)Pevz!3392b)A(~WEEUfSGiv~5~Bwvk$M zw>?@1wq;m*wwYQ3_TZSo(r4Gg=yr%@Wu~Sb747R5KA#gxEO?4 z6e8jVM!JHdUBCk_U}ZP(fIAr16`Yz2ThE0(=E4edVF#j+O;N~#8@PIRlqs|M@eeX5 zR&=hT&Gp3b5sr-Iw&sK>^lx*CNwll@R2zN!`crxCKZw7-ES0NOoGeSxVPRYS_}yah zqnR#k{h38FewcznVBrI2*72yZ>&gq+@tp6TDOs2=@2hDvruZep zKg9iHVj~34q5u1dKi={Dvq2trCO)dFL2@QPu<_dfclqf>7DhDs*of_KwHd!tP-x!h zNcT~sk(tyt-@K!a?)qPxTNX9e9o^fFza>%n;gIS2QAppbk=X9x?)Q|>0L;uCcho^2 z*V(lUqwtV&vF0jFQU+9wZ7EmITn?6*{pJM8UgP3H84DI1WyzKg^46Qf59N7qO&6>f zw;IeQ?ds`9OssrimLx-a9os-^$>oX2<_?|oE9_UUq;8>@%SCGG^qaEl(?FJXcax^~ z1p=qFXAYq(%M~W+CfLMgh)<#Kma89I9`G1sSpS3tMGi&gJ z3G7;FEl|7DN~%7t%xRZL@Be(S&8xkm_60}SS*LAJa2~xmBny=DZh$y>Sc4n=W6Nui zDJI#Vs%PVgsIUC1+x#PO2Hf5cRUa{v=d4wUcYcgw^pRVpz$_l+Ow0$_s6*zM=M0R| z&2~P5@01pFh4PV=G0KYSTGG4=R0#!T!utZKs0pp3x$W}sR(VeimU0LRiXB~E)i3SQ zvokfP;CM|-+(L+02GoxH(wy&I$xcmt|A0?-9eF%`z zWy8(iZE58=)^F`Q6S?~N9VQ^?%UG%_6>dR+75>e;MnQcJ zPStwwkf{QzOw)W!f%eFz&0Kathx9D!Rg1sVC7B!F=GwH#S}JcS3WCj2CTii^VV3>` zrfZr8IaP+0OoN_(lIHFQ8OYh!p#Lfwt55Kn2@(el>30cLi=8v%=?pBjAq?(VVc6T=r4qQbas`Tb6iJ+uhX!+VW;%lzOPR2L zdtGq4aF=txaBuu5{gfO$D)X}Jn^H3A7A|*qque`n@(cVZtfKH|UG;1EW^y(E=ut_& ztR!m0f`?0Wg|5AIkq=MYTN2l$*wbkZKdvJ6`$k_lus`G%m0v`$Z|&C*q~knW{U^__ zTDmN0Kb4>NE6GTrP+~(&11fkuz5lKsXF#d%wMBX@RV}O*ySA@(yLPU&)bhYG$FjQ5 ztA90tE`~0V4mi=TVZg-=ILpKHI=n{+}M( z+Zu4h;my9S-d*@Z!8Kam`18NT`eFZ`1G)bnO=Slgqik*esh(_qN%Y@yrD5|w4UTLT z9w#hDd3~A(`>}eN{pR6++cfLxs`g)XBtZVJ>My(g-}Xkf@{HdHb^pIl{Gapwzx4U; z3Td^RYE*QJ#{OEzpFIdZpds zB=bWxJCEi8G5)EbT2KuLL;jVo%=>XjE(=8%1{#sTD+&Yo-P4>qxq5~g1A>s#6aLAi zdxGN=!g`MTs9#w~CmEnsHqE>FgY!UdUK^^tpVo~7zHkZ*MDfQ7I6W7$mkV|9fEEsO zEuhU319yWmXq;t0dOZ;@j^FqJT~cfz4Q7W8_&lri>gGh~s_L`f^kDkE)tIsimR*nQ zrO#q#9CT_h0)sv3YVzo?bn^7}I1;t`*-ef7k-W5_BoEu8;dKQ`t8;wt?q!`riUzG>m30%fJI3+Epu{Fbct?3RUHWz6nGArYIZABR} zHVpy@t~WK9hQBk_>Q%lt=(V><68)!X=;16=vpm%9=lhSrLTY0=DsvFK9PvM?1xFOf z;>SU=9H&b79|0+O-F@u4XB~$>nWj+fiYN5jxNtHyI8c}na#_v@tE$0 zXWwMWE48%F&<%P@Zc|~jErQaZcOi~M6)PUbTzbj13d{pr5y2QQCc~)Eg9T3=o-UC!KKXKBqZ)V z>7(t_IU42}6tDf;=o4!U?ll%mm&1d#3ofydg-Q0wnxxqULvPlpeQsp*Uz@9(H}}!( zBrHB3+qq2og5#Px`hAuqn1npP+6tuNjDUQp64FqNP}$6;$-I+BbA|*ZzWZ{Queh~T zW)&`C{Pa7NpxVx5B76LojeZ3>6`1>$eoBGgAK&rbWFP}=Au>Zx*hiN(Na&A=hn7bS zaXWWewtL)r#fx-kucd@DHD*P6~gnX%XSJ!6L;cqm`vI2XbAPry9JwLGv{#d_YiZ6jqqWh#vzc5}SoznK`tLRPt z&UQuI(Yvx}I7(*tmtw=Smd5mNwokzZ&l7zxxzyIg-`&Zc$H2$H`q8HbXfpj@hO>-m z=LZB5TB`gYaxe0!_^HJCY#q7{RjjyQd35qymN{hLm)K4Vjtg?uiBq-@i+>fK|J)9{ z;m^-koMj(^0dC>_7XdEjlX1dWj$y*}EPrS{iZjAE!v*|=n#JEv=Ey<6F?76q4#1hL zGJC@p_DCN8Txt4vtz-@CCs77?{mEIK>f^M(uthy@COxS1I@}Zs`t6~Mxr_Ni?i9Yp zFHS7^Qc{rtcLIB2s&MUg4g2mNAE`wp<*=hJuRF;_y2BFQFz+zv%+y-Pn$(?ukIYSS z;(=tIG9uHxDnxCc$@mRoZ|6=Py|uVgFB!$7^jdO7t%+3Aw2GJFy=h@Jac8HoZk+6g zIW0xdhnbl-+4)t$jMJA~QYy3a*L%n}bRBxUy8D?Zz}JX=;02hM02a756xe8nzhfAEuaoRn zu@%q`{cIr9$1i`Gkb1_@3=$ij$4q~cGDY>^I`NMz=D2@^c)`r zrQ!Giq=CrK{9Y)(s{%lq@RN zyl7(btAziYBzwy?8Zqk1O5J7}VT;*CP@@xsps8Z%6Qv;wuIa4VR^@Wg`k1P2SHji@ zSJMR!S3(+hYOw&hmc#NrNkaGXJ8d-KNMXo#&RsK0QJs zBS2ZI#w2t{Vt)@+N=dNVu0M><8P$3n{g zt%d^jb#)0~Hl&zwsRzdDadozdM^-M1}sB2tlC zD74OdS^8v#;iN5@ExFs;?abSlze!CDUkV0$&a`xc!Na>}gH%*m1Dc!L6lZ1krIM`B zw1I8t*0bC6dxIe657}p?XKD93`CkzneAq8J3ME<6H9PUrbokRtrnQCA3(G60%S(pq zX@PT8Pd@(K+Jn?#rfs_N*MC6Ctc`;=C-$q&-U)QJ`7_rP}jX71uMd|g{>ma z2tV}ZF;%@$#_L7h@V=i~RReF2Pa_@lg+Lv|F1n$V9*glu`4SX!28}IfInZLa^*R6P}Cf;(u$KtBbHyN!4*~=!+98ra( ztoRZI-5jkyU?yGs)|^~$mo{r^%Vf{a3%_h5;ZPIS3~Y7sSVi<%7wHK4W(1|gInn|k zROy&QYCod)x(2$iy?d*Ac;bHhF4(1a@HBp5_5d*9XWU+4-sWBEEz-Wr zeMTQa)PeB%&?D1Mu!?gt-&q|svx>WldzyD$c+D8*J&B~8LXIMzM}E{Sv~km9D1M59 zVrnP9ob7wXjz7#tzDyS1FNoA9sepZm$|R`-(D&?L0U77}G1ek@UHof+jHYC4QeP9g1 zDIGC_b|R|ZC(*)|E8*tLMN#d)D<{yWcA513zyu=v2g1H;pEtg8LCSM-Az#kFEfGy% z0kDvShiwC$Bkm2sKD(Ice+h9`yEbTpC&~GFEGi_n9!kY(nUyw8So|)nfRK}Dw|~k< oBq2~_!KT*p!Px+wyn}^cA3NbX*UCD0UE$OdIzDnw9}Vq)0Hp{x4gdfE literal 0 HcmV?d00001 diff --git a/influxframework/public/css/fonts/inter/inter_blackitalic.woff2 b/influxframework/public/css/fonts/inter/inter_blackitalic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..b3f326735221a808198a3d1340859c6037109209 GIT binary patch literal 108564 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 literal 0 HcmV?d00001 diff --git a/influxframework/public/css/fonts/inter/inter_bold.woff b/influxframework/public/css/fonts/inter/inter_bold.woff new file mode 100644 index 0000000000000000000000000000000000000000..80f70f058e9a201053d29834df248c5e43002ace GIT binary patch literal 143100 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⁣IGj8_ox2u7(^UPb&yZ70CeU5kUb4LD)7<8H>ZAOz8`gh`B zix$t+=)En~*?HETzJ0e7f^+VrG@Nd0pVhG8WP7{T(7WfUzI{*iRQ^65nAUK>fQD(Z zd&pa66jzqLr7Enf!T3Cuy`^Ll06KHUxQ~dTJ~;r>nYqxJ9gO1%vJAcC%V>HdQJ(Sz z(mAJIzFNYcWscxa_+xlc=neMFo*eu*^=Il!hyy$FFD|dMeBwDqgu+-t!9SmZL8nYu z<-I@tV{@22-~bLqi{dea*2SOoe!k=;;}iHZ`FBDlKuYr;wgI<+TZ05rnm%Y3LG0KD zniCXB{EY|-lJpy~u1b+)GxC_m*_%vqpeBJ5=|U5Aq0A{R%vJGViqmjP93Yn4z;b4y zvaKRE0v5g`Sc-9j$s~trLTT#mG*x#dkajxfu?-Un?sUK;@C6OOkuZ7!E}P)ek9+#rsprS1Wa`z0WV8Hp3}**k)-1_eQjhq_plAi z)8_#Wa`;hZDzk;_NGWz>))NhjTnzJ_cQW%xBx$Y^9+HwqkSpDKbeAilS&c-50`*|< zmrXQm1|EnnG7JYlVJP;w%cP0UOqw*Go}Pb~-K<160EsF@3lIg5@oN|g#YqY*-ep=c zZKc(sv%IZPCeQCD@q;uYXjVrsQ>hZ82wscgBPl+TnNQyWFOi!d@^zJML%@On?90-c zXBCZFmhDrS83s+6_P8A$s>8!@I}(2ea^rU?@d0#TARoSeu196|8*3a>QB#N+CQXMK zFwhAG!VGCT?NVzCR(!hI&x`QT2pX1d>2NBoIS^y4&afRzB#^fp>4yeGQ|OO(-?O`i zcjFq6q9o_ftI0tZNEU=auG{?7+=?aGy^L{kjHc4B10;DYI~Wg#>LfK($HVhFKnmVK zC>(+q{=6m@bfyfmpAY(m7vQsa!T0ab0!l&)1n4p9#IMr) zkTVcSi6}&w1r4P4Wf_MC(6#~NP*(cXpcCT=trRl^@j&VML&g&ifN&Vjct4b$z|cl;0ha)4I1m2- zhl{~Q{252#C_?u=a^)BBiPSt&NZ}G_*_JJS^9u~QqYccv4kL9AGa9!a;AlGqzwI#C z(T+aYO=Iuj4UjT*QHO^8=XGd2FmKhO4vqTBzs!YrjxUa^&DUG1=1rgmRRd!k2dzsn zb_VHlDLf}m%A%z?0ct=ayb|xiE1{7w$6TCK(MkS?W)X6e9UBbvvM)Ef8BeeHvC=~$ z;V<~qa9}PrU(uj+a?vG$P&E|)xSF=OZA^{Gr2*I!4v2=O7G0=>U23$XwV_(nl7RPM z3+~lZ4B_{29==sABg+2@GycCA0<;}4e~arbFBIy2%MfEJ1TSn4f z6I2Vr_fNE3&^V@Y$*LWlm{9PBh`O24B|G=V&+wIcjbkF(4Uqd5bVCRc?2(`ugM3jA zY1BK=4gLm2#+s28=!Xy}@B|r0%5yD2R-+&M8;XoaJ`{Pxu>1!NCddTjhtA6g*zzCv zF9eyWAizN!f0{o+kVynNOAs5jHh-AgMv%$oR7b%}5ctysnL@Z9A#ohg6aJ9kO^~VP zIP1ZWe=leVGL0bD2yz9c3$c8Af=ovhXq@ZJ2|iC#h9EOYoKv#lp5Zimxq7sdQQWu; zn!Qd;B7Rod*e`^u*B*O8OZk$j1bl#Q(q)x9S-C?cb8|}I15gP8I!A8@MHc1;#R_mY z6@HY$;&I&SIQEQ#=cyvX2(QR%%&maN3?fwDNyqpM;VROpDb>`J!tqpiAB%M(;k5y~ zWZ=db*v$a1M`BL{p(b3FjChOj65c591%hO@7}RAr1ZWP|R8vN{LOUVzy|Htc7)S#b zd}fscp2sz1yBQ}isnR+GP>vm}d8}ML0y<4_V4y+1+AGjaBRfkoYtr%TOz4FVZUWz6 zux$)&n&L4~aT9(RLb}jlx8AN@`}P(L!vmCGnA9Hh8FDl;`FNt`)u!z@9&zY4+!Z`U zFOp4v6iE+Mm)G=4mT*#*znom08J-I!Gr8arOn^L-b8&X!f_2KZ?}E-h*k91)uZ^&| zEAfN71Nz?uzm+S&?{5G8ck%s|%fMz4EW+QGEydp!Ed-lgeIYArxQlIasr1V=`h~{# zE>d04#)R~RyZ=hK65qeu|H>k;S-K3Y7bz6JE_%j19^5@4+Xp}W`4fg2N*IdCJs3Gb zy{G6)auJ9sDdultn=mg-%4-lvo{iGOS>#O66Jj`4Id$4qS-4cKdGw}zh8uAk&;Qyg zFz_j)z}(w+pv9+_{{Bz!2Hg7Gdl~-~P6;knd1%z2JESJm-zM(u2p> z*{t;L$6a(ido`Nt1w{gP{1`LuMMvM|;17ccr9t4gd^x_4hupc1hv0k5p?2@fpqqab zVz&3bg5S@V7yg z&e)v(pYzI&^EgU;-bP>S%tI*KF|F3PW;V`ax;2>Tg>MAz&Ko`d|2enwuxWPh3qxvV z)*4j1`_hbBL(lh~Z9@O5pApFL_xf4#5B1Z(K;@+M$)b+k!S`1!2kV9Tb#oC|&+7|~ zyAO2MZS6HJpiJ-ZI$CXPc%T3KiaDiwk4d+nO?gAfiim+Qjjzy>t~}DCJ6U;O2Sk%E z>IJG7{8^2EvnqS~-faW?y}o;^yfzkY`oM$f>-i{C;|q&%Q|6)U|0?;yLfo9GHfN5s z2QlcL#zUwhYLN}~Usg%_v#dP+j?Q0UqbJeH1??*Mo2TN+Np(ECvG2XsAl&V{r^=hQ zO{+*VCQWfVBt%w>PG!d{uFXXtGGc zpbjC${DXV~MH|QdDJ^;c)4OL>kXKN=jg3R!w3dAl3q>@(Vp-b#>W~M0=?m&VvkqjB zAHBdH>kGd;agHm1hY5K=yhq-<%mO;$G*YJEi{Qdi>Q;7sy=1AcCMPV$|r{(L4 zeE(g-vde9(a_SiR5kN( z?JhAVV zq`NF!oLyP#Q4dCpcra@8gW+{y z<381`w6=MCwA#w`V+V$F!4VbKO_;NyYD$x;6`jWKJahmj#to_v0W;If7%D{h zBL-~Potjca3_H5(g&MMCS5GopW5CQ(kqC-AxX=GIY4T_BH$g9HD%6za>lhW%pjW5;157hnPcJx$4*}a}b@y$a=OrPZO`wK;+QGvEwT}ys|fMe=@qv0N;mat z3O#Vez~|S3o{#XeE$moYB> zi67(jP>(tC2s}Ic6ec5~N6b$)=T~(Rt-<$p@5T34AqLgO&3Sntz8p%BCt5MT_v)~8 zg|EPI)wDa4x8|^lkB+*w4KcxiL2?JDagEXB;jwm&+twXoZQI2FtpZv%4Dxbm*Ed3( z5$JgYKU}>VOWg}WZ~I<7YEQJa=~4nO4sP2hsE|wR{xM{Gjr(qJU%mN57H;pi9G{Eo z;OM&oqQkrLT`$Zj6x>d5}X5L@;u;=I>t1?<^n2I&(H1XQtRJr5g zj`g}$wqp}^cANZY$hgnOV%arkc~mUlzUB;xmc46AQNX0YLSf3)=@5Z3jhnZ3e56u5K6c>COAF zDal=L9~jvZUq7%3e;wuKdKqHZT8(O7e{RjtI7jW=!- zv18IYtkqnflja)WQY5h9>^1n=+V%MP>glPT0WQJeNwZgj=lZo!XpJ8z4AVc+q@=eO|PK0!jjRj7Vr*&c6Ap-yua$BR~qPmrd- zeZY4OzqGS>{D*O;DCvVzdqb-KnH6 zU|kz9#M$215YHQj4;VNbX*}QdCt~(OvfTHW_6wu<%BTDvW3UfNlu^+;{*}YM zXDq5hmcttjtyu4Bi}-d9QobeOzlX*T;_qtVtgdqwH;Z`pBGjt~lx=T8=1QL~>y z(IhMVq~V=5Ci{fp2RrJujcsq0^2Yx_=P z*pc%V4j4Lb76LS%WPcyYzPp@xl5<3uS8#iUj?2UI3gXWWb)FJuM(P0NwA9*_}<_+cMo*0!! z43~{>y4wX!9kl68SQz+jYSgBLjhG6>aDr>CR&9s6I@fI7W(bF-mFNOCz>$Aeadq|Z z*|=fotoefmEnH0a%_I3ZMDpQ=e3YD+SC=ey>2u~c2Gj22!w}EqKWB|Ro8fCGB{RCV z&C0j;2iKDLBR&rk?9$R2j3&7%^kwn;;T@mvOwsmkQmm5mr0vY$dTn~jt6&^rKWS$v z8&CYro3xaD3W9mh3_B#~!Z3Vs`1GLZHeOFASMKha=@97|A0_uA&y3I5LeHwNUEv}= z<*Z^N%gVKo>8f!RZOk4ce(LU0MQ_SlnR(s3;0ifY^zv0J@UgDdtl=p8LbiTWhHpPn z#Cz`L&W+lZw6^tt@b!9pAgk3-SEt0b*?n{_wVAFpEq=^i55B&3(W{%c8ewZwbZfWqi@dwzi`jOGWoxBdx5c;GTh}U6rn6luSxGxH zTePg7l(luOl*sf+td?0tn$8TZSSuq%T4FB4=Y;d$hJUmSDYxRAJ1<=6+kup!UDf(^ zd&yq*{L^uW zi&Ob_*+ZS3Ne>AApT2<`zWU-Le)48h|9&gh4(cb5>e86O8V4$;uUSWQK&|$8tYwdv z_7*}5IXKtum$v;xsNCb#Z(YL1roklAcgjL@&8-+WM*+>6#W|7&>cN&!G$EDAj{~E!rU`bLI+k6zoi5?ewxLIJ{ zt{Z;GRjgdTjmU+^?}X4`$EkOQTztquC_k}swy23HyK(wpojb5+&z$FPyM$LT6!QwQ z4Jh-f)wINQ)114PEan?xt@mEoZR*+;2+$?=GuvJKn&0J7)hI_i%iH1&^f;;RK}YIX z758@zwYDjoPF6GNa_DCzQWL>avR&$PBIH)_FmH$7VZe)P3;q^S01A|%#=5xeQBM}?PW$v8|$}dg#dlz zkj6o>W%{l*qdGKV)q{COF0iFl8Oqyf+geGCGN1$Ay z6;y@F(&h^Hn69qccP_e0p`5!^gt?wKb-*$NXeyCoo+5|N0P-;-^1dBmYGI}hhrL7C z6uoqng$E&|&tPha-%mP-Uuyk4k+-7*@SaHMMpy>Xn^}w zA`&lu*?py-U#spN$=}xO6G(#Ma$FPoMq{$8a@k z*_ndm#f6a2(^~WIEj@bS_n6U+VJ2QPxA1^H{Tdh@6N{1pr#hID8*ce{6?@d>W;Sj+&)J~` z`>{JXzJXFV|zt1vPIH`Le zdp1Z4PcC9p%7GpN=!|gC27~F7Vv*Ok93yO&t3SUi=*?Aer?)1 zc|!2T@t3n4oECK%FwVtssd0GrFdMse>5V7aS~tq6-^13rUQ%i=YwJ|@WBTIOJ(fC( zy4kI#>`t4J(Q%?qbY9c9`JA-JsqqOZ4XRhJ%Q)7pRIXC3auut|J0OUl3F2caD1s>U zaz3-!-5}(T{oJTfN-487r+@7g;-7=xFZ$;j1EHG6m4@vqY*TK3yQ61)To!(v*m8lL zO-6dF1@3k&#m7DH+YdMKi!oOg7^1c<)YY#2aaX_nS)Y5Q)@atXLFFt2=n->^{i5yo zAE#gjqLLsHd=nS-^(tgj#ekpOK z^DGr_#}AVi_AOM%x^gM-=~R{X!&Ld13k#^N=-Z`zz%u8+xOK zQ{c`N4*q=la|rX0pZ__p`ezc)i^NMd$1_CBMqx4u`Fx7R@xd>dho8CKpMSNa{Z2*h zg>a5`%$DotktOl!5NKEupd>U1#j8tl6`?W!RH1kI??(VOnvvXdegqxPk%q}VFJep# z;YXNEh(X`E%^VgQn|=I9C8VFyA?&;&M!z6-P7t4IG~y(q(PT2s!Q zMdO@Nc`fF6O6RN@*@PFvDK3Q|=M)5AlY9wMmmufOaki3Fku=qX-v}I`$Una+^I??N zMGIaGMJ}0dSAUDj&B|M#TXBHFWrh*_FB2NTGZkZ7WBC``kZktnek@Z87J7%P>Ju`9@598*s{Rwj4Ot}bN%$#A{6Xbz8&LKisPvbl^Bd73Wt|UbsnURAePF0%f zu^G8V;zUy9i5a;`kOu5ff;=@NFGwms8t0iA$suvdQp(Ts>jptu(^N0ah$Qu4?KwMw zyfh>6QXeLaBCpKIQ)w_;i1Q-IYcmon4Q4u0!#ls=y)5~W?Bk++kZKf42oYAt) zbZ_qrcL)W0;2<17#$WJDeCPR!_3Ia{TC+}`;Z#J3S;NKCt(#!~wmT8Y8XoH^)iW`i$O{VrM_S)sS8eZuq`K(u~%T(bePHT&z~M ziJ@5L)RfFw^)g%8h<4Lkk6n)#IddGx#VaF<;D3!MJp7q%klA%^;l_RzFqqyDyb&C*I_ z`F@(mB46owWzm03ovm9uou)l@s5i5DRJr<b#EYnn~Rz5On*;7Ym&;%)WA^8>^OHxZARBh2$1BN@pTK4hXS#&2`2+kZ@qeFw#yv zIe19w%_dttePvvOSkn`t6-+BlAc~}-T8<(t%JXl72-tcp*WG!2=O#@Y-9Wo9=aGy3 z*fwp4Il19m`TT;c*TYR83P?e zyIENyH(sn=%8*dITznb0QmJ%Aonvq&(bl#nPA0aKi9NAxdty#(TTg7;wmq?J+fQs8 zU(R_?)%UZzR_(R=ZdUKLuluGxNm@KPS-zJn{eJgU5d^uEtxvymaK~@Ng40d^6rppO zGnZrQPJbO0GP82XMQ``7iiPvxZGvu4?sZrrjLTBk1CG2yv+cm5<;Md~SIqWBS|Xcm zsCUsACL0}zhfHbWTCo9VzPLGs;;4|KeE|rs`(cg$U5%Z)9xW9R=!S9Bmm1sSnJx#E zB}c6v%1HhG7w?azgfwr`5vC&vbW4lF>j>QxMz)RMI5MXuWOfJ}SQU%wGucNIv}vFf z^qL%@i?@J?Dqtr$lgvy=tk1!26^q4WN$S=RvQ>t6flLsJbtvv@MH7r##a0OWbpTVJ z^sBkp_?&u#OoB(S8EF72MW3x<6Go|bKchWIL5?C6UzC~}T646E^eiu|o^3`G0ePP; zWCe{;cPSVG41oI_3-WRIVGf3x;qU}&Ee zgkIO+-I3c|gDDe!Gds7&M%4L4zf$%kjQu?dFOI!y+@mJO}YsDAGy%h;ZWPhkU=3`<+C-^x3=hI}L zM`#U5s=`3n(?%5N{~W*VV06MlUnYO$JuhCdXV4KzLZ%8WRA1@E1K%w$H=H`(PIJ;d ze&Wrd$l?VlG@!gGh;Woi1wqK?uS;d}Ons*(d))nJ&QX=cn<{-PrQ6R;R9HgWtT@K3 zE&l1i2`EF@B#oZgDmI_wLtkZEFHHvpos}`hSK^DZ z6?pggf@ll9;e4D-_l8nvcv`@dS_Ua7qJ@nWtd-WKbF#m-u^xqHTRaR~oqme5u|hC= zPpmr6Z7N!uh78);ct57sG>jK&-6{vsSx;-TWxr}Ys-cZkABHwqXYCg@H%~RQo-JC_ zJKHx`C1tJz^DIfo*oY_28rp`mCDAN<|10x;^2SH0_#^36Qg#oObRD3=yARP^v()C? zuA`(0u(8RRb(ZUHQDHSdh z9BT6E%ai0OhBG|;n%~#L{`k?_Jqkwec{|w__|s!**dUZKZN_U}?ToD+Ue^(n3SXA^ zf=eUsH%rm3ZPnQ)yEaK$KUC_L=12bS@Wd{w*)v72JUsu+wjDtrrCGS|YH!XA3pzzA z`>%M@Z|_xtp&Z|Y7BqtySf8@PpC@Y?(&Au;WN-n^E%7%fq=J?kS$fpmwxfhXb?g9t zT+?DoZcrnhkUB#&4{}uP9aqOnj;P(iJ7^+E)!P(S@c~+6YSY%!;(yPRo73kf`rNfH z4O0lJA4Obgyytg>&*KG57EJ_c8YGr7ZMRWBAaC1QYX=gTErzc0ndtJks(xm<0q+xZG7a>y839* zOg4Y-60+y7GkQ2!i_@DPWb{ z-Cz&HvjTyJnVEt@UBg*jvD3g&P^MD7wUv_emIfipoTsAh{VTGuDoQ#YJjawVvWt|8E=Z5AvdTr~J4>5~?<>eOD z8C>(-atD2fm4aAl3fjE>Fjc-dtr zIlve=p7by2sxCg7fuIyuM>Bz4YNX;_g!_>C#{1bTID-vYNHe;5iS;SMem3|%B{~v& z;mPvEX1A*1pA*_;8#pTUL(x?2Y$DMzX!5@O1q4<^4bnNU3sYJx2>|qh!dhg9`EKqY zPh<=16Wqs|)vs}8LQixbL#k_-30iQ=$_Fw!moC;q+QNmPW{}f>M=r=YbaZ_nham`A zNeb+nuYgoKF9ChTAq2V6TSH8g$X|zP^|VKStJhbawDZ?OWZ1U~0ADfzdzT_xG)U{; z_`C=;DuenAQG9B!0idCN46f~a-bfgB5N)bj&C}s#v{}A8YL^ft8{K=3Cl9boB3CAF zGy$}Aa1c^{o{z%XdI@WF>VzPudW~k)=w4K=bqS+`4>^4@Z4nzB*f`y3;6mzg;U=2^ z4TW4Br^1`WyNq$P;X|ghh=ez9ns2kCCb<>}Y+SN)Fl%*s@ij z)67Z1K=M|l3>mtSLoGh)m-ZV#`7qx*^Sj7|gv7oNUqi_N2S|l-7i+}p#UfV=IV{3> zq2rSxsFdQjBLU_WNTl4ZB=usycxPC|xftb4@)PK{$@%!Wx(C}I2N!5nBqHCSVzTGT zW;gCC!h8b?Nrse5aV6IoT@z^aFHy~7_am&#;1@9~^W4$%B5e)SFxlVlGvo(lXc(Ka zWO*|xb?2YFBBN=r^BB{7;Nu>pY-Ixu;J%Ci4A4HCaa+>MvZzLbmOm3y?3uyQhf=cn z-6Y4xf9H9-EZeMab*<<$DXYa_iJXL@8J`j#-7e2xi4iXoNLp%JP)9Td%6&%dK?0RL zmk%Zplc#1l2`91q?WI(kf;QWbIgup0{R$BeLnJd!T0GVHTlumE9^LRX<($#KNHPX! z$vTH|W#c4fXulqM(IlOiMB4&#GPo48QeJigI95~UhZJc=oM>`7QSvre#4r_lbCR;a zXop0d;xUcw(L*iK76MFU=t|UPe%1_+7tzVph%67gzT<6Tcx5xbiG*hqtZ&FGh3GeX zGHk_juFTF-6^YJ+$VEqw5|7~*Vhe4Jd^mN1&b5I6WVxCn~aNN3Ar1~Q)*?xi^FjVXk#v0ZYdFRf_G;z8ZMy1K2&xTqV`Z% z69@{X6HNtN--R9B6v2Q5Eq(o{u@C7r{qut%y(6HB`pT9c)M*kf zlWe4;4SJ*Ik<*`D&y*L`E{&Q>a-*4?^+q2{uaf|j(jdK376oUl-r~o`F*3NeD^Sj+ z2feJRSh{ROGg8I$k;L;X#0kYVk(^r6s^pz6%tgBFiVYyO_n`LsQb(%unFP}s5I^wf ziqbntjGsN;6g4WVsVjCzM@|nDPBnT1(c$Xl+<`gG0NeHih(}Tz@>-^&H$=z36Mhm$ zftRgl3<#R~bFJ^O?+?Oqj&5ReS*VnLv5(-!mbZ&oj7G|=Q-h742;R*3C6!wxg5p)9 zCH3|Y17qU}kKzB5)Q;XY%KX+qlj=#QB{i-I=5^Fe!apA~Dhf#;P=j>+9Xxo)tM#{V zMaL>*y6pJ)zUHu9#x+^mx_oG0Ij}-l#jM0Gk|9Dp_D50z(sLX}!AEV!#JA((UI;&` z{@zY3>}e19qY9vaO!iySr)lt+3d;hgH|Jf)WuHv%t%hieT+|j z`5W9$TA?m`jNf6&ut98}XL0F#h#R(@Ad)o>HUCT-_FKzzu+`PkHc`I|*VT_EV@6Mz z^oi?_F+YG|ZB%jxTGF8}GccL;FY%7DyLIYST`T*gKvb_6Ls-Of~O^3RTdLKYvkU}?c~DVGCzs+i=!I%F#JlW?ICs!sq)Jo3vNjP=g!{N;X2aR3q<%x&X zWVOfVQy}`9$jF!A`XgoD$Hk)UnyO9W<1ixIcuLAk{HNO54>jbu*D$vF!w0T-$5uYO z%d4WY;TEY%*3WR7j`@jL-A*Gl9NbQ$4Tp6i->TvoO`~cgT4a&m`aWNt#lX4_upwK0 zu#l}dpLTrU{28Kd3?e_Wmh#)v@~*0wf~h;ZeL=`sf<1&&ZNpM_JeE}Dp;Q)ysp7>eyFlB}Px zEQ+zhMrA5d)x)%wu6D>NTYqNRm%O5_5|cm+M7)}<-+9IbLsx>(i3rQvVAi%0SXu7a zp-w;+aOsDY#o)#rkEcQQNLA>=^Tpsk`bj|UP}HuZgLo-2 z8Nes+V1gbS;8=Mph}^GW@wzvbe^f+-`XC`2Q5aUZ?CEn-;`D^%Ob{SJ&BvmW8!f6! zqncyeughn-1jCpcX@J20`YR`uilG0a5@QP)`~Z2tKTXV}#2DSDg#|cJ9ozb|&ONCG zEL72bK6O@8tmgVFt#FmBGo{G)lqH)QMg^?`OcbYVv3(;dT!O!oWu6qw;7_mFpRDA9 z{Ezzu(xXWg@WlAk`3dZSbO-1=7Mz5X5}nV$PC45XI*jR@zx4Yy{~H-6`G7ox5upZ2EI?C%(>=2F$i9-veVM@zhB zH?2v49_0??f44daJ`Bgv2f?%4ZqQxbnG%oD3KxQ9ke_;&Ze*FCginp1{MPECzG4Z) zEz+{^PL(`3r(w`FDOJ*;b!?doS)Og?^BJ`lZ3a~nE59&;sw$!T6~c6T$}>(g~-3=}lNe3MXZVm3i64*Czj%qO7itvAY*@82nTa)g;QHXO=9F zbIs<=oaTRm(3)JCYJm#-iW5!&+(joFH}O;)__O}lM;!sh#j5NM!6B&!nSYpMT7-J^jw^#H?Ta?B9JGAw&%6pWbEG zx&iwretld4l&o34aAgM~1ZGu~<{G>_vs=+qI`S^;(J~{gGCcp}1=WX<$49@UW zhBlCx#_nlto$VeNmj4kNM=uB*MZ673c{23CR%@nW@z_Hv1)E*f!5rHrtJn05N>5}D z(9vzGXUi9}p~wuAUwmK9lQKRt=)zk@EEg);`iPHzLm=E@=Pc8+_4;vjL6K$nV2m-v zys*sDE1hQz_M6vcw?iFVAshNZfHvF-fDr~=+u@w2h*~0~6Kb>W-F3UU=DA?hSI+;3 zsPdJfXrxjp{&l?a@qs<4DBZFM) z0kW|L(fa26?Y<>%8Ct_MGy*v$R_?|bFxhS|Tezg`;8@}wv&6x~7Ocep)jB6FN?Y`zi9Vm^G~($87Q zR%9KP8=XjsRQQLyW~ZrV5Q|P@wr5GI*eUfT(iwJ>arGhUo z-QJn|G7MQEcoye-_4kXhz+DQ+BNi8Xom-EQnM@~4y_x4WT6bon<2)y|l%kpt0(fpI z{Z~f`IYTwQ%%iimd-#-ij?t-N)jfE4jUXVJ!M*nQK3&A~?9NDO8* z=Y<(HVqIk+b4R}ByCmsI2U6YClx>t{Z&#)DNO!D@tm^~M#b#Lo*z`Az2K-755ZxF_Ni~Aq>h3R{{re|D1 zzcW1@AuVAL8lK;9p9IjJ5WnNew|Jn-_e^8bfyy7dJpABmD%t#|?IQa9Wv>=803?ca ze&Z5ym7%DD%7@aVCvCf?oXdLa=~V9@JLEgj!#H`9Z~CwqU`}7_VjmxjEduLbU*N)XFB`3 zNP6yaiGPs3Z6*XMuY7=A^jHUB@JU1!ibW%LY|TpiZVTAe(fLWQl(KS!p~YN)zQGezfe))h=Ge-9BFqmp=OfIe=~yml|T_s1L+J7xM?^Jz2*sW{aFjYEcRpfnC+&I$bo8j`rz%rH5Q@n)k9mC`=rb7!tHH2d%cmI zal%YuqSHK1?$5kNp(Z3Nfa0(19C$RTU@WI!p9mCa#;fHterapuxU@0#7Gxz1WAKk; zBk6NH;(0tq-8#S2j3=1Z(`8`AuC3c}5>NC?x!tpKYyvy+1UVICJX-m|pTn6#-R#4k zTOg|6kR}pHw}hJ>ixb)%S$Af*^bg1t@RgL+p%-D< z4w5SZgBd8^sla&kQNy1HTNmw3o^b@6Bnq+?1_CoEI~@PW_R222h~X>+LXuv;EhzBrZK z%Am}7`>f&9=)uP*&q6#~crnu{zNLN7LTDl(78uN0MO@oAtzgf1mrx|R10a+)_#j*c zTz%_dQ%uhAl=rt4{%M$ZI$Tg3e~K}5uySJjQP6+Yfr^Ok-UShbU;l*Aj$VDuiVs9? zAvC_-G!h~h`h4uK3?cCwKL6boKEIv{L^8vdM@oqNHMFv^vi*t|R3)Jv(*h%#mUkfI8tQ~5y2sFg1g;^MS$SpUi=)vGQwH>pKSwdz zM)1Ax@D1KM(ZvANHOvVqLf<%|-;+gXpy)L+9#K#k{2QQZ=xfJh1q9H8TWH;KOFyuW z&vv-tMC#rsN-(yLm3e;~C)6`?REhN~cp?g7Cu;8WME9B%VXl(CI{PrndhdFzrm3mNl#d~do56tl0EgffN)QROe^@$4jl9c zVrxgYV{5PV-KwU zA{&TF`$~x=KF{n9Of>x>7euTE>IuDmu}=P&dZH&Xiqh@kXQokng2=mIQ+j|t5%nKLdgx|Us(B%pB56rD1Gy>3D>iDOzTWh-y;UCskkiu zH}vQ`!S;tV^vTE9w{Q^O=jGG(A^rYS>wkqEugHHVJtY77zv}EiXbAsxpY~+6-~6{U zvVl9>17VLZApp$H-rgq)N?z-GN-U+#x}H$^7i&r3)0gy!jLVUpkcs`?|34ryquy_| zK3R=s^`2+8zUYbm6ZhW|{woHasa!!b08Y^Z;5;WXZR;h1xuadi=tuVDg-k=J@#Dpn z7B#@X*aqA1?xzCon((keSI%{9%`^VOmQ(-WH_oaY0qGE!~NGvuZ!DYnZ~f18yY z)q|*MkqrZuQ7|)xwj{KLxcHSE^^cfDrBRZooh3B@R(;|Z_ORTZ9P$B>c#nOb`+9YJ zcpZYMoY#$pIK3iae-|`ty1)ta$Ei$qfVpn$YI^Rz_zuY$P_%Bvou^v5el9<<&ISdS z(t)H9L}9!oN$QHg4{Jf0Ph9>v>^wHZE9jpGMzNecqMu^3_N_h26ZTcw{@EK0q)#vo zdSn%lrKt#o?z)=pC^M_XMgyXkGbHcY01JK+&}4*DdCG0a4vHqqcFvHG`LlSAu^yP~ z0LK3}fxe<6yGnOpx~>m3pi$7P5rC||>>mVwEz5I$sK#SjgU`n+IQ7#i4VN34oGmk-6H^Yky>B8{3MV zlVc2{&T>KW52H6pkNLq`xUA+en=pP1FMs&vy{Md+57E1wLTC4uWkq8W9)%0sz5B`4 z*#c9?+-YYWa>J~4?y&TrlXi5H6{05YR+--Z5{P`j0W=UlARUXc5t39-t)`|;{h*zTwGI`=QjYIJD!n; zTITQiV=b7dI;tR<1pDl$9l3X~HHuBa35h(7h|p5?;6sz)AuZfcdd~g!MS>2VLUv19 zRw+=kyMNJMgEh8JN*+mF>lWQHz{ccX%73%XEh<kwf~+r?(@p!W0Cw8hx6Z*S=EZGjMdrfXlG%iejP#~7DbG>+V7ZWp^pyzg6#-F3$t zMUrp&*Jo+*7Ae;NOW0_dp!nNvkqEQ)Fn8WS>TQJ}D1o6`y}7f@U-qze8LbX;rAYK% z^^~~87u2h~_N-`}kJprel|8M`o~o}?Ffqed4~{x;+0Zj4^Nx&ughChWUw@0fQdn#l zUye*r=q`Hb^hZWCFgHMXOlmSYsQ;LZ;91~^8@0b95vIu)c_Oa<`(Yv-M1Ay31Y}yEbJMpHbb+f8iTFk04>BV;-Wsb)Q)Em+u|JO+#+gx)&4M z*Lk40&y-~Rxh?6GAc8+b0n0bk$9s$Z`q?)Mj6;750mLE(>7S^hVr!Z&t!v$_pUNkf zO~mfu@fjqLtv_it1ui{ej1F`R9+Vulg^2XCHeDdSGJC1ircXc&;|RpOzCt1azF?g1 zWn91Q9UtHSd}tUIG2BZqsq>1f_GiE^Pu2O-hLji`=j{cd7&%S9fJuFuR(c;Z)8SiF z>vt>E4piQpjoQVQ6=#SFl#TF}>jm)SAQT*z`K_Iy**VMPn8^&A(lfTCzt{YD-KIKj zxL`a2zFY>TdB|~NIwwhBvC-M$LF$fb#I8ge&v7j_`0vo&YzvVg!!!8xxjJyw7ft$u zm&$ofpr%!RZa|e5R1mFPj7-13wO}!WZ)&!Hd$1I@*buCFd~>$1dFoy-o(`Gp*_O7s z0@sQ4nZRFSTTaEDtY_?b|dk1Fr|RLO9|S%n5IR* zjxSwbIvf=_x&DZ%dMJS3q2kDBn=7;st*S|xkA!Ysl!S}foh05k zr8w=w`*3^oFCFPJczi9Um+prkIgeBNM1l5z*taRdNK4|R0NklOju7T43Za-r0)dou z-0hscQ&*W>V1vtDagSO)wMnPIbxgm;jw^8@Bbj=&iv{E#i)pb1W4EmHB{%0@0U z_U{P}@gO-zT+VQ=X@bqMhj?Z~Q&*JJ?dwjE2l=(t-73PHWZQwDw11~@^Q-^fdZ#pr z{}d-0CW+FhBa=yk;!3{#)GK7h+8-YpMIQJ2#UQs3MyZ4mwSd@54ZGZnJ#%k9j-kR2 zG;Fui*(gfxlw|?$VZ81}ggL3bRii3i!Kt}luyHaqxH@>CM>e;FrX=0lr$G? zA)J7XAjcx*YqoORq1S2c{&sO~*s+Q<@)i1{Se~UwP$2ZbZ9iBYwDw_?b$!Be{TDU?jG+t z-D1?bUWeM9ZeNjzpFuP`u={$VR`;BMEhrW{Bx0iQcl6HbPYVxfG&m&CUIp^|jaA}j ztge7jL65u=qzE~r3+?%AvZ#MdzcjDa9%15Rk zU7VU?zjPAz8D~152gn?y)yyn}n}MF$nF&mWQ{-i$s#eBTEswR7fWHU}gd|wLR_{AK(*v(?uN6=880U0moS=hX(%VViom2&r#Ox~>b zSeHfWMJ|Wu=TroEWZ|W)1DcN{EYD25>*6}53APFBi!z;%92loPTuPOy64LI4h1PhU*$7vqVpev2 zSy3qhLdE$1np+S!g*3GYe#zNf4&T^!bimnl4r^$bK9UZ&k0c=^w=)@x&HtT{<7rn5 za=3MO%~;7x_v=Fs;!(KelG=&}S$ZeeUox1q361154-9^Ga)9?NIkCNoAy%8yR5!O= zFb#2#bc0$LSgo^Uu3!3R;ucvkJV^S>_m_E7i`uO~JG4@`PD0*x) zoO?3E<3u4o_bljP&N|uv4uxn%1pT8OLbscNz25Zt?4&Iof&km*hUgZMN8vmc8BIK7 zjA+~4TZQ?R82ZMe4Sc(M56}vtYqEytvs7q)Ga$yv(@*w6+gfBm|KwJE@Qn8|U=nLh7ON?d#S??H0A`)*s0efusNxG3bF+?=w6{%dfu)5Sb^wD9t8efW6- z+a6BW$I8K+n}0U+*Jrnm>3e60cE96hCBV5!5Ci@;Gr@`J$EG|Z7>G299tBEUGBtFm?pJp0oYbYbCl^y^lpC|KYf62wA`Wh*V2XD*c+ zHu!Y`B`V%8*Gmjjk#ab=bAa0fg4f?VW=sIrOxGl_T4^1H6}ZOA{z1vb1Z~JOW63$t zJ-0Bq=}#euxinLd$I9X~{B#WkJW1gY=QG&+`35~W05dY$5v8t>NaU;UGN||#P>x*N z4}U7?XoiX|qWNKpfjMY={UX@Hl^A3uq3tp-=sY3SA$bgP3A{1)y5s!RBzcOA_B3FD z;fMgP0}wC7R;QATcRkYu1t!V6%ExZZ7n4L(6>V{Ow#vga*(4}2V{@~ zx6&h)ZM7=kg+X^6c+g*4s8j>9_4ZGnNH2V238OoWB!@?E8<*n(u*?W%CJets=6 zu~dUu0X@5dP4<&(*(A*_$028rIc!gj?6|Pjh1_s6iLSRM(z6ua)Qm!IKl?d13g8SQNZ_+NLV{jUz^St>JZTG1f zN;PJB)f@XDJ^m&}3A3^{=f%9&%-a62ehmx~n(G*ZrehMr`!VMdea%LY68%WRw}F)? zFhuy)cux9h@^a481_9BG2WhHIM0oQ?PUP8Ht6==YWVz4?lLKK2F`m%lYvGhNWb9uQ zW2XIy3DHQA4Ds~9M2|Z?BO2dTmo*SJVp5<{{~>0@_aX|ACAePM6r_5|CI?f0b(tHs z^PjLh(t}#HvSwc_utZVqD}NpzqE%fKkEWUbVjK(QzE&MZfI@owfw74<3E{uay6t*f z*}#28UN&3W195B~OU4L-bRyvSLr z^{Z0J34Xv8!q(3N-y&75-e{zHVd*E#rP7VW0y6jnn^&o*3@G6^NY~{W;z3UCfdBp+ zbFngMC~j^pV9sDYGVS5Y9P^T~-ex2#^7c#!bN3@*QI_4_zqp}171w&(FEDz?D$&p1gCUrfHK^ycJkAmm%RE{Qk1B6rEzu0sI$y3tt3nRYr%-tuhxx1}S z8O}=IG2o_2%gN+VVkh>eakB=iVKjky4UAqFPX7&G88IdfiUQKp-$<}2OUMmNi@u|x7ojZ ze^z!M79k1h1oj}A|4=1qF=r`?>gZ-&rjWAGtK(4)c?j`YrV$|YI%~%$c?xlN0#?m)9^PGprYTr45g;5GMyxc_${$#7vW%(kxQ%zR`K>29CyQYw3s& zdPCU?gY1rQ2#ZJS`|fX4@?^q$ZSR#91~QFXNh3^6aU=aSbKW8dirqn*Q{nh>har-mD$N7imTpEvrkRK2`bS5vF`XJ@P}_u6kb%#9P3!bKyU zR+wHRCSve$lVJS-+_Z44jX<+Wz3c8l%yjxG_01Vzk1= zKq<0Tx80aw`Hg#r)wQ^J7ArNzLL$^uqZ)e{7&v{=>cf~sLFwfx9X85?5Wr|Xs)uD0 z$xTSF<+6=BD|BYb&3khkSy9*~P`fX@C0XQ}Ib@W<+b~I(f8X;;7~lhM%q#;fku-)s zDbwS)GV9Rr*@U`H5x10pIV8)7KPvV@87P1~so3s^DkL>sI?)-3mF2OMkNVChk#Gj> zv-4-;2}9QtQNDNCVm;YUU;Xprh*!QOZKb9CbClq~n;??FzOPf^OT*C>jXF3Z3NLj| zafW=smL=vbTx}QX?Pf9R`bTw%04{_7MJIZdi_6{5)COzrEe`j`%E=5Hwj--p`~)<`@uEcZm&yGD5s04daqFa!7$4dms`AXGmNPxjbu)m^V~cg zU$PYHgjRe~B6a*I=~I`1hj)mVJEUyqizRB*Y(ncMnJZmBJ4YpQO9?1psq4+MR2gfz z7a~i`6n&Or!^y1WX2@6(3H#0vBo0b4{kH`C9t#yb#&*sJr+c`9%r_M&H8(F{b1+x7hJEh9Z;kLQVrb38_g(x6#TT`%{u6o_^UY{&Br zS{3LBcUntUy4^AgD)z1Y9c;#$jkKr=k}X`;%hl#ZxP^Tc@sp-vXjN!Bm4oN`q7|t! z-Ir22_OWIBC<9lma>W@U&tOWf6QdL{srY|JkCy!)&xawOEgA8YWKO?58}xc%`9)=P zLHUROsI=cd z7&;gv8I>)j;<6|YVf$HW!9iUGJu)MvZH8R0U8|uT#mY%}aS%VQLtoOHNgLJ+-%?fF zN=rue-788)>{b-p5I;6!@Md=fNxQq=?UcXm*-`^Dv_9J`LuHDzgAZaOrSicJJV1yro@w zH)5!HD={|Pt%ae#GA0cs27{S+P&~1zZbYO-jwK#c_+eYO#Lpkaat2|b!>rh*)173f zR6&Hm#Y*+P9YNO)EX|f!wOf-D=3ADFOawT>YxQGECDk4I zYuqLk2`Nn0qM>d5RrI>;U+CMM#Jk*fp_~pWTE=8-lF)n^KqKzl`d_>A8Ek%k?`5n9 zp|{C8*On_SWlMGU<;^b5Em6yLsa2jG`>&uTZ!UoRH_jX|B*Uu+RtjB;2911D%u5~u znobG#dqD+ZBS&E)vkLTF=6~GW%5r1Z3L*kk#D0KilPo36m}s=|ZW4m-pm$X)YY!MA zlf<~2<+@ATH%5~w_7MqmzoSM$^4_T9pgRC8(xnduM*p~1W;w*h%28wf-FDI3>kUt{ z85|gW7M^cdA9pSVxRV|u=>udP6N?IG%Bbo;vwvt_N2O{ru6r?Hz|!Feyc7F#N)Sq# z`5!{2VY1|A#KJL-+1r4UKD*P7@TfNCP3buTXWsu6tqoqBbRmO8E-e`Pgv*{NrF-=g z2`9fB!NykLZa~$-(iEcSmSttgjsFNMqjH5$&BN!u8luret7l7#>|}i?Od)Ok^*)k> zANQk!m2Oq_@cnFzF_9N`3mHLU{*<76W`kE~9_2hEXH~4;(T3;Kw+E}nJX-vdJBX35 zFE2_1B>iwFwdTh=0!Z70&0e;hr7^^DXiG}}s_E`|aR(1Lc|l{U>rv8p=rL$(P%oFJ z&U?OzjhczeaL^RP)EgIH=K;RY5>u{WkFx}TMDGSO+H(C)&nP}=W2Zl%Y#(;bmRV7p|9D$F`X0EyAhyJtMoucPTnVN+8Qq1y;Hoc^KBhF zEdWf}8Jz@Ij7hbQ$lxPAL{l!C;M_qR7_YC^>Yj*B}9H#s`=m)Ps>m5apiUpQ-S46wiJMKbal=OT&SgCIb}u%&_36`8j| z;VXq&kLB=#s$h;OA%T>#95d&N$I%bcrnT(Q{TM`mbDYRlb=5l{uimvXKrX2)3-bhK zcmzhs+S|-Cua7s$^K;{XXRe>0FF9P`X5u1(ItWPMSVdJrX0Yp=eb&~r@|2UtJmWt^ zdnoY#-q(}#&V9+y2fVbLINwsJ)BC>k|B`Vc|6(00P)!WMdXW@me0c-=h<*qds?P^> zYrV5I2Ttj7OiI6;K~!{J?{5PRux>+yv3m9I0F~GitN^XF?r!==V~jTog4b83P%p^0 zSClD3Qj+fM1zX!c+KeURPw}5FXt5L~pjSs%{VjDbsEmH>+kbHSJ4dz5^r_t*q*m<5 zQtmv#t8|695^1Nt<>taB%^at6|fn#7Ym+kh_0! z@m+f$M8)j=cFS4z@$!tJNK9qt4yQR^JQKOnd+R*h3`J=n7#>Lluy^-4$*tqUc)jUi zLlZ4MtKz{PROyu+5&9E20XQE}o`Oypez;0lZVYY}%L^quKB{&W(ndg4%+A2k_f70H zT1-N+R?ZnnfE{FX{L{JNCh46>tpQLCo-*BO$$f(+&;zDlShYY)#Lb;2w%*KIVfs(Dbg>+M)Z9LPo_G(;qmfea zVOzb93&2*>D4UZg7FAXC##*@?dSXVW)lgaG$y&ZlYJT58E~G+6LaHpwM5AJn41#zq zk0jYl8eipfVLCof`!ysc)fK1)43MEZLoRz@uCfj(WQ%2H4m`Ay0Emn4< zXeT@mjupNaC38}R0sBs}uB)W2XE3gHKbp|s8tJdV*q7C@Js|6R<*D1tqAjx}c7>t3-Ck%62 zV*6)czEuINd;@o~%qTupth@5jstUcuqCoJ0)MClA7wf<*cIRk)L1@)0wHSx_v=`+^ z+F1HUgMNr4e+&8=HTLYpOSd=Q_5R={z92!=nM5mWm2!#AbIC`wI_WZ|q&ZRRCKT2) zbudyO>vR4|j!VC7^|sxVRD&eO_!I_Vy?J`v@UFg2SZNE&L%9VqSnr(2Bz#?%g9bEZ zHHn=6xj<1186#H~ElEKg4+-fO5bK#%^SL<#>J!C|qu66~#HFsaCfuehqvhJ!bSRc- ztvAnE35Xosw&WGp>U%grlzgJcO}M_J!=||^)*sY50g0BCwAm)BI(|+IZbr|@Cw4RC z#6c)~ac}ZeB@|h&D@I?2v7%8s)g|i^)p9%%eNY={uUr76oiVTeFW@?71cPgdH>%wt<>0<6nTrb?)G`fx0tNcI5)5(k#htFK-)CNsXicypFZgHE@E#Ce@27cwylIf?hTk^u ztc&!IvKxBPS?^rkK|?5kVLK-!(s}!52;id-V;yWcun(3=K^Zlo;@!6s3lhp!*p=}% zT)L!}ST1HQ|8pLuKy+>GE{mJO9qZZ#Wy2;u>hQXWkr-NT2_7{l&pOsDDKl$w*WP0w z*nOK5ggRoyn#NtS-rD3GO(Wx~zNq_CiUeG&B#-cNhm0~}`z4w_1RXrtbBGYo9 zVoqUSkP6j?Z|K0@!ojeM6oI+6i{xdzxRd87<|3}&COh=P7-(KwiQju(Ex>0Y<}rvE zMoFc>7W6VdUz=g~Pl%zxu&I4r_uXPJUGdw3C%Nj;qtERqfa8d(D|-6*ML;7om34aR z+&R)N9M$Q*H0ewsff_E zp;0$pe`( zdo&$pZ8v!aaJhQd6L{av)p-9BCtM%u!CIWV0Wx~e9s`Ri*9_8k+A^a5)uH&~<{ah~MS`UAmW!FAQf7 zk>5?Edrio1;j-}|Q(U^p`sO7X?=vN(v#cKiG#oCoXN4>DMoGD^%)eSR->K0ce+`d` zOBh$97ewy0GE@tSELy04$u&d$eB&n8jb;ZyoLeSyId4{}I(GOoXS?cBPSrXl6XPX# z@Q$Kv)M#BA$(MDIpE*4F!!E`0a}k8JIr1BaSwWZQmogX;oLR`9;I@bpNV;Z02r=Le z5t?O0Ko_J}Mwo7-UZ@V226+XCbmZNxj~=nZosS9~b^|)p;u`yg zXu#Ur*M-yAXfl`{dGB{sZ>=9cWXO2GrWG!78fjdnbT6WuyH`Y5VUKb|M3&UY^YS_h zGn%&RK}7G>d3i-^r`gSSgUO!6D@yQ|;si6!^O>A1shroE^tOHk)bE%`XEuK)2`&3}T%^N2y{|&dmiOSQ>b4 za=A*-@ubo`hawRnnj$o@Vc%1omI<$!I z`PEVQa9aEBeRScj`i^NSc4EYC^3M8&b91m%6yIWOCUybs(B)|{2q1{iju9SGw}9Up z2w38Tn14Avo2#u?yV)RThsw>Sl*3m&rj7LqfigF*KK13}YB!qLv%@GnXKEn8<}bj$ z*@plU3CTf3T#w&Yd{!ZZQWV%jz|Z+S{vrZ%VNf?3nVQb*cXe1kJ zB4lF?(Kj*HxH>Q?S)Yc&^@=bCG{dhE?^S#nzsZ_TOf|zB?HFopln*tS$e&=A(3SIM z*p+r9J8=Ye+Vo9EP&s*ZU8;yZOsS@aGLn1dM+yRTon*Wc$+!fauDh`0?GM4I&~+N2M;(L2caREf#E{keJ*B zfkZr+kdu;@tTkBn(AV#t38_qy>xQO9mX0W%A?4t=unafpP+!E)Yz3m1W6WZ1Df=1m znmRg!0BvVRGmE&A^crM~30;oW#17e=t!&1(YuAmJ* z@*o?8fO>P9{mq$=nUmo?<0{pJRTA;Xo0Ewf0pgK#iiwq2qr@1b(|mqmJfjLYr1)>3 z(KKTh2)+1Hc1MVG7MBOfSh@()P(;SbPmC#Sr7!Fm1gI^x=iST$ zetKRzWB60cj^+xDz>Vr|zgg2)47v?XUO*uTiN@XBn)dBDDOo2vI<)H!(Z;**sPILI zJ5c;FG&_s;$v^FVXNCj=HUrQsUL!3ciLIy}u#yzk3xR9@;@;sNo-szeABCu7~l_f2wZR zxh(65fUJzDGVb@A^!(wHY`a&tPEBFL|Sa4c(}~qhn`3bZm(6sH=Sk z4@!Al4y@N~J$QKUuBw^I$yM0iIXSlS1_gON$;I@X?c=hq5i_E{_NFUS1#I z3o~QIzN*1_Y)ky3h1geG!89gTlqY;)lR!d)#pK)*bU`cGIDnR94qq4-B_RaKTSk#9 zQY9R3T1b(dKV%!w7=$=pY(m}X^p=x+0gB;9U>ydH!QRk5F3l@0;-W8K2N-;i866adM2sJm>5t!FQILG#B^jF zDK9T>p@y=CkOD(+2kqq-FV@JGS`xb_{{tPgiZ2+u9gUp`O`#iZFWi6e;u=iA-4L5s zj9+W&psjBBWN_Sm!rjhN0>ebeeUvbL4in1!h?UxIJt zrkPbLXJk||FEA4ZG5l-Oa_wGA2JBxLTnRKr1c=tdsqu1XgVd(XahnmTDa~w%B{Q|T zaG5YDnUIy0P$Mg=dd)U%Y7)~WVx%OZovQiT0VTgWs7f>33qOG&_bjwS#VMx!Y;l(( ziJcb{z+MZKj^q7s@+$wD@JWaDxSq)bhj6B5yQ!*tk&Ws8kE+D>_5*uJZ%s%>e!BiZ z2+%09tD%>OP=Guu=csWf-zhT2p#G#iIL#*4ARXgn#w&&|u;jBx^QN8V6z8nIz~V0+ zEt_V~C=Oa!f!E{VV%oEDvoGH0Is{yxu4Z>`^=X4rs~gHGzv@=vV}GJ04O@0WfEqzJ zwl&k8F4B?eHT|&3+;gi>&z&L7MSz+T@y78;p4;YELHV08i$i4% z=Jmd4#tfJ>)4g7YA2Bmv`iz)2?WTClgqbt^ui&%0@%`=x@ICw%-(dQ(BaMCMZh&g? zH{P>qvHX1#-7`SAo z!@rsa_+P|FPP;#K-~XkFzyEoNJ*M;X95AI&p-P>-o)+pg&a+VYEDwHA7q?4z)Aj3k z!!`G5(>L3KV ze7m>mm};&#Z2qoWd9gsf%8+idqBY5Kla+ao6<^b^ADjjTg!o^^7q<*J7?bv7;AT@7 z|ImQDQ2BtN{VK0e&-Anl20xgn_o&sp;WW=suO47uyl{!d?|0WNmubuG5eprklRSOS zW~pP_uD0%9-Ni;dxCKyq8h&u~`xX3PdfV2m+fE1lmG4);cLv@zd)fD8Goji)|14ZH zWBK=GvzdLnlanjdbdQ=hDJninI#{cF`tb23I*?a6N-;fWD!>uMYkIMWJO?1e7A0+* zXIA9OZM!UEYgYHES7SRath08L@6ZLjV<$JB5VocLya2F>uqJxjtI@OlmI zEdzys+jzyxUHHd9#;Qe^K1Vx;w>>jr(MTu`mCxX%_rpo(>G3}igBFphxK+4{e5e8Z zzjaV<#A+2)_Ic^K9k6_2siz$Vc};*-69Y(P+=CzXIE3%-!cymeiLi2lXO(vEOI^d& z3v&}aaG)19X)YSq(u!H%xK^&1XV1=r;9K&OTy44e2M?|%ePEhcggdY8Am0py?8rBh zdqQ+UQC2TYA|HFd+#i0L1U>mHzPNlDCSJTq`uhNJ5T2twLa&v&O}-&7T|lN? zVmi{9O-S@2Qh&3zq`!Y&fA3kw$DQXdaJM@`UwXXC0 zMNJ$`htPVkxE>S&&p1sSZjW!I;ihR@v8gjRW?akZ-WsnY<0sy`euHr>XE1Mlp$T*j zhdita$CL3gU%U)Y4wv3BI`2(TmRV-pA97&5@9M*NmhXlGA#CL$hY+B(2r(aoyUKlz z{=j^|WMghE;qK1>;jVmZFhX1z5*#Gk8)9iK zevW@s4)1>~k||a-S?8D7srhX0NZ23goz|@L5y>aUbdncn4IAA;pc+!hu{n0 z=Y!cMudwOeJAhXtucM^S;xxMUFc%>NQk1~s3uf;Q2qum{cq4w*s(k?1hz|IB#`MPo zuD}=JrfFkjE+mTVA?+rwLBQoHKQ^7)_H){=%@?=u5t}bv-FD;3rYl#sUN@P5TwKH* z5fRb#a2r|I!x{300Kzq)%ueOJQqa?_;pF`U84w?!3l2F~rcsE!h;H{h)UZ>jHAfmF z4Tz|YSXFSM>)8IN7(LA1;&j)*zYN&G;-cu7^+jft(=K} zhMT2l=s#c=<|T^I)ZX5K2Po34kE7FyW-WR;+ONbmY}xC;4e!`_@Xrh79R%KPe9PwJ z#x-v_UZyW~s)XEX`&^{GjwHG?NEJ#UFG!%h*CRTI!Mtt;7ztYX5iEK zeX0gn4kDgH=F3;$+eOd&oWX}K;_r*ajjJH=Jg&TUqbu*=)}{CQjUK$6Pj{(nLcCu! zCV=rM%yAAb@1gyE(r#zRXKRZzUC|1G=`_q>L%ET% zV==1>bmm=nx}u=ic7hGXm?U8JZG(zfNB1l@sDCkA=01cDat(7{y-~n#JPK#;=!zJ4 zAa>+tSlvQ4$X&hOlWNfNngf;_vDn$J+j^adI6M& z-nj08A{BW)qDV+(o-eNXHL*p@i4$A4n8;KFJ$``emI@AtjSUDcg#cL1cc!W6iELFK zf?}a%_#P8=&WC#ximV}uF~v0Br}yr+V0HP(qGd@dPJ{QbgL6WHIL?%s_Y(p7htLUE z=;#bl7Veh+6e^aW6=I5c^_bwSJFNM>v~1DH@~ap0>pfk59}_x5D4hv19n#&qNo|eY zP2*+Cxz0bjlEq0(kj505kXuYfzy~3RFZ6VSV*--Tdecrq^xJknz5MUE6;DlN*AcRs zO5rdYJs+v2CpeQ!-4QT9ANC_>d-IMW0@Ru9$*vNvAbYxMXI{4Rr$f)qG4ADyj;YZb zqIZ9GEIT*VAg|C_*`_ns@-{6`X@UUVmsSe{#lF-HJCH+8POQesDHvD?qJI#Sf;F&` zfw$UZ+@M65y|G>&_Ip4XqjO*O=tP_(tiHSh>!qp2*Vy?2#4xrGuqOmE+BqYnpAZNi zc0nh_};hCWbSw4%)e?QAJG4f1jVvP`DZ@{49`jL-Ueb)3=X0D@l=u@jzeYD8LTg8wc^~Ci=Btd!iScb-!#krOIjS<23M^LbRAGu0)Dv6j)B6c!I)X|xQkel5K zY<*x$D6U&rGB6$Y_OSc+?1#Nb+~@RP!}mCs~t+Z624`)<@-rfb7!+@-f|`G%!O z3l;Iif%%S5HnJbo8M7Z0&mXL<3mEWdCUXWN@og=1iRNw>j+`fql3!msd? z2*G;?*su8VBBHEU4e4nmuU>{kJFQC-t}9;TVcX?2zOj5QgdK)T_`sxfE2p=MgAw2w zv0fU7PvO^i0%ya(jMa7OM-ADF0KFwM)W%{HS`k^j<*JRMMng0e(ZZ9ATN;Ib^>V0I zC#bYrRO!Lh&z%b%L^Ng%PWqj|POH{3tf-U0yWT2iZ_Pz~S&B)o$g}vQv(wej%uk4@ zn4NPe(Z(9T;l$eA*CRj+5#oOdCFq_n~++-t!aY;>iP{ zH+03L#zA*T{DPZ9?~z;@c6ht_Ew0W4kT3Z7a{F7zk*>a7Z??G=S8Z;j!(gz|VQ3_3 zUcbg4fNJz&?pAq&l~tg;7YsrSapm1WU3p#U{w8(*h2_ohI?#i3{*m{!VGXlW42J4f z_kFJpuhy_!x$vljC5m^ixPjf0Q?+VRys+f}Q^XKf8|G4LD4W-vA8P8XZARTViX!yq z%^rU<*W$R>9# zJ9b`{(WqVXni0v`kB%Leq}S})sao-5<9oZIDfL>~Sq*DYKZ7}GJ%oDq4ogjKCRVa- zy{1)}(iO|uHecN)F0KNk)@t3lRzg;GLPG1-#L2f6g50(I4Bka7MF+mY=696B%0XxP44t%xM;M~4nen-GmvjZ6k7!ZOH0 zJ5ATLm9=yJ%^T8JpJifAzU(<_tmv874}r0P#)Qjh{pkDFVBSe#G0aw+gNzOQs}*<{BP7`&Ep_Kzjeg=(Wv(`xfZF5g&iE4&PZ^2$l;2y9^lTVB4uzw#78!Zr*XIdSdSVpm@ z5+jH%*Z`C>Z$1KzNvUQH8C@J=ZjM`J>KYdI1fSe6d!?tVH%|X}&yQ>5J7`VMMHwzG zp1pgmx+KQOOLgma+Ju+jpBUrCJGvyT(JX&@HgCg)FTKwV7;vHAPXus*-_5b&H1h^5 z^1K@c;N$#mxPt?Um=_V!ib6%CH~VhtWB%(3$m#X!+t^$$4gT^gMo%Pmg5@Qdb{TCT z1d4^=bf+d=GKShY&KU_-KYjghFMQkhZt%XO!hP>V-O<>IBj({;U%Mht``X5(F>|^M zAn)XZ&iMJqb^{uxG^^oN;_BhEubbT2&)g^P;hYi`3P(LZQLmQdms+E^;>>L`M0Mtt zhmDEmVPl>Al^H(r_sWdbc8)xnKP*U&d1}TtLjv zFOoBEWtynO1he-c4hqfv9;&Gkmis*nyJ8Fs^p^i3FwK_8E=QBHBh6QK8RzuA0dmL2 zv4?R6Mxzc5hSN|F8ZcJ#d$++Waio-SY1za&{fAbcNIC|@AX_O%_($|a4yaJRuMS5~ zg`&$oIy$tVtHNwMvF5(cdC+2Zti1tGm)7C-WpN!?KM)4O&9V-KJZCO`xKhBy{6ntb zPWW*met@lU|2}w}Rk@!_$H8r=RB!Un*%J_;R%D(Oo`1AbKmggSuBVQ4X0wZYCL-l8 zGmi=8z$pf@Ve4hjXuMjkvE^BgKp^^3yaJ2fj5i5p1GaN|<&4-*~f+X%0Q zolvFqu3NJprgzJ=fF_T?U(b@m&MLiGfv@1|_-!d@R3F+v*s+*R@|;gvt$E8k%f3nm zA`WqYrP3hk?jX3bjD`wHB>5f*J)xPG-=$Xl>~P~C=nO?66heYS?i?@LE^kOQI6%U3 z7biV_dmSP=ep_>y~^;aJm(5~uax*B~lcgQm60JCQ>dby_N&@zK@WPX>b`_Eh;0oSh?hE@h zPb%d93y*x^{Sxn<<>$F{K#Mf@BG3z54GlBw-BykCUU0pT=aTt8^R9V#EYJibPpqG* zgG9iuv1!_c_@vK^uHtJ87eeUeC0~=u|1%9(a0a|t*CcIH5?;7^!Mx?m7SERtYRV+y z_Y%|1;{V@DAI%mJpL1Jz(ZeY*!2lfi6f3l}ce7NlfM3Li+WQgSJ3GK@dEe$~g^Ixd z5M%3hck*1Z)Y)g{O?USNE4^1=b$6ew(F~m1v8M-A1pFAkSoyee%@3^2KDcfbZ^B~*Kp3p#_Z@9MoS))fyGQm+l$@iH}t^E(X@yZWq;?Eov=C!OjI zPVcZk=bO5)(WK~R9$hlJOi9Wu&Ne6I#bf$ZkCW}_HqSq6=^kizS5sT7VoP>-g zUT-1uaB3Z9lGMFcJzO6T^*a88^l-cDHt2D3rdQvSJ?htC$|pDcA!kZDW(A~S_?t7O z`&U*BN^N!hy+p@?nhiM@nu2*`C-z7_)o16c)F4gRDG1y(Y~# zg{e)B;-znVNKTKZ){@F=Dtq-g*)u(vsj%qBBa*+yDbm53DLqfh-ew5#&Hn5>>{QTs zxa;(;V`@4D+G$(#?LW&AXZqZRD*N9+VAp_vaD4h_Jg(l0Ig+)l^UeBuhD>U=8yshy z!E3JFG$Gu#Iv&i(lk@Wk(N#Magv`gus?MrZGwk_dFboQTKg1OC%ezq%H|)G%^mKRU zI-RbEv)jM}s^hcx^#%OVAICCFr;O`60l!Cp7=(nTvKdf52#X$Tpy_vBDuoMi-f$Xv zYjSgW5&e`6X+sfW{u<)#f{(oge~dwX%Y|Q7nwLpJ*32&^9jal?L?Zc~dHUPJW>GH>T7jkes8^CSGZD0=W z0MiI^5LaYnFX_r~jcqI3Gkbx`^)!F`6jKU{?e+V5FttLkYlKIoR`$ZZyoKj)Is9Zb zNvR0r?uLuu9$Fkz8xw0RbrDv~mNUgYGi1Bz`&EsFEwQGNIaaG zDs_bZ5hU_;Ny|E!)>7}oK!YJrkk3|PmP@iHZsEeXcuYxHCTYKp4gZld+RADeNwHDV zGOtJqE7=MxpP^*X2>wE{9?ls`;%0@Jb1sY^X^#xEvKpQ9V_X{>DGRjgX887-G*%uqKbu96~q#u#j4;_%}0-Jo;7@An}|}SB8tSunoP_F zsKw>5I_g?nGIu0FE<-I%ag7cI6Qoh@NE))jD2GfUA#-U+rs*~f$wYC+kqAs%r9+&L zshTX5=8qF6w)lJd&P$s#YTrDeSd#T8GUaSivrES+#Z&e;v|QDqRBU`{>lSO_h&qZJFCnlK5W_((MG!35{xD+pkz zIgUW%(2b4E#wdKG2#E+$yayD`r4f-v8qM@EZI_AZ=qy3g2yCOEd=Y|&;|sawC~HX+ zx5Md&b2crP<7x8uuoRDgYsJ8>fzdWC9lrq=={sXZ zqGD+@yHnu`f*h6I8Di|*2<)QpfNzBFY1uVNT1|S1mq;{|1JDDvBTVZ18K1)Q;UmQ2 zjU`CEehQ0WF@B05PsevLH5KPr9{-Z3Yr=kPg1<)NuxP#u6C&M&Nc;~m=sl1snPN=K zs1M_j9|6>das7`_1i5#m1S!$fLs2g^9lnUdM~UJZNeK{{D_tXT9`F@RwZ&<)H<#bG z!3K+xmRDa%2Ty8%bUwIzq2}|OhlWMgvHn7eJH331h!!&v!fT43>^m>xPuN*6tT)%L za(UYpYubjn&#Tv)t5DjZ#p>3G&GX_*nZ{ZR|M&I`%XWZgeR1f#g3QiaBowbrDq5$d z>9x9ct{Ryr&6bx-yE@p+XqVMtw*9oMcAe~PXSQq8QQOA8)#{dHqF&y<`C2kzkC(0_ zck7%=mPFaP$z3|9q;~3rm^_wWXIi99MQ(o=N%uz-_xxzveo02dj zQmt5W#gDe_6g;A?_KQP%1utGl_)dJK>qke$mb6J8-=KJ8EKIelH=$l+R7^>$Dq5E<%lpje+-2d2x}^=ZiM}%Ua;ApbNy;6CY=QHXEQ{_azNW3- zu6-vhzU>9kPo6wCKD3+Owryv7+o|o^b`(BVYuB#Y&R4H+y1Z_k+_ei8zo-;v+WPM$ z|J?VzF3i|C^J35QGd9kUBBrh1JY(y|Y3sL6-b{=y?Io?Wnsl^+_&RqcLIf#FkhTPbN zs$r6o#F}Q&*C7=unpNZyau?;?t$4*_a@-4-Pdx@TT`ILX6+bFn!cwDA@yC*`oK=bFH>=<*F^Qm$qWH%1y1r3RNn%z}>sAED;`FVpX@!OQXWVqLy|p)u4V{*@g|v z%HQSE(h%es`82#pe2WCLF@w}DWl=fo$1<- zstAl@`I6ikv50ag7thJpXOPt{vL&3}^ve#0&?Nj8C*Zw5@w(f6y&)uQCOE%%2{DJn zB7L6EY&dO8TCsr{nFA3>W%-xvWkEUiMs|X|9P(>gsBrP+OOr_|gsZcgO~nagd*Wu= z6hZD-Ny}N2Rw2ra1N8>-0VW!sfP|mbe1z>d7vi9wq&>Ce7=A?R=taIYKBMh}l*?E0 zH{wMTvw8l)m#e%P!p%QO_j`}1G$*)Itu8&C9M5jg5F9$Ru)pVcapu^CZjm;O)qN|! zjOvM^CZb4Gk~5zZ5HldF%kUy?60;_b8->qSObzyd5>>+Wp zFNw_0oTzdYiWRR=fk=3oNcfgWXq)e5P*O?mqBt;=q{5LWSEeJsSbg{2Du-qFw)Cm*kk+qXnnPMI1bMglw)nHQJ>5B@tXR

      KXnW@u9WQr9ZxsgXAcAk}-3}?2HtwLoW7p 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∨sX*%~*}x)yJ!6AN5zIGBeQ^k{{~yB?$Mh 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%0DFAEsM#na)J7(729;4f#o%UgCmUwNd@vSht9i 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 >h!M3s<$LjF&A- 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

      g1lSwSU{QUPfd}vVtOF65i4_#g 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

      7)*qszh8??EBH3- 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%0Di=QPVXt1j3Y92PFcu#CmVgtvw-`*zmhq&&mMz0y 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 z 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 literal 0 HcmV?d00001 diff --git a/influxframework/public/css/fonts/inter/inter_extrabolditalic.woff b/influxframework/public/css/fonts/inter/inter_extrabolditalic.woff new file mode 100644 index 0000000000000000000000000000000000000000..20c45c44f5bc7cbfa13798529ea5a3063ad134c8 GIT binary patch literal 149372 zcmZsCbx<2$^e>cBiWG{w2X}YZ;O<%i1g8+(in}GnHCPK2DO#Ywml~v40u+ivp%A=< z043Ou-@KXk{&{!i?9QG!vUB%*_MCI?ZkVyQHV!Th4$dQ;Xq;yc=f}*Cu|h;SYXs(;ftw9*B*I90%*~>gSGwTSfPvUy6hC zgr(>Ek!)Z1;xqw_;Ts(%3CBG&KRIcv`BudKQJzX&Z zhNR3WuYbRz8Z+2(ec|%L1@A>QDUR$evH1VaFVWju8ypH51 zk2G|?vVogYB^gb%huJ!u~rVw!nGv`65r~_sPPWOOojC zlptE1Y$0CCxcE~F@Pq&GrOA>o2TL97x2nEN44&?bY-?Sy5Bz*Te}3sH{Zh)p`SQI? zmgZwd)7QETJ`*n-Q9m|vwGuOI%-S!9xC?)Vl!*<76+~@LoslLz7);0lV%gv4L*t$wnH6nNK?D5JQn)~q;8zT`!$c0 z82a4jbH&dZFp)f_9$*kSn;c=+Cn~}#VihTOnrG%mbgB_)@WneIZR(pHxep+H{+lA| zyA6*#+VcyY2CZAu{vL|Mk~qyi&VXl}@}6kICfe(9a=T%)Zwqa^(U!iw4$_<3&L;g= zE9cP8(Cz3tzcTGTu}Ph8bGe&i!e0;8e|OxKs#)DR^5hF; zmzx*EnZh5YPCJfyqobcS6cfBh*L=h=J6hDOzq8*|5dO?SI{BTdyD89`aLe092BBdM zF#74>R{Q$SvhPX6^XA-+Gyg~m~r-$l-0 z=$aUM-?qWr;_s)aT_d#JyQ1{_%$f6RG=YO|zncO47AGX~P0v{7+c`KyT-b zFYNtL=>kn_(;t@}9E~|(0o6ezS&iSfQss;sFQ|_2F z_qPO7-yI1=`x3VxG4NJUg9 zY_R*0%|>n0f@E!-OJYs-z9LmL|Ky>b$uO*f;Q49T?{{UuPtSHb|CBIaeO#k!Rsv$- zgDkI4meiI8e>)eiOIlpS>uq_cTaKyEXfJ9%C$n(9%5zcvEMeM8@)k3Ucb=m!7L*sT zt()*XlsiM6&88KjN9WbC&5?t^js8ZO>}5v$o6r3|al33b*<6b^K-Qly{}u0<*o;Aa zThZisIx~J8M!y$cE_Myg zPhZG>iT=~}pHuvTFM*OTty{(9&j&Mp_juRMePUFoNKR;-r0n-{?$jk7P`dg(RpI-h z7%bm)&#V2sFkO93dt=w8_j)Xn>o9_rD4=xBk<6Y1AyPAW zJ`HZ#U&j9|X8tW%rRQ9oEuJ3QW3o%NO4Dt|^R{g)2A^xwAbJ=!yh1LTE?9i0KFb@4 z+1bc}nPLf|=W;%t^+`Zzd&p|fU;fK%tnE>6VfGOIwHT%`fwci~yFQM)4?N>rF}(IV zW4omc)SmpC=j^7aCEZ?;?lJU+Aw(3&>4(7R4RNmmjq2CW=i=*>5E5s}qR!eHC)%K5 z`mt)OFn{e7v8Qi7Uu;Mo&3mAJ);za8m5^|b-Klxx$GMZ=w=ESk_A@aV zkUA7~F8JZ*Su61e!DU_PywBTF*h3Bh#nj|2*D|H6@a0+3sIDAPTVR*t4fDV@)3mGH z`Z7N5t<>XpjL!orwf3m!bl|Gmh=urHI5vIgp#T&84hp@2vTLd>P<>0dk_ z$BqegS;i*xzERY6)S8MK81LT@ijiT9Bk0r@Zm$)a$wo=sUWX@q@X%ojl+oYsAl0}|6>dW=o zL3g_~8%reKkCvw%WSbY)D5Z!kdCwojFD_|cXo$yd$z`4|G1U2J6@T+Iy3qV?X-5>K zm_H_V;a1x7Y8x4+#DdZjom{4nvs3g4AKEVd4Yy&;%FRxtb0Z8-3w1tH`=%kd_Q#02 zr|0omH(9$$+vg8*Q^o&i%DOh+U6P-I11`Tkon%bx2a)xuDF{Gnyz2kyMj03_SdD+R zBfCoJG4)45f9p-#MmX4WdV;k&eo_h(E9I}i8pgPM&nVo61?Oq5Bx;Ke#76v^CYryy+Xz5d;N zDRBN;x3&J3w&#RgB?I@DW{hG$?h=VoVjo+8DpDOmLgy>Gu7bK4g$39rT4bYrRz)&ZBNjt}zA< zfYLR%XZ#ji_5`J+cJGRPDPvp51qoE&c}hpFW1)`Q^SP3e%WPuXM3~wLbqSj6ozSxY=txntvoA!S1P|! zpTPe}hFf0sbQ}*O_#ggRhV2mka~&Lss=Z3;p`SXc&Xw@bTM<)z+7c-^cRrf))rDvS`9eP&Upj645{8$KR_iW~y#v>D`w- zHOCGoGhGM#Vz0l~FaGOn(1*{Z64!IrYtEN2sh+N9s61@-Oh)Nrt> zd=esFa}%fOc<;2&gQ=t2^usGWlL-p;rj9QZJ=Y{)v)|d6H971<8+0wo-8>+n{NRY)`LY4QX0n!ZU0*1y zeh|j)eKl)y!pw$bFvTTX?kc_AXm>m)Sz!4S=Ls5<=o*FDcYy>OFZfQ6THgj37jUWW zQGC-N4v}f)8JGK}TH?Nwg7qyZNCtYJv`$H|TaCGSBw!f#PP?RNr7-@dxpt4Z>5?m5 zwF3m(Gc8vl%+3L(#;Xzs+1ueEJzJF~GTNJtoXo3@_c|=dE7t$+{DDWcr#P5-sG_;t znta^#OWF9`_*242nxRYX7w^qNXx=6KU8SD9F{|jb7?5lAqbqafe^x;q{IYcV=s2C0 z`?;+5ituh(iEl7RjS^V~Sdvswy{u!Mt;v`E`X9Xz;j?T+_*zq@P4yx5sqD2_LQE6h z@7q_Zve&l))fhKVdZ#(@KVAJ%oo~eNezl{@cwfjFaamrLln0Y3*at2t=fb~b*Rm$$7$-^@1e#QhKXWWR_F%SnZ42CN;s|$BSC~xOZe@%L_4p z{k~R3_|Sn}$;W_>jo`RkI;l1$l@}I_962b@=C^Nlim{bGxta!Fj^52ui`j8p=H#+v zzyDi;B{fYRB;k~`P6=E)CX6D!Dkq3|8+RPD^Q=8+yD06fcjKSRqkmOjkM4B6qvoEW z_Z_Cr{vBAc$BK65HK~aF7vkHvN}ycrD#D{{A7$KBJhvDKHa9ZfH9Vy`2ve1Iaihxi#z)B~uXE6JQ&*$%X~%U9A8UAv8|T-%VJTZDT|KyIVCs^;{j z#q<(yH`fe?gTs3;b)#+2%k@%q4_L_R8XJsbSS<4@!WiXAe6|l;Q;l%*mK(Qg+=X#8 z?3$h-h&n1is_o2pDvv{T4IA(Pv*qbboX@yC7_8p68NF{?YLw;18;h|b*AARq58!&i zu{BswnKdda5?5Viyh_C0d1Bds z+u~F~Jkb~L`wmpT%cm*4-s7$K&Uy85-BitxQG@k-#4ADfSoub+WfvtAVu8`_whn~4 znVB5Vm4aT}6a;T0I&1z}7VU9N?v_@wZkNRFi^h^vW-D&R9o`bgeqzh8l32~=YxVB= zw*v|xZyM(115}Fj&Z$uP#xyEhSb4K)Dv=;3>e;n#-L<{*#&4s((b&nL9HKP4+^;A) z`kD-sjDJ{$U#H6z9Fpb>I-ND7t$s5=eJSQ<>4yC_cUZsw@~_JM_Xf`H50m3mAMpzJ z=rruNH)$Ayg6r%y)8z?u#sHbCA-yPxui07G!89i$bjD@xN=`LAOGM*?opC&8l>{sW zt8yPdlKQ0bMFM^jCz3o=zfwC?)_47UTYiz}P}EAam9*KW%j4JUPKwmx`|y8aAM4$J z`B!6Ds986cQN=NG1e?u{bqO^|7=& z;vR26r0Qw-Qp^wha9W*}qrdADhlf+Yj`JQr?W)J$C#72Wb^QNErAvLr!0H*sANv4V))gi%CmZ{1uk>RTCV{ups(3#m)xe|ND@fK5cW=#`W<^mzHm zmsWbD(R+B5*#z`oj_l}GiNxe%xMqsA~i45;>iU#=d}(qWf@;e{Zjifn|7K1lCx8LKFgPRQLe*tdt;0| zf0iRmdoArFbA-yN>+1}0(^w0-0!e;%i-xy?ri(QlIx*zuZWifpp{DM&{o2nmN}0L= z-XU|?w=Je`YNj+RGW+R6WcGXCs)+Ar#ePeohk*=WVjACk>4`nx6}*q-h9Nxk3ix~) zd`lwT8H&BY4X_dkcmLN{pdFQ(UCp-aHM&8i$S}+Zx)#M#d!nT>#$kISsN&vOQ6F1q zYYX!*Ea_ICkF+IlkI6d^kyf#v@!eBCy6~EwRt6b&yX<@-%j_K0HCQS371MIl`(}~Z zNIfgt05oVTCKU5=>lt0i?xQUezJUDEa+& z+u${ypLdCTL&e?;>}HNmao-MH=3rexI7vY`U7_`rLhG5zNw_Drmpq-rOiNQcnkt{C zCQ!5SK4MggGg6Br!-o1j`!Cfm$!Q1Vxtx_cdZ3yyru-I;Wv$TkLuxrdkj3`9vF7`; z9Q-if`@tnl{(WYx`YQg++7jPu+iX%TzGBiCcX+_+TG( zf1P8zk>lgO_DZ3WdXPI(15=20L|xb9-kd1(+;8SYZRS*M=D==v!rr<4`Wnq(F4$mU za9+IhVXHyR&24A&UFITru*eS3H|3}A`}MCo87)^Q%sQtzS3j*2Mq5(~6eGe)sINr- z#s=hHufl0DuZ)l5qE)u4l(ddxS?%wSi#U4k`wKRY$$C>(7@~dh#wsjdo%VGaZm$I% z4+y!4_Kw}^Urw*8#P(uW(`A{@>~Tkeea2U%!|d1J1-<#a`tQtERVkIxeTpc4hdNJ zbfSWppSRZv_?>mDe%HohwdzaHsIp@>@uRpzZ1siw==O)cot^JxG5(>0a#9!lV?TVX zFP*XnSB|1bn|Xw_=O#_BEn4JkTQzfXKhm6Q0=9U=^ZCX;3Z7MQpq|w*YG0C%L{i@h z>+9vecBB-}9FU)KGi}P5aDB1%@b-J-KmPjfZ{^tiv^`V2z;N2x?X!`npt(|UuS=r}4rFWs7 zM_V$zGDlAo#zgjt{^&2Zea#Sw4}b9mF>Pkx4fJc|4EdeLRNxlGhHma?ylHyH|2Eb% z;t*V5Eo4IcgQMHVQIytDfZ2F(uErd#%7gL>AzxLMnahSK4LY7f70J5IX79C?=M`7F`;isMbIfZeA_1%w>B>N8d=J;koh~wiHqmlTdscLyVoEa zT}@b{$%#23#?)G~ex}IMJ4UM>Wt}L7g=Qc<=iXpjum;2Ci|0}2-LZ{CCI6b*fz=ts zNlg`3R_CH$EsV__&c(UrOw5g+Nu2)(oK?*hef9%1YilUxm=MtbvwMMUs`p5<^GIp> z?NMy!n$le0kt7eIY0mWM?613%elw@C(-rA5^?^vfmx@FltvK>1%;3AOsiSTa@Tz)1 z_(XA!VuCah@%#jyCE13lb9u{ThBzmWnJ)+mnX|@bd$*jHh!&$uo2GU~T^dVW?4HCp zFw9y_29^Erm0HJ0Oeb*de_X7tEOk>Gc6pTg><+~;)%>xIdXu)X?}R{AU} zQ>=-o(QolefC=k%meZSzQcdpy`bMiVQ}1m0mibaK?+W_nYhCptKz@!uA4HxWo;oJ0 zWX3f&Y9R)*p_TzA+zG3nkoD_^I68hiFzxtMVJ?RuAPg}>w9Gd5@3;Zq#QEgfMbzXH1rLD&w2iVRqBw?o3fki9s z=Uf~QH^cV1ZL7UCElllG3n#8#a`h9!Qnb-oaw%!i{$4#K3nf~ewi(muC;7^Xh z3-zy-4*YdJ9i6|TmJOWFG8ue{-j;dP8qt@N7#%n}Fgn^4{rK%bBr`39AqvMI{N%(w zXh{~jn4q?{wDGR;9S=G^vSgf=WOf&WAG2mTPj=3Et2#U{lJIw@*(Jeer@uJCzlRUR zUwYb*#>Zqp=20v!Tp)m+>7Q$;_40g@B=b4SqtzBf`gnX27(C)0m@hnV{Vj(4vXTY!CKL^*{D{Kp+F)xglh%cJ1zcUA>u8@cQWQK&TDqWThoOCs# zd;i*BRou45DlCkgY=oUHUsv25$NCIs3haKz`X!ddj&6kf0ff^lhO84TuQK#X%Whu= za<0nvF5RMIPp`4Jx45w?!$s?}1!Vq@QxolD1BQ(Qf3R+QU*oOP_sYn!mIsfoip%hS zIJWB7+KlO)?hadjvh3h_+1x2S9Q7MIuyC9mb+FcVd91+;C(7w_9HCx7N|~u7mwZzB zG~o&<{(52l@>V`p0s+3(ldJQ0yyj77t>TXDY;<`nKn$#h(u-cSuyohVyX6k>hlS zXETj|gN$R%|9;$dA`)?JcaC9X`V)=ta?*#pq(<=EPDUBe$?Pi_M^s{MdYTGHrd+Js z6@G5vR}x=D!f6-ux4AB$;lM$Vlb%RHk=>iq`G|wTXKNm-pO=66o%IYjf2>$jKB*4- z8GTeC+6Un~sarg%5cm1mxfXE(4O<-8cRXtif4?Sl(sSGxwfFZ};-u0G9)_;KR)@`I zqP84RfAl*?6=)5DCKD6ava3_|8Qu}ylUk+TKNk3;l31|mJi==&Uc3_-F(19B6%qI3 zvtzsq{7Sg^(C)Fh!?;V64`UYO>ncs}dI;qEIq5LbFzK+VN~FT4fWd<%*wx6#ztT_e zR^K==oa9_>_&u9XBC`+==zTM&#NXI!T^OkY>I<9GKUh7Vi5kWqbo}lzWX~!{{3h*SRo!6jTzXh-SK} z?+8!Dte^H72|1%Bp*o&z%-D+w+!{{S|O${3D8mAOmdHtfi6)0PRDrfi>g zOa0uNVVjpb)r&r)B{4hgi>wf$JCeAkaU_Mw!)3i|+#%l-nD40Wd@q+1FY=UJjTbvF zY6d~h|9&y96ECMSf68&?0XD$&ohqzP_oAWByG3SL>&CYNKrTmH{sVq9E$f1&{!+== zmij(_P9ccBSK~5JVAUyQA6Hy0x9}>mK@iyqf>#O!V{B~ecE?-?HA(li`16%pDGn(4 z)0G=%_I>y(hFWJ182J;28pZcU`8#84YR=6IX49}MyywZNeSJ3zTbNyegqKAG%*s-9 z&w}6#whxZAiZSH-G5y-=){tK6zt@(v25wSIlBRA3=2G*L=355AQj-w#S;H`?&lnR4 zL*s6gfl1kfX7{3jnf(N)`-g$)^`v6Re-qZJlN2Fe1TA$Y)k7u(%||DLLT28Xy_xV1 z`RZ#)Hfei4R&Jg<8KJa*5f_+{uCTdWxcxNc{oK3eqJ4#@8Z=)M4C_du{o$Hds8z`Vfm!}&5*g(I@q!B?zwhfwM=6M z_h1u9{OxycbxX1QrlxeW9|qc%s~|He0}snt&1O69%qG8%CixxC?nwj7I=<_izK`Z;cF%rS5Ypi!J*K_5-TlWdvgPBv6JuM;!jzAQ^>&j3>>x

      8p@+o*vY5e!!EfIf6-PKsi@v7B3bE+^&U5&k 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 literal 0 HcmV?d00001 diff --git a/influxframework/public/css/fonts/inter/inter_extrabolditalic.woff2 b/influxframework/public/css/fonts/inter/inter_extrabolditalic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..cca9829bb2734ed83ebc752aa1954328a662cc48 GIT binary patch literal 111896 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%0D)1nExHgQ|k@$MkXXc*qSf0q!Ndp$BJc z-fFY|T`t<$4p~r(tL-E^HOoVEFamPtUP*D;6`{dtF?8bv-wEXj;rvS3whVpx;;Lv2 z*KUr%v@zisL6fnb-PPbfapJ*er%=KEP7+nYQq4E-2AOAF4@p&=b@+^s-U2JlyBJ-Y zy%Vya5w{Q_?ogz_77S#+ft4W6rv%-2@$+Y;qER`k+hK4S0h4j=U4Zj~z;dCMjHR>Q zm)YnuE{K~2-RGe+vXOKJGWaiUhoL1s^O4?yE6uxTU7Ld)a!e!ce!8e*kpegvFuMSK z)SzI@Tc`D4an1(X!BbV{KJ{ilP0YJLJ6k+>on=JBGbG_Mf+pkKyQ>LR=m+pV*vb(e zvrl4D??PuBIBiPVw+w#%?5begyb|nD^z5kyOUQK^abr;sLF;J%;Q?n=Q?j??aJz%eT)CEq)HuVaG@BESvn4YiT#ItxnBW|;RMbvVv zOL)jyaS5tS^`|d+O+e`~YU4Q|pzHh~bZk5*V>$)G%U`kOJv5RQS>etU7zM2Kj(F8c z>ErGpZ-3bg$#B@moi9<)-OQI#vxQpCzmcTrM`^D%PFMGracPrYh8JNh)@62g=`^!&(F{?L>o}pk* z#xA(Vjm1DjtzQCg57{dS@09=k1SC>>QlY~PGUbn@ulIiL?xJ;IFf~ht#S?Oip{MWQ z`9~6-0Ah7OnTwM8BKA7KE+2&QWiux9Ifq+iUW}j|L+a7!s^xbp%W@IjdShZwE<+z| z@^ID7;IF=iuC+@AdrKe<)1G?jT$k1Zqzf>H)n*e5!vR=l=4Gah@L&#Q?S3IV0acbp zxtGHo{p7FY3EcSKMgH%)HnTQsf7uDNU2)`pC%G26#?i_fR2V})Z6@uA#qA3Bck-ieQotP<;CQ) z=i@ot3fT7fk9}LllGVvm2VT*CxIS1Jo_QktJd;K``+3^sLAhV%7vZPbw3M?~B8R4r zm~+f&|8Fbvr;eGkyfgk^gD})|Y3kJIjxsiR_xJuaY@L`YO_h?n`U`sgK}jR1Z!34 zmsHjrZITo&pelE>rWy%U=57kLF0_IM6F#W=wFHDwx0XZ%Rj9FL=sf+9@n@`qtxWf8 z2|wR7>vJdCwgt1h8q;Fa-y@RtbJZ&$k%|2?tb0!tHM)2k6#NFIGemwi6FO$k@*S>4|LWFqKC!TltM|TL*T})B3S^4xL#nl`q!s6k z$*VpwLKbQxzM;!O`~6nNkJZ+1Z%s>sv%mJ~B*nm%Wv?FP%x7QF1GDM0O7bH%j%zb@rIdkvGy@QG1~_f8;f zFp)mW?lg@dxlUG6FtFiLbzLYjZ?JUCV6u!PblNfBz=3M80>#=7|JfSi$WnW7KMO`$Z;H3aoWOtkDB+Q(V%%g>V3;_e!AHh0 zYMR^TRv+0>d=-Mp;iXLXV0rPgzy37Vo1N&OpjA;nBSb zoqGzgIKo09b#oG6oLfHtuv*yde804~KqztKb8FX%eGawXSv7A5Q4}9K%YBWA+ z9Dk_S^D{QDcp`(>Y=DAE5Z?Yc`|Qaidl)HxI$3J#1~}rg2hkf6`Sw-g&G; z5-n9%LYn`8)^X9B(mlBHQppkI;`BZ^KSfeouL#ti9}+^RO?)l+uo5Wx#C`TZ{{?`m z#P>9`iv)c^R5b1Ey|6a|-r4mWX_4=-AkZcg#d?=uTLwRTR4ND;&*3qtL6ecz#R|~z zE{;Cy?1vfHoYhl3o(GqkTY8IsHu8mE05#IYI+2gQJQz}KQ*p`aE-Z;7JaWINXC3}F z(#hv~kkh&`DAoto`5P?T_$p-?UH%zxBJ1v3YnGuXrf&jzi@shqH4x7I`j>pK0Jd#V z_#2N}j(6ZE!e?9#`UOEi|L(OS7iI{UFg*VdMTWlVGd6mGB5k~wu;2p-#+H?w0@`#? ziUQY<2g@bxIg%N-hW?Jm!c4~x~td=Us}a|^U}`Li1y!b!;w@ZMGDUz z@q2x$#%+^FvCtIIwCDgas1C$JnbfeH;Q%n|?p4QQ+3&4Qt-)zT%%ZE6zd}4sk)r9Q zky_iUJ>S#+0dCJ|cww8jE*K({kc7~G!a~Z|YxNPirOB9xZlhI?tbUEe&zyM!Obg$* zZw(n6$I`BA3OdBWu|Uwf(Gq|4jqIv&&y>hy8nAbt6@UWbyy``3xV2+-jn#;(#A94t zwod0>{Z>xZmL?D4;a9(?b`$H-h>jOjP3^7V0k}#ehkT!k0iE`@wraLBzIr_xB;fM4 z3LQ7g)?>k!{i9#2o6#hG(8RsMFHl*f$|%kPI9?J)>aaF;kyhz-@faA~lta6&%ES2k zQtG+2bX}j#%$dGqw(@@&y8`Hwa{7f-Lx@@q!3kPi-i%z!C5El|Lx+j*E8u1VriKqG z+AFeqjx0-G;fry={8<>iQx^!>Rnc3f6GVLSvSi)fJ|SIzDOWVi$EQ1=U;GMr@*Oj^ zivi}@aw6i!pw3+EM2AAK)RZ(@6CC7404=ou4a#$UaMs$Z?t{ZzRzHNV2}WAu3nw3z zRs2Ur5v+F4b>0fxo_mGpPk~Lb5Ozc zX3LBN(N}nK!}51n*3Y~f&Cnk%kjJOd&M6ccv;hpo*~jrrwC&k!gl}xuZ##;6Efzu^cQ#S z{{;2R%&&L3Rj2j6(tP5%zq0I*5WD`g3Fo()2B!+iN7QE9zdN^7mY?Rf{$AKD@UTj` zi01koK!@96{;-UYx(KC-{!f>Vlz+)>`y;J;d3E!4MZngEj(KDi_K?b{KCSBogBjM4 zh3lLFe!&hHsAq%oW};%u+8^wZUaQEW+f3(RS9?{Hh$9i|!Z`pnWnbZ}f}#x1_}c*? zACz&-{0pU3sTp~lcdbB{p`*}oJwr^|o*(p0<{jrOwCkP$EM=$RMCPPPO+ly(8xR^a z(ZLH6cDq2y#Oy~@QTTbe}g^e{W8UxPY8p8^~GH!n#tx$_WsZ zqx_1ypa_)VzCm=;;~+7NiA+X@y-*`VtYifvL;g%DQhPI%OYv2x0si>YNX|Vied)6! zO5vAF7+D-I_oi~6=f+wJ5XTsCaw=csD%tGeSKgyt9kfC|`ooC!K`Yb~#D~?*Y!RLnVFr^W?Xg5w~ISRJ( z3Wvx9hGkGgTts;>oy}2-SLvAa1^^flpgX6Cr=g+vw$lzG3L1Q{;|0+g^q1~G+yrfB zO};X}s#8{W9I$>(sPpgbhWvhYx7ExWVV~IP&p4Q>h`R+U>g5za$tl96SdNB8n=Av1 z2Xg({8|#biVEU{_T1IvPH0c7$TuI%y5b>{;pVe^MOZki{LE7D-CEIHleby8!QW#eqo5W9>f~@=Vz-VvphY#f17q}r%#T`M?`p>m7ae=&rRR1>|7iXxOL$0_>Vtzfxwh_{LhrUGHIinGP&u}p&1Z2(4hsSor0+)(|_JI zlcWy9Hdu^^Nwss=M{Oc|-v-MaUU$hq3sV&`-_VfL2g>&8>}el1T$NMAq$(J2o;3cV zdCGE$$Y;1)qho#NeSxX;`7RA9#O>su`J<jeYrd%|9e%){F|3?3om$V+RClFojOh!| zASdE;;i&PVjJV{xgi%s`a}>{=^Ro85jDq-QxGI3(82`<4TrgW;J7KTX^=(V(=puy} ztAzNv`3qOqzC8hoZCobS_S@HGR)>{MB8)QZWGo-n;xB-N&7|85D7ye!YAES_GuSS) z_p8|SeaO@o#Ps-YdoQ`@epjoA9$i|GBVx0f+M6;i@h}a=f57qOMPR3a4BhuQ>;AcA zLkDxQKVOQHuTA#L$Ft4TUb2^3J@jT~Ge(??%m~nhRm;i9pFWaSohs9qql$)ewz|`M zsk2gfY!f%%FegYoQIG0sr#I>Sq_MciAnF2)<<=pWyRS!Qw*4x>Nb?~Upur@~zW&66 zR`FhF!o9H|EmQt`OV)RUjD^!rVdBuy$TNOU>)NRYV3xH6BG(JCOHT!dqfyo$T)FnE zgL9L$!^{!}XO<~^S;T~iWUc2~CLyf?rE)DHjq)|OKQSkGx`H1Tge_)f-e3m+?FV|l zXj=PM{Shpo;O%^m7KX+zwt+Ofjd7?R_B}1{s6OIB2*V}p$xm4q}BDL&DG-{kT5TE6A?s9?aMM?tj{?{Z>EJp?m7+ovWk*eD3Bo zaGv%^5?y=Jc<$PuNHH}z*K^FZ7IoOg>kU4u zEt;30V*b0>Pwf(|oWbW33f?_TkG^?E6xFj~e(S%0B=lhVq{ll+xCU>6=^Mrn*i6ei zPVQOq(tx&GvVriRE!U)-_IR7vn5$Qtsf{98*RsE6xARaJ9q@}0PljB05N-dtT2Fu6 zDg)0cQm@$GqMbDn7U^CnFNCHk5+9j zi)64dCoXpxwk$4@>tAbWG}Ld>!XrSWDG*&Ir?qyB{WH%bjT~xSqi@FLGh4B2c-3~o zy3Rgw0`FZU>_hnJ@*2UJXAWfi8U3)eRzj^;Hy-C#f@-|*S;0%eBCsDZ4#PN`8PlPU z3+)V_XmVs>bIXEGKX`31kj;#QGE$h+bQw7fHaItr_1JjiVU05l~bcW%hh-zuiR;kioyQI^dW>K0KC2 z?!&yW1OY0=i}8z{?Gw(Zi__`BJvnKgAow}U9dMQ_3du- zVYrY6$$}j_V^j5&zMyr0S_)2?86l^9Ns{fkp--P>Xe@7j0?(DwBi9pJOoEZQ>%J@P z3b}W}u);6KZC+-k6Ka0;jZPIBq=i{I3iiKVDYe?~8}-2_7sEm;HdTI~K%_}L>QwrT zzy2(}U*O^M_5w!~t|vfeo+x-qX-E%IpcKz?b*1#jaUBJe#xx;Kso9rF*P< zTmIW32dv3vR2k88aTiIDhbc^|Qjyab= z^fzeY_N%(`&gy=Z?lE$i*ydh!mSiA&*z&NWX619{tcp|B=#?|OG)o1x+=?5r1iK&9 zN|Rj%SY|E?@UPaN5Zb!E5j;u$5B-Go2oY%ik$)J_3l9JNG#ox;_pLR|tW-m33E=_U zpLba^UtKS2PfN=RlVwX}X4gV`foa*sNsmx?Q|>vonc)=p#ij14?Ic`O4BNbDCA&h= zTJ-&NRPuYDqTNpzY(>X=RzfLPK!P@}t3%;8w>aSVHvw-6 z)Zlz!%L{k0AOK|Td8vQ#Pl{-9&joAlLTk0KN&Fdu3yK${pr~8jh~es!mvgVRfW>Uv z&oJ$7Y)~zc#Dh})z1Rf=$Y}^y?MgvjQo)Abq*~@Z_EN&^nj=N@fL1P0|C?lZ*1Q&= z&$q)oe3@eDkQ0m!Be;n9*X)#@g(osLax~cUI4m#e%sq1H`cvE3%c9Cdyr+1={Gm2M zlZ|XAr+8X9OYb~ZN1eUN6IHSWc*HDd^)?m+p2PBs#OP}tVnLv_3!VvM$p$eGT(!ko zhLOMC|NP$I{?FGJNY>l8+BI8P7wdj#z(3)wYT1&*W7rt(>$3y1qlpSk5c$+5_@oM; zsj^!Pu>ULa{)9?@YM=O`=zHQll90z^RKi?DMjO2n`LO)Q4p-Lp&qFn$NPeMa(3Jh4 z1L-HANrI$ue^GOQz#~{%4sTA5I!lFR(Cwf#EC@IOvUfmXdM?@Y3Kj&Oq}ZHGu?c$w zz%T@;a35*uh3AgeBrifB1^@`c-}WW)TEt)ii(KBU1MxHm-~X74PH0Y&YS2cB>`sEsG=%feamr;FtSt$Z@7 z$oRf>`0{bhQ1<;+R`0uH*GNKyBT8_gOsvl{*G6E#17yqG|0q^+SNCa4RjH7*iBI?S zWmxy|MFSsz`K|7I#|Me<)*y=1V(<^Kb6iD1oFlekB>oPlCl{%s?e@i8dy=I4^ZIG8 z=lDku)_<5BP|OVAAbyWg6%T-Wm**ApGGXa?zgY|Bs|Elp;P4QR^tkb<^dAo`$Dy6P zAV<@aV(1_i|CapTA#L<|_soRW%Iif71{b-(NKjg^N)FsBqwjEJL|$4A(Rx>0$b$|G zR$WlPkfFVW=$GVtJIweq?)Sr|64vMJ<$e+>l68frKuDo-B7_>KR)ApbSM;?6>x}Oz zFuD!5-<-7`_+9j_9Tr8e^@jfxJdc|6LXF_5GlO)}Pa!`>VX`x8z`Hb^fJPr#W_Z?8#yyF&f+OqhwY`G*P<5=+iQ&!T| z37M5eApDv~%||Q<1oG6ioqxfYy@bq9*1uCqd#Gs#spEQ*@R)u`N&ySGt-cMZbtRNNZZT+S}!y~k|J3%=2-O!8Q9U(}a|N{?of zBa!)G4CGH!Jr%Z8_s8m-AcWT!MnIB0an=rC9;( z0Y?#4jY}(#W~Ei$-&I0Br(DoF^_XH5ghJf?picgrCun}{ zF3h8N$wS)E0{P4Yiwodz=|)|Mjv{zfw{3dpjLB_Fn z-d?~!ZZz$iZSTT)s{69@bC#0+!*YI#Ol)e;MfwO6-krC0^mnl|@3pOU2}?HJ#w7OS zy*<{|<1>`Mxz>0sFi8Jf0(+?*$9x{~U3l3hXXWHzuMzyBHhAaQ6(*#s7003VjKMOriBaT?Ag73vwPcm#Rd%s1lh6mS|b+ zBjo9v@te^~cAV7xC{_6Q2Kq{TxdENIT4%oAJtsA)5Uw7InYJ_R<*p`8{6C!&aXzEI zlgU^xEL@y57bUn%%47Ql?e=_pMF1&J*BE;l5oDOHYhX}~_#9)DRcj~YZqNvU;hnP1 zw{RtL8s>A)dT9>gi(!laW?n?_YA?=82pS=Y+SKk3c91lAZ^u|FeCSgUNj18lcK91IGd#hQ zlR|38hRMg!FskOd%(6zFtyzKX!3gTK3fsf}e7fI5DF&XPYuKs^-KL6Js|#%@M*?EM z;ZP?#D+njJfmLWQN-7wntpSBI(Bzcrh7`tgP<&qUzYLfaKjMFRMc*P%t{~m1x72b7 z36^JO*bo7S;?yByxwe+Wz7()e1ZR9n;pVP&6X<}+a1`osmKbue7Q$JaC-ImIZSRIf zo%Ia@eB<;p-^bRi&(YSEy-~L*6Kg+Kw64C3-o62ve-yl8D~Hm9DbWm*ZIyLw_~+Gf znW)Ur3O2z}g?y;lL0QFlAHkVNcUYxI9^ARC)%G+H_t9FF(BN}+nxrN>&4<3)5Ph#> z9Z;PSA^u>8$ouWmEPO+c>xM}Aq?4SeWc&NlfOR` z{~8A*$^*hrkBACaTSb_zd?o8c`uxM)7{Zj$7XKN0^BzwSWyWN(pT`CI=(2Gc2^%rx zQZ2`K0@=?6b6Yf5c+gJ4`!r>WioDr2;_N6VMF}Xa1MA~AyDyl)o=zI4@Y#^D)H2Mo z|EL*X!{1d&w%@BYp>Jb&{3P;~^O$zVpUE89b+G>~|9FU2=KZ9G$@DRf!;g|FOuvoq zY>h3t35p79vMN)$08illeNOqpO92r607)Og(o?Hz;kg*oW0Pe8yZjTu=@(vj+uom+ zw!U>kz5Ga%nOGqI_l->6taMUF#zufRn)O@Cm;GG4S!H}$0@}fS8OxoZ3=~@IdQiL^ z#HF0yT7ilq6!aMJPe1Srx+|AnUV9I<@>}ty!d}@y_g>p~^sT`9dlJ4k!+yNgGc64( zYzaqSp?xnlu|~@A0p1z-a@?e|qYA?Xq)1m%HXwSzXC|qX4I_QbnfKv;p}hHH1HIrD zNWWsJw+COF8#k92f*8{T>fLaMY6rbnK`qGVZ)gLgUjDV2Fzb-u@Qkye3!)gDVX;r- zk~kQpX3-)1lnRolreR9%SPLOTZU2Jbr%@md@oFIpt86d+SV?+J5~J)>mL9zyr8s>p zeye$rWz-4xbb-MdW5xE7dY2gS8!#SuP*UptskJ}{e;A?I$~1L_osFutlgrd?M%*G& zC&I30Kjqlm*jKto>nYq@eE%W0%q}A_matS+V?Y5#_$T`@B}RMlR~LB%WbDIcdOpHt zy16lqdg(>s9<2S+4i?ppL5Ps~SbedHA$k#`ofnn}311n*f`kg{iwDfF-r58RdW$FL*I6EhvE3Ae@KsayS#B(Om7nPt86s>{# z{I|?`Uk)GRuD>5c@eGBAyKv(7`ENV;FD-!2>7hP%Wcuk3UBee_*bEjHiC+R|+U!Z3 z=no!jI#%?X|NQ`c)E2FcJSop|Z^Lz+YGR}VsEF6iShiw0+G$nC%6XQH;QAD;iy>9Y zfZlb&+XU>tJx<7VT8qGojbP_r3A`F<4K7G7ds->8HA8 z6Dk384l&a&YYe2rshlnT@6kPHXH{pu?GxXq)3NEupRUgQZ%+Q{+uHecm7a@=U7607 zj{p5qBNf*QBM|zH@c)Ks5=)T>f0MAhD5VF?_0yEH%POOGjbRdT83pC3Jrj+Df^F{F zlyySS%Q4lBGQ8MjEH>vgoA1wU%3G<4M;RyyBAv!Gbdf|aA7aug(>hk&`VSq8itdV- zyXPEut$2Ma28)yiIlP>C^{7AyZksv!Q75`}d(g3?f*^E|G|2GjO-}Z9>OO|GK<)TW z>Zt>zNbM}WQiX-^6C%IVxnEt8yG}^6LG}_Mtgr>{4#-vMeVV2wlF-E$$b~h9@vH8f z=c?eWtQ{{syU%L5MZaw*PY4|as7s5}o*~ut|4*P!GVYJbleoiP|KUlV0%fMY#reMO zs`M^wj}J=s={;hJ>t6zjWrQ6A$uAE60iAZVHcqCKFMuXZuKpM>)Ofx{6-ZF~t}`Ew zDwb&BtbTDJ4BcNAyw}ya1+Kwn<(|*>-0SMvf>dM3`Tf9VvBtN0NnPry1RAQr+XTQ) zmHsKI?gD#!D?Y9D-snG>SoV6p?!S7SJhu z^+J1qF=3x{FvL;pR$ofHc5d>W+>#Ci9mIM6LTzkL>CKxe!N#DHnu_={b%D*i^zdaU=W@gQw1M|#XMtzEZ>}}$}#}7^66@dn?NEGhc$?tmQgXf>Fb$@Q2Pbj-` zvc^&kdWA>J%{-^%P^qb>*Y&$R`trqJA zP@bg3GwBCi!E)p(66z6Rgfz;`6K{wMN7HBL%(9;bqok21pZ2&9m~@WeQiDTUU1BPN z6Vh*t3!_67xJrM)O&SHb3FA0Yt1u8qs8G2?Chi~PQF-=?3*4HiIIQ|=9L$y(mYo-h@{e92s9OcAO>M~0aR!_14F1=&TOu3zo$elwWEE3#7q z72l(#Uw{ycUqUGk+&6Wilq~kHy81tJr~T91*+5DKOuUM8=Y1_XI|t zmSPLIedg`JS%bPDAp=jt%#R)tat3La#s*es(?~sMW4ySx_g*up6RD>3=Y@ltF|V`w z2{?EFqAn0+zElMm$Yu{2d1Ao8142T)P#Z%&V+8_b5Q+LN*pKz-Qs3?VYjP<5A4CeF zIPvd)BKE9JJUzZvV6%i-SacHZOZjE=`0X#x6)2Ivq(Xdd)K8njcFvtV-944Sj`H;i zZREf4AA;UW?1&0}v#X<5pneHP>map;LEr2K9Ni$j(VEUU6Tm4{3Vspq4n2T18|C=r z&N;=%431xVharA9K*$Gu6Y}ELPC%ml^dgRF5J6nUPj8|V05wFg{7IKoa!z+SkwHj2 z7RS4YPoC1$E_D_pw;zw^T_`dW>zBI<016@DXe{@DCevC271}c7foeO678vej2R&OuZi!ySDBgp7E+Rw;u*k@UE$*Q~ z>Y=o`dnm`UCC5;Ui*cEtCRv4nX=4$MRNz!>CAow;{D@!?M!o(Ibx0Q2D&CGji(=$#PsXK~Rb3?3ES zp5xs-GJwS+RV@W9ck&5Sm#_p0>L_4n0F8$$n+pJvR;C%lUTRS@nZs&gpr!EYh2lt) z;txpKizM^Y-W&B{Wg6W$xt^)k*H!JS+s z9+ILfPDGUg+rKt?w1NFZn3x&(B;l61LjI+PmZoQrIg}8RAtMajO22I@6sE4&!vhV- z6p*OthK^=rykh95m5Y7`_gumql)yP01m^?prvj^Piqe%E`inijtKfM zRJg~b`)`!Q-9&@!M1x%w@nPxyOC@eAf*!zNL6n8138A4$tPQ%MZo0sdmf_+Q+?q$% z*rsrMmLa5#oha%P#lMy5%f?9r?ukzh3y>baT_)MqC&h$0q$b{y7b?34*fzGXZR>1h zgJ>%`v^-eMJS}~jf$#3eCP9hT#sf}queHAu<$kO4BJ#}SO~sui*Z<5 z`g8t^p)%K$y6#7BQ)wTKLeo-TaGwuWD76Ro7gUq6$4}~^(bc4}SQK#hf{mX3D~i|} z@^BpL%8}JZ?%LV^H7gtPH;NK3L&`O-Xx&&G(}4m^0xBY4HjFh&k1^MfS~+%Wt|$OA zK+M0d;pMiFKXVPK9Gv4aYQGp&jrq5W;;%qf3Z=$}L)Y)Gv59DO@^k>bpB|0JY0&cV z^{+*g#+O?|{;E%X3tP~<4tCDFFo21~u4ya|gBvYRo)Fw{36IEi(4gd$O2&imaFlCG zZR+jPH`8$&Gwst!+f%o+dnc4JnNy0IL~>14bk&SRUc z&qPP>XzpmJXh~-3-$>4oUX)KJgbmj$6c>R>gQ=u&;?5%HLOC^FW2qs8#?g#yNrelL z>gg1q!tp0RN6}h!!mxmP^B?i3PF$;cj`Ekrh(Afxlc630xmEnb&mWv_7c_pX|FC%R z{LS~8pfAflRJ?jVG*2>L*|Fe!;#co!twiC5uX=4?&AwDn#+6@#v{wECc67Vc@|*OM zNKxNBM4;gCc*^F9CX1bc^=$euKD$WKQQv4V(3%p)MKr7y_8ch{dXuRmuSe(!XXR*) zX!@*924J(uEqYaz_o}8i@8wW+aq-J)xYieyX$Px{R2;*t42I1{k5rxxtiYvG^je)x zAC#S(ZLSH`62<2O(>34iN#48W)%9D86rW0(9i2Re-VGUaBZwO(e;Y~jUlc?Re_ z#a)aI7WMCAlewzq4g`546Th+(eq~~oAx_TrsnW|ggJl2ADUo(4xw3R>I-oe7x^KGQ zUieewzFIy$dmbWU(N)uMDV?3q9LDy}zQNU#x` zQ@H=)+`o*xDu^tx&xbRkORfko_~^WfS&>q%IY+ZKFH9+gOX7z7I9n}`=`_^PoiZZy z>2q|OvOryU%YIdn%x#a=gT^ScJld)Qsm!>>;Ww2bA^PiZ>g$hhEhp0nnOh5GHr^<0 zhn^Nz4NQ^xO=D{P4;EAm-eFDiO+|TGV9CCr3KyaToK=;^cQNJ>XDyopP!a!s{s*m= zUf=;0M?tJj>LX$YIv!t(ewqJX4XA_{$|kDwse^)YT_SfnmyYl~!SW$&<+Er|hehxw ziDBO@Y$A4pQtn?q!8soO(z0c+wzSWoh6v`v1?IpfT2u)@F6E{Tkp6#TpWIt|oaDg? zdKQfidaeyGqa$7?HeclZx63A_tMc> zivVG1Po}*qgpMkHCeOilLcQg!bs3~!~KPZ-H`!ddN}!eFpO({V3} zYZzo!9DRAqml+qHXrzApKiWpVt4gOfpkTPI{n%`Lp-nfhv!p;(_AJj^KsW>5yt&BO zg3!Kmi{~icGWtma4e#5QU?kddRljL=_Uj-~jW8@xZ`ecSoxox+c-?7Fm3IbY9)6g4 zN0Ofto_b4usJc5p=rwy9$?xJ}&-26yDuP5MwF=ydo!sNRdEO9$Igics>b$^N4v79d zg&IME*~3BG#QtpSC-om3t26G3s`3A0LL#D$YIg$Lq{pugaWv7icj*vS%uYOv$wfoF zsP-QoUz@e~pV8m^rfp$@R9B(m-;anYGlP7FB4+em__amk=5_J^u;C`FytL+NR0xJ> zFaXk)9Q+j>5jcCSAUMR@t1qy1OXoW4ZeWj<3WpFH2}u;E3P4)`WD$^AW`VCo5JPOu zWAKa8lgMgS0G|aDtpxZ85X5qaB!2T-=@TQR9JFl_Yr~I3zOw>rrPi0gOpMSG$vRa4 ziG>)}2%wuwOuOqHjGtfoyVpd`3#L*Ss05FEk9#GZdmyyeZ2fS6< z0WUZgmgg`U=pOAECUwUgvNAYfWSdHLtsQr>p9W{{m^ZJ}u%G7ur+D$^M~cmxAP@1* zB%6EJSaZ3VmP#C)K?|mnEGi(P|1dkuk@;+KuHioU`&dnSO!!SL_{<oYRZHOfkW4QTU`=aMcuUGGkT`c4xkd1;%;50Xeh<+ zv8dX*z0ll;qoPeOt+TDSSTmtL zW->gCq3xqrmE{7$YC(zxSj2hRIXcM58T%nF#mf$?0=Xg%hQ%hE0NqdR;dQ4qKZj)&Q3pQ3O8rpDEoc>3NZW-bm z7MS(i8>+`&sE_hb`|JbpngB-YV5{zz=1aV30lxbL1RsI4@A6dpDuNpn!-4i_V9{I2 z2$n&@qWgZ|WZy^gRy1*RtEZ^PCx_0G3rH$Hem7e&@1WD=MfUD}#_O?B_x=<5Q|s%c zy^}CwLz+^e>W9a6^nPR)HEqlf+>g{C8rwr#;Ls|_R6Cl$bwlqg2p*XgWtIFiJdpNJ z35@+L-;6=>j2ydXJ=;(T6j)$B=I!d^_QEaZD72P^bDtxO04I5B)P-0PO_tq>w#cPi zktWZfHFcWaxtAlU@1SF_q>g3a0;JrZ@*b$l%4k37BSLTPkohXQwhUhy)EnDa8Kx-* zyLhrk4H;+jRoy4Uc<*6}{0r)j!>0PGU>FmEb>#5WlOa(yVgbGY83v@tA@yuAu0AoJ z^xzec!~N;tNdjgo3WdWj>`Mp<%0!{+(cwH)iSeCH+tD#=kaBc-OI{*y!CQNY!64*vgZ4Mcd}8q50h7X~sA%qwCtQs8WceE(FGCrSpaI)qe5L zUqeHI*)~hg@WsyIOBzl_+YlXQ@y*08IMo=s!d4A`!FW9=1aaK{Y_YF)Hra~kFpp~_ z(qL4>Pv+p($fCp1gH%HbPe6qk#sMLp-%eHK9bf3naD)oo2VE*%w*_ z&JGB-2?`&UI&%$SRXRbyaZL9@1Zobd6uFw9U>F@qT@S}NbNEGJeE%3MtG9}f{7N&p z!Tw9Kru~(X9m*|qZ0*IjDL@-WKdP9UTE0u82|PXfZ6>dL`sf-M7lvCU8sxTRbH|T7 ztEc&o%y-c{d0K>59#2K(yTC{#HaIG4&?(-W*UJqcWe-*QoVAd9>7J+=f5xT z7h=)pOT%D6u@Ffi%A1e{U6@@QIfY9{DDxAv@SqG>UoASCkSE$b_evjvnW=HFv9)mE zZ;YiM1KT;{Tl5RyW|C@Jxm7M82(wonSEQt^*rl}yJU;tvF0W$p@CwsPaaL_s_?8?( zT<2^j&9Cp)GsZX%P3Y<4smMH67`DlX$`TH_kMnuDfzVhYV8@yOkbG1t0g=Og+AE-d zgw3{VL<3*GdO0JE1%T!`W?#fhUf6ywg`G!Bo&($xS*qi*$@)x!g>=co1)RK9xNJ0s z=dPMBGQ*Mxq#&x)n@fiCSp54vYR;l5tg2PjdV!E99*L;r9okf&^(I^jUdx@;$*y5D;sAo2_u;BWTmnhM1wov}Gwx;Q2d*#F zOb30?T<=-5DzeaNIBI1ana}Zh2ajuG98UdNPJZwP>l{lX+^0GmGsPkou7f#ZX)N@& z(H+%)HQ_eIWVm?TC6Pq8!g%Md5uwgu^T$XE%bCJzxbIG!6Ic^2mD#K+Er-OgqV?iF zKcqSWMNhWJUh$CL1Uya#5dX~m5I;AMX@wmKoMp$prwAZfY9ZchO!@@CQN3>S`L#`( zl}!cnyTJ18mSGBIXrzuUW~EOfwQxpW2*=Pmg$jevv%~5iVKq|or_8|i-Q_0Izc&Ll zACl_Y-+^?>>>s6+M+2?v%~Z<}TK|jnJnf;lAIfPD2V2)MP(7BJYwlU9`C5S#T|n@? zPxu{pMWEn(Q(A{ppQneIQ#uf|so82V8-V~d7T8+|S0I#q`2mKmVVDx8=q&`x@Lk1& z*AaspqTt!CGt%)(bBG}hVem|cBko-dADc!!iCt|RPuDk+nLtsS+y3su=uXnb%|QK! zB*5vx@zSrC?f-8Vhv^+U^F(|BMD;9 zF}5`|6S!NwF>+nw802*5+-OWD903LR#Yn#v-3FIJl!l)`c#H(FJ_I08GX`&_CTAwu zGdeJ|naOT38bI*2P^?+&^z+o0Pt0lkAdHE@Vzn8Upn+(qFP(08ke^x_x-B5ZO=^Y3 zvk9LC7gW1&p}=A6QchAu)QwBvRDKxyDgX?)q;V_6>fMUqON-=eXhi7(iMxbSz~JCW z=kv6Nx`M)Sob?iS8IoCDou#Y8*?DLtpqR@uk+`IJHZ%U^1AA5*tT8j&%uX9{7+63@ z<0nw z^=ws5<4P6%g-{|0_@d_5Gi9hs{A)>22Gu*a^&u@p0y@^ZQ!4a!CNYcUyE2x2K-7fd z-js?Eh(RRS-eNL4&j_(p0S{Z2|?t# z4EDMr9l~)t;HO%pT#2`3db^4vi@JIOH^LbJ{GvrA)!$09UqA>(0zFQDdhke5M4)T-^zn_A3r z`WrE4!RyDpFt0D|%c`hzI=Euv>n_K|E03z82*B zUug!`vfD38YHLiJJJ#l@={Q^=Y3K!y6tA@=12|26@>j3>Ot+{Z03vWSe*470OJP_J zIVMhb;2Ul@$h`m}pO;j1LB^>*Izsf9;h}-&ml$SDg6^P|Wc}@d0nna0UZ#|5`)@h0 znvX5w&7;5zD}Sp$`9F8bH+%%<5#aD5Fn=Ngq(?q1*Qm_^D-A%o z1F)+JfZ@N?)1XHHD6Gz1gA?nB);fyTY!Lr(uG?WyH;J=mF8U&TkmR_q(bT_ueerki zb&E?ueav1^{SokJezl|~CtvNyjd?LJ7M$tjX0Zo1ndv<4?tAL+92uzxZ zz@z87G5=iCdRyods;V<`ctF9)CG}HO{b0X7S4eTy8)oOydw`{aw;CM(BPdj6#2vP2N{Zz>bO1$!VRR|g-EIy;Ec1kUTe)AbwU1M2&YfAIO5 zzl^?aqQnjA?6-cdLv!t18TW~D6)2OIdGb&!IDO_ndQqmgR8l!#XrX>WD`xfjwNI|i zbK}9QPYk-VfgyvzOHYiuFXJvBU4LmfXbfrjDF^I{VKg z(@CN$L=ukFdaHk_8&p5MbR_!dFY1wX;YjcymBxX>{?m%${w&tth3k6-A|>l-f!}L0 zGS{2&Ssx6|1mj?^#ud>&b0gt&)@Tfj1!H@-Se*W7q$`5y;mPu(1w!Z8S`EyNNpY6> zdMo3nl4#6gm!MW>e`|I{B8^qz9N4C7jyKOUT$O}-X5fTm6Y4SMS^fpGBFN6_BvQ=J&Dq?@3i3mG2CDRP)LPvrC5_D z>mqxH_gM@8=8%0$O?^2)TFKI% zIdH_z^tF7`^}wW8!0`%>ET4mvrWt;5>V?l5RZt7*m zB%DGwuLx_HC67a>h9(+i(ihH#;_V$p!xlZhusy4RgGQYWhEOeZOiF(UoFZ%cEG7eD zzc6f_TN}4{TQ5QC@Zft;7wVr+i-#bCDC^dum)YzEb;xmEp zIr4By2GdFFBZKT35gBKcagHK0BnQMgD%_R!90UH9J$E5PKF+*-(n1tx7f0d1J%-3hvq?;+#_JMOHo8;J01R3Tz~2HC zXYO~uW`0vBAJ8TLKY20e8B;yUeU9?egA^fE>3XtocyzVjtVda5;oQF;SNPg%B`rf}ZNl|~634xmh6$Z;khLDD$h zIDI7JehtE)V=4oBTSjyY0Y0a39X+5)xa4qiZscsY58=5P3fcDqo|&>A2z8=Gg-?j; zMT_XgCsg=O#BVIDBc8`Vp|h5ayiBaev?M4K){#XZ3uCWLfGs>nZ|UNRfQS!iHp}4( zdJM&}hj^beAJE_eC>3amKS@(Yl zuXp{@*7B>+EPQv-u7!?ZhNta1{f+plA53CJ_zvwA}2iINV^e?tx#vJbRMq z&yhyq0jLap?+snW^R*Rv zrM;9eh^r^w8-HUyIPM1WI65TWGW`cuz8Ra}|5rz4;J+M=@w@d34;6WK6Bg}6EM1XN{U!C^<6(J$OK3Rob1ZBu@5$jpv(rntYp z7L(%mc-XV8MqILm_lA|`IRELVt>zm)F4cC2ek<2JCngaa&J0BXyJdg`nNV-dDUGot zL|#k?sS-os6$~+7@IZIms>D57y8lK+%+e%oFWrBs!U5c?KC{I~|H;tnoa?R02W*zViP$oNeu-@QGW4pN#_z0Z;UH`MMwC$BrJHwIDXQyKF;RoTX?W(a; zyyF^11sY7h~B?f{LsEQTGFlD|_H{wy;&TWQVvu*Q;5+ zV~F1d-l*ieC88@NMVu9OF)>42{4PPfWuy2eLkVTcSxm?j6TbHF(QZGM{g z?zFj-(S`x8CX>ZAzh15;?>z;n2OEeTewu`~^fjsV!;h3L)imkhjL?kZA4*KuqiqM7 zR@(bd9GYzI^I4}TR$%c6LDcxhdsHP^-5-LG+baHE#-vBTspD%PDaFcWw#6j1 zxIm>&MmYbf{(qyue!qRCkYZcT>ePP>mP4HZWtk>^h7p+mYHix2QPRFOufk`K-`%MF z(thjO%eIh%7Xl>rVP3joGo;={Wq$#xN0EaYFG3DF;tyW#P)yd2-5_RhRo@x+&DA-n zWcE5YJ4C*C-eVihz2PYPu&b5O|DY{ekSLMizz#;@5ADQWSI-$;k;HV6=(!uAD zshGhbgV0w)_|BH*!HwRJ;xjl=1K0gIR>VYSd`V8Q$rHuWBx1Rly>x@mo>L?)1879PU*8MzqPsNcjp{|Q`PqGz9->%^=4(ah5M1i=Ocsn7NgIzX8KfYaYz zWfLusHeVV9q&W+ZV1j=@W{S6d6NJ2^$?g*`mxO0s;~R)n{|T-twS?ydv&XnWUk2wb zUwR3|Wk8}4>sCT`EAhf>7<&wWFacj+LBGfilVA=Mx&R3)Gs3KQ0NR5^AJO+V{0{0_ARy|R!tlACLaAH@!*Sp; zy`&VYkQ~*}@Inon(wcRUvVj8++roo| zerftKnoxUN5Pirr%Z$i)XMhYcO^H|3u3i87UUv^jD<4%Qffq&wwF}VE(8u=LnkYMP zu0SWc_@$}G8>Cst=^}@1fd%wITFTx{mxKpr^DadehU%VfhPY1{1O9>i95Mc961+s< zpEk#RvAPO%bcB$nA7W!U?PxR5tjl~32^pcH zfp%|JNBXPP=KNF5mZFHX*ZAZB-biyGzh8O3N9~urB4mb%l~&@2^jG+lEB0Y1*xNg3 zAw1P!DT>H=LClGgb#3utb*8dxxy?%;~e3CtCNzA8S2eL=Bq z!j1oCnhVW=t2AT=0ng)cZKIud#_6rc>2LfnVOyg`1S`v!8+BclgU5V1ZydEF6{2MW(NJoT0Mxqwa$mC z%k_~kP_m^?o`7rhl&SV5jn|(4UW25`Nbt0(bOr0!-_C&B0csdH%|>uSVi6G_e;jV{C;x77GI6$3guiXlYMjQr&uK0ZdBBmD*8@s+9$XuC6!D3#$zP zrXNJ_jESH)$1tA`3_z&5DtM35R`)(#cPArQeHNc4+z%2*?fs%ZOIN6ukU@4^2GWjB z{X&1dP?{N-N$1M&V$wq|Z@p|*wX#6Z#p4pWT$&fSSq9|+E>LtfhvL2~!-2Bq04K*X z3cQhi6Qr((e^f;iAB`3>J<@h_3xsw$qk_&(-p2fs07uPTLdflhVjL|k2Yu#X+nwlU8y!4idmRhkXR$Y~tiU zxLgMn0(uE1TuAtpts`btk8+HCoG+LcQ<+OnjGaM=`9Z=0+;K!RoPhShUTn>AEa4C75jYHaUYv! zq3#8xhsL|(%#{G;-)-1iou2@eWwVKMTU`_ZqDdG;O5vn54MXFa)FH$a?Pl-hA%p}* zO0O@jc0U*b2f>Ku%G+DY6*n|?CPJ+hJ4BnVlnJyl2p&er_7KRFDiRKYO;d@b3RN%` zM$B{*3Dt7o(D}D+@VK!A#at;}Q~+Oj9zrb*7mAFwqyndugo%YF>yAA8 zqcMeY#BmRVQ5hl>7;I1~j8YsX6q~JA16io0shg-JVPe4^M&#g(3V;=NsT8*Z-0tOT z;jz@`Fz@|AAeSEt%!>Xe_JC zx5Yis-XcN<7U6wbGcAZcz`=1yS2%ZPlO$oDz3AdQTx#~S`C49R+FpU^{B$^Z1EsxS zkeTXDm)QDJq5{SM_9}8INRe?N3hWsa+N~b!5Ri{fXTn*KdIsc}WzLnwSXOm-Xpw4g z2>lVY8B!0tzK7_#JLa6zKfDNv4DP;TcG%7ABX^V5X2UH55Od|}7$HuP$eM5ny|G{a z=`h@X+~PA$Q{$ACmS>Wy=Btf_;+X*f!4ccEfwFEM82H6chQ~FbvJ%XrD%)q`$@h*~%Q(yknWW zr3xlu&=xQ#R>1_}3*eYPG`b(hNd4Cv?czaHa3G2D0CYCR)#0DUO1=Vl9XVoRw=MqR z&c73e^*nVm;};UWG|F}`d57xT@FaVotRHw|flQ2&DcBLYHTRe~%Bu<_DrI*uwJQiCwfEB%jOKtnSXy6?=@V=>Y zs0#oMrDv;S`xP8MaUa&;l8~07lDy2N<-90BLVz(#=C0%+xu!#MIiYyInTYM8+!D;P zat32UtGrzIT1G`*#o}+g_o;niZ_b*!&YV$yW3N@ib;}4w9Xx~PTXUH*Ski!`;9Ujr zbu^(Oz7KiHCphzPX_F6aa#j_(XwJ?Hc!Z76N^2uP&~RdnRtYz~&EJ=t6=Kf%^9bUr z6Dm4;^+M$PL~AgHMew$JaoHW~lPiGu_w7)vpqo?dw-`a>rZ5~n%Z{a#C=o>eOC1Ve zy0BW+j!vI4=6cR*S-IBhX+Dk_k7fMRm8{E0o4SRL>3uXrl{F_;6WHOh8QBGt7U=SJ zUPo+^1`$0Ra_H8)ZXQ233%G4sPli$n7FdoT}qdt5HKf6LTc-<11zN>_SK}{ zqV;m!K!pt2nQZzoFSvMu)U)+Vl|S*xa3RYrZHIhA{C|4dn*h7B?NB=i2o3%~nw2ct zxXBnKUPDlIbc@D+zxyh1Q#;)3KzX}Dp)8ScgVx7--qCT@9lzs-2ab+F2GPsmJ;~kM zL%5|SSOP9*>A9MCt%rCc_8S~#g2Py5BT{tL11CoNcSVn1tBB9MyK*ELc`v!!T~Wd3 zKHXrSNP;KLU4qNc^hY~eTUHFfSd^FO7>2{P9zge!6T8my;PLchw1mz^gLz?P_MoCN zIr0qDwZ>hy(GxtghWyVCiI|u*? zr)f1S_C*Ga*t)YOsM-$Yi)j@SeJ!8_aM$su3oQNOb63l17K+od4@=Z#X*X@ul!>eW zk>dl`w&x~zS_k1;L}PpQId4Zl2t|Nb2xA_rzqVs&ycW;vfm}a25yFVOBq|B6T%z0t zOX_pTRfZhTQvR3Uh?wvwZy7A_90v?f({D_BjILMzKUY_MYthY&K8 zQ%cA(yQ^K$_GmVqceOj`;HJWAsvJ^U(|NY0obdpRrI{%EQw!K4K&)8#1g}^1cU>qn z_PKeBQ}hlN1c0Ceq-^L1WO#Y`MNB9S*?qa3F#kNjy^Uj<(YYVEMY`6zQ(j_)=;7Ps zkPB0MWv!CQHA1SzRE)mM(Qq0QlZp(19Q3f*00{)*@a!{`b@8c*P0m~V5|_2q=0&{k z(~Blxv9*@#KXy3J6NJwb2az8>k5v&o2N#rC&K;g+NO|}(Cz%f=L8x&RWoOf%+sB5F z&Qy?Cs-;WvNkEL1sX!oKkdt8b(jh@pRh4&B3s^9lG2fV(A?Af;2<2P2^DD>$3W0@tn^RW5-i>*Q5%;1 z{rJH(kRwpxXGjbW1W~)#Hp5&{HZoan5%O|k*vm@!1!@zx@U6b`Wq3lw=8Hvp#g%|a z9WeWJmo2M6P?sJuAa+^!F4A_@!8}q6fA$lW@nJnCVeFs%*B8<52mv=GjzXA>V0WFx z1}Bfn!i5yP-NVp;bO9bkT(VOD1)@U8q@Vid4Dt`1V*aJSN?DyB#lpC=s#zg~&+dZB z7EB&Cx3a93qs)PIQF{$OOMeI;f?J3B?jlAk9Se1lz@+9e@%iuNTuW@t1&H=R@o&9h zMWH0zE5l19(p^|2WO+&?3Z#l$H#2koJ&p%8`@TjR2Rqkq`%# zH?UsEYxy8c=U&8Q!O_EHiuSTRKl4_Mzwn8UE~ThX{T{YjWrt`D8jQ9%316k>H{M$S z9Ijw5;DrK19uM$^=T~KpHACl>F)l?%oxwMVNTP!Yuqc^()Ku~aj41%(vU(d!(OV@Y z+Q`5>)cBlgWR^~R(qGK*NZnP*Omxl*xj0?_uE?5s?YwaWEwQ22>BxY2*O{4X5!C9e zyNkhDB9u&vjuw!Fig38o_F}V)D5G&m_1c}@=6!hLx~ z?+K}ZP^P!dT{PCjJ52?BRRb}no#ves-fdiEoCS_Q7}SE5C)sa@uR>eG`ETl;h-@1j z;-($=6NszQ_+YZR;~2c9Z{c$UZejLj0(nkQEPb3?}c6*h`h5s50|Jy|ap9rs(ozVpcV7Xj0W-Xd)jEDVTQk8>X zQR?qO5|ln7%{uP3#a_92zreWNzsoY^`K&9I*AEJ1TM6SEhJ^MFu>TcUuKU*VE&uc< zad%K~wc&)=_oDr`cDhBXsi!8J%vrs!(tcOhNM)H-V=-ABcF<0o&IC6t&wz}+Ozh7q zYu*kCAW{Htta)Eq`~f5)iVrf+-8?CLf(DE+@tfCwNzU!Oy>WH-&&$2je;=OT?z=Iz z10p7XQu+u_D_1dI*H8sm7=vQxo z$KBC_5W&Pf$!_mpdyi$ zu{ywRE{|LEuB;X`_Jb561qBTZO(GbJPe&JqU7h@FF&cwKZRd$S{ND{eu~mN8&y5$f zyJh|W?@Q71b!zvihp+ZEF5$`Ha1r{JpW3xhy-K8}!T4O&fl}bL>0li+!VMu^Im-6^(R_ab<93Zk-Q0t`ca@X+#q% zwPVa?t92u_{Q5gqPj5(=t9sbImdI;WkdD6t zcKFxyl4=YAn=-#AHgQDKOeG~CLJ=UtzcTwI%@xtX(Ta=Lz6bXw-FSNVG0 zL#-oYH@ob5?0?iqs1>n5g)`05(HGJmLEHf-Q)Bv=t-0FPvg@5}QkT4rvVkW7fiM4Kxss}(iw3`*tZ7_(( zc)O>`-C$OxpLWKQ=O+H~r#Q4y;mv>EG4ewzq#wW5_`h>>6F!-c2z_}8ooY7~xhiEJ z=_U-1C+Tf$^>C!J=wgd9f;qza+Fj_a74+JCGn*b)p)@y@{5eLa3xFRBe_0& zc3t6k%ZedLh;i?1!ev46XanjBNscw-p=%uRmAXBN96M^_HR>w(5%@En?ur zfHkqzoa1J~)9@wQYKudMhMe*3>sbuMvpMgNi=$IzH*bzvz#PAGMVTwDwZ@04&h@4m z@m1Q$qe^W0x`u#SG&Nj8pQqM8B+BBQ;VBfKvP|zL6)I&K9H;{(xS2bK!!_cPU-?xEY$(JWBM{_)>sVAH z()>aPAkZ)vt^1FQ6+EKiHwe(#Bs}Qoz`l#G*Ccwvd4|O1-YPbrw(-|4Y*f$(aa(q` zZqr)$P73&-`7$@%xTa4Q!1kc99aa8f%q7Cn0qmLRZon(=cA*FWq5FoVz31;P7LJ`( z2=2L0Yumz!6)HwWwsYYW2rigwW*H*^p`6%2|02cCb+YYegL%~Rn=Po5Cox%|y-VdD|hQ|MUCUx}Pv>~oVlAgTJ=QqK{GF9GVk z&!z*Sz#}|L!H_JT>)>oX3L#KnRHVFBuPX**^Y1Ip66ev@BU|&5)gtPN^HB<}yX z-93#OlIQ^Y`J1(ImsP2+sgG2wJ0#*OhA*niVsvO0I7!-LvT1Z z@S-w&6B@-`ot@v1)$Rl5y`^;?8s=5L$R_4!C@8xd*+(L6X!SG74H@W}#Z&zKa>nJ0 zq4H=+Hd?3!;DlCe5d7U3b3=aw%|!XboQPc3S$tI`%rLv=Ev! z1<;K3f)#zHp*UP#2GOWkM85rY}0y<7+?KpHL{Yel$#A7 zF&9}q{UWhY{K`@u83QH36i%{$5;4RABj*UYV5W_$giuh_Qb49mH{#0(#ktJ@)5>#u zI3bFHP#Ni_G$K#{Pf*6B{8#^c2|`S!d_$vAcE3pojjtF-K6P~x(AP9t!@&|;F^>L( z+9WNonvWZ(BIeV>5h_AKZtOf6ORl`$iOa%e<#yJP>#lU+vT&KH+rX9?v`TvLVUo=K zri49qWw!Y2^UPcA1-H}5V{h(-2V@+$N=`gXRGhz{!wAz_bQxlPRb zQd^$B5wYwiTrCEEk8QrKQ?%-)P7#X%UIvI{e=gJQvBfDtt-3R?&C+lJSYDm0O+HU; z5PDh3+C@rmByipRIWXCM#_XQCWBuF**@F_V^yh+?lK*VdKCUxKJy`k*z;cFoCztVB zI~M<^l%y?58W&hGfVcAOR$ea|>HBScmA`-0$Iuj>45Ib{)bfsm@fpiZ6~ORa0MNTC zv4&`S1LU#TZ7dIk`B&eB8_5D+hYPYZqU~llbvZsQ-G4u);3a;Wa;{O-ZxL^NA ztzSsO_$<D`%eHs*#6Rkb=Ea?8`04cWgC)Bzl1r_Jc(CSUv4?qLqTYpx2%9k7$mHj_fDBW8*)NMMj=H+pDhOXZHvN_Nm{G+ zvBU_8+^ubp@C~e2PlB_lqXp>&6Za-GIXhYa zP1nc>fs72B>vdHLoyBN*TPbUK3JVk!9y;6Qs-QF#Vr8u)ofIwmn4qYrE5;rdmB^tY zXDewVM`49v!jJZDfTA;gUg0LSLR1 z+K6QKf$=vE4}xqIHLJQB6Nidj*^@<}Xu6oTu2Hwv0FZKEFE)&c-eR@smteHHR-b=Os>75Rr6qY4ysX%Jbt*RhQO}X4mTBH0sBzt1bYc z)a}IweeUp*tz_Q+>kJs*C-D`Z`Vto_?G<+q+y(~8N+>0;bF+n%>?+cuRLI#D)%Bth zFtK;*i%eaG)?C@Lw#CgItzqj7*!Tt+Ykd-$*ML{@Ub@uk9+32Jk)+gf8n8+Zu?Hse9@E~IBAZBWXJ`ZpbGvfJMh}w* z3t^-WM%bvWO-&`81q6z$CeojY)qQx3UtecJ!FANM3aR-DgMbX>`Tpora;2(p{i}DE z``cIOJqYzlfD4XT8X;=sGo6SdkF zQ>Ewb0c_elPqsg%&`=@`?|Uy!H9x1*P(t;udUs9PF}zSZicKeyY{%fX!Wk&mP$~w9 zD9d{P(YTgXJWb}eCIUA0JhW2v;I}&i`zh*t92DR^Pt`r9wepEHJn6pJTyudd|H)z{ zE1DZE15Q9f{jsU`MRtloPw-g}_k!i1c{K@-s4eisp1IAfA(f80E|Fpat&K5J*bRKaC`3^xa0-EHg zAh!{7OI6&DV`gkm+iIRh^UD+dkB=YNsB0(zocaOlAN?PndM8uY3I@>s`(M=g7>G;` z%KCISM#ASncQU!`TmmSr9AyT;nmiZRmZ=tC@UbNe=4=B_3xBZR9DM$bSx^%X7BGCM+h4*y zLpvc_gAdSyy1-%wKFnbC)1jE+wYU-Y%DEhrtY1L;w|$0aKHRncc#9-J-vF8za&Gd$v5R)fUdjweKM0b3`!+G; z!W2ixO>T1uX|`K=&feBmF@cm@A{30L=*=$OkY8Bz1ZjL1;#P!s4o8J$2Z|JO1B?x$ z=Y+^bN-IdHY4c z;0sf?u?1;=^M*ZT1{e#<$W7wthtm+OtU%yW3xX?}oh4$j6ut{ zaaoCCX)hQR6i!XvdgQ_u(>=}D5aI3$Zh4CUk;Z-$m}N|Ua<1;w@h&6 zU_kJ7+W@U^Nd^nHY_ObOX(7ij!+UMBuq7)_OSUath4TN+vBTLE>=#Gl8Ia<#1{8S>M2=;M zBplA2XAKl*CR2B4;CeopE-&y*-3fy2_Qr&sn>qr}u3?hANgewu@BljFCd!?z%yOOv zDU5$WTvi%`kt=V_T8aloj^0QV7{tw_IV|0#tnSY3EyC@utap)a6~jLZ6N2$;#A0bx z_N6O_7z5o*gE1g`PVTeD|Exh3g=*)@MHuwz!H6pN&f_R2^)r#Pa*^78asZ^tW+ysv zva+zGtP-8rs6&4p&&imZm-I>o-I_Ljf ze7B!7?^&|=Y~Bm(NKP-0-{4Y)^RMo;|9OsHYjFG5^O`HKpw`%Y{hueo0w-Bd+e>cZ z3Hd%gcj^ijbA9jZ?#KptIe{}rTOxC%M_A)=BhLjw>#J)I*GFc+fVWx-Z1E4zEt%$R zIiZbs?c>6nP?Z$M%k$FEhQCFhJ)7b_WZqPeYZLOrOFH!N`@h(?K zGhBicb;o&}?Ox!8TIF-(^OHtiG)&l&)_TvOsr(FrcdD-@>x1Fn`+dr-Z;8?5V7ozc&Q zAZS{jb5e?^wV*$zHHIJe6;TjC905@4zQ7;u&G)!@^`vbj;Ei7;_RahDjhVNN04-B$ zcI1_QK3x7Apm@@AzDKMzCeO?PPC-XjOa8NX?Is|-+qltK=zjOW76~(=LArhMHV`3J zu^qwsY#rOG@jtS);_2O|pwM(!_|tGTug`&c5hWt<>|wGt%sT9|UM5ZialddM5q*`ESZ6(?O7g6FBb%r+6aM zw8JFrrIdIP;*_fzQk6aZg;sqEsR{@cqJX7Dn+f1Az4GOBbH}eHjp(@ROjaNy_>8`) zK&#g2-^9!sCqSLKJGamBEfnHs@#$F7P?4`rZC_$%rF#Hd^nk}EZI=>E64womPaAI! zyc2GbZ&qyHtJ(<;j5a>&m-IZL^*p|e^SW;iS$_Bz~bbyaI0LU-gHu?!8EGWys}PMKDABLTMamaU*M>y^`VJzO3l zgp!V1{cf%^BZf2!A_kgP7(EE}L4Y%^p(KJravt0`Nxzt7cNWtx{czQ}K$i6k>6M{1 zCO%exL-BO1c1W?NkN8`Y{TXgmUb9BEeKKH%r(5=DO(@|5Z@PC(S##Vl7783j#oN-^ zXDLgAgT?Ud2W@lMj$VkWxdh-$nX$7L*+wLZW=tXSP$A5TJJZU*NfFqgzWH|lx3-0h z51h~(rMy(K*q;Bq$h7z=T-WS5fTrlp8V|>02Iml?Zkt~}&$&NiW7?}g;lru4?hVPV zJ@j1_j$C@{$rAkW*%k72?wThzha<@4oQtZmS{j9%c=coaWe5fb##GGUxHqC>jh^gj zvAzcLraA7(m8KRY8~6E;jHwUG;mD}ttjJKY{!?!cEM7Ew(tnip z5G<4rZ(%A09Cg#*d_;Jf0L!S6V+6tIBZ#Sy7QLby)UWs{D;dbFvM|yXU+F#vZO^{i zXMZkWY24hr6!2k=JLmjvr`{zM2_Aj?I%sF|n(yn|SO4*kYdj!3uL#eD1pgw)x#dCu z21_Vz(^1CAFTPO75Xk^p%JF}59a*u-I&94HRUa2jU`;3u1`cvd9cC;t(iNf-*A`&$ zFtU%FpMvLxd{`fINd}1sofVPOast9}gS5L|VJV#X{$VJ%{KmhHZ5;)um}6vAP}Y`p z#Eeg9E@yfe5`|RWTGtx2(yEHk{5<2tIRc2@6?}nK7)NGYDJc|ek4YL`8X&|W*?^)W zV_s>&X7$ZjCr;*IGkdRrGQO;u#9QQ6L~`e1?^c?fava(HUg#%Sf;0;-q66^0pkv&n zl;pg?I^HJJ&S*U;acuyxfKw8DsLGoCKPg4)teupiC)5&EbVEK596v=b~uLT zACsxajl{RWDzEFS3HAvov{cm!GudPh5S|8NLMN1AT4SjM*n;!}Ez7hnO!zxLM*^WN zOeFuz;NB72rVEuJ71;lF6x0A5p2b-j2y;N`JQvp%t`z>f)2k`I?Hm+O$et=J6db3B zHC-906|s5<1`L}xPg=j91_M2ePzYIDG%sCA4@JYas``HLsEJZk69liSebso_O-wr7 zTt%St`s(n|Bg&2++;izwfUb4EpI{T1(1a3fc+-1%s`&+_86|S?-P?=u9nZ|X7ekP4 zI19xZD4J@=;Iu*+D7IixJa9>lo{PUK`LWM#ZUgv`05Rg6AJiA0b+xRB&%2jA^xX!- z!DCNjBIe<;rwWVs^6cW4@`*4Oo6t@H2b!oJuf^IPKq8)i5^(VHliH_Oh) z!-M5x=+k~P!1a0V)$`9D98g)>w>;`}{zx_V{MIAQIk2Eids~Za!kaq7B2bvM9issz z_m{#!{pO~|;@;~;4JR1{GlwTwu^mx7bfx}OevaMegQ8k@Q~~2qr>j(IqWb8VYF&2^ zrxGVqGLckMC8+Kr!VhdbJWsbhrqNJB4exp{Pc%QL(on+nuX}e)*)Y9uItr{4OSWNi zT44+nI8*|IMiMZNb^4APZA`X3xG`l(-H>IZ1*C%av4H#QyK4mMJJ@bE$*+rOZzq3QU zO0P0taQdQqKoAhU>;0Wk?}EZP7Cjmn89v+Nq7pib(6Y8tHgb4YI4Ug6=XO_-J%t!K zgrt+awi^VAj4+GOszeSIOEN126%sPn?VhZcxI>Yp2OVqN;*U| zD=1UhCZj2pt}z}89wcD%g#M3UnN)LkEs-&{PH5+oHM(tg$n#EFP&Okv zMx5?TNo4lm&}$<1iYr9B-cNxlLWByg+umoYp|mXN=7q|};ze7W!EEdo5YnWuRJV@(?%-y>)gv-4{l27?hAOo*S6C+H0d3qfc+ zO~7>t1k&6qg!AjaIlZ%szuP_aExo?yD-R2cYtsC#eGkfc)iw%D#8DFZ9JNk)k|bMDahP6fwWeHf zR#~uCYcuV)g)>Tn8=Z*%ngKyC579Q7%|QMEidl#w3@%mbsyPj{$ZEI}sb9aQkoWI= zFC}1~526b2nf=oN*#U(XnK~UBTxO3O;LPagcGC8beFT!D%^z+T3u(%(KWle3TvBvc4vg zk{nY3lH0P};-U&ZYiE_=(&{P;?XyECKsZmF25tav4SRsY*MTa5WS&jcI{&c{bx5V9SG1WBD;&cr(t<&9CCf%;K zj5{DwORe!I1E$2Uu_mOm7eBc&-yOHNOfN-XK99gCr*VMZrY71#MS2xx)1R{K;gpaag>I4%HsvFXFDp1)`p zC&bsuHCtc4GIEfv4?TgA7h2KOclgBydAE(!eAb5-x7$+t2 zs0J%pD3z01xI%lK?cc^GeH&$h<@U-BJfl z)u$f%-7R1Bhe*Arb0q~Yr#)|$bBkSji^mlutu7u2fe;6sq}z;C$&WN5Jy}1PBjRU; z+{PU9e}8G`C+BcAE{q%6aBP*smm?cnx}l% ztn}yR6=BqdMy$xXUDt}m_n&`nr)B7dJYT9l$RMM&<4Cb-#Ca;EdM^x2 z^BzLSG<)21OjNdmV8uL)i;zNjdCsXSAloXPQ&CQATWjNE&5KOnD_H%E5%B$3?$qe# z+%=qbIc1CS6Rs=Zn-Z7FPydqp-oDO(C6<@fX(pRaO5adi(zPcwNP^>{DKF#=+ zAu6;8r{UsRzh;!nbNnZj3unW`>QX~dNeL=dYqm|2m6GM?mnQxq3k-B}6GE&h`CpNF zz#suQ@>idVCS|0Nq0%yLS08_&Xp$?xmEDn+(IY9Xy0SCN;VXg9eTi=aDX&qHT&)t| z=GGf13Hek#;*4}8g~-j|JaOIw%4}e*gy%iT5v=+ynv|9@JUO^s+x-%2Jj^#A`Ay7i z>jUHQ42ZE_sk;#>v3zdz$pmwn9-dmU@FtJNcc(!c8X2h{qOv84=0Fz!UQj6Cg_BZJ zCAvYoU&u#kDi!yy`k|JgM&PpbRqq!AU#*)rX3b|jc=AC!33MaU%5QY$$-|TN$@8$B zk?}XnxY>-ZT9T#n{z39y%@lKNdp$CWj!@A_Lh%0Rj#*D%+xaigY*dAmIvL4I`R^B& zPm8wiI4uA4qRbobO$tUYld7o7=1pUVq`#3vZQ8eo|KL=7FoL|W)e*@?BMy>7G5$x@ zSEZFm+hmlPO;-+a{|8Jp7{IT!i#&XOelAz%Q0oL2NUR6*FQ45b>!O>-1B79RUn1CE zx;;b9nJy*rq72*vTeb@ofXgcf)Uw%MxC2ySvlUb!o5qJTBD0g6KGD8)4~adqEWS+- z=O-;s=Yv^yFa0?YmnAuiHc7drmnywPHmsl`6&LQ_qCSO(aLxRw+_7hiu#=tPYI1ZQ z#nvKh+>3Eb&pfJf#ViLn8BFE=FpN$x4Z=h?y+|^UdtfM$u@c2L}cw)2H33{=l)R!TSBbVU%1Jy^h~V2W@39N zv}6z~piI(8aejE{V)+4IqFuF4fGtw){@q)YvX<}K?cZ1u>yv{2qV~N)@`RRG8>Wnd zNn9o0y=li5X~R83=yYs*7vBQ6aZR~>u&J)j6tx`SmQTYtT0un+CI?N_)EZWD)mYJC zxQ9?#9a?}p-S?GKwCojPt>7y|XC|Mdm`H>3#X!1z%z?e-txbl>|M;aK+6oBQF}WvZ zxrB_kY{qF-#&W$1uxB6yC2l{p6!fsfG9S0cmLw;Y`gU=1-;$I#*!*Q*h#H5(QZY<#es7$^xjhL-`KudpnS-iwQ>%A_#cWFFZMg z=A`N20?0O~s{fXzBoJRQn(N59w+o@Z7`Y4LMB>nPyiu&9$eaMp#AZiD6FDI+FtdZ#x#F@z4jI~;S5T*}G!Pm(PR&byXAYWQVz;P=aL z`!y^e>Dn_enn3PW@uQ@T3_F?`MHX?5GtGlbL)eb&i zV-kuhe5cg4XMcu~;f!POt+T0WGS)PeVXuFl(ymaj!3L26j|Mya%!HxEX_`NOXa%%ZTcgC@+8g1AV-wh? zKC}l#H|C=SK{KCX>2#4Hx8c4y1Qti4M3O^pEE6Lsnm zUAarz>O=m2n;KQWn{mL%ynPYD1!n=yzP-YQw^5UuOYHu?;)p}19h2RZwz}fyt^y6;yv^zT-;}N~caOr{*yX?Ag|0Z{IsBNGTAZ?0ViJ zQadFX>;fy-)GlU}H2rH!j>m0To7M00T6Q=fMh{UjFhzLARLPuA8L_zZ-9+$gTJfLw zn#q@rQ4mv!FC6jfexy?Y2Q97%+a|4J!?(_WQSCV8w8QMCzGDzNd5#v&3Lg<}guU#T z@EzF%`tBvjlPB6*?qFyl$Pi&Ll&>%*NMqAYICY(rsWUCo`?iRYC4aQ_g1W9ci?}7+ zYTPz5e?a(0ui&2(eBO!dI3-QX@xpv6^85rye_h|v0AAF?FE@Ahf6LYTJxx=TfXbM< z)l(%=GigRVkMVD9sP0{Y88zEumRY^9`f{O&u(i-4EEfubs$B4m2^hD!+yu_{#-5El zU~d^fm-f`!Hh)w6ZYE%}1}X;d1hq*YCn3RioKTs!4uUv;cDync;Ej=4)R5%cw5ZNk z`s2#Iy@Jye17GEmFcP)G@6K~etK6rry!g?e_-P$}wW|12fRQ~Q@InVHqG}=+<6|)V zPvuPXSIP7h98@y-YGBDkcyP5$4Cz4X^hxN%F2pXgBiaLdRj5ioxIb^47bKe@O~7sf zo|LV#jz>`PkOm{pXGvm4bnkOJt5?D4Z%>VR5{V}BVze0V$!f=e%Oz@% zTMa)>{ccX|1oNBRaBzN4>|n}tye63VGY$5lXT74GxAv@ELxFe5r9?QG@iZu%Sh(!W zasLW;Tzzf#sp=jG<=j53KDX4~z+%Vje%!*#6bYDD_`otItCKmSl{&CzuPE5JGlDOO zj((OXy~m3|<8}fGPBEj>NBmo$5$}?3*tsOIXUjawUR!va;W^x2wv&Qh1rT(CJt__{ zZER9+ON&b3%;L^Y(1;|^(RhEVZd)AY1Dg9V>k|35&|; z3dfB0D!{2pbeq;I%H&zA_x-e0zTR79sZTw48Bw$p%5xTOe!L)$i<(}_voDnjQJ(kV z;G|+Mb{(YeIln+e#*uH!;5r;093w~3eC}cQOamF;vz-Hyubt185jcBK+IVMkedB#b z`E}d7(BP}1#ieX-vAz8E0`_eG4cIFm(5y=GGo^`lwq-(C0J!4}c5`jSx17|yb>QDL z@{>f}OT(fh_NkluRaNWY)vjuvy)#Qnfp&m^ZLB;}tQt+RULT8knPM;J!FKAUu~Q4I zb_m*r^g|0m&R|9*Ah(i`*%i_cH2ijgnKqF}#SiI}ss%h@;GVPas2=GN{8ME{cpX(&J={-q~YH48u+*tdmcSWY_94#w?6;F#+NgHu8KPG zYp*2Cj|Sd4uxEo~Z}Vrn9c8Z?e2#-Ee6L!mT2nW^UUS|}UUnSiN7OeTqj--EKKbtMzDsKT@j3oMLF+Bn zUB7al<|BNK&e5I~Q`P2%7F@9>Qoio|Fht>78NwewR++4JnUDFQ4s1b)6YN!z zCsNCw)Au#m60_zZK<+LTsJu_K?V!Y~?dVa9V@=LzdO#$J%Dl&D4@hE9Q0xuZ`&Jhf}bGNAg!I{3R9$q>`U4nsOhcw!j zm3>P>cAejy0T{uo?4y#?Wxg{5WQKuNhVj3FT8U@<>0@6VXLDK-(j)re3`a_zr{vr>7(x)fUe)Ns!jZU5&g|os( zMAPhbj}9oPIGwu7_34~}5W{Cmok$(juZaWPc-?XQ0!f1or_xZz!?K%%y1L7U)`7GT zU|O0fyvl#{*F}XK_}#uc3Eq?9jjpW**BK_REHK|%bg6Ik&KWwKbeJzyQ4`--_Mpl_ z)XBQ_XyS~0fNT{DXM1B$+eg^T(}-&%_#l33!04D4?1n10zz51b{|Zs~H(k$ebe|^T zKEwMx_w!N6ct`~)M$|H-exI9eFh8R|mAE4G7e4wioOMW0v z$XjB-#CAJ11&s1OPcQA8Y9(D+>ke(tT4K0hj+-H*hDb!!nH08ua%m0v7yFdtS|kjhDpW=4-rd@(C^>nMT1y8QN~HLXRD){;PL5uvp_SXzIawP-A@x7W?@ebZ`g z|7sY~^z}U>_I9;@71L>n0fX4<%p+9yiXf<8uW|3pl2Rbj@TqO9JAfHhU)v3fa(u2> z5sashFxX&EJ8)b(&pNtO#sW-SV_o9?IIaokyBpwB6up&;86M2Jz3tj%z8Oz07a5QRK1zvW3`_<-bwbkxMsG2GcslG}|Q4#Fa`O;1CC9CZ;&UEM8ZqjA`CszZsTl!ul z!o!LF*B!7AoqVQIDY-cpmO^5WRBwOHLk7)c3Om||%+fE1$ip%Wb<9cEx+9;Gt%F5x zpG|v=uDhZq1GMW?PD0Z_ujtm^EznTf(9SD@LBaisuS8-H%voCX)R?9v(l7sd2CO5> zI!<~fvuqWZNo$-i`WQt_JLW3sFlBLn^2qK}$_Op&{^hN9Dk2kve*E#P`wM}jw>CtB zQIE;Q>txDIrHrW)8c%r>1!F2EtHvd!j`rA!vBmg;pJ@lEwYi;*x48G1%o0qzhuebM> z^Gv@Vp#~2seXOZVZRqz7tc?6i!f&Z*PTwuSlY{_&f`CYZfHr~vS%Lt0f&i%wznD`I zCA&qXgS>aZD+vvBdeH;Oi6cRT>Og`76RHCW5>zM$7OI2O`G-b=`OZh6+;VS9eMH(^ zDgt<#G?&E*M*G#e`vMSB@zmCi3#?dpOz_gt%n<9Fd@tDC%k=%ruE6rB3w|FrIpi=rW`HsrWBJG59Y*L|w3n@f5F|<&2_#6Y zG7?UZh-DUlnnwDl(9I~36yrh|v;gV)i{LP>|1?1Dy&GZKVhV)yRt;kz!1wK#7z9Ak`F*Y8FT}L8O=i zslE{TP<4&;o5$x9WB$&wQM@azyN|i|h17JJTYMkd{~%sRI^wkx5U-7ac)*4;K zHoqOP+XPn2+p?<12bkV0zg0FYcAwX+6aPLL>1n$fYQo`d~QRNmAST$wx8gi}^?Vi02azg5YMAeR-4)zBJ-)*mQPm6>}2W*QOJB^R_dA z0~R#i!1$OFe9k7;UEQC8 z4*j9G)GZAasvoA4-ZE^3A(^sdZ#iV)cIVn9?;{WMS)3MB%e}23rI+7!FUQ`vy&xEH z1QG?HF<2a)KqQeVR2rSZWU)D19$z37i6v5*T%lB{HQIFVu>qoHfb+k{QD!Rb4%)yrJtR-b?%Y&RsZ0AG?yxcs}Bl*LzaFdcZN%w*)J`peztyr zK*JAwA1A}QW@~zntOnBPWy3v0lZSI|b z3I`+|ZEh$7pm|DOD(zLIhw3bpxuxZ7X9csgWcxyKlqj)7bDqns%}V-!?j39)x@dXI z6(n23LF<}tL+5z6-?R@NC4D|W(}9zYj?RakybgpoWJTg%qA*+xqS($BhoSg>n-fU+ zTN3+&lq6t8=Fuc`TT0L5)IR+2h$bZ+8Opp;%GpBX8^t(bZtuWoM{RfYkb8P0+H2j{ zoPa#H^opuu8&)4_)*t(9IRf*Z<1wLK?;)qu;jAvK|$fy$NyP`oGmD%y`T_-1-HtKkm74W=& z=CO|$v>MJW^S5!Z@4^4!kl;{6hqde?AubG$ez4m9U+?`_kZ4Y<4Ux*D>{mwKsuvO~ z#({?qNfp?@xWKx*bDvJ0VF}r9+<$#d?9<=wYts{(bN2(!P^{_7D*7*VcO=D8r7?5w zT0a22jFK<^O=mTM{ggfPovBtn_gmBKL5KU{?A~!xKX!BTv(pJW7tBksNK-a{9WDrU0)Uw{AWQ#z`wVrN*Y7pcFYunF14;{JCHj4N?-14loh9e}Z!7VGz) zK}KwvY4_4+oPNdwGEibotD;xCGjcv1U@G5KGT5Q7({OygF^T(u*uB;>n6&4^;2sTG zug})u!FZ#>aeYCtgifvI6>s+ov#fE5t4ijQ^hD$R2Y zvJ2#w?4*v^X7z`(tvy&i=V1}Ndk}2Cf5?P6^ZS&k`uEuyn#*{Suz=WO`gpp?;NQ%7 zlphq>JpJG%7@PNpDX1zW#p_w!t}WTNqD@lfQO?5)YsC9TsgT3l5I*X-zP(bXcY*CF z4RV#NXwNb9O0h9Ajq-ZofcbSdE}L!6KW`qr|EL@6p`q031)u*aCehC-sajVxMl0@% zmYfD~M!ooBgg1G)L5>_T7OxIa>3+5_G(a#iUhsSSSue&9<=iP*RavC={QNr*=kOvM zDX>^J$^myTsVZL#^myOSTRkX8!YN`a<1Gh4^yjtCj_`TE&8bc*o4m3=*nNcgRa~8` zM+BD2=ZZDgI_KO2FHxQVKj+f&p6&>eDc|E%^wFnP$U$2q(-X8s-~>Bp7GFm`YCuv?>85`RLUFMRl>- z&Y}G)9cb0$lv-D_z*l=A?P+*@*&rIoqv_hvyEc7s)^;xtpDg4t^V|)Ww9O2+;(XJ8 z;m?4xEq>Tj_W#pYpM6r?D-&k_a%_gvhi7?ag0KCD0)GA3qxdx(UGs$HKD7^#z6~~z zaudILYWwi*I_|Jw7(ix2lCEbv-_ZYgZ=&n}eS(SQ7NwkH0ZnBe4w;JdO8y2R`+#JL4RG5>xMSksL++d=@r43AlN%vCCmyvNFNnIdHMAtvLc^k{OK(fFk(sZscnsC$N0y_X7t za!4?8r#NmDR1N`g#1M?JJi>=+D42>;dc?fhOw_IlbynppOeF#b;;2<%wF(R(!2TeR z8zZDGJ3}YUljuhDtPK>D-QS=t78C=Je+A_PNQW^U$9lmM(!c^r!5;cph@*>ueuj+Z z_d0A%N3bn$F#Y7nkcsc$iY!J5KQv9Ig~TRs6el(hjx0t*Z(#Ewg2WR+?)o`NoynEd zQSD`I#pN3B&G1(4i9k~=&J>DRURO}d0aj%}Sx3CnV72cp@S$JIyqx+KJi#np0W4$b zr)%sExj$!fD)9}J#HOV9*3rlZ{*wpUqMnfvA#Fn1gtUnuLL?H&3Pd_0!=;sle(Eof+j;dS#a=K6<1Y<>Ybt zE6u~~soW^91)dKGf_n^c8dM6tqefx30Bk_{Y|$G&G~$IVhxAB!vU(!21j4U9zUQ%0 zu>HdKLQgman49BQU_)a^ML92$${kW}yAL;Kw3u_1_?GyVYyc;eh@~BG7TVb(oPt0YXss5IO`XRCeRW-p36k5j@mqYPC3tL{RvRnT#~Wbl)0$4 zQVY3WPmNH?PW?$E(eUtwW$59W(PxN@Mp(Xu32wNR0D5 zTtKwLi&BP%g#56~vNaiq*4t?z**VqHJ){+{l72Y~!Uz@R)(jwQLEhA5 z`+UDfji7s|T}zyX5X2EfHUebv(b@GAxZ=Z15k$);5L>x^o}FYoD&`3)VjvuW`a@a9 zJcN59nHlsLNxIczdqZAB_tFORFACelSMW9{C{XTBJ~ro*$sh5FI0C6dKl)Jim8g;aMfc51MtnZ*5*ik zSc^f~09Kxk&I%zy{(3`T)@yO=5ZZzZ7f4Uh)VT-2r{q_>!y*Y}v1j3%(UPRT%Q+F` zircFPVy;j~2upYaZ`rh?x}yRIvE7{mS~Rk3hD`W1TYV#jr!mmjkhk==a+~Vherr*= z?o!Y3C9-v7oHfhCLpmZzHWBg|bmBd}W+ljC&wkVe5Opo@cmdZ8B|%{XaZ7juU*9y; zMIyymH_)PJ35Iv(R~tl-7Xw;`Jo#N$oA~XlRqC_B_QsDsN^}%P&=E*1T+5@7+6)m> zb%~>R36;@`Mtj>M`aB{?0}U}=^W}R@p=7IQ>eM}hS4UR%>$v!$cxHXN$CDS1Da46! zYXss5##okUO0{MzU-2NpcZqP!8JoGEN>I)!v2R#bujyi2?9TjlE?+mv4frlNvTS0ShFeHH7No(?p_wl= zgTR{aO9w}W5xEQC^T$}W-~LcjZ>h6?GuSL>L56#a+avo=%DGznfko+ls|Go^%9}!L zRgu>_8Lo09$|^u5o}H{wihVSxzKOGm{a|8mlOIXs9YGq3R=ec)+CQfxIOc*;ls;V* z0q3ZTQ@{4=e({wF9$hWJi$XH9Vn9j?U}>FPZvK)+K&|ej93&t zL3+jZirgnGCdzu6;rWF_#Q)4lL1%3iE7u@PnO5>06X%4Kl`|Tti*hGEBq`GuIbPV@ zDQnoe)QhhpC2#~saLkU-@2S}QuhJhvB;kS#3_XPg*(b%Q2cnyS;ureGTiUV(MN`+h zSMP{z)6ubWNepD)8i6ScdK<}PdqOVhjZoq$%D8OAPh+KAji?;U@1aF+ac+fM zv>_gzkw~+A1{^|E^%CB@=g``V@k;G(I5Wz0{8d>{}xcM_{wuZPVaP z16O>Aduf2<4l!huqESv#@6C9^1*;7V2cTL<)u`o%9+JjJz(@DwOfZ^si?M|ZUblnk zAiyWtbQpYk^4$bFAts351`!g{@iw+iYKIU=U}Mi{kZu>_Q&u2>_yo}U;cX-AM%e&F zfZZal%NiZ=w(MhIQJ_q02PDKkIS@et*f$6nMea2TILK!`dGe+%%^BaDfKt(Xf~%JW zoURm<$p^LJUTQ#u7L$upF3pHma|IbYw8*d36t)uzK;vlfv zd4cm*z1d~#82noFp_Nx5k||q)FyHZ@+f#W(5i&X|y2n}rRU?e5=f6=^9O)&6-5|A1 z#I`mtzZY_#EMz3u$H`c-CT$ln*E+2W8<>;c)HqsS8v=p(1x<)gv{HaTTc(dLV~yH* zVyZt2BU_AVY>phfT_z4O$^x0kUw}XC4Qq`7A!A>R9^t?FMcKJ3@kqfs9D*JBR`CpS z?@=Fz7-xZo$9w?RGuV^k!8ck38+2U_e!i-A1pU%fY>FcV>Nf9;o_l&36wpXq3r0!w zQ*`SvU>$aQ6>xFPdWE@w3*Fyi`l&VaAyRgBb~K^(PsTls7&-!tU; z@G@`bgJC@@_3-IcZHA-97c_O8oFq9>x!l$RZGcVTV?NR)NdS2o9!Up?S@5iR>u@$y z;jyg@G+Ya-y+Ca^My3T1?(`fu%8%R9Fa7Z}&H5!|ZyS>bW`4RPn0Sv(YfTX!MME}Q z(n}a#=5<|P?H5JJ7fguZ+B`%p*C}N$$@egYtBRkSp=}yqPx`d3;qM171QS_u83SY! zSFt>*GmCtX&a?alBV)2PRFGboxtM?F?v_&qho*cOoUOD}0X#U{ej#nv)av2Kx~{R& zW@~^{`!!dBaJV_!P(?_-u_Qo#02{sloJB=VXh|UbvuZ)|>pEM_EiYNuIcfE^EXyzj zB=9X=oWAecS|^4x{9>rHe} zc9mN?f;=#yC`zFfreH=YD?9I6o$38|DA%ks8xF3ybytXl`z#KeNImfV6lqCOP9?GP z>TP{vcxKzOb!^+VZQHhObZpytW81cEt7AJIJDu*%&EET*@7(XX=imCX)|_+I7^7;c zN>%(*n3;28@CN5Fx+adK#8&1Nt~_`m2yq||Wc}(Bf%gnsFNnsWZ-|>nk+ADF-U_~P zwodq=&wO=5wd+Xh

      T(NcVS)Xq#6dD7?80E{4=7#gij#=ANE< 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 VnpVAC!A{{Qou` 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

      Vge5vd!=FNNed1vmMd;j=mcD_Bk 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 literal 0 HcmV?d00001 diff --git a/influxframework/public/css/fonts/inter/inter_extralight.woff2 b/influxframework/public/css/fonts/inter/inter_extralight.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..f927cd91a9c6b1a0238919d9ee9a198069fdbd3e GIT binary patch literal 104128 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�SqNi%O0d2U#MnvrBX$TQHJgtW5v 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 literal 0 HcmV?d00001 diff --git a/influxframework/public/css/fonts/inter/inter_extralightitalic.woff2 b/influxframework/public/css/fonts/inter/inter_extralightitalic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..8565cd6b480fad25df2fd1de40d86f372e1d75ac GIT binary patch literal 110820 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

      4( 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�D{oblDTayks3tA5 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 YYxt5uo37MJhw={M^Wc=|ofFvq0q>Nq2LJ#7 literal 0 HcmV?d00001 diff --git a/influxframework/public/css/fonts/inter/inter_italic.var.woff2 b/influxframework/public/css/fonts/inter/inter_italic.var.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..fe6faaa581da589799fd504f1b5a2008569c7f98 GIT binary patch literal 240240 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

      %&-%G7!f=KT2_PcE>nnLvfbLYU(>zK1J`* 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

      dr&w?4)Er?35OV9RnJR5{Ib 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%0DS%9T0w7W`LnT>p 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

      het(`dTQMl9M@;5?4e`f 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 TkX0D4x8}j6Axmgk#EJHQRjs$z literal 0 HcmV?d00001 diff --git a/influxframework/public/css/fonts/inter/inter_light.woff b/influxframework/public/css/fonts/inter/inter_light.woff new file mode 100644 index 0000000000000000000000000000000000000000..0df2bc7c532a9e195b2b7149a4d01d9f181d9e65 GIT binary patch literal 140612 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%0A}Jd)A)F`vmPF41hoT@q<2Q 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 z≤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

      g*vN9z({Tx3l{YWBy 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

      d|cCP@*u9 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 literal 0 HcmV?d00001 diff --git a/influxframework/public/css/fonts/inter/inter_italic.woff b/influxframework/public/css/fonts/inter/inter_italic.woff new file mode 100644 index 0000000000000000000000000000000000000000..9c21aedc8ad02a7cd9175d02be26993ed31eb45a GIT binary patch literal 143188 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ÿ=>GX#frICEr==Vo9N5TkP10#4Am;Je9 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 literal 0 HcmV?d00001 diff --git a/influxframework/public/css/fonts/inter/inter_italic.woff2 b/influxframework/public/css/fonts/inter/inter_italic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..734944b11befa718db980642fa773f9b4addcbed GIT binary patch literal 106604 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 z zF2wh!$f!hJeVJq8WEqNg|4>}5jPuMaIyidfI zFpV3J%HQi7DhcqV<#yX3W$(pmmg^!E9?49M9-~o<4M-K@bi;S`-8q`gFr{t_Z{>9Z z*6iBu1bc`bk8;xJuow)V^bKuyA>IvURqeM6`Yo9ny6;3Q{jT-eqYdI_<{XSq|LyVS zdN)&KARo3~nh@-l|1bThfb(ncZ>+@W@5FNx?)}%I>;mO;aZBxA{Y^*v2mY|=i#Q|b zYvv%)8n|}`PZO>GWHSHgqG!~kRIT`H`)Qp?uH}(*>}n~e1GEiNZ{suW?|m(`T%m=p zbjdXI;@att3J|4+6Qme~3sm0f>5svEO3GN1*lelg+YaGthIV-g+35g6FBjU;$ud50 ztxaW`_D504nRmBH_X|oLlTqaA4iMAB2F+3Upwb}A?%f6O_lT&k)?HbK`(P=_V`-{} zg2`!YLFrbX*@6PL!|cg^Ldn32>ql2vwf<6j4gO1dc8%-Dvupq0+&j1DCA#wXQ=7oG znSXKC&Nrjj@vf>{210d8oTh(v6Avg{`yvC_Gd2F`wnvtLur8F>Z9sdav>Kdv)QLyl zEKJ3ByJ{4q`5Up=g@Amb+24$JTV*#NyAT#z^$mS(U5K9T^46S^!2`j-dtfZ=w}AWU z3TelgR@^dGlb|HHl*BV>aL;Yxg94@S`$AR)#gt&mYtU>@$9CiMmf;0;;uVwB;9t!^ z`iy_OO@#zeb<>3&CcTR@4DA&O(S178CkO+PL!XM8e$IjPMC@mJgG@q6d7 zQ+P2wpfJJEbG2$ah*Gk*E-Me=5cfb%1oiy0F|r%InJ#?I6^uO3viL!*R4iN%>_%V_ zJ2L9yXtAsPf=x(pmEts2cQM{4$^w-|EZfx}0v+YIIP3VYcyL4V?!GuOE#OI6WsiL9 z8}x*WfC9ivI;)0;HMEOug3b9R64Q1yVjpxK9t7&$a%x-}SSA@b*#5ZW)`2cewz@|Y zvCa$~f7ZCPRhr($lN>eUUcC3Xu)pLYCSIByPOCw6RsV3E#i!9Ndm@lYyDeL zSc7?Zv%O*95kxs{V*n21IYW9P4}w zFx;JH2~-c1*@R%gs{vpg>pU0bnP-Hx$8+Ff-Te-XSnyqo4kZ8C?)1v#%FP4uc`&u> zL>AaB5@<@z%ChYeYHqb>&2Z=eo)MWsHz(I6*vW&+yrzI2$9%u+0G?EWHI{E4|zHt-eM+H`g2(^4_FE<$S z+NFz!iOhbX$GX)1sOP%;zMt={vIy?X{hNGUZ#Vb2Q^V3!a?xuC#7K0tku+b+5z)fu zt|{0~o_iEQ9`h*dgje&HMPZ-vdRgGG6F3n((`K@6EbYQNq0z^A4O+Vr!A&K&ZhlB4 zD4M&+2aSn8Xy}W$9PcRAblVqRcG~zFx1hl?(nVZ;18ra6=g=#@mXRtv?fVaPQe?m=L2M^{Tl0`8FycJi%p&58Y^5v9i{<{I7ggYHph za#u@ff|Gb#pWro_$*gOA(42s|&NL_rz2HTC#eBuX019F8 zMPK&(vw2@8P(KoXL*X9lNuf^;tOX0Pr{8K~KU;e*MHBj># zUR{WKP7u)_RoM_VfSH2BiPohbkCqR8a=W~f0#jCts5_+gDX#mNu0X#2%GIfcMNzA) ztBqr`qpM|x)zkkJD53LbKI?6C3X)zqr}rX{?FO*LyReDN=6R)j!Eu=3I(O0KD-qMm z%ldYvcr=areS&Mq-@I)`GW8|3rIZ{Q<(HOJOp?`^=3oZs&y^05HkewzWaWzKLm;hd?m82gK<&6@1voW$hRGDu2#tJ) zAv(=$Da@W9WGX{d2z32v;x>NP>r?HPkxD(|;BQ*PE)O11X_0scVroBKI5{TYLEC!Y zZe1C)PVE7tp1(C{4~%k_{5eTWcXsDSu&XLCJ&vzbZ5m5jM!hoNW?{Nko#?S`xMrf$ z0xezf7t$De2;{w|Zw~}HXRBb}9%ZlF-sPQa(tFa*knyR1ky{05GjPD4dC6?xA^uhSq9THB(Qr(!26i3TzE-s;ptrAl zWodYX5T$I$Bf&e7b7;9_KC|@8b__i80X?yLbg2U6SB@BD^W4*z#+4iIj-{$6BTrLB zp1*KvDxjr+t1gdZ{;^4m;J$x#Lhm^tlfg1+i~eq)5dFsUV>N&L-*Bct7k^Nv zM2s;t%(MDRXz+;!v}f-p?#R962im!A7a0_4+hJyemfkMOfe;!@&yNf)8U}1@>ne(Y zY~<}~*%%%YtHPd7{Jhs^>f+I7qVHz7JUrUlT zsxs=Lma2-@F7GgB(&Cm-Mb7)spkpHGv}4dtcDoDA)1P9pr&QEMt8)8VO-uJs*I28? zV-a@v8~&WcWqYqsV-sOANdzkJ^?NKf0W=U;+pPtCYVr3RrEk}jZU;r1T%a4}X6zH~@%sbq%xR|=RHLbBeQDe7=?D{N^2O#IgYd<_+ zitUT<2Tta5zXTC^iDLlxlw%c+CTkJ5!vDFoS}G--FDc@~%CcVNoRUsTx|Ly*c1n|u zPI-OGOT|~#&(_x3K{L;J`C;ds(uAIS*Jg;JE_;{2rDAv+0%DpXN7CBx5_?xeAOWg`=^ zk#Zvq@0ZViZM%f5^+rO)-rl?gY6ZNe;H!V1$MNn8?``#uM)-j}zCKh85?i+P}^8mR$X0P@O`sKt#Rvu|s$Liz{|I zG7|E)x8lrUbgg&>=TLM7te;#gn2V${sJDr2PZ6owt;i9Y+O6LsE-Su$?nT#bm5!Lz zZcW>?&~ELHP_F1Pc4@t0S;gzsuIRzxH&O^g?Y%aG%njM&g?gl3lXYIyt8}@mRJ+sS zlDSg@i!8Dy>tbbpJhNCsA_tjUlBW4z(H3^S!Y>SZ_4;%z*P3*+fX%;~Yl)?P{;hE( ztJ_7ck1848;fj?^Dm>TOp&eXnIGY;2hE+dS(d2v0U-NmdX{l2nd+}F~b9nY1fBiRA ze8MK?BcsOJgF?V<3pCL8-){6MlWDT={2^8UOAMT|%!1o1qhK|Nc8;M)v4`qmf=TMg z!nNAjTD>=&1asHk5pCcC2jD>ee!k-r zLWlO)-MvM|e~s?PtnrI;ajS+ok>lsUT-Zg~$M;`o-)r#Ns}X<2ZjUmU>DN-p{KDFv zW-zN>ies$cR7TlUGFpaeEFFJ9`Q5$i)VOfHyP>J8y% z?oWUE%v?bvbPCai{e-?n#~d|Z?|D9A45v}B)nv-o9af59RAd7Y7Um(uN5 zfD}gx?g(_K=)$~BMS?DKbWl5?yoRhaf<>4AtM3uYyxz-E>-kIjd_UgzT3`B z3O$(IYH^|DeJ5L9Bp*dbCcYK2r5AA-Fj@&Cv~JQ-%_{Rhs&Mgwuo;K--QKDsU+##t zYOb>e`)0IGGGU-h2qSY#v-P$27P6Q^p)H#)Upin)JWWkM zO;LVsq%z5wuq22tYlSK7JNWohQJiSFT6R{gJ2K@IH4e>N&AG6NH)B`Lv)Xx z6!4jf->m{slFT`3snA6S=DX1LT^(B70mlUx7a-9BGgo$Wr7mhN9Ozp{U8JUh?jyur z6OK?`fK|Yr7plG^a}K*|_f_W5l#=*W1~4k^!wC2z64nbAutHbW(2%$i$$i2_T!8Lp zJb?U@@kg3A-Ft2fKnYN8*HdeG0AaX*f)nPsOD!^y-$sO35O&~Yt8&-7_KHc2f~K$D>Ei5Bi`VV5JF zY)RA=Dh-+j<*Zty zn2@mqtOAwP@CBRD8SCWh`9|#q39{mLn6|7yi$r#F&H{1oTne!P^&8EV1g9erFO}I8 z2E(PgkR^_A0azV_1#4eUAL!399d4OHdHRvXw`hdiyiR8MyTWO0DjiRiHPxW<`VdD> znw7VG?0ua{sJk{SA*9Le!u zbXNm4rS!(118s#K$0}#gub4rfYG!g9ff6ObuRktFtNpGxV2y~ua?RA^(1~H{Z`krA z0DBPB95Kr9tE$-NGFuI|GoD?mP{DFr1=HFX_k9^l;#C(eQ~_8jeX%N5xw)9lNlw)1 zoI~pQ^hc3C0B_S&Y_xY8^I0woO0WrxKF_5?L|}qhdY&12CKJyM=@C1aVD28==yOiR zJ1YDj&oIt}a~NH~jlPM5%TCRlkY`2tm+g5peht;HEoH+;UfN~FrDaEN`NxC1p&Q^W zmrLl)?cLd(|Ksf=?&Bn4@{to!_>}qN`t0y*74rR!79Lt1jd&@Q5m8^L!;h}+zh+;! zgtACfu$-}-aZj-X(8xoHL)p8Py9K+;HdZzo8QDF4HmNOUw`WvW!4BArb!}jNB8rQP zI3BY36^1ai_6Ck=V^6`rqSNY{8F7!Y)1pxim;9+fkG3v_+xf+#pa+pPQ`{+3&#$;M zAI0@dYa$PR`DIa6D;|(veF`k3Dsq`AyjH&^32dOMaGBn|s%n=>TqL-1@&i4kK2@w& zL+3=V4gE5oa@VV%3y~9Dvmr*G1Z^(Qe%v0~`F($~I`><4G=W|7h7_M@7^tdG@irz1 zOer)-KFJs!S`%1uHiq!W{3KY7->!YPzoE1$ZKZaXPiO2wnAt?qEE6)H&CTjv4L2#_P)Jvg=gqQtg^%8H?6ZZ#AgA~xlvvU6122o z{xr)=P)|#I@hgtM8UXbp6{hrSh;%)zS(16XBaPe`612){w`0sTHDvfxa-}N){?=^8 z^VeP8U1Avb7jqsioYM#h7d27OJH59RW;kNlFmnPG%tBG_xYIve?JeQcAjlkn3Y?^x z)r=V~np5bHs_0GVf30~FNCKD05bun(6w9*=V1)~(JW}*H{P00_C7#M%J9$=$RMsJRGE(~K-(q!kr7> zTc$7*KQ)BjTnh^DXf~%$N*sL}Gmthsz+9_EzqKDOG#&BcwCKpJHQ`cf0!Cz&voNls4jB7?Y=`o6KL@ZjgYzEoK!>g6H0XWFpS!yq@0jUIcum}V+O+;w;wkB;ewcK2Cm9@7wzleFlkQ0!Eut z*Dbcr)$Aq?!!>{WRoPpDuC^u7;<}3JO&7+BSKPw{BrpA#)WZz z4p9AlcK>}Gwp6KW@nB$aHQzNSWKXPFVDVHhub21vynE$2=v|Dt%+!$o4vBZ?3V8ckFU!r=86!cd@VzACHkGK81c;NaFa9e3&2q;7k-j0(PCVd2n zkGeLE<5zD#l8hfJP(wQ#)b7&5HHxx{+PTGb6=D(mE*~km*6&7{M z0^Q}mY$RS_EiXyHWR0_JJzDtU2bVWEWAUw)~)c=(VRcK9N_x zlC6m&>RxVL15LAQQ1*<=`>bzs!!a-6|5zNcU}=vB3ewc2DhfLZK6_9ts+`V!JhSu0 zxV8X}b_G8c!&(<o7 zVq*%uNI1Pn42XPELS?r#;U#TEsaXUEjXcHe=&cXbRU%=%no*^gHpHP)u@tIKbt1Iu z$z~-U7lT5iFCl}%$7?l99j5ifmmvrOzD&iAOiL4O_cn8zsjmyZQJz%hl9_ct+=e}u zLR~(CLH5a7?+QrG70V)TOM)E^yq^j4@jJ*rL(wlqajPLzhZGh=wXG36iSJuLe?d67 z^{+t`XkxL(u3fWsrAze!3EwrL51o+c>JHM)|0lW%KU;+dPRw*v_QG>-&f{+LdABVq zUGrAJh+hsu`CBF>RmQYSr!5QBQWZ=gCqXv*OhX1G1gB(AE;aH&l5OvsqxnQLN}VTd zG*%RJt(FdA$i)2UI~e5)j8nHzUasx&*|v_lg{9Vo97jvW9BJ|GwnJ2a6=RMg14;R~ z$FU|~Je=bZ!g)slxxi42*c^e|-v!;|VJ*E5ZtG`ZH9?cu z`MZEUxu@%~A)%Z1Y|rlJ3Y!-*l)#ZA9Tc3nJnYRNIDX{M16}TXiZdez+}?9WkwXSZ zLL~|5PfUmezojYE%}+a?6;q!VrFgp7REauO_Y*GUO#JH%^(?99oLzkn5BBJZ^TeRJ#HF~2P0J6x>4?Nh83UA=psNb^0)N5qI;boV)d1J>j)#h8l-1(`Fn%4bAFBOSj!4pAr9wl?)`Kq;&mV_TRp)2{>EdtJyS!W-Q3RTU#41y|~*F}N)m8vcf7mbM3vS67atTV)V) zn|ts#Rhng%W5A>Q_OM%R7Z1N zXC0E8yJfWr^vu~5n}*r_9rMI5rsaL4SK;7ArBk5?yi&*#1Ek>Zp#WaVXAO+~p;I9M zyi&{x08}S(sb-VDGsnszB{Rp$CM7bD$s(mP&*A2!>*2H6i@*yv9_bxK6UOhsJg#pAs;)&@RNw-mZiri&K2>fm6I-BlX>2YsPS;4zZ%OU)Be$g4{^r(^$Fvxc@B$Nj&`@P zm3FtumFA1(V|A=@6U`2v2ZTdMk6BV0j>(cPoz{0jUfx4&9nRLM9dE61JAxU`FteQ& z6A|ggb@lIU5E>oZR!$uR*6bZm)`1 zkj;(#gyRq1l}KL0iEK~=?@A`GKemk*osAcY&5h24V+=1`bK)2$5Ar<^f}962&x1_o zK{WCpSQ9Uv@Q_^L220_FP~ir8p$0)W0NfQ2`3*4m4WQ@-7<30Qbaay!)!{1$;zkg^Vyhf#?{+*2IQ?C70@-Eq9#LWvoFydlvRfRg>6kL16RG zLH6?Vt8A1=$d&(~L&BrV8b)BjrK8Q2>BJ34n>&(kT-Y2fvj)s@lDsBF zf-)B^K9(j~KFU~ck35#=uYCQ67Ufop+M-=E(~OE%Ai$7l=%8a4NG7&A8Qu!gNxwm- zaV2p3j=EZ?mQJ-TwK>Df(CKc{q6FkQue)&k&ahfxl5T>IX9l(X{&}_LsqHbR;8i^P z1Uue-0Zzr$DmwR{a`cNf7kYXPZUBybJB3Ag#Zhs!*4n&wdE`-eX7PN%PjKczb`w zQ2L21lOYyQ(kA9ZOk^SR^z#PB$Yy(5fCq&|UH$@iWt5`)7flJyMbda+slXA>yiSwm z$^344XuFK3219wnOGp)D8CAcuC(o|bT;S=3ricYUb$LZ`xzzh|x_K3TUE*@0Bf9yo zO_R~90qm2ZzRC3TrF3ijLDKc4Eh_g@I!l%*UHcuL+JQ{er{c3nXo zdRvc*8I&@f>Pm_M1X^L=eQxH{|H!J^2pBe%Wt41LhyrSlZrjY~06`LSFP~cMy>79* zxDMBrCB_mNLm?H&97%#EwmoV|8E^Vm(;(-{AH_2&Z)_#l2UHBC9cq!Q3dbAcy=H?% zR0a?Eh@zydEb44&uxdffL?`ls_{5L>T-*vOJNYs}nVin;&FUmd@L z(Ba<1&}qD(j&45UKDTQ7*dUwsjhTv$Kr`~)+wiJl4sYhoBq!t7KUN&`F&*}Ni-~&- zme6XtIhHUYH%Rhi!uh#gu#^~@``@wk1__}lZlmQz+-{&4{b2p~(J0aIo9Jmbst=kv z-j_IgO^)|plK+lraFP0>m`!oaMSnUd>!aoLrtn%S>g!tu3MCOj zZ(Fmdul1|H(qvypro8Q^#PW2aqEHfk(dh8Y>aQ=0jp?FkZp@osW`4EG{;D3YmHqgl zB?D!QDB)2~`~OE)-yU4=z*MPeo{tH$(i`l}fx9nY(>w)d-{K1mR5q3|Ub>$L8CggI zOLy~xH7S&Y2yv5#qhFrcqeCwI!qJu0reXD0n5=n4|BGB$hE>)tEjGf=?4mD{I5uTa zzECtT7rgYZ<30YwCtp3Lo*duiAM3JyFM%isq)k(48 zv-WFRfR^Z*W5u^Zgy__KHu^i4O{!lODHkF>YoIW}De}$uYes+GPT_SKm+Y2h|AmzU z(ALS-jUNW6aQ*Y)@SALQ@=>Dt_+vI@Yg1vpgp~Jg<$1%C($z^0J0flR&Z!-{LB4ua znI+mkd9+crf5vG0&#U%p2lK1MYoyM3@EfkxWRz%)_zSvIxcfH2l;+A5~Ybpsob0y_@fE@2~AN=(VhwE0}lhQ1{L~Q zEYj;p>mYULb^Ud_b@O#4mN3g)%bI?#f%SOGD9Qv%-pPTNe}e41>`Yaz2A7RGb|BS5DbMPZ60cH;hW>|*jzu4R8F?{~&yibi_1C`L ziu{LFifd5*hsB;^NB(a*=D8-O^8d2fzV?8VkKW9?UwVH1k#&ufF^>IDvH{3{j)A=Y zho(~4))-UAf3YV#S{C}xvC_2tUjm1>3rxH%da?R659ZS^r4HN2|Hq~|Pgk}7&Lh6z z|CjxB_y1kq@OF*~L{QKF{l@<}?*E(44`=^J5l``8IXT-DzyD*p_-|zYuf1rT$Ns-w zS?Q>&2}X~C@PE!7D?<^UER_W2`EOu8dhWsGEVuwuB4ACbj`7tPPlUqeI(_X*22jv5TMe~m7w+^!t{4EV#fjZuy z%Z@uhDJe6Hlq!Mn_ToK>;AUX8@RP%_3aR*Tl%=yH$=JpBB*<7?awYMQ{=m8n?*q?u z)T49aq8d}{N}o05aoF|}g=kh~T%Vl~b>_A~0M6~U2HgvMQ=MMrJ6NBCMWWD$mf^>X ztgq$Y?aP$3g89|Pb(H5D>~lp9Qh_JL@S>+7vs~xOl>;7e8QmlFhu0wavstF`{cMC@ zq@J&%$MNY>%F^=r%G0vcK%zz`3g=e3*6W_R*;X7{D!XX+(Wu%Y&p!;^>_z6vYJZCI zbUj7&QBZ~&iI8QurIrehx^g(Fx1Q47upC+}ImMSZse4qOlRA`X?Fzx<$lcHrA^D2O z@o#-Znn3fwc4#n)7b$DSFZFmBNpx*Z>_zhU4R zS(xOUZHSp&QTJt^JLH8&{G(Y}?08aNjjH&P z#;1X-ICJPtlQ^f0<_&R*eUGHCY1lQD=VY&<{PaNzU~QLD!9(urWsneafn2Fc-P^pk5FIQY-xW7`voh`qZM69NMvf0YjIGnIkfjUt#1{(uL9)$X1Y zo~<>+Nr>JMT>lvEwn`>A#(H8r!f?Uxm+LiisnzAfx-;SXlR;Ue zPzH0v$1IH95QPgU%@4b7K4xUTnS59`3A0ylwAnWzL+kCSe% z2eBo}<6_Pq-ao*z(;J`-@dq9s$-ATkm{`6dG|Rm@MD2*qSPpu)_aKAZUeu+R)TL$0 zBDSX1Lh#kJnv+<`^j8gDSC_GFjFkGkro4*!?5tc)L3J?g%r%?1^4!AhAzY3Uq{m65 z|6u)CUs?fOJd1UF{4POrbQtVR+W;5(P!$^cql3T^jkEXU=Ci77ODH94co<+tTqVGwuoj2c-Afqx~ z5q09S6XzG^7?)16L42Qtmx(wj7hIJ)Y8jA~7(WoZzR7Sigx8MyFnYzVafL!gWd(@E zvRMV9RklQ%I1WrK0YBYd2L(*0e1%V$PiH4bYM3w%I+quSDM9!0f~?=a#JS}O{4HVvUNXu{mXY8#yo`o? zS06hKurTRo0^mw;E8q9AZ4)#I0<&BtxNG(Zc)OVZ)DqmI_kEoa>SI9x7B~9@?nuBa z)cZb_i)dly&T4O@u+vSlQ%4d8Wou3(VVU)}AI_4zrJ9Xsb*04bvy9M%?Ze2BaYB$( z(e!cC;J{l-Yo>MST%>-w>ie}Hn?vjAJjZJx&3kodn0b~XGQElX2${W(+HkuF7>3o^ zZ*IdLfmbW}3?h+37se1JZ_RhbMVL{^PKN_t%OnD*XwZI_UPQWMnZO>(%xkiq zU&aIgJpoc#r+7fh;y8Y59-26m`k{Op%+PUG=P#z3uR3_cR0KLuFFsFoTX@G{+B(LF zYm*6$?MEdu7c|~$HO$c=O*rN_x=vi8>EjI*)qe=65$xX>bc*%)+KdfF*BSBOEoZr% z$$YZ$o_+4{KivAfEqG4pxg&Z+1=Yw5P!z8<`MxK5bof$AJu6*|SyxWH6Gh{AFufMI zkpx{%1_@Y5=4ubi%}sI9IIB;y?0i zRv}EV5gAD+xIkPj73Y!SdAUKnmOO2k{eq!QyNk^2j-TGs95EIa#=?PXr89I z3>#m#6dz}}jEY}S-d7_!R#~B`@Ey1t7$HTKL_LzYBe^4eU$>ieAN4P>g*pW|)O(?+ z8w?oPzZfDV%^v)^y-R#iiYO6d1g8z|f_Glury~r4=+$#BOfS+9It3Zfk6h^QKmHP9 zNdMY}m8Qd;UOc1CpZ=@7f~>rFq>+Mmp7hzrpZx_FL_@!NLZGOi++P7j2J);6zWTcB zZQ`@Lf2j{anF`c(Z%RfBGi_t4h%v(ce)p96rCHMJUH!{u zQwbFs?VrwZv%Ul0hBtn|Gw-3&>EJ6?3wZFkZr{Vx)LE4L@6#=8H2yp9&Jf9eeRsF* zSgI!8(!5Va)v|cigi*CZNklK<7MT2%PPqgQqk1g0;Kkkjd)4DJ_xsPm-}<&}ZLfh( zzgH6bE_A$`@%rvECqjY`*;47#2JuJsG5L2*IXXTS5A)gD>3GVvF6GGze>paFP%BiP z2+T=h3}=0}VL-#YwS+p@MQg3e!N=pzj*jjjJ|3b50Iy;9h7&q%0vbl!|I!08*HBo+ zE_nmb;}+*&n3I0SofYOC-X-3Go%`$;RAIOvsE>NDWGBx0tGk7+nuytTjCG9j{M%o* zv_HJ3;3U)VG5DKst*^gq+`dv5Jx731wUge@_0uq8k8lyK62=Yi!S(SgAnFlW_?4Jc zy+=2^v~rbMvcH-&>E#g3dm~Ziur0`MCaeiN#yB$I(dx3h_%KDZ1U(?UX L*ziMrB&7cV_#4`s literal 0 HcmV?d00001 diff --git a/influxframework/public/css/fonts/inter/inter_bolditalic.woff2 b/influxframework/public/css/fonts/inter/inter_bolditalic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..1b2dafb9da557edccce47456d861cc2be25a0dfe GIT binary patch literal 111644 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