diff --git a/.eslintrc b/.eslintrc index eef33ec8a0..69c731b079 100644 --- a/.eslintrc +++ b/.eslintrc @@ -78,6 +78,7 @@ "has_common": true, "has_words": true, "validate_email": true, + "validate_name": true, "validate_phone": true, "get_number_format": true, "format_number": true, diff --git a/.github/frappe_linter/translation.py b/.github/frappe_linter/translation.py new file mode 100644 index 0000000000..bb81e848f1 --- /dev/null +++ b/.github/frappe_linter/translation.py @@ -0,0 +1,28 @@ +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*(.)*?\s*(,\s*([\"'])(?P((?!\11).)*)\11)*)*\)") +start_pattern = re.compile(r"_{1,2}\([\"']{1,3}") + +# skip first argument +files = sys.argv[1:] +for _file in files: + if not _file.endswith(('.py', '.js')): + continue + with open(_file, 'r') as f: + print(f'Checking: {_file}') + for num, line in enumerate(f, 1): + all_matches = start_pattern.finditer(line) + if all_matches: + for match in all_matches: + verify = pattern.search(line) + if not verify: + errors_encounter += 1 + print(f'A syntax error has been discovered at line number: {num}') + print(f'Syntax error occurred with: {line}') +if errors_encounter > 0: + print('You can visit "https://frappe.io/docs/user/en/translations" to resolve this error.') + assert 1+1 == 3 +else: + print('Good To Go!') diff --git a/.github/workflows/translation_linter.yml b/.github/workflows/translation_linter.yml new file mode 100644 index 0000000000..bfa34f8147 --- /dev/null +++ b/.github/workflows/translation_linter.yml @@ -0,0 +1,22 @@ +name: Frappe Linter +on: + pull_request: + branches: + - develop + - version-12-hotfix + - version-11-hotfix +jobs: + check_translation: + name: Translation Syntax Check + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + - name: Setup python3 + uses: actions/setup-python@v1 + with: + python-version: 3.6 + - name: Validating Translation Syntax + run: | + git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q + files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF) + python $GITHUB_WORKSPACE/.github/frappe_linter/translation.py $files \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 0296f38527..38846af3bd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,14 +7,19 @@ addons: - test_site_producer mariadb: 10.3 postgresql: 9.5 + chrome: stable git: depth: 1 cache: - - pip - - npm - - yarn + pip: true + npm: true + yarn: true + directories: + # we also need to cache folder with Cypress binary + # https://docs.cypress.io/guides/guides/continuous-integration.html#Caching + - ~/.cache matrix: include: @@ -54,10 +59,9 @@ before_install: install: - cd ~ - source ./.nvm/nvm.sh - - nvm install v8.10.0 + - nvm install 12 - - git clone https://github.com/frappe/bench --depth 1 - - pip install -e ./bench + - pip install frappe-bench - bench init frappe-bench --skip-assets --python $(which python) --frappe-path $TRAVIS_BUILD_DIR @@ -99,8 +103,7 @@ install: - if [ $TYPE == "server" ]; then sed -i 's/socketio:/# socketio:/g' Procfile; fi - if [ $TYPE == "server" ]; then sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile; fi - - if [ $TYPE == "ui" ]; then bench setup requirements --node; fi - + - bench setup requirements --node - bench start & - bench --site test_site reinstall --yes - bench --site test_site_producer reinstall --yes diff --git a/cypress.json b/cypress.json index 7d853271b9..ae0c45c3ae 100644 --- a/cypress.json +++ b/cypress.json @@ -1,5 +1,7 @@ { "baseUrl": "http://test_site_ui:8000", "projectId": "92odwv", - "adminPassword": "admin" + "adminPassword": "admin", + "defaultCommandTimeout": 10000, + "pageLoadTimeout": 15000 } diff --git a/cypress/integration/depends_on.js b/cypress/integration/depends_on.js index 375b690fb2..93417014c5 100644 --- a/cypress/integration/depends_on.js +++ b/cypress/integration/depends_on.js @@ -1,8 +1,4 @@ context('Depends On', () => { - beforeEach(() => { - cy.login(); - return cy.new_form('Test Depends On'); - }); before(() => { cy.login(); cy.visit('/desk#workspace/Website'); diff --git a/cypress/integration/file_uploader.js b/cypress/integration/file_uploader.js index f4ef2a19f0..5e9a264189 100644 --- a/cypress/integration/file_uploader.js +++ b/cypress/integration/file_uploader.js @@ -50,7 +50,7 @@ context('FileUploader', () => { open_upload_dialog(); cy.get_open_dialog().find('a:contains("web link")').click(); - cy.get_open_dialog().find('.file-web-link input').type('https://github.com'); + cy.get_open_dialog().find('.file-web-link input').type('https://github.com', { delay: 100, force: true }); cy.server(); cy.route('POST', '/api/method/upload_file').as('upload_file'); cy.get_open_dialog().find('.btn-primary').click(); diff --git a/cypress/integration/form.js b/cypress/integration/form.js index 9d1210ca2b..23fc57fc57 100644 --- a/cypress/integration/form.js +++ b/cypress/integration/form.js @@ -6,14 +6,17 @@ context('Form', () => { return frappe.call("frappe.tests.ui_test_helpers.create_contact_records"); }); }); - beforeEach(() => { - cy.visit('/desk#workspace/Website'); - }); it('create a new form', () => { cy.visit('/desk#Form/ToDo/New ToDo 1'); cy.fill_field('description', 'this is a test todo', 'Text Editor').blur(); cy.get('.page-title').should('contain', 'Not Saved'); + cy.server(); + cy.route({ + method: 'POST', + url: 'api/method/frappe.desk.form.save.savedocs' + }).as('form_save'); cy.get('.primary-action').click(); + cy.wait('@form_save').its('status').should('eq', 200); cy.visit('/desk#List/ToDo'); cy.location('hash').should('eq', '#List/ToDo/List'); cy.get('h1').should('be.visible').and('contain', 'To Do'); @@ -41,4 +44,21 @@ context('Form', () => { list_view.filter_area.filter_list.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 expectBackgroundColor = 'rgb(255, 220, 220)'; + + cy.visit('/desk#Form/Contact/New Contact 1'); + cy.get('.frappe-control[data-fieldname="email_ids"]').as('table'); + cy.get('@table').find('button.grid-add-row').click(); + cy.get('.grid-body .rows [data-fieldname="email_id"]').click(); + cy.get('@table').find('input.input-with-feedback.form-control').as('email_input'); + cy.get('@email_input').type(website_input, { waitForAnimations: false }); + cy.fill_field('company_name', 'Test Company'); + cy.get('@email_input').should($div => { + const style = window.getComputedStyle($div[0]); + expect(style.backgroundColor).to.equal(expectBackgroundColor); + }); + }); }); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 2d31d9a988..7816d5526f 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -186,7 +186,7 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => { if (fieldtype === 'Select') { cy.get('@input').select(value); } else { - cy.get('@input').type(value, { waitForAnimations: false }); + cy.get('@input').type(value, { waitForAnimations: false, force: true }); } return cy.get('@input'); }); diff --git a/frappe/auth.py b/frappe/auth.py index dba8b05a62..1353acf10f 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -219,7 +219,10 @@ class LoginManager: user = frappe.db.get_value("User", filters={"username": user}, fieldname="name") or user self.check_if_enabled(user) - self.user = self.check_password(user, pwd) + if not frappe.form_dict.get('tmp_id'): + self.user = self.check_password(user, pwd) + else: + self.user = user def force_user_to_reset_password(self): if not self.user: diff --git a/frappe/automation/desk_page/tools/tools.json b/frappe/automation/desk_page/tools/tools.json index 235498724d..2164a4ce38 100644 --- a/frappe/automation/desk_page/tools/tools.json +++ b/frappe/automation/desk_page/tools/tools.json @@ -3,7 +3,7 @@ { "hidden": 0, "label": "Tools", - "links": "[\n {\n \"description\": \"Documents assigned to you and by you.\",\n \"label\": \"To Do\",\n \"name\": \"ToDo\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Event and other calendars.\",\n \"label\": \"Calendar\",\n \"link\": \"List/Event/Calendar\",\n \"name\": \"Event\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Private and public Notes.\",\n \"label\": \"Note\",\n \"name\": \"Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Files\",\n \"name\": \"File\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Activity log of all users.\",\n \"label\": \"Activity\",\n \"name\": \"activity\",\n \"type\": \"page\"\n }\n]" + "links": "[\n {\n \"description\": \"Documents assigned to you and by you.\",\n \"label\": \"To Do\",\n \"name\": \"ToDo\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Event and other calendars.\",\n \"label\": \"Calendar\",\n \"link\": \"List/Event/Calendar\",\n \"name\": \"Event\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Private and public Notes.\",\n \"label\": \"Note\",\n \"name\": \"Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Files\",\n \"name\": \"File\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Video\",\n \"name\": \"Video\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Activity log of all users.\",\n \"label\": \"Activity\",\n \"name\": \"activity\",\n \"type\": \"page\"\n }\n]" }, { "hidden": 0, @@ -32,7 +32,7 @@ "idx": 0, "is_standard": 1, "label": "Tools", - "modified": "2020-04-01 11:24:40.804346", + "modified": "2020-04-20 18:21:14.152537", "modified_by": "Administrator", "module": "Automation", "name": "Tools", diff --git a/frappe/boot.py b/frappe/boot.py index e6d1199b19..9d5dbe1909 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -17,6 +17,7 @@ from frappe.utils.change_log import get_versions from frappe.translate import get_lang_dict from frappe.email.inbox import get_email_accounts from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled +from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points from frappe.social.doctype.post.post import frequently_visited_links @@ -79,6 +80,7 @@ def get_bootinfo(): bootinfo.success_action = get_success_action() bootinfo.update(get_email_accounts(user=frappe.session.user)) bootinfo.energy_points_enabled = is_energy_point_enabled() + bootinfo.website_tracking_enabled = is_tracking_enabled() bootinfo.points = get_energy_points(frappe.session.user) bootinfo.frequently_visited_links = frequently_visited_links() bootinfo.link_preview_doctypes = get_link_preview_doctypes() @@ -268,4 +270,18 @@ def get_success_action(): return frappe.get_all("Success Action", fields=["*"]) def get_link_preview_doctypes(): - return [d.name for d in frappe.db.get_all('DocType', {'show_preview_popup': 1})] \ No newline at end of file + from frappe.utils import cint + + link_preview_doctypes = [d.name for d in frappe.db.get_all('DocType', {'show_preview_popup': 1})] + customizations = frappe.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 diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index 78f452db21..2daed59074 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -16,7 +16,7 @@ global_cache_keys = ("app_hooks", "installed_apps", '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') + 'sitemap_routes', 'db_tables') user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang", "defaults", "user_permissions", "home_page", "linked_with", diff --git a/frappe/commands/site.py b/frappe/commands/site.py index fd43cc8cf3..2b4923281c 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -1,13 +1,23 @@ -from __future__ import unicode_literals, absolute_import, print_function +# imports - standard imports +import atexit +import compileall +import hashlib +import os +import re +import shutil +import sys + +# imports - third party imports import click -import hashlib, os, sys, compileall, re + +# imports - module imports import frappe from frappe import _ -from frappe.commands import pass_context, get_site +from frappe.commands import get_site, pass_context from frappe.commands.scheduler import _is_scheduler_enabled from frappe.installer import update_site_config -from frappe.utils import touch_file, get_site_path -from six import text_type +from frappe.utils import get_site_path, touch_file + @click.command('new-site') @click.argument('site') @@ -68,32 +78,33 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N make_site_dirs() - installing = None - try: - installing = touch_file(get_site_path('locks', 'installing.lock')) + installing = touch_file(get_site_path('locks', 'installing.lock')) + atexit.register(_new_site_cleanup, site, mariadb_root_username, mariadb_root_password) - install_db(root_login=mariadb_root_username, root_password=mariadb_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) + install_db(root_login=mariadb_root_username, root_password=mariadb_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 = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or []) + for app in apps_to_install: + _install_app(app, verbose=verbose, set_as_patched=not source_sql) - apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or []) - for app in apps_to_install: - _install_app(app, verbose=verbose, set_as_patched=not source_sql) + os.remove(installing) - frappe.utils.scheduler.toggle_scheduler(enable_scheduler) - frappe.db.commit() + frappe.utils.scheduler.toggle_scheduler(enable_scheduler) + frappe.db.commit() - scheduler_status = "disabled" if frappe.utils.scheduler.is_scheduler_disabled() else "enabled" - print("*** Scheduler is", scheduler_status, "***") + scheduler_status = "disabled" if frappe.utils.scheduler.is_scheduler_disabled() else "enabled" + print("*** Scheduler is", scheduler_status, "***") - except frappe.exceptions.ImproperDBConfigurationError: - _drop_site(site, mariadb_root_username, mariadb_root_password, force=True) +def _new_site_cleanup(site, mariadb_root_username, mariadb_root_password): + installing = get_site_path('locks', 'installing.lock') - finally: - if installing and os.path.exists(installing): - os.remove(installing) + if installing and os.path.exists(installing): + if mariadb_root_password: + _drop_site(site, mariadb_root_username, mariadb_root_password, force=True) + shutil.rmtree(site) - frappe.destroy() + frappe.destroy() @click.command('restore') @click.argument('sql-file-path') @@ -317,10 +328,18 @@ def backup(context, with_files=False, backup_path_db=None, backup_path_files=Non "Backup" from frappe.utils.backups import scheduled_backup verbose = context.verbose + exit_code = 0 for site in context.sites: - frappe.init(site=site) - frappe.connect() - odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True) + try: + frappe.init(site=site) + frappe.connect() + odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True) + except Exception as e: + if verbose: + print("Backup failed for {0}. Database or site_config.json may be corrupted".format(site)) + exit_code = 1 + continue + if verbose: from frappe.utils import now print("database backup taken -", odb.backup_path_db, "- on", now()) @@ -329,6 +348,7 @@ def backup(context, with_files=False, backup_path_db=None, backup_path_files=Non print("private files backup taken -", odb.backup_path_private_files, "- on", now()) frappe.destroy() + sys.exit(exit_code) @click.command('remove-from-installed-apps') @click.argument('app') diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index ec30fc19b8..da9d67be3b 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -522,7 +522,7 @@ def run_ui_tests(context, app, headless=False): password_env = 'CYPRESS_adminPassword={}'.format(admin_password) if admin_password else '' # run for headless mode - run_or_open = 'run --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open' + run_or_open = 'run --browser chrome --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open' command = '{site_env} {password_env} yarn run cypress {run_or_open}' formatted_command = command.format(site_env=site_env, password_env=password_env, run_or_open=run_or_open) frappe.commands.popen(formatted_command, cwd=app_base_path, raise_err=True) diff --git a/frappe/core/desk_page/users/users.json b/frappe/core/desk_page/users/users.json index 30455b86e6..f1f43a4ef0 100644 --- a/frappe/core/desk_page/users/users.json +++ b/frappe/core/desk_page/users/users.json @@ -27,7 +27,7 @@ "idx": 0, "is_standard": 1, "label": "Users", - "modified": "2020-04-01 11:24:40.767676", + "modified": "2020-04-26 22:36:14.311554", "modified_by": "Administrator", "module": "Core", "name": "Users", @@ -46,12 +46,12 @@ "type": "DocType" }, { - "label": "permission-manager", + "label": "Permission Manager", "link_to": "permission-manager", "type": "Page" }, { - "label": "user-profile", + "label": "User Profile", "link_to": "user-profile", "type": "Page" } diff --git a/frappe/core/doctype/data_import/exporter_new.py b/frappe/core/doctype/data_import/exporter_new.py index e8e360dc50..85f933be69 100644 --- a/frappe/core/doctype/data_import/exporter_new.py +++ b/frappe/core/doctype/data_import/exporter_new.py @@ -71,7 +71,11 @@ class Exporter: return parent_fields def get_exportable_children_fields(self): - children = [df.options for df in self.meta.fields if df.fieldtype in table_fields] + child_table_fields = [df for df in self.meta.fields if df.fieldtype in table_fields] + if self.export_fields == "Mandatory": + child_table_fields = [df for df in child_table_fields if df.reqd] + + children = [df.options for df in child_table_fields] children_fields = [] for child in children: children_fields += self.get_exportable_fields(child) diff --git a/frappe/core/doctype/data_import/importer_new.py b/frappe/core/doctype/data_import/importer_new.py index 1f446cfb39..3dcb902482 100644 --- a/frappe/core/doctype/data_import/importer_new.py +++ b/frappe/core/doctype/data_import/importer_new.py @@ -351,9 +351,9 @@ class Importer: value = cstr(value) # convert boolean values to 0 or 1 - if df.fieldtype == "Check" and value.lower().strip() in ["t", "f", "true", "false"]: + if df.fieldtype == "Check" and value.lower().strip() in ["t", "f", "true", "false", "yes", "no", "y", "n"]: value = value.lower().strip() - value = 1 if value in ["t", "true"] else 0 + value = 1 if value in ["t", "true", "y", "yes"] else 0 if df.fieldtype in ["Int", "Check"]: value = cint(value) @@ -610,7 +610,7 @@ class Importer: "message": msg, } ) - return False + return elif df.fieldtype == "Link": d = self.get_missing_link_field_values(df.options) @@ -643,8 +643,10 @@ class Importer: if value in INVALID_VALUES: value = None - value = validate_value(value, df) - if value: + if value is not None: + value = validate_value(value, df) + + if value is not None: doc[df.fieldname] = self.parse_value(value, df) is_table = frappe.get_meta(doctype).istable diff --git a/frappe/core/doctype/data_import/test_exporter_new.py b/frappe/core/doctype/data_import/test_exporter_new.py index 7464d6edc5..faa48a35f4 100644 --- a/frappe/core/doctype/data_import/test_exporter_new.py +++ b/frappe/core/doctype/data_import/test_exporter_new.py @@ -20,7 +20,7 @@ class TestExporter(unittest.TestCase): e = Exporter('Web Page', export_fields='All') csv_array = e.get_csv_array() header = csv_array[0] - self.assertEqual(len(header), 24) + self.assertEqual(len(header), 36) def test_exports_selected_fields(self): diff --git a/frappe/core/doctype/data_import_beta/data_import_beta.js b/frappe/core/doctype/data_import_beta/data_import_beta.js index a594b213c5..82c490c524 100644 --- a/frappe/core/doctype/data_import_beta/data_import_beta.js +++ b/frappe/core/doctype/data_import_beta/data_import_beta.js @@ -337,7 +337,12 @@ frappe.ui.form.on('Data Import Beta', { let message = warnings_by_row[row_number] .map(w => { if (w.field) { - return `
  • ${w.field.label}: ${w.message}
  • `; + let label = + w.field.label + + (w.field.parent !== frm.doc.reference_doctype + ? ` (${w.field.parent})` + : ''); + return `
  • ${label}: ${w.message}
  • `; } return `
  • ${w.message}
  • `; }) diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index 4614dd09c4..6d8ee41a5a 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -11,9 +11,9 @@ "label", "fieldtype", "fieldname", - "reqd", "precision", "length", + "reqd", "search_index", "in_list_view", "in_standard_filter", @@ -453,7 +453,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-04-15 02:26:03.310781", + "modified": "2020-04-19 21:54:13.783908", "modified_by": "Administrator", "module": "Core", "name": "DocField", diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index f7c9cbe28a..904deb9990 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -477,7 +477,8 @@ class DocType(Document): field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict['fields'])) if field_dict: new_field_dicts.append(field_dict[0]) - remaining_field_names.remove(fieldname) + if fieldname in remaining_field_names: + remaining_field_names.remove(fieldname) for fieldname in remaining_field_names: field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict['fields'])) @@ -498,7 +499,8 @@ class DocType(Document): field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict.get('fields', []))) if field_dict: new_field_dicts.append(field_dict[0]) - remaining_field_names.remove(fieldname) + if fieldname in remaining_field_names: + remaining_field_names.remove(fieldname) for fieldname in remaining_field_names: field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict.get('fields', []))) @@ -710,9 +712,10 @@ def validate_fields(meta): if d.fieldtype == "Currency" and cint(d.width) < 100: frappe.throw(_("Max width for type Currency is 100px in row {0}").format(d.idx)) - def check_in_list_view(d): + def check_in_list_view(is_table, d): if d.in_list_view and (d.fieldtype in not_allowed_in_list_view): - frappe.throw(_("'In List View' not allowed for type {0} in row {1}").format(d.fieldtype, d.idx)) + property_label = 'In Grid View' if is_table else 'In List View' + frappe.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: @@ -731,8 +734,11 @@ def validate_fields(meta): d.default = '0' if d.fieldtype == "Check" and d.default not in ('0', '1'): frappe.throw(_("Default for 'Check' type of field must be either '0' or '1'")) - if d.fieldtype == "Select" and d.default and (d.default not in d.options.split("\n")): - frappe.throw(_("Default for {0} must be an option").format(d.fieldname)) + if d.fieldtype == "Select" and d.default: + if not d.options: + frappe.throw(_("Options for {0} must be set before setting the default value.").format(frappe.bold(d.fieldname))) + elif d.default not in d.options.split("\n"): + frappe.throw(_("Default value for {0} must be in the list of options.").format(frappe.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): @@ -893,7 +899,7 @@ def validate_fields(meta): field.fetch_from = field.fetch_from.strip('\n').strip() def validate_data_field_type(docfield): - if docfield.fieldtype == "Data": + 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 = frappe.bold(_(docfield.label)) text_str = _("{0} is an invalid Data field.").format(df_str) + "
    " * 2 + _("Only Options allowed for Data field are:") + "
    " @@ -901,6 +907,16 @@ def validate_fields(meta): frappe.msgprint(text_str + df_options_str, title="Invalid Data Field", raise_exception=True) + def check_child_table_option(docfield): + if docfield.fieldtype not in ['Table MultiSelect', 'Table']: return + + doctype = docfield.options + meta = frappe.get_meta(doctype) + + if not meta.istable: + frappe.throw(_('Option {0} for field {1} is not a child table') + .format(frappe.bold(doctype), frappe.bold(docfield.fieldname)), title=_("Invalid Option")) + fields = meta.get("fields") fieldname_list = [d.fieldname for d in fields] @@ -924,11 +940,12 @@ def validate_fields(meta): 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(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) diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 7f763ea9fc..b35abfa861 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -1,46 +1,45 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt -from __future__ import unicode_literals -from frappe import _ """ record of files naming for same name files: file.gif, file-1.gif, file-2.gif etc """ -import frappe -import json -import os +from __future__ import unicode_literals + import base64 -import re import hashlib -import mimetypes +import imghdr import io +import json +import mimetypes +import os +import re import shutil +import zipfile + import requests import requests.exceptions -import imghdr +from PIL import Image, ImageFile, ImageOps +from six import PY2, StringIO, string_types, text_type +from six.moves.urllib.parse import quote, unquote -from frappe.utils import get_hook_method, get_files_path, random_string, encode, cstr, call_hook_method, cint -from frappe import _ -from frappe import conf -from frappe.utils.nestedset import NestedSet +import frappe +from frappe import _, conf from frappe.model.document import Document -from frappe.utils import strip -from PIL import Image, ImageOps -from six import StringIO, string_types -from six.moves.urllib.parse import unquote, quote -from six import text_type, PY2 -import zipfile +from frappe.utils import call_hook_method, cint, cstr, encode, get_files_path, get_hook_method, random_string, strip + class MaxFileSizeReachedError(frappe.ValidationError): pass - -class FolderNotEmpty(frappe.ValidationError): pass +class FolderNotEmpty(frappe.ValidationError): + pass exclude_from_linked_with = True +ImageFile.LOAD_TRUNCATED_IMAGES = True class File(Document): @@ -608,8 +607,7 @@ def get_local_image(file_url): try: image = Image.open(file_path) except IOError: - frappe.msgprint(_("Unable to read file format for {0}").format(file_url)) - raise + frappe.msgprint(_("Unable to read file format for {0}").format(file_url), raise_exception=True) content = None @@ -698,7 +696,7 @@ def remove_file(fid=None, attached_to_doctype=None, attached_to_name=None, from_ def get_max_file_size(): - return conf.get('max_file_size') or 10485760 + return cint(conf.get('max_file_size')) or 10485760 def remove_all(dt, dn, from_delete=False): @@ -715,7 +713,10 @@ def has_permission(doc, ptype=None, user=None): has_access = False user = user or frappe.session.user - if not doc.is_private or doc.owner == user or user == 'Administrator': + if ptype == 'create': + has_access = frappe.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: diff --git a/frappe/core/doctype/language/language.json b/frappe/core/doctype/language/language.json index 099b383980..eed29883c1 100644 --- a/frappe/core/doctype/language/language.json +++ b/frappe/core/doctype/language/language.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_rename": 1, "autoname": "field:language_code", "creation": "2014-08-22 16:12:17.249590", @@ -41,7 +42,9 @@ } ], "icon": "fa fa-globe", - "modified": "2019-07-19 16:32:12.652550", + "in_create": 1, + "links": [], + "modified": "2020-04-16 22:11:33.066852", "modified_by": "Administrator", "module": "Core", "name": "Language", diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json index e2ec921679..2a9c1a4573 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json @@ -13,6 +13,7 @@ "field_order": [ "stopped", "method", + "server_script", "frequency", "cron_format", "last_execution", @@ -63,6 +64,14 @@ "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 } ], "in_create": 1, @@ -72,7 +81,7 @@ "link_fieldname": "scheduled_job_type" } ], - "modified": "2019-12-09 11:10:21.259929", + "modified": "2020-04-05 17:27:33.480562", "modified_by": "Administrator", "module": "Core", "name": "Scheduled Job Type", diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py index 3cd994ebfa..c179054550 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -70,7 +70,12 @@ class ScheduledJobType(Document): self.scheduler_log = None try: self.log_status('Start') - frappe.get_attr(self.method)() + if self.server_script: + script_name = frappe.db.get_value("Server Script", self.server_script) + if script_name: + frappe.get_doc('Server Script', script_name).execute_scheduled_method() + else: + frappe.get_attr(self.method)() frappe.db.commit() self.log_status('Complete') except Exception: diff --git a/frappe/core/doctype/server_script/server_script.js b/frappe/core/doctype/server_script/server_script.js index eea8558456..d7f4c3e536 100644 --- a/frappe/core/doctype/server_script/server_script.js +++ b/frappe/core/doctype/server_script/server_script.js @@ -2,7 +2,45 @@ // For license information, please see license.txt frappe.ui.form.on('Server Script', { - // refresh: function(frm) { + refresh: function(frm) { + if(frm.doc.script_type === 'Scheduler Event' && !frm.doc.disabled){ + frm.add_custom_button('Schedule Script', function() { + var d = new frappe.ui.Dialog({ + title: "Schedule Script Execution", + fields: [ + { + fieldname: "event_type", + label: __('Select Event Type'), + fieldtype: "Select", + options: "All\nHourly\nDaily\nWeekly\nMonthly\nYearly\nHourly Long\nDaily Long\nWeekly Long\nMonthly Long" + }, + ], + primary_action_label: __('Schedule Script'), + primary_action: () => { + d.get_primary_btn().attr('disabled', true); + var data = d.get_values(); + d.hide(); + if(data) { + frm.events.schedule_script(frm, data); + } + + } + }); + + d.show(); + + }); + } + }, + + schedule_script(frm, data){ + frm.call({ + method: "frappe.core.doctype.server_script.server_script.setup_scheduler_events", + args: { + 'script_name': frm.doc.name, + 'frequency': data.event_type + } + }) + } - // } }); diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json index 36c297cc26..bef3dfc60c 100644 --- a/frappe/core/doctype/server_script/server_script.json +++ b/frappe/core/doctype/server_script/server_script.json @@ -22,7 +22,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Script Type", - "options": "DocType Event\nAPI", + "options": "DocType Event\nScheduler Event\nAPI", "reqd": 1 }, { @@ -75,7 +75,7 @@ } ], "links": [], - "modified": "2019-12-17 12:55:07.389775", + "modified": "2020-04-06 11:24:38.161555", "modified_by": "Administrator", "module": "Core", "name": "Server Script", diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index e2c6d3b7b0..9522b77b4b 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -7,6 +7,8 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document from frappe.utils.safe_exec import safe_exec +from frappe import _ + class ServerScript(Document): @staticmethod @@ -31,3 +33,39 @@ class ServerScript(Document): # execute event safe_exec(self.script, None, dict(doc = doc)) + def execute_scheduled_method(self): + if self.script_type == 'Scheduler Event': + safe_exec(self.script) + else: + # wrong report type! + raise frappe.DoesNotExistError + +@frappe.whitelist() +def setup_scheduler_events(script_name, frequency): + method = frappe.scrub(script_name) + '_' + frequency.lower() + scheduled_script = frappe.db.get_value('Scheduled Job Type', + dict(method=method)) + + if not scheduled_script: + doc = frappe.get_doc(dict( + doctype = 'Scheduled Job Type', + method = method, + frequency = frequency, + server_script = script_name + )) + + doc.insert() + + frappe.msgprint(_('Enabled scheduled execution for script {0}').format(script_name)) + + else: + doc = frappe.get_doc('Scheduled Job Type', scheduled_script) + doc.update(dict( + doctype = 'Scheduled Job Type', + method = method, + frequency = frequency, + server_script = script_name + )) + doc.save() + + frappe.msgprint(_('Scheduled execution for script {0} has updated').format(script_name)) diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index 2e1a5ae8bb..e03504f30b 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -66,6 +66,7 @@ def get_server_script_map(): script_map.setdefault(script.reference_doctype, {}).setdefault(script.doctype_event, []).append(script.name) else: script_map.setdefault('_api', {})[script.api_method] = script.name + frappe.cache().set_value('server_script_map', script_map) - return script_map + return script_map \ No newline at end of file diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index b17548d994..b8e16bfe25 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -97,47 +97,49 @@ frappe.ui.form.on('User', { }); }, __("Password")); - frappe.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 frappe.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" + if (frappe.user.has_role("System Manager")) { + frappe.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 frappe.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) { + frappe.throw(__("Passwords do not match!")); + } + frappe.call( + "frappe.integrations.doctype.ldap_settings.ldap_settings.reset_password", { + user: frm.doc.email, + password: values.new_password, + logout: values.logout_sessions + }); } - ], - primary_action: (values) => { - d.hide(); - if (values.new_password !== values.confirm_password) { - frappe.throw(__("Passwords do not match!")); - } - frappe.call( - "frappe.integrations.doctype.ldap_settings.ldap_settings.reset_password", { - user: frm.doc.email, - password: values.new_password, - logout: values.logout_sessions - }); - } - }); - d.show(); - }, __("Password")); - } - }); + }); + d.show(); + }, __("Password")); + } + }); + } frm.add_custom_button(__("Reset OTP Secret"), function() { frappe.call({ diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 7837c90d2b..8370af6808 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -551,6 +551,7 @@ def update_password(new_password, logout_all_sessions=0, key=None, old_password= res = _get_user_for_update_password(key, old_password) if res.get('message'): + frappe.local.response.http_status_code = 410 return res['message'] else: user = res['user'] @@ -718,7 +719,7 @@ def _get_user_for_update_password(key, old_password): user = frappe.db.get_value("User", {"reset_password_key": key}) if not user: return { - 'message': _("Cannot Update: Incorrect / Expired Link.") + 'message': _("The Link specified has either been used before or Invalid") } elif old_password: diff --git a/frappe/core/doctype/user_permission/test_user_permission.py b/frappe/core/doctype/user_permission/test_user_permission.py index 870d3c7029..5ccc8752cf 100644 --- a/frappe/core/doctype/user_permission/test_user_permission.py +++ b/frappe/core/doctype/user_permission/test_user_permission.py @@ -9,7 +9,8 @@ import unittest class TestUserPermission(unittest.TestCase): def setUp(self): - frappe.db.sql("DELETE FROM `tabUser Permission` WHERE `user`='test_bulk_creation_update@example.com'") + frappe.db.sql("""DELETE FROM `tabUser Permission` + WHERE `user` in ('test_bulk_creation_update@example.com', 'test_user_perm1@example.com')""") def test_default_user_permission_validation(self): user = create_user('test_default_permission@example.com') @@ -20,6 +21,26 @@ class TestUserPermission(unittest.TestCase): param = get_params(user, 'User', perm_user.name, is_default=1) self.assertRaises(frappe.ValidationError, add_user_permissions, param) + def test_default_user_permission(self): + frappe.set_user('Administrator') + user = create_user('test_user_perm1@example.com', 'Website Manager') + for category in ['general', 'public']: + if not frappe.db.exists('Blog Category', category): + frappe.get_doc({'doctype': 'Blog Category', + 'category_name': 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) + + frappe.set_user('test_user_perm1@example.com') + doc = frappe.new_doc("Blog Post") + + self.assertEquals(doc.blog_category, 'general') + frappe.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') @@ -88,7 +109,7 @@ class TestUserPermission(unittest.TestCase): self.assertIsNone(removed_applicable_second) self.assertEquals(is_created, 1) -def create_user(email): +def create_user(email, role="System Manager"): ''' create user with role system manager ''' if frappe.db.exists('User', email): return frappe.get_doc('User', email) @@ -96,7 +117,7 @@ def create_user(email): user = frappe.new_doc('User') user.email = email user.first_name = email.split("@")[0] - user.add_roles("System Manager") + user.add_roles(role) return user def get_params(user, doctype, docname, is_default=0, applicable=None): diff --git a/frappe/website/doctype/css_class/__init__.py b/frappe/core/doctype/video/__init__.py similarity index 100% rename from frappe/website/doctype/css_class/__init__.py rename to frappe/core/doctype/video/__init__.py diff --git a/frappe/website/doctype/css_class/test_css_class.py b/frappe/core/doctype/video/test_video.py similarity index 82% rename from frappe/website/doctype/css_class/test_css_class.py rename to frappe/core/doctype/video/test_video.py index 551b44e3f2..0bed1e98d6 100644 --- a/frappe/website/doctype/css_class/test_css_class.py +++ b/frappe/core/doctype/video/test_video.py @@ -6,5 +6,5 @@ from __future__ import unicode_literals # import frappe import unittest -class TestCSSClass(unittest.TestCase): +class TestVideo(unittest.TestCase): pass diff --git a/frappe/website/doctype/web_view/web_view.js b/frappe/core/doctype/video/video.js similarity index 82% rename from frappe/website/doctype/web_view/web_view.js rename to frappe/core/doctype/video/video.js index 449c0949b6..36ea240a36 100644 --- a/frappe/website/doctype/web_view/web_view.js +++ b/frappe/core/doctype/video/video.js @@ -1,7 +1,7 @@ // Copyright (c) 2020, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on('Web View', { +frappe.ui.form.on('Video', { // refresh: function(frm) { // } diff --git a/frappe/core/doctype/video/video.json b/frappe/core/doctype/video/video.json new file mode 100644 index 0000000000..26a407c05c --- /dev/null +++ b/frappe/core/doctype/video/video.json @@ -0,0 +1,106 @@ +{ + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:title", + "creation": "2018-10-17 05:47:13.087395", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "title", + "provider", + "url", + "column_break_4", + "publish_date", + "duration", + "section_break_7", + "description" + ], + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Title", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "provider", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Provider", + "options": "YouTube\nVimeo", + "reqd": 1 + }, + { + "fieldname": "url", + "fieldtype": "Data", + "in_list_view": 1, + "label": "URL", + "reqd": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "publish_date", + "fieldtype": "Date", + "label": "Publish Date" + }, + { + "fieldname": "duration", + "fieldtype": "Data", + "label": "Duration" + }, + { + "fieldname": "section_break_7", + "fieldtype": "Section Break" + }, + { + "fieldname": "description", + "fieldtype": "Text Editor", + "in_list_view": 1, + "label": "Description", + "reqd": 1 + } + ], + "links": [], + "modified": "2020-04-22 12:09:49.057403", + "modified_by": "Administrator", + "module": "Core", + "name": "Video", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1, + "write": 1 + }, + { + "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/frappe/website/doctype/css_class/css_class.py b/frappe/core/doctype/video/video.py similarity index 90% rename from frappe/website/doctype/css_class/css_class.py rename to frappe/core/doctype/video/video.py index cb9e7483d4..fdbd3a1abe 100644 --- a/frappe/website/doctype/css_class/css_class.py +++ b/frappe/core/doctype/video/video.py @@ -6,5 +6,5 @@ from __future__ import unicode_literals # import frappe from frappe.model.document import Document -class CSSClass(Document): +class Video(Document): pass diff --git a/frappe/core/page/background_jobs/background_jobs.py b/frappe/core/page/background_jobs/background_jobs.py index c8a2352968..4a94de4ace 100644 --- a/frappe/core/page/background_jobs/background_jobs.py +++ b/frappe/core/page/background_jobs/background_jobs.py @@ -28,6 +28,7 @@ def get_info(show_failed=False): if j.kwargs.get('site')==frappe.local.site: jobs.append({ 'job_name': j.kwargs.get('kwargs', {}).get('playbook_method') \ + or j.kwargs.get('kwargs', {}).get('job_type') \ or str(j.kwargs.get('job_name')), 'status': j.get_status(), 'queue': name, 'creation': format_datetime(convert_utc_to_user_timezone(j.created_at)), diff --git a/frappe/core/page/dashboard/dashboard.js b/frappe/core/page/dashboard/dashboard.js index ad65b05894..222a31a863 100644 --- a/frappe/core/page/dashboard/dashboard.js +++ b/frappe/core/page/dashboard/dashboard.js @@ -76,7 +76,16 @@ class Dashboard { } refresh() { - this.get_permitted_dashboard_charts().then(charts => { + frappe.run_serially([ + () => this.render_cards(), + () => this.render_charts() + ]); + } + + render_charts() { + return this.get_permitted_items( + 'frappe.desk.doctype.dashboard.dashboard.get_permitted_charts' + ).then(charts => { if (!charts.length) { frappe.msgprint(__('No Permitted Charts on this Dashboard'), __('No Permitted Charts')) } @@ -92,6 +101,7 @@ class Dashboard { ...chart } }); + this.chart_group = new frappe.widget.WidgetGroup({ title: null, container: this.container, @@ -110,14 +120,46 @@ class Dashboard { }); } - get_permitted_dashboard_charts() { + render_cards() { + return this.get_permitted_items( + 'frappe.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 frappe.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 frappe.xcall( - 'frappe.desk.doctype.dashboard.dashboard.get_permitted_charts', + method, { dashboard_name: this.dashboard_name - }).then(charts => { - return charts; - }); + } + ).then(items => { + return items; + }); } set_dropdown() { diff --git a/frappe/core/page/permission_manager/permission_manager.js b/frappe/core/page/permission_manager/permission_manager.js index ed3b0d17db..3f5d7c0a7b 100644 --- a/frappe/core/page/permission_manager/permission_manager.js +++ b/frappe/core/page/permission_manager/permission_manager.js @@ -45,7 +45,7 @@ frappe.PermissionEngine = Class.extend({ setup_page: function() { var me = this; this.doctype_select - = this.wrapper.page.add_select(__("Document Types"), + = this.wrapper.page.add_select(__("Document Type"), [{value: "", label: __("Select Document Type")+"..."}].concat(this.options.doctypes)) .change(function() { frappe.set_route("permission-manager", $(this).val()); diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json index b274033f80..394f38b56c 100644 --- a/frappe/custom/doctype/custom_field/custom_field.json +++ b/frappe/custom/doctype/custom_field/custom_field.json @@ -41,6 +41,7 @@ "in_list_view", "in_standard_filter", "in_global_search", + "in_preview", "bold", "report_hide", "search_index", @@ -371,12 +372,18 @@ "fieldname": "allow_in_quick_entry", "fieldtype": "Check", "label": "Allow in Quick Entry" + }, + { + "default": "0", + "fieldname": "in_preview", + "fieldtype": "Check", + "label": "In Preview" } ], "icon": "fa fa-glass", "idx": 1, "links": [], - "modified": "2020-03-16 14:52:43.954709", + "modified": "2020-04-10 11:57:10.392218", "modified_by": "Administrator", "module": "Custom", "name": "Custom Field", diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json index 51a5c0b85f..cd57aa23fe 100644 --- a/frappe/custom/doctype/customize_form/customize_form.json +++ b/frappe/custom/doctype/customize_form/customize_form.json @@ -20,6 +20,7 @@ "track_views", "allow_auto_repeat", "allow_import", + "show_preview_popup", "image_view", "column_break_5", "title_field", @@ -203,6 +204,12 @@ "depends_on": "doc_type", "fieldname": "section_break_23", "fieldtype": "Section Break" + }, + { + "default": "0", + "fieldname": "show_preview_popup", + "fieldtype": "Check", + "label": "Show Preview Popup" } ], "hide_toolbar": 1, @@ -210,7 +217,7 @@ "idx": 1, "issingle": 1, "links": [], - "modified": "2020-03-27 15:06:35.443861", + "modified": "2020-04-10 12:16:01.320411", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form", diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 7d081953dd..9efa555152 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -32,6 +32,7 @@ doctype_properties = { 'track_views': 'Check', 'allow_auto_repeat': 'Check', 'allow_import': 'Check', + 'show_preview_popup': 'Check', 'email_append_to': 'Check', 'subject_field': 'Data', 'sender_field': 'Data' @@ -53,6 +54,7 @@ docfield_properties = { 'in_list_view': 'Check', 'in_standard_filter': 'Check', 'in_global_search': 'Check', + 'in_preview': 'Check', 'bold': 'Check', 'hidden': 'Check', 'collapsible': 'Check', diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json index 350d159541..d7887cf8bd 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -16,6 +16,7 @@ "in_list_view", "in_standard_filter", "in_global_search", + "in_preview", "bold", "allow_in_quick_entry", "translatable", @@ -381,12 +382,18 @@ "fieldtype": "Code", "label": "Read Only Depends On", "options": "JS" + }, + { + "default": "0", + "fieldname": "in_preview", + "fieldtype": "Check", + "label": "In Preview" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-04-15 02:26:59.673750", + "modified": "2020-04-10 11:58:44.573537", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form Field", diff --git a/frappe/database/database.py b/frappe/database/database.py index b083ff1014..101b97c915 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -124,6 +124,8 @@ class Database(object): # in transaction validations self.check_transaction_status(query) + self.clear_db_table_cache(query) + # autocommit if auto_commit: self.commit() @@ -277,6 +279,11 @@ class Database(object): ret.append(frappe._dict(zip(keys, values))) return ret + @staticmethod + def clear_db_table_cache(query): + if query and query.strip().split()[0].lower() in {'drop', 'create'}: + frappe.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.""" @@ -769,7 +776,16 @@ class Database(object): return ("tab" + doctype) in self.get_tables() def get_tables(self): - return [d[0] for d in self.sql("select table_name from information_schema.tables where table_schema not in ('pg_catalog', 'information_schema')")] + tables = frappe.cache().get_value('db_tables') + if not tables: + table_rows = self.sql(""" + SELECT table_name + FROM information_schema.tables + WHERE table_schema NOT IN ('pg_catalog', 'information_schema') + """) + tables = {d[0] for d in table_rows} + frappe.cache().set_value('db_tables', tables) + return tables def a_row_exists(self, doctype): """Returns True if atleast one row exists.""" diff --git a/frappe/database/schema.py b/frappe/database/schema.py index 28e055f382..52dc2ba917 100644 --- a/frappe/database/schema.py +++ b/frappe/database/schema.py @@ -137,16 +137,14 @@ class DBTable: if frappe.db.is_missing_column(e): # Unknown column 'column_name' in 'field list' continue - else: - raise + 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 - - frappe.msgprint(_("""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)) + 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) + frappe.msgprint(info_message) def is_new(self): return self.table_name not in frappe.db.get_tables() diff --git a/frappe/desk/doctype/dashboard/dashboard.js b/frappe/desk/doctype/dashboard/dashboard.js index 19ce8eb1fd..609e943995 100644 --- a/frappe/desk/doctype/dashboard/dashboard.js +++ b/frappe/desk/doctype/dashboard/dashboard.js @@ -4,5 +4,21 @@ frappe.ui.form.on('Dashboard', { refresh: function(frm) { frm.add_custom_button(__("Show Dashboard"), () => frappe.set_route('dashboard', frm.doc.name)); + + 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/frappe/desk/doctype/dashboard/dashboard.json b/frappe/desk/doctype/dashboard/dashboard.json index c177ee70ac..c17bc3235c 100644 --- a/frappe/desk/doctype/dashboard/dashboard.json +++ b/frappe/desk/doctype/dashboard/dashboard.json @@ -8,7 +8,8 @@ "field_order": [ "dashboard_name", "is_default", - "charts" + "charts", + "cards" ], "fields": [ { @@ -31,10 +32,16 @@ "label": "Charts", "options": "Dashboard Chart Link", "reqd": 1 + }, + { + "fieldname": "cards", + "fieldtype": "Table", + "label": "Cards", + "options": "Number Card Link" } ], "links": [], - "modified": "2020-03-25 21:09:37.080132", + "modified": "2020-04-19 17:44:36.237163", "modified_by": "Administrator", "module": "Desk", "name": "Dashboard", diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py index 5c344956bf..b85e135071 100644 --- a/frappe/desk/doctype/dashboard/dashboard.py +++ b/frappe/desk/doctype/dashboard/dashboard.py @@ -21,3 +21,12 @@ def get_permitted_charts(dashboard_name): if frappe.has_permission('Dashboard Chart', doc=chart.chart): permitted_charts.append(chart) return permitted_charts + +@frappe.whitelist() +def get_permitted_cards(dashboard_name): + permitted_cards = [] + dashboard = frappe.get_doc('Dashboard', dashboard_name) + for card in dashboard.cards: + if frappe.has_permission('Number Card', doc=card.card): + permitted_cards.append(card) + return permitted_cards diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js index a130c1d6cf..f8d5886b26 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js @@ -9,6 +9,7 @@ frappe.ui.form.on('Dashboard Chart', { frm.add_fetch('source', 'timeseries', 'timeseries'); }, + refresh: function(frm) { frm.chart_filters = null; frm.add_custom_button('Add Chart to Dashboard', () => { @@ -59,6 +60,10 @@ frappe.ui.form.on('Dashboard Chart', { if (frm.doc.report_name) { frm.trigger('set_chart_report_filters'); } + + if (!frappe.boot.developer_mode) { + frm.set_df_property("custom_options", "hidden", 1); + } }, source: function(frm) { diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json index 676cdbe24a..b5201a8b1f 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json @@ -22,6 +22,7 @@ "aggregate_function_based_on", "number_of_groups", "column_break_6", + "is_public", "timespan", "from_date", "to_date", @@ -33,6 +34,7 @@ "type", "column_break_2", "color", + "custom_options", "section_break_10", "last_synced_on" ], @@ -98,7 +100,7 @@ }, { "default": "0", - "depends_on": "eval:doc.chart_type !== 'Group By'", + "depends_on": "eval: ['Count', 'Sum', 'Average'].includes(doc.chart_type)", "fieldname": "timeseries", "fieldtype": "Check", "label": "Time Series" @@ -124,7 +126,7 @@ "fieldname": "type", "fieldtype": "Select", "label": "Type", - "options": "Line\nBar\nPercentage\nPie", + "options": "Line\nBar\nPercentage\nPie\nDonut", "reqd": 1 }, { @@ -213,10 +215,23 @@ "label": "Y Axis", "mandatory_depends_on": "eval:doc.report_name && !doc.is_custom", "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" } ], "links": [], - "modified": "2020-04-08 18:54:36.739183", + "modified": "2020-05-01 15:22:59.119341", "modified_by": "Administrator", "module": "Desk", "name": "Dashboard Chart", @@ -247,6 +262,7 @@ "write": 1 }, { + "create": 1, "email": 1, "export": 1, "print": 1, diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index b2a6f0a0ff..7f3dccca74 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -76,10 +76,10 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d if to_date and len(to_date): to_date = get_datetime(to_date) else: - to_date = chart.to_date + to_date = get_datetime(chart.to_date) timegrain = time_interval or chart.time_interval - filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json) + filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json) or [] # don't include cancelled documents filters.append([chart.document_type, 'docstatus', '<', 2, False]) @@ -92,22 +92,33 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d return chart_config @frappe.whitelist() -def create_report_chart(args): +def create_dashboard_chart(args): args = frappe.parse_json(args) - _doc = frappe.new_doc('Dashboard Chart') + doc = frappe.new_doc('Dashboard Chart') + + doc.update(args) + + if args.get('custom_options'): + doc.custom_options = json.dumps(args.get('custom_options')) - _doc.update(args) if frappe.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) + doc.chart_name = args.chart_name + doc.insert(ignore_permissions=True) + return doc + +@frappe.whitelist() +def create_report_chart(args): + create_dashboard_chart(args) + args = frappe.parse_json(args) if args.dashboard: add_chart_to_dashboard(json.dumps(args)) @frappe.whitelist() def add_chart_to_dashboard(args): args = frappe.parse_json(args) + dashboard = frappe.get_doc('Dashboard', args.dashboard) dashboard_link = frappe.new_doc('Dashboard Chart Link') dashboard_link.chart = args.chart_name @@ -351,6 +362,13 @@ def get_year_ending(date): # last day of this month return add_to_date(date, days=-1) +def get_charts_for_user(doctype, txt, searchfield, start, page_len, filters): + or_filters = {'owner': frappe.session.user, 'is_public': 1} + return frappe.db.get_list('Dashboard Chart', + fields=['name'], + filters=filters, + or_filters=or_filters, + as_list = 1) class DashboardChart(Document): @@ -362,6 +380,8 @@ class DashboardChart(Document): self.check_required_field() self.check_document_type() + self.validate_custom_options() + def check_required_field(self): if not self.document_type: frappe.throw(_("Document type is required to create a dashboard chart")) @@ -378,3 +398,10 @@ class DashboardChart(Document): def check_document_type(self): if frappe.get_meta(self.document_type).issingle: frappe.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: + frappe.throw("Invalid json added in the custom options: %s" % error) \ No newline at end of file diff --git a/frappe/website/doctype/web_view/__init__.py b/frappe/desk/doctype/number_card/__init__.py similarity index 100% rename from frappe/website/doctype/web_view/__init__.py rename to frappe/desk/doctype/number_card/__init__.py diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js new file mode 100644 index 0000000000..184fe5e6cb --- /dev/null +++ b/frappe/desk/doctype/number_card/number_card.js @@ -0,0 +1,119 @@ +// Copyright (c) 2020, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Number Card', { + refresh: function(frm) { + frm.set_df_property("filters_section", "hidden", 1); + frm.trigger('set_options'); + 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('aggregate_function_based_on', ''); + frm.trigger('set_options'); + }, + + set_options: function(frm) { + let aggregate_based_on_fields = []; + const doctype = frm.doc.document_type; + + if (doctype) { + frappe.model.with_doctype(doctype, () => { + frappe.get_meta(doctype).fields.map(df => { + if (frappe.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); + }); + } + }, + + render_filters_table: function(frm) { + frm.set_df_property("filters_section", "hidden", 0); + + let wrapper = $(frm.get_field('filters_json').wrapper).empty(); + frm.filter_table = $(` + + + + + + + + +
    ${__('Filter')}${__('Condition')}${__('Value')}
    `).appendTo(wrapper); + + frm.filters = JSON.parse(frm.doc.filters_json || '[]'); + + frm.trigger('set_filters_in_table'); + + frm.filter_table.on('click', () => { + let dialog = new frappe.ui.Dialog({ + title: __('Set Filters'), + fields: [{ + fieldtype: 'HTML', + fieldname: 'filter_area', + }], + primary_action: function() { + let values = this.get_values(); + if (values) { + this.hide(); + frm.filters = frm.filter_group.get_filters(); + frm.set_value('filters_json', JSON.stringify(frm.filters)); + frm.trigger('set_filters_in_table'); + } + }, + primary_action_label: "Set" + }); + + frappe.dashboards.filters_dialog = dialog; + + frm.filter_group = new frappe.ui.FilterGroup({ + parent: dialog.get_field('filter_area').$wrapper, + doctype: frm.doc.document_type, + on_change: () => {}, + }); + + frm.filter_group.add_filters_to_filter_group(frm.filters); + + dialog.show(); + dialog.set_values(frm.filters); + }); + + }, + + set_filters_in_table: function(frm) { + if (!frm.filters.length) { + const filter_row = $(` + ${__("Click to Set Filters")}`); + frm.filter_table.find('tbody').html(filter_row); + } else { + let filter_rows = ''; + frm.filters.forEach(filter => { + filter_rows += + ` + ${filter[1]} + ${filter[2] || ""} + ${filter[3]} + `; + + }); + frm.filter_table.find('tbody').html(filter_rows); + } + } +}); diff --git a/frappe/desk/doctype/number_card/number_card.json b/frappe/desk/doctype/number_card/number_card.json new file mode 100644 index 0000000000..5fb058d8ce --- /dev/null +++ b/frappe/desk/doctype/number_card/number_card.json @@ -0,0 +1,147 @@ +{ + "actions": [], + "autoname": "CARD.#####", + "creation": "2020-04-15 18:06:39.444683", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "label", + "function", + "aggregate_function_based_on", + "column_break_2", + "document_type", + "is_public", + "stats_section", + "show_percentage_stats", + "stats_time_interval", + "filters_section", + "filters_json", + "color" + ], + "fields": [ + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType", + "reqd": 1 + }, + { + "depends_on": "eval: doc.document_type", + "fieldname": "function", + "fieldtype": "Select", + "label": "Function", + "options": "Count\nSum\nAverage\nMinimum\nMaximum", + "reqd": 1 + }, + { + "depends_on": "eval: 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" + }, + { + "fieldname": "stats_section", + "fieldtype": "Section Break", + "label": "Stats" + } + ], + "links": [], + "modified": "2020-05-01 15:23:29.550243", + "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", + "title_field": "label", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py new file mode 100644 index 0000000000..2c5655beda --- /dev/null +++ b/frappe/desk/doctype/number_card/number_card.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document +from frappe.utils import cint + +class NumberCard(Document): + pass + + +def get_permission_query_conditions(user=None): + if not user: + user = frappe.session.user + + if user == 'Administrator': + return + + roles = frappe.get_roles(user) + if "System Manager" in roles: + return None + + allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read()) + + return ''' + `tabNumber Card`.`document_type` in {allowed_doctypes} + '''.format( + allowed_doctypes=allowed_doctypes, + ) + +def has_permission(doc, ptype, user): + roles = frappe.get_roles(user) + if "System Manager" in roles: + return True + + allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read()) + if doc.document_type in allowed_doctypes: + return True + + return False + +@frappe.whitelist() +def get_result(doc, to_date=None): + doc = frappe.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 = ['{function}(*) as result'.format(function=function)] + else: + fields = ['{function}({based_on}) as result'.format(function=function, based_on=doc.aggregate_function_based_on)] + + filters = frappe.parse_json(doc.filters_json) + + if to_date: + filters.append([doc.document_type, 'creation', '<', to_date, False]) + + res = frappe.db.get_all(doc.document_type, fields=fields, filters=filters) + number = res[0]['result'] if res else 0 + + return cint(number) + +@frappe.whitelist() +def get_percentage_difference(doc, result): + doc = frappe.parse_json(doc) + result = frappe.parse_json(result) + + doc = frappe.get_doc('Number Card', doc.name) + + if not doc.get('show_percentage_stats'): + return + + previous_result = calculate_previous_result(doc) + difference = (result - previous_result)/100.0 + + return difference + + +def calculate_previous_result(doc): + from frappe.utils import add_to_date + + current_date = frappe.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, previous_date) + return number + +@frappe.whitelist() +def create_number_card(args): + args = frappe.parse_json(args) + doc = frappe.new_doc('Number Card') + + doc.update(args) + doc.insert(ignore_permissions=True) + return doc + +def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters): + meta = frappe.get_meta(doctype) + searchfields = meta.get_search_fields() + search_conditions = [] + + if txt: + for field in searchfields: + search_conditions.append('`tab{doctype}`.`{field}` like %(txt)s'.format(field=field, doctype=doctype, txt=txt)) + + search_conditions = ' or '.join(search_conditions) + + search_conditions = 'and (' + search_conditions +')' if search_conditions else '' + conditions, values = frappe.db.build_conditions(filters) + values['txt'] = '%' + txt + '%' + + return frappe.db.sql( + '''select + `tabNumber Card`.name, `tabNumber Card`.label, `tabNumber Card`.document_type + from + `tabNumber Card` + where + {conditions} and + (`tabNumber Card`.owner = '{user}' or + `tabNumber Card`.is_public = 1) + {search_conditions} + '''.format( + filters=filters, + user=frappe.session.user, + search_conditions=search_conditions, + conditions=conditions + ), values) diff --git a/frappe/desk/doctype/number_card/test_number_card.py b/frappe/desk/doctype/number_card/test_number_card.py new file mode 100644 index 0000000000..4aa1ecf282 --- /dev/null +++ b/frappe/desk/doctype/number_card/test_number_card.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestNumberCard(unittest.TestCase): + pass diff --git a/frappe/website/doctype/web_view_item/__init__.py b/frappe/desk/doctype/number_card_link/__init__.py similarity index 100% rename from frappe/website/doctype/web_view_item/__init__.py rename to frappe/desk/doctype/number_card_link/__init__.py diff --git a/frappe/desk/doctype/number_card_link/number_card_link.json b/frappe/desk/doctype/number_card_link/number_card_link.json new file mode 100644 index 0000000000..ac035b32d8 --- /dev/null +++ b/frappe/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/frappe/desk/doctype/number_card_link/number_card_link.py b/frappe/desk/doctype/number_card_link/number_card_link.py new file mode 100644 index 0000000000..67ad7e70cd --- /dev/null +++ b/frappe/desk/doctype/number_card_link/number_card_link.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class NumberCardLink(Document): + pass diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py index 26fc6037fd..ba0e5c2216 100644 --- a/frappe/desk/form/meta.py +++ b/frappe/desk/form/meta.py @@ -196,8 +196,6 @@ class FormMeta(Meta): self.get("__messages").update(messages, as_value=True) def load_dashboard(self): - if self.custom: - return self.set('__dashboard', self.get_dashboard_data()) def load_kanban_meta(self): diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py index 3a8815ca71..109dd25f4f 100644 --- a/frappe/desk/notifications.py +++ b/frappe/desk/notifications.py @@ -268,8 +268,9 @@ def get_open_count(doctype, name, items=[]): "count": out, } - module = frappe.get_meta_module(doctype) - if hasattr(module, "get_timeline_data"): - out["timeline_data"] = module.get_timeline_data(doctype, name) + if not meta.custom: + module = frappe.get_meta_module(doctype) + if hasattr(module, "get_timeline_data"): + out["timeline_data"] = module.get_timeline_data(doctype, name) return out diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index aaf859e7fd..164f6389eb 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -242,7 +242,7 @@ def get_prepared_report_result(report, filters, dn="", user=None): columns = json.loads(doc.columns) if doc.columns else data[0] for column in columns: - if isinstance(column, dict): + if isinstance(column, dict) and column.get("label"): column["label"] = _(column["label"]) latest_report_data = { @@ -299,6 +299,7 @@ def export_query(): _("You can try changing the filters of your report.")) return + data.columns = [col for col in data.columns if isinstance(col, dict) and not col.get('hidden')] columns = get_columns_dict(data.columns) from frappe.utils.xlsxutils import make_xlsx @@ -310,7 +311,7 @@ def export_query(): frappe.response['type'] = 'binary' -def build_xlsx_data(columns, data, visible_idx,include_indentation): +def build_xlsx_data(columns, data, visible_idx, include_indentation): result = [[]] # add column headings diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index c0a198f5e5..082b16c17a 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -10,7 +10,7 @@ import socket import time from frappe import _ from frappe.model.document import Document -from frappe.utils import validate_email_address, cint, get_datetime, DATE_FORMAT, strip, comma_or, sanitize_html +from frappe.utils import validate_email_address, cint, get_datetime, DATE_FORMAT, strip, comma_or, sanitize_html, add_days from frappe.utils.user import is_system_user from frappe.utils.jinja import render_template from frappe.email.smtp import SMTPServer @@ -533,28 +533,37 @@ class EmailAccount(Document): parent = None in_reply_to = (email.mail.get("In-Reply-To") or "").strip(" <>") - if in_reply_to and "@{0}".format(frappe.local.site) in in_reply_to: - # reply to a communication sent from the system - email_queue = frappe.db.get_value('Email Queue', dict(message_id=in_reply_to), ['communication','reference_doctype', 'reference_name']) - if email_queue: - parent_communication, parent_doctype, parent_name = email_queue - if parent_communication: - communication.in_reply_to = parent_communication + if in_reply_to: + if "@{0}".format(frappe.local.site) in in_reply_to: + # reply to a communication sent from the system + email_queue = frappe.db.get_value('Email Queue', dict(message_id=in_reply_to), ['communication','reference_doctype', 'reference_name']) + if email_queue: + parent_communication, parent_doctype, parent_name = email_queue + if parent_communication: + communication.in_reply_to = parent_communication + else: + reference, domain = in_reply_to.split("@", 1) + parent_doctype, parent_name = 'Communication', reference + + if frappe.db.exists(parent_doctype, parent_name): + parent = frappe._dict(doctype=parent_doctype, name=parent_name) + + # set in_reply_to of current communication + if parent_doctype=='Communication': + # communication.in_reply_to = email_queue.communication + + if parent.reference_name: + # the true parent is the communication parent + parent = frappe.get_doc(parent.reference_doctype, + parent.reference_name) else: - reference, domain = in_reply_to.split("@", 1) - parent_doctype, parent_name = 'Communication', reference - - if frappe.db.exists(parent_doctype, parent_name): - parent = frappe._dict(doctype=parent_doctype, name=parent_name) - - # set in_reply_to of current communication - if parent_doctype=='Communication': - # communication.in_reply_to = email_queue.communication - - if parent.reference_name: - # the true parent is the communication parent - parent = frappe.get_doc(parent.reference_doctype, - parent.reference_name) + comm = frappe.db.get_value('Communication', + dict( + message_id=in_reply_to, + creation=['>=', add_days(get_datetime(), -30)]), + ['reference_doctype', 'reference_name'], as_dict=1) + if comm: + parent = frappe._dict(doctype=comm.reference_doctype, name=comm.reference_name) return parent diff --git a/frappe/email/doctype/email_domain/email_domain.py b/frappe/email/doctype/email_domain/email_domain.py index b6585d966b..08583dc228 100644 --- a/frappe/email/doctype/email_domain/email_domain.py +++ b/frappe/email/doctype/email_domain/email_domain.py @@ -39,7 +39,7 @@ class EmailDomain(Document): except Exception: frappe.throw(_("Incoming email account not correct")) - return None + finally: try: if self.use_imap: @@ -48,9 +48,10 @@ class EmailDomain(Document): test.quit() except Exception: pass + try: - if self.use_ssl_for_outgoing: - if not self.smtp_port: + if self.get('use_ssl_for_outgoing'): + if not self.get('smtp_port'): self.smtp_port = 465 sess = smtplib.SMTP_SSL((self.smtp_server or "").encode('utf-8'), @@ -62,28 +63,15 @@ class EmailDomain(Document): sess.quit() except Exception: frappe.throw(_("Outgoing email account not correct")) - return None - return def on_update(self): """update all email accounts using this domain""" - for email_account in frappe.get_all("Email Account", - filters={"domain": self.name}): - + for email_account in frappe.get_all("Email Account", filters={"domain": self.name}): try: - email_account = frappe.get_doc("Email Account", - email_account.name) - email_account.set("email_server",self.email_server) - email_account.set("use_imap",self.use_imap) - email_account.set("use_ssl",self.use_ssl) - email_account.set("use_tls",self.use_tls) - email_account.set("attachment_limit",self.attachment_limit) - email_account.set("smtp_server",self.smtp_server) - email_account.set("smtp_port",self.smtp_port) - email_account.set("use_ssl_for_outgoing", self.use_ssl_for_outgoing) - email_account.set("append_emails_to_sent_folder", self.append_emails_to_sent_folder) + email_account = frappe.get_doc("Email Account", email_account.name) + for attr in ["email_server", "use_imap", "use_ssl", "use_tls", "attachment_limit", "smtp_server", "smtp_port", "use_ssl_for_outgoing", "append_emails_to_sent_folder"]: + email_account.set(attr, self.get(attr, default=0)) email_account.save() + except Exception as e: - frappe.msgprint(email_account.name) - frappe.throw(e) - return None + frappe.msgprint(_("Error has occurred in {0}").format(email_account.name), raise_exception=e.__class__) diff --git a/frappe/email/queue.py b/frappe/email/queue.py index b35f5944b2..ce512de276 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -6,7 +6,7 @@ import frappe import sys from six.moves import html_parser as HTMLParser import smtplib, quopri, json -from frappe import msgprint, _, safe_decode, safe_encode +from frappe import msgprint, _, safe_decode, safe_encode, enqueue from frappe.email.smtp import SMTPServer, get_outgoing_email_account from frappe.email.email_body import get_email, get_formatted_html, add_attachment from frappe.utils.verified_command import get_signed_params, verify_request @@ -347,8 +347,20 @@ def flush(from_test=False): if not smtpserver: smtpserver = SMTPServer() smtpserver_dict[email.sender] = smtpserver - - send_one(email.name, smtpserver, auto_commit, from_test=from_test) + + if from_test: + send_one(email.name, smtpserver, auto_commit) + else: + send_one_args = { + 'email': email.name, + 'smtpserver': smtpserver, + 'auto_commit': auto_commit, + } + enqueue( + method = 'frappe.email.queue.send_one', + queue = 'short', + **send_one_args + ) # NOTE: removing commit here because we pass auto_commit # finally: @@ -366,7 +378,7 @@ def get_queue(): limit 500''', { 'now': now_datetime() }, as_dict=True) -def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=False): +def send_one(email, smtpserver=None, auto_commit=True, now=False): '''Send Email Queue with given smtpserver''' email = frappe.db.sql('''select @@ -377,8 +389,13 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals `tabEmail Queue` where name=%s - for update''', email, as_dict=True)[0] - + for update''', email, as_dict=True) + + if len(email): + email = email[0] + else: + return + recipients_list = frappe.db.sql('''select name, recipient, status from `tabEmail Queue Recipient` where parent=%s''', email.name, as_dict=1) diff --git a/frappe/exceptions.py b/frappe/exceptions.py index ef75a36e03..9a1c1fb0b0 100644 --- a/frappe/exceptions.py +++ b/frappe/exceptions.py @@ -78,6 +78,7 @@ 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 @@ -95,4 +96,4 @@ class DataTooLongException(ValidationError): pass # OAuth exceptions class InvalidAuthorizationHeader(CSRFTokenError): pass class InvalidAuthorizationPrefix(CSRFTokenError): pass -class InvalidAuthorizationToken(CSRFTokenError): pass \ No newline at end of file +class InvalidAuthorizationToken(CSRFTokenError): pass diff --git a/frappe/handler.py b/frappe/handler.py index 6e0bf7a6be..e5a7f742ae 100755 --- a/frappe/handler.py +++ b/frappe/handler.py @@ -14,6 +14,12 @@ from frappe.core.doctype.server_script.server_script_utils import run_server_scr from werkzeug.wrappers import Response from six import string_types +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') + + def handle(): """handle request""" validate_auth() @@ -148,12 +154,14 @@ def uploadfile(): @frappe.whitelist(allow_guest=True) def upload_file(): + user = None if frappe.session.user == 'Guest': if frappe.get_system_settings('allow_guests_to_upload_files'): ignore_permissions = True else: return else: + user = frappe.get_doc("User", frappe.session.user) ignore_permissions = False files = frappe.request.files @@ -175,11 +183,11 @@ def upload_file(): frappe.local.uploaded_file = content frappe.local.uploaded_filename = filename - if frappe.session.user == 'Guest': + if frappe.session.user == 'Guest' or (user and not user.has_desk_access()): import mimetypes filetype = mimetypes.guess_type(filename)[0] - if filetype not in ['image/png', 'image/jpeg', 'application/pdf']: - frappe.throw("You can only upload JPG, PNG or PDF files.") + if filetype not in ALLOWED_MIMETYPES: + frappe.throw(_("You can only upload JPG, PNG, PDF, or Microsoft documents.")) if method: method = frappe.get_attr(method) diff --git a/frappe/hooks.py b/frappe/hooks.py index 2561399a78..a63fba14f9 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -89,6 +89,7 @@ permission_query_conditions = { "Dashboard Settings": "frappe.desk.doctype.dashboard_settings.dashboard_settings.get_permission_query_conditions", "Notification Log": "frappe.desk.doctype.notification_log.notification_log.get_permission_query_conditions", "Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.get_permission_query_conditions", + "Number Card": "frappe.desk.doctype.number_card.number_card.get_permission_query_conditions", "Notification Settings": "frappe.desk.doctype.notification_settings.notification_settings.get_permission_query_conditions", "Note": "frappe.desk.doctype.note.note.get_permission_query_conditions", "Kanban Board": "frappe.desk.doctype.kanban_board.kanban_board.get_permission_query_conditions", @@ -105,6 +106,7 @@ has_permission = { "User": "frappe.core.doctype.user.user.has_permission", "Note": "frappe.desk.doctype.note.note.has_permission", "Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.has_permission", + "Number Card": "frappe.desk.doctype.number_card.number_card.has_permission", "Kanban Board": "frappe.desk.doctype.kanban_board.kanban_board.has_permission", "Contact": "frappe.contacts.address_and_contact.has_permission", "Address": "frappe.contacts.address_and_contact.has_permission", diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.py b/frappe/integrations/doctype/ldap_settings/ldap_settings.py index 558f7117c0..80dfef2693 100644 --- a/frappe/integrations/doctype/ldap_settings/ldap_settings.py +++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe from frappe import _, safe_encode from frappe.model.document import Document - +from frappe.twofactor import (should_run_2fa, authenticate_for_2factor,confirm_otp_token) class LDAPSettings(Document): def validate(self): @@ -237,6 +237,10 @@ def login(): user = ldap.authenticate(frappe.as_unicode(args.usr), frappe.as_unicode(args.pwd)) frappe.local.login_manager.user = user.name + if should_run_2fa(user.name): + authenticate_for_2factor(user.name) + if not confirm_otp_token(frappe.local.login_manager): + return False frappe.local.login_manager.post_login() # because of a GET request! diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index 5cbe7c0a02..8e6c8d58e4 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -60,6 +60,7 @@ class Webhook(Document): if self.request_structure == "Form URL-Encoded": self.webhook_json = None elif self.request_structure == "JSON": + validate_json(self.webhook_json) validate_template(self.webhook_json) self.webhook_data = [] @@ -130,3 +131,10 @@ def get_webhook_data(doc, webhook): data = json.loads(data) return data + + +def validate_json(string): + try: + json.loads(string) + except (TypeError, ValueError): + frappe.throw(_("Request Body consists of an invalid JSON structure"), title=_("Invalid JSON")) diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index 7af987f4bc..93ef78df7b 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -48,7 +48,7 @@ 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', 'Custom Script') -data_field_options = ('Email', 'Phone') +data_field_options = ('Email', 'Name', 'Phone') def copytables(srctype, src, srcfield, tartype, tar, tarfield, srcfields, tarfields=[]): if not tarfields: diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 9ab1ef7799..feeb96898a 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -11,11 +11,12 @@ from frappe.model import default_fields, table_fields from frappe.model.naming import set_new_name from frappe.model.utils.link_count import notify_link_count from frappe.modules import load_doctype_module -from frappe.model import display_fieldtypes, data_fieldtypes +from frappe.model import display_fieldtypes from frappe.utils.password import get_decrypted_password, set_encrypted_password -from frappe.utils import (cint, flt, now, cstr, strip_html, getdate, get_datetime, to_timedelta, +from frappe.utils import (cint, flt, now, cstr, strip_html, sanitize_html, sanitize_email, cast_fieldtype) from frappe.utils.html_utils import unescape_html +from bs4 import BeautifulSoup max_positive_value = { 'smallint': 2 ** 15, @@ -288,7 +289,7 @@ class BaseDocument(object): if k in default_fields: del doc[k] - for key in ("_user_tags", "__islocal", "__onload", "_liked_by", "__run_link_triggers"): + for key in ("_user_tags", "__islocal", "__onload", "_liked_by", "__run_link_triggers", "__unsaved"): if self.get(key): doc[key] = self.get(key) @@ -564,13 +565,20 @@ class BaseDocument(object): for data_field in self.meta.get_data_fields(): data = self.get(data_field.fieldname) data_field_options = data_field.get("options") + old_fieldtype = data_field.get("oldfieldtype") + + if old_fieldtype and old_fieldtype != "Data": + continue if data_field_options == "Email": if (self.owner in STANDARD_USERS) and (data in STANDARD_USERS): - return + continue for email_address in frappe.utils.split_emails(data): frappe.utils.validate_email_address(email_address, throw=True) + if data_field_options == "Name": + frappe.utils.validate_name(data, throw=True) + if data_field_options == "Phone": frappe.utils.validate_phone_number(data, throw=True) @@ -678,7 +686,7 @@ class BaseDocument(object): # doesn't look like html so no need continue - elif "" in value and not ("" in value and not bool(BeautifulSoup(value, "html.parser").find()): # should be handled separately via the markdown converter function continue diff --git a/frappe/model/create_new.py b/frappe/model/create_new.py index f697d8051a..2142d544fe 100644 --- a/frappe/model/create_new.py +++ b/frappe/model/create_new.py @@ -45,6 +45,7 @@ def make_new_doc(doctype): doc = doc.get_valid_dict(sanitize=False) doc["doctype"] = doctype doc["__islocal"] = 1 + doc["__unsaved"] = 1 return doc @@ -74,11 +75,9 @@ def set_user_and_static_default_values(doc): 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": - # 1 - look in user permissions only for document_type==Setup - # We don't want to include permissions of transactions to be used for defaults. - if (frappe.get_meta(df.options).document_type=="Setup" - and not df.ignore_user_permissions and default_doc): - return default_doc + # 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) diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index d77898020d..c0d2c4eef9 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -210,7 +210,7 @@ def check_permission_and_not_submitted(doc): # check if submitted if doc.docstatus == 1: - frappe.msgprint(_("{0} {1}: Submitted Record cannot be deleted.").format(_(doc.doctype), doc.name), + frappe.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"): diff --git a/frappe/model/document.py b/frappe/model/document.py index 03b21ea667..04db33ca69 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -268,6 +268,10 @@ class Document(BaseDocument): if hasattr(self, "__islocal"): delattr(self, "__islocal") + # clear unsaved flag + if hasattr(self, "__unsaved"): + delattr(self, "__unsaved") + if not (frappe.flags.in_migrate or frappe.local.flags.in_install or frappe.flags.in_setup_wizard): follow_document(self.doctype, self.name, frappe.session.user) return self @@ -329,6 +333,10 @@ class Document(BaseDocument): 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): @@ -583,6 +591,9 @@ class Document(BaseDocument): 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 = frappe.get_meta(df.options).get_high_permlevel_fields() @@ -1318,6 +1329,9 @@ def make_event_update_log(doc, update_type): def check_doctype_has_consumers(doctype): """Check if doctype has event consumers for event streaming""" + if not frappe.db.exists("DocType", "Event Consumer"): + return False + event_consumers = frappe.get_all('Event Consumer') for event_consumer in event_consumers: consumer = frappe.get_doc('Event Consumer', event_consumer.name) diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 9c71f8c0b1..2321e0c22a 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -425,17 +425,19 @@ class Meta(Document): implemented in other Frappe applications via hooks. ''' data = frappe._dict() - try: - module = load_doctype_module(self.name, suffix='_dashboard') - if hasattr(module, 'get_data'): - data = frappe._dict(module.get_data()) - except ImportError: - pass + if not self.custom: + try: + module = load_doctype_module(self.name, suffix='_dashboard') + if hasattr(module, 'get_data'): + data = frappe._dict(module.get_data()) + except ImportError: + pass self.add_doctype_links(data) - for hook in frappe.get_hooks("override_doctype_dashboards", {}).get(self.name, []): - data = frappe.get_attr(hook)(data=data) + if not self.custom: + for hook in frappe.get_hooks("override_doctype_dashboards", {}).get(self.name, []): + data = frappe.get_attr(hook)(data=data) return data diff --git a/frappe/model/sync.py b/frappe/model/sync.py index 62a96718b9..d8b2c654b2 100644 --- a/frappe/model/sync.py +++ b/frappe/model/sync.py @@ -38,6 +38,7 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe ("custom", "custom_field"), ("custom", "property_setter"), ("website", "web_form"), + ("website", "web_template"), ("website", "web_form_field"), ("website", "portal_menu_item"), ("data_migration", "data_migration_mapping_detail"), @@ -78,7 +79,7 @@ def get_doc_files(files, start_path, force=0, sync_everything = False, verbose=F # load in sequence - warning for devs document_types = ['doctype', 'page', 'report', 'dashboard_chart_source', 'print_format', - 'website_theme', 'web_form', 'notification', 'print_style', + 'website_theme', 'web_form', 'web_template', 'notification', 'print_style', 'data_migration_mapping', 'data_migration_plan', 'onboarding_slide', 'desk_page'] for doctype in document_types: doctype_path = os.path.join(start_path, doctype) diff --git a/frappe/monitor.py b/frappe/monitor.py index 7181bd92ad..b056286cf9 100644 --- a/frappe/monitor.py +++ b/frappe/monitor.py @@ -23,7 +23,7 @@ def start(transaction_type="request", method=None, kwargs=None): def stop(response=None): - if frappe.conf.monitor and hasattr(frappe.local, "monitor"): + if hasattr(frappe.local, "monitor"): frappe.local.monitor.dump(response) @@ -79,7 +79,7 @@ class Monitor: if self.data.transaction_type == "request": self.data.request.status_code = response.status_code - self.data.request.response_length = int(response.headers["Content-Length"]) + self.data.request.response_length = int(response.headers.get("Content-Length", 0)) self.store() except Exception: diff --git a/frappe/patches.txt b/frappe/patches.txt index 0e02423639..dfae76a671 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -272,3 +272,6 @@ execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Account') execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Settings') frappe.patches.v12_0.remove_parent_and_parenttype_from_print_formats execute:from frappe.desk.page.setup_wizard.install_fixtures import update_genders;update_genders() +frappe.patches.v13_0.website_theme_custom_scss +frappe.patches.v13_0.set_existing_dashboard_charts_as_public +frappe.patches.v13_0.set_path_for_homepage_in_web_page_view diff --git a/frappe/patches/v12_0/move_email_and_phone_to_child_table.py b/frappe/patches/v12_0/move_email_and_phone_to_child_table.py index 4388d3c849..12680609d5 100644 --- a/frappe/patches/v12_0/move_email_and_phone_to_child_table.py +++ b/frappe/patches/v12_0/move_email_and_phone_to_child_table.py @@ -1,6 +1,10 @@ import frappe def execute(): + frappe.reload_doc("contacts", "doctype", "contact_email") + frappe.reload_doc("contacts", "doctype", "contact_phone") + frappe.reload_doc("contacts", "doctype", "contact") + contact_details = frappe.db.sql(""" SELECT `name`, `email_id`, `phone`, `mobile_no`, `modified_by`, `creation`, `modified` @@ -10,10 +14,6 @@ def execute(): and `tabContact Email`.email_id=`tabContact`.email_id) """, as_dict=True) - frappe.reload_doc("contacts", "doctype", "contact_email") - frappe.reload_doc("contacts", "doctype", "contact_phone") - frappe.reload_doc("contacts", "doctype", "contact") - email_values = [] phone_values = [] for count, contact_detail in enumerate(contact_details): diff --git a/frappe/patches/v13_0/__init__.py b/frappe/patches/v13_0/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/patches/v13_0/remove_web_view.py b/frappe/patches/v13_0/remove_web_view.py new file mode 100644 index 0000000000..7c9109fd03 --- /dev/null +++ b/frappe/patches/v13_0/remove_web_view.py @@ -0,0 +1,6 @@ +import frappe + +def execute(): + frappe.delete_doc_if_exists("DocType", "Web View") + frappe.delete_doc_if_exists("DocType", "Web View Component") + frappe.delete_doc_if_exists("DocType", "CSS Class") \ No newline at end of file diff --git a/frappe/patches/v13_0/set_existing_dashboard_charts_as_public.py b/frappe/patches/v13_0/set_existing_dashboard_charts_as_public.py new file mode 100644 index 0000000000..80c6105440 --- /dev/null +++ b/frappe/patches/v13_0/set_existing_dashboard_charts_as_public.py @@ -0,0 +1,21 @@ +import frappe + +def execute(): + frappe.reload_doc('desk', 'doctype', 'dashboard_chart') + + if not frappe.db.table_exists('Dashboard Chart'): + return + + users_with_permission = frappe.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 = frappe.db.get_all('Dashboard Chart', filters={'owner': ['in', users]}) + + for chart in charts: + frappe.db.set_value('Dashboard Chart', chart.name, 'is_public', 1) + diff --git a/frappe/patches/v13_0/set_path_for_homepage_in_web_page_view.py b/frappe/patches/v13_0/set_path_for_homepage_in_web_page_view.py new file mode 100644 index 0000000000..66f878e4bf --- /dev/null +++ b/frappe/patches/v13_0/set_path_for_homepage_in_web_page_view.py @@ -0,0 +1,5 @@ +import frappe + +def execute(): + frappe.reload_doc('website', 'doctype', 'web_page_view', force=True) + frappe.db.sql("""UPDATE `tabWeb Page View` set path="/" where path=''""") diff --git a/frappe/patches/v13_0/website_theme_custom_scss.py b/frappe/patches/v13_0/website_theme_custom_scss.py new file mode 100644 index 0000000000..0035283428 --- /dev/null +++ b/frappe/patches/v13_0/website_theme_custom_scss.py @@ -0,0 +1,10 @@ +import frappe + +def execute(): + frappe.reload_doctype('Website Theme') + for theme in frappe.get_all('Website Theme'): + doc = frappe.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 + doc.save() diff --git a/frappe/printing/page/print_format_builder/print_format_builder.js b/frappe/printing/page/print_format_builder/print_format_builder.js index d0a3379609..4e049d120a 100644 --- a/frappe/printing/page/print_format_builder/print_format_builder.js +++ b/frappe/printing/page/print_format_builder/print_format_builder.js @@ -441,18 +441,16 @@ frappe.PrintFormatBuilder = Class.extend({ }); }, setup_field_settings: function() { - - this.page.main.find(".field-settings").on("click", () => { - var field = $(this).parent(); - + this.page.main.find(".field-settings").on("click", e => { + const field = $(e.currentTarget).parent(); // new dialog var d = new frappe.ui.Dialog({ title: "Set Properties", fields: [ { - label:__("Label"), - fieldname:"label", - fieldtype:"Data" + label: __("Label"), + fieldname: "label", + fieldtype: "Data" }, { label: __("Align Value"), @@ -485,7 +483,7 @@ frappe.PrintFormatBuilder = Class.extend({ }); // set current value - if(field.attr('data-align')) { + if (field.attr('data-align')) { d.set_value('align', field.attr('data-align')); } else { d.set_value('align', 'left'); diff --git a/frappe/public/build.json b/frappe/public/build.json index 75a89e5010..d56907b558 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -1,4 +1,7 @@ { + "css/tailwind.css": [ + "public/tailwind.css" + ], "css/frappe-web-b4.css": [ "public/scss/website.scss", "public/less/indicator.less" @@ -90,6 +93,7 @@ "public/css/font-awesome.css", "public/css/octicons/octicons.css", "public/less/desk.less", + "public/less/module.less", "public/less/flex.less", "public/less/indicator.less", "public/less/avatar.less", @@ -103,6 +107,7 @@ "public/less/form.less", "public/less/mobile.less", "public/less/kanban.less", + "public/less/dashboard_view.less", "public/less/controls.less", "public/less/chat.less", "public/less/filters.less", @@ -295,6 +300,7 @@ "public/js/frappe/views/gantt/gantt_view.js", "public/js/frappe/views/calendar/calendar.js", + "public/js/frappe/views/dashboard/dashboard_view.js", "public/js/frappe/views/image/image_view.js", "public/js/frappe/views/kanban/kanban_view.js", "public/js/frappe/views/inbox/inbox_view.js", diff --git a/frappe/public/css/email.css b/frappe/public/css/email.css index 40c6149927..92ac433fd2 100644 --- a/frappe/public/css/email.css +++ b/frappe/public/css/email.css @@ -1,64 +1,82 @@ /* csslint ignore:start */ + /* palette colors*/ + body { line-height: 1.5; color: #36414c; } + p { margin: 1em 0 !important; } + hr { border-top: 1px solid #d1d8dd; } + .body-table { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; } + .body-table td { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; } + .email-header, .email-body, .email-footer { width: 100% !important; min-width: 100% !important; } + .email-body { font-size: 14px; } + .email-footer { border-top: 1px solid #d1d8dd; font-size: 12px; } + .email-header { border: 1px solid #d1d8dd; border-radius: 4px 4px 0 0; } + .email-header .brand-image { width: 24px; height: 24px; display: block; } + .email-header-title { font-weight: bold; } + .body-table.has-header .email-body { border: 1px solid #d1d8dd; border-radius: 0 0 4px 4px; border-top: none; } + .body-table.has-header .email-footer { border-top: none; } + .email-footer-container { margin-top: 30px; } + .email-footer-container > div:not(:last-child) { margin-bottom: 5px; } + .email-unsubscribe a { color: #8d99a6; text-decoration: underline; } + .btn { text-decoration: none; padding: 7px 10px; @@ -66,20 +84,24 @@ hr { border: 1px solid; border-radius: 3px; } + .btn.btn-default { color: #fff; background-color: #f0f4f7; border-color: transparent; } + .btn.btn-primary { color: #fff; background-color: #5e64ff; border-color: #444bff; } + .table { width: 100%; border-collapse: collapse; } + .table td, .table th { padding: 8px; @@ -88,53 +110,68 @@ hr { border-top: 1px solid #d1d8dd; text-align: left; } + .table th { font-weight: bold; } + .table > thead > tr > th { vertical-align: middle; border-bottom: 2px solid #d1d8dd; } + .table > thead:first-child > tr:first-child > th { border-top: none; } + .table.table-bordered { border: 1px solid #d1d8dd; } + .table.table-bordered td, .table.table-bordered th { border: 1px solid #d1d8dd; } + .more-info { font-size: 80% !important; color: #8d99a6 !important; border-top: 1px solid #ebeff2; padding-top: 10px; } + .text-right { text-align: right !important; } + .text-center { text-align: center !important; } + .text-muted { color: #8d99a6 !important; } + .text-extra-muted { color: #d1d8dd !important; } + .text-regular { font-size: 14px; } + .text-medium { font-size: 12px; } + .text-small { font-size: 10px; } + .text-bold { font-weight: bold; } + .indicator { width: 8px; height: 8px; @@ -143,33 +180,43 @@ hr { display: inline-block; margin-right: 5px; } + .indicator.indicator-blue { background-color: #5e64ff; } + .indicator.indicator-green { background-color: #98d85b; } + .indicator.indicator-orange { background-color: #ffa00a; } + .indicator.indicator-red { background-color: #ff5858; } + .indicator.indicator-yellow { background-color: #feef72; } + .screenshot { box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1); border: 1px solid #d1d8dd; margin: 8px 0; max-width: 100%; } + .list-unstyled { list-style-type: none; padding: 0; } + /* auto email report */ + .report-title { margin-bottom: 20px; } + /* csslint ignore:end */ diff --git a/frappe/public/js/frappe/chat.js b/frappe/public/js/frappe/chat.js index 8b1c09ac93..f54b9e5cbe 100644 --- a/frappe/public/js/frappe/chat.js +++ b/frappe/public/js/frappe/chat.js @@ -2523,7 +2523,7 @@ class extends Component { h("div",{class:"input-group input-group-lg"}, !frappe._.is_empty(props.actions) ? h("div",{class:"input-group-btn dropup"}, - h(frappe.components.Button,{ class: "dropdown-toggle", "data-toggle": "dropdown"}, + h(frappe.components.Button,{ class: (frappe.session.user === "Guest" ? "disabled" : "dropdown-toggle"), "data-toggle": "dropdown"}, h(frappe.components.FontAwesome, { class: "text-muted", type: "paperclip", fixed: true }) ), h("div",{ class:"dropdown-menu dropdown-menu-left", onclick: e => e.stopPropagation() }, diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index a1418f9149..b5046d4b12 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -86,6 +86,14 @@ frappe.Application = Class.extend({ this.show_update_available(); } + if (!frappe.boot.developer_mode) { + let console_security_message = __("Using this console may allow attackers to impersonate you and steal your information. Do not enter or paste code that you do not understand."); + console.log( + `%c${console_security_message}`, + "font-size: large" + ); + } + this.show_notes(); if (frappe.boot.is_first_startup) { diff --git a/frappe/public/js/frappe/dom.js b/frappe/public/js/frappe/dom.js index 819ecb526e..7b59f9da08 100644 --- a/frappe/public/js/frappe/dom.js +++ b/frappe/public/js/frappe/dom.js @@ -85,6 +85,10 @@ frappe.dom = { ); }, + is_element_in_modal(element) { + return Boolean($(element).parents('.modal').length); + }, + set_style: function(txt, id) { if(!txt) return; diff --git a/frappe/public/js/frappe/form/controls/barcode.js b/frappe/public/js/frappe/form/controls/barcode.js index 859cbbb22a..c2314d6664 100644 --- a/frappe/public/js/frappe/form/controls/barcode.js +++ b/frappe/public/js/frappe/form/controls/barcode.js @@ -48,6 +48,7 @@ frappe.ui.form.ControlBarcode = frappe.ui.form.ControlData.extend({ const svg = this.barcode_area.find('svg')[0]; JsBarcode(svg, value, this.get_options(value)); $(svg).attr('data-barcode-value', value); + $(svg).attr('width', '100%'); return this.barcode_area.html(); } }, diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js index c1ba41ab16..41e06537e1 100644 --- a/frappe/public/js/frappe/form/controls/base_control.js +++ b/frappe/public/js/frappe/form/controls/base_control.js @@ -152,12 +152,14 @@ frappe.ui.form.Control = Class.extend({ () => me.set_model_value(value), () => { me.set_mandatory && me.set_mandatory(value); - me.set_invalid && me.set_invalid(); if(me.df.change || me.df.onchange) { // onchange event specified in df - return (me.df.change || me.df.onchange).apply(me, [e]); + let set = (me.df.change || me.df.onchange).apply(me, [e]); + me.set_invalid && me.set_invalid(); + return set; } + me.set_invalid && me.set_invalid(); } ]); }; diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js index 0dbaaeb63c..f3f04ec4d8 100644 --- a/frappe/public/js/frappe/form/controls/base_input.js +++ b/frappe/public/js/frappe/form/controls/base_input.js @@ -180,7 +180,14 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({ this.$wrapper.toggleClass("has-error", (this.df.reqd && is_null(value)) ? true : false); }, set_invalid: function () { - this.$wrapper.toggleClass("has-error", (this.df.invalid ? true : false)); + let invalid = !!this.df.invalid; + if (this.grid) { + this.$wrapper.parents('.grid-static-col').toggleClass('invalid', invalid); + this.$input.toggleClass('invalid', invalid); + this.grid_row.columns[this.df.fieldname].is_invalid = invalid; + } else { + this.$wrapper.toggleClass('has-error', invalid); + } }, set_bold: function() { if(this.$input) { diff --git a/frappe/public/js/frappe/form/controls/code.js b/frappe/public/js/frappe/form/controls/code.js index 0648ad6e22..60825c82ad 100644 --- a/frappe/public/js/frappe/form/controls/code.js +++ b/frappe/public/js/frappe/form/controls/code.js @@ -9,6 +9,12 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({ this.ace_editor_target = $('
    ') .appendTo(this.input_area); + this.expanded = false; + this.$expand_button = $(``).click(() => { + this.expanded = !this.expanded; + this.refresh_height(); + this.toggle_label(); + }).appendTo(this.$input_wrapper); // styling this.ace_editor_target.addClass('border rounded'); this.ace_editor_target.css('height', 300); @@ -26,6 +32,16 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({ }, 300)); }, + refresh_height() { + this.ace_editor_target.css('height', this.expanded ? 600 : 300); + this.editor.resize(); + }, + + toggle_label() { + const button_label = this.expanded ? __('Collapse') : __('Expand'); + this.$expand_button.text(button_label); + }, + set_language() { const language_map = { 'Javascript': 'ace/mode/javascript', @@ -34,7 +50,9 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({ 'CSS': 'ace/mode/css', 'Markdown': 'ace/mode/markdown', 'SCSS': 'ace/mode/scss', - 'JSON': 'ace/mode/json' + 'JSON': 'ace/mode/json', + 'Golang': 'ace/mode/golang', + 'Go': 'ace/mode/golang' }; const language = this.df.options; diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js index a7f0050d65..c943ec89bb 100644 --- a/frappe/public/js/frappe/form/controls/data.js +++ b/frappe/public/js/frappe/form/controls/data.js @@ -96,6 +96,9 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({ if(this.df.options == 'Phone') { this.df.invalid = !validate_phone(v); return v; + } else if (this.df.options == 'Name') { + this.df.invalid = !validate_name(v); + return v; } else if(this.df.options == 'Email') { var email_list = frappe.utils.split_emails(v); if (!email_list) { diff --git a/frappe/public/js/frappe/form/controls/multiselect_list.js b/frappe/public/js/frappe/form/controls/multiselect_list.js index 3e8dc21dca..cd86bdd767 100644 --- a/frappe/public/js/frappe/form/controls/multiselect_list.js +++ b/frappe/public/js/frappe/form/controls/multiselect_list.js @@ -18,6 +18,7 @@ frappe.ui.form.ControlMultiSelectList = frappe.ui.form.ControlData.extend({ this.$list_wrapper = $(template); this.$input = $(''); this.input = this.$input.get(0); + this.has_input = true; this.$list_wrapper.prependTo(this.input_area); this.$filter_input = this.$list_wrapper.find('input'); this.$list_wrapper.on('click', '.dropdown-menu', e => { diff --git a/frappe/public/js/frappe/form/controls/text_editor.js b/frappe/public/js/frappe/form/controls/text_editor.js index b35c92c1ae..4e18b081cc 100644 --- a/frappe/public/js/frappe/form/controls/text_editor.js +++ b/frappe/public/js/frappe/form/controls/text_editor.js @@ -147,7 +147,7 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ [{ 'color': [] }, { 'background': [] }], ['blockquote', 'code-block'], ['link', 'image'], - [{ 'list': 'ordered' }, { 'list': 'bullet' }], + [{ 'list': 'ordered' }, { 'list': 'bullet' }, { 'list': 'check' }], [{ 'align': [] }], [{ 'indent': '-1'}, { 'indent': '+1' }], [{'table': [ diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index c4c739dbcc..3b6ccd9a5c 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -125,8 +125,9 @@ frappe.ui.form.Dashboard = Class.extend({ }, format_percent: function(title, percent) { - var width = cint(percent) < 1 ? 1 : cint(percent); - var progress_class = "progress-bar-success"; + const percentage = cint(percent); + const width = percentage < 0 ? 100 : percentage; + const progress_class = percentage < 0 ? "progress-bar-danger" : "progress-bar-success"; return [{ title: title, diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index e714418375..82478db707 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -184,13 +184,7 @@ frappe.ui.form.Form = class FrappeForm { frappe.model.on(me.doctype, "*", function(fieldname, value, doc) { // set input if(doc.name===me.docname) { - if ((value==='' || value===null) && !doc[fieldname]) { - // both the incoming and outgoing values are falsy - // the texteditor, summernote, changes nulls to empty strings on render, - // so ignore those changes - } else { - me.dirty(); - } + me.dirty(); let field = me.fields_dict[fieldname]; field && field.refresh(fieldname); diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 31d62dc445..f5a06311e9 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -22,9 +22,6 @@ export default class GridRow { if(me.grid.allow_on_grid_editing() && me.grid.is_editable()) { // pass } else { - if (!me.grid.is_editable()) { - me.docfields.map(df => df.read_only = 1); - } me.toggle_view(); return false; } @@ -268,7 +265,9 @@ export default class GridRow { if(df.reqd && !txt) { column.addClass('error'); } - if (df.reqd || df.bold) { + if (column.is_invalid) { + column.addClass('invalid'); + } else if (df.reqd || df.bold) { column.addClass('bold'); } } @@ -527,7 +526,7 @@ export default class GridRow { return this; } show_form() { - if(!this.grid_form) { + if (!this.grid_form) { this.grid_form = new GridRowForm({ row: this }); @@ -536,13 +535,15 @@ export default class GridRow { this.row.toggle(false); // this.form_panel.toggle(true); frappe.dom.freeze("", "dark"); - if(cur_frm) cur_frm.cur_grid = this; + if (cur_frm) cur_frm.cur_grid = this; this.wrapper.addClass("grid-row-open"); - if(!frappe.dom.is_element_in_viewport(this.wrapper)) { - frappe.utils.scroll_to(this.wrapper, true, 15); + if (!frappe.dom.is_element_in_viewport(this.wrapper) + && !frappe.dom.is_element_in_modal(this.wrapper)) { + // -15 offset to make form look visually centered + frappe.utils.scroll_to(this.wrapper, true, -15); } - if(this.frm) { + if (this.frm) { this.frm.script_manager.trigger(this.doc.parentfield + "_on_form_rendered"); this.frm.script_manager.trigger("form_render", this.doc.doctype, this.doc.name); } @@ -550,7 +551,9 @@ export default class GridRow { hide_form() { frappe.dom.unfreeze(); this.row.toggle(true); - frappe.utils.scroll_to(this.row, true, 15); + if (!frappe.dom.is_element_in_modal(this.row)) { + frappe.utils.scroll_to(this.row, true, 15); + } this.refresh(); if(cur_frm) cur_frm.cur_grid = null; this.wrapper.removeClass("grid-row-open"); diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 8a88ee0c0d..5aeb29b1ed 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -498,11 +498,18 @@ frappe.ui.form.Layout = Class.extend({ }, set_dependant_property: function(condition, fieldname, property) { let set_property = this.evaluate_depends_on_value(condition); + let form_obj; + if (this.frm) { + form_obj = this.frm; + } else if (this.is_dialog) { + form_obj = this; + } + if (form_obj) { if (set_property) { - this.frm.set_df_property(fieldname, property, 1); + form_obj.set_df_property(fieldname, property, 1); } else { - this.frm.set_df_property(fieldname, property, 0); + form_obj.set_df_property(fieldname, property, 0); } } }, diff --git a/frappe/public/js/frappe/form/save.js b/frappe/public/js/frappe/form/save.js index d40b3ed341..8386cb6c7e 100644 --- a/frappe/public/js/frappe/form/save.js +++ b/frappe/public/js/frappe/form/save.js @@ -21,7 +21,7 @@ frappe.ui.form.save = function (frm, action, callback, btn) { remove_empty_rows(); $(frm.wrapper).addClass('validated-form'); - if (frm.is_dirty() && check_mandatory()) { + if ((action !== 'Save' || frm.is_dirty()) && check_mandatory()) { _call({ method: "frappe.desk.form.save.savedocs", args: { doc: frm.doc, action: action }, @@ -36,7 +36,7 @@ frappe.ui.form.save = function (frm, action, callback, btn) { freeze_message: freeze_message }); } else { - frappe.show_alert({message: __("Document not updated"), indicator: "yellow"}); + !frm.is_dirty() && frappe.show_alert({message: __("No changes in document"), indicator: "blue"}); $(btn).prop("disabled", false); } }; diff --git a/frappe/public/js/frappe/form/sidebar/form_sidebar.js b/frappe/public/js/frappe/form/sidebar/form_sidebar.js index 02caf25557..a145e47149 100644 --- a/frappe/public/js/frappe/form/sidebar/form_sidebar.js +++ b/frappe/public/js/frappe/form/sidebar/form_sidebar.js @@ -69,7 +69,7 @@ frappe.ui.form.Sidebar = Class.extend({ }, refresh: function() { - if(this.frm.doc.__islocal) { + if (this.frm.doc.__islocal) { this.sidebar.toggle(false); } else { this.sidebar.toggle(true); @@ -81,12 +81,34 @@ frappe.ui.form.Sidebar = Class.extend({ } this.frm.viewers.refresh(); this.frm.tags && this.frm.tags.refresh(this.frm.get_docinfo().tags); - this.sidebar.find(".modified-by").html(__("{0} edited this {1}", - ["" + frappe.user.full_name(this.frm.doc.modified_by) + "", - "
    " + comment_when(this.frm.doc.modified)])); - this.sidebar.find(".created-by").html(__("{0} created this {1}", - ["" + frappe.user.full_name(this.frm.doc.owner) + "", - "
    " + comment_when(this.frm.doc.creation)])); + + if (this.frm.doc.route && cint(frappe.boot.website_tracking_enabled)) { + let route = this.frm.doc.route; + frappe.utils.get_page_view_count(route).then((res) => { + this.sidebar + .find(".pageview-count") + .html( + __("{0} Page Views", [String(res.message).bold()]) + ); + }); + } + + this.sidebar + .find(".modified-by") + .html( + __("{0} edited this {1}", [ + frappe.user.full_name(this.frm.doc.modified_by).bold(), + "
    " + comment_when(this.frm.doc.modified), + ]) + ); + this.sidebar + .find(".created-by") + .html( + __("{0} created this {1}", [ + frappe.user.full_name(this.frm.doc.owner).bold(), + "
    " + comment_when(this.frm.doc.creation), + ]) + ); this.refresh_like(); frappe.ui.form.set_user_image(this.frm); diff --git a/frappe/public/js/frappe/form/templates/form_sidebar.html b/frappe/public/js/frappe/form/templates/form_sidebar.html index b611557c43..30b2205bae 100644 --- a/frappe/public/js/frappe/form/templates/form_sidebar.html +++ b/frappe/public/js/frappe/form/templates/form_sidebar.html @@ -105,6 +105,7 @@ diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index 0aae8b361f..15f77fada5 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -203,6 +203,7 @@ frappe.views.BaseList = class BaseList { show_sidebar = !show_sidebar; localStorage.show_sidebar = show_sidebar; this.show_or_hide_sidebar(); + $(document.body).trigger('toggleListSidebar'); } show_or_hide_sidebar() { @@ -686,5 +687,5 @@ class FilterArea { } // utility function to validate view modes -frappe.views.view_modes = ['List', 'Gantt', 'Kanban', 'Calendar', 'Image', 'Inbox', 'Report']; +frappe.views.view_modes = ['List', 'Gantt', 'Kanban', 'Calendar', 'Image', 'Inbox', 'Report', 'Dashboard']; frappe.views.is_valid = view_mode => frappe.views.view_modes.includes(view_mode); diff --git a/frappe/public/js/frappe/list/list_sidebar.html b/frappe/public/js/frappe/list/list_sidebar.html index 880a91cf81..37f0dafb96 100644 --- a/frappe/public/js/frappe/list/list_sidebar.html +++ b/frappe/public/js/frappe/list/list_sidebar.html @@ -26,6 +26,8 @@
  • + + {%- endfor %} +
    + + {% endif %} + {% include "templates/includes/navbar/navbar_search.html" %} {% include "templates/includes/navbar/navbar_login.html" %} diff --git a/frappe/templates/styles/card_style.css b/frappe/templates/styles/card_style.css index 595d974011..cf90ff0bd5 100644 --- a/frappe/templates/styles/card_style.css +++ b/frappe/templates/styles/card_style.css @@ -31,7 +31,6 @@ } .ellipsis { - white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; diff --git a/frappe/templates/web.html b/frappe/templates/web.html index d2d38a6320..f69431796b 100644 --- a/frappe/templates/web.html +++ b/frappe/templates/web.html @@ -1,8 +1,14 @@ {% extends base_template_path %} {% block hero %}{% endblock %} +{% macro page_content() %} +{%- block page_content -%}{%- endblock -%} +{% endmacro %} + {% block content %} +{%- if theme.based_on == 'Bootstrap 4' or doctype != 'Web Page' -%} + {% macro main_content() %}
    @@ -13,7 +19,7 @@
    {% block page_container %} -
    +
    - {%- block page_content -%}{%- endblock -%} + {{ page_content() }}
    {% endmacro %} -{% macro container_attributes() %} -id="page-{{ name or route | e }}" data-path="{{ pathname | e }}" {%- if page_or_generator=="Generator" %}source-type="Generator" data-doctype="{{ doctype }}"{% endif %} -{% endmacro %} +{% macro container_attributes() -%} +id="page-{{ name or route | e }}" data-path="{{ pathname | e }}" + {%- if page_or_generator=="Generator" %}source-type="Generator" data-doctype="{{ doctype }}"{%- endif %} + {%- if source_content_type %}source-content-type="{{ source_content_type }}"{%- endif %} +{%- endmacro %} {% if show_sidebar %}
    @@ -60,4 +68,11 @@ id="page-{{ name or route | e }}" data-path="{{ pathname | e }}" {%- if page_or_ {{ main_content() }}
    {% endif %} + +{%- else -%} +
    + {{ page_content() }} +
    +{%- endif -%} + {% endblock %} diff --git a/frappe/tests/test_permissions.py b/frappe/tests/test_permissions.py index 7e9416f68a..364469f168 100644 --- a/frappe/tests/test_permissions.py +++ b/frappe/tests/test_permissions.py @@ -201,7 +201,7 @@ class TestPermissions(unittest.TestCase): doc = frappe.get_doc("DocType", "Blog Post") # change one property from the child table - doc.fields[-1].fieldtype = 'HTML' + doc.fields[-1].fieldtype = 'Check' self.assertRaises(frappe.CannotChangeConstantError, doc.save) frappe.clear_cache(doctype='DocType') diff --git a/frappe/translations/de.csv b/frappe/translations/de.csv index efeb68df62..18d66591b3 100644 --- a/frappe/translations/de.csv +++ b/frappe/translations/de.csv @@ -63,7 +63,6 @@ apps/frappe/frappe/templates/emails/download_data.html,We have received a reques DocType: System Settings,"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.","Falls diese Option aktiviert ist, wird die Passwortstärke auf der Grundlage des Minimum Password Score Wertes erzwungen. Ein Wert von 2 ist mittelstark und 4 sehr stark." DocType: About Us Settings,"""Team Members"" or ""Management""",„Teammitglieder“ oder „Management“ apps/frappe/frappe/core/doctype/doctype/doctype.py,Default for 'Check' type of field must be either '0' or '1',Standard für 'Prüfen'-Feldtyp muss entweder '0' oder '1' sein -apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,Yesterday,Gestern DocType: Contact,Designation,Bezeichnung apps/frappe/frappe/email/doctype/email_account/email_account.py,Automatic Linking can be activated only for one Email Account.,Die automatische Verknüpfung kann nur für ein E-Mail-Konto aktiviert werden. DocType: Test Runner,Test Runner,Tester @@ -120,7 +119,6 @@ DocType: Dashboard Chart,Timespan,Zeitspanne apps/frappe/frappe/public/js/frappe/file_uploader/FileUploader.vue,Web Link,Weblink DocType: Deleted Document,Restored,Restauriert apps/frappe/frappe/public/js/frappe/form/print.js,"Error connecting to QZ Tray Application...

    You need to have QZ Tray application installed and running, to use the Raw Print feature.

    Click here to Download and install QZ Tray.
    Click here to learn more about Raw Printing.","Fehler beim Verbinden mit der QZ-Tray-Anwendung ...

    Sie müssen die QZ Tray-Anwendung installiert haben und ausführen, um die Raw Print-Funktion verwenden zu können.

    Klicken Sie hier, um QZ Tray herunterzuladen und zu installieren .
    Klicken Sie hier, um mehr über den Rohdruck zu erfahren ." -apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,1 minute ago,Vor 1 Minute apps/frappe/frappe/core/page/permission_manager/permission_manager_help.html,"Apart from System Manager, roles with Set User Permissions right can set permissions for other users for that Document Type.",Zusätzlich zum System-Manager können Rollen mit der Erlaubnis Benutzer anzulegen Berechtigungen für andere Nutzer für diesen Dokumententyp setzen. apps/frappe/frappe/website/doctype/website_theme/website_theme.js,Configure Theme,Thema konfigurieren DocType: Company History,Company History,Unternehmensgeschichte @@ -971,7 +969,6 @@ apps/frappe/frappe/public/js/frappe/list/list_view.js,Share URL,URL teilen DocType: System Settings,Allow Consecutive Login Attempts ,Erlaube aufeinanderfolgende Login-Versuche apps/frappe/frappe/templates/pages/integrations/stripe_checkout.html,An error occured during the payment process. Please contact us.,Während des Bezahlvorgangs ist ein Fehler aufgetreten. Bitte kontaktieren Sie uns. DocType: Onboarding Slide,If Slide Type is Create or Settings there should be a 'create_onboarding_docs' method in the {ref_doctype}.py file bound to be executed after the slide is completed.,"Wenn der Folientyp "Erstellen" oder "Einstellungen" ist, sollte die Methode "create_onboarding_docs" in der Datei "{ref_doctype} .py" enthalten sein, die nach Abschluss der Folie ausgeführt werden soll." -apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} days ago,vor {0} Tag(en) DocType: Email Account,Awaiting Password,Warte auf Passwort DocType: Address,Address Line 1,Adresse Zeile 1 apps/frappe/frappe/public/js/frappe/ui/filters/filter.js,Not Descendants Of,Nicht Nachkommen von @@ -1360,7 +1357,6 @@ apps/frappe/frappe/social/doctype/energy_point_rule/energy_point_rule.py,Referen DocType: PayPal Settings,PayPal Settings,PayPal-Einstellungen apps/frappe/frappe/core/page/permission_manager/permission_manager.js,Select Document Type,Dokumenttyp auswählen apps/frappe/frappe/utils/nestedset.py,Cannot delete {0} as it has child nodes,"{0} kann nicht gelöscht werden, da es Unterknoten gibt" -apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} minutes ago,vor {0} Minute(n) apps/frappe/frappe/automation/doctype/assignment_rule/assignment_rule.py,Assignment Day {0} has been repeated.,Der Zuordnungstag {0} wurde wiederholt. DocType: Kanban Board Column,lightblue,hellblau apps/frappe/frappe/integrations/doctype/webhook/webhook.py,Same Field is entered more than once,Gleiches Feld wird mehrmals eingegeben @@ -1787,7 +1783,6 @@ DocType: Notification Log,Assignment,Zuordnung DocType: Notification,Slack Channel,Slack-Kanal DocType: About Us Team Member,Image Link,Bildverknüpfung DocType: Auto Email Report,Report Filters,Berichtsfilter -apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,now,jetzt DocType: Workflow State,step-backward,Schritt zurück apps/frappe/frappe/utils/boilerplate.py,{app_title},{app_title} apps/frappe/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py,Please set Dropbox access keys in your site config,Bitte Dropbox-Zugriffsdaten in den Einstellungen der Seite setzen @@ -2520,7 +2515,6 @@ apps/frappe/frappe/public/js/frappe/form/print.js,QZ Tray Connection Active!,QZ- DocType: Contact Us Settings,Settings for Contact Us Page,Einstellungen Kontakt DocType: Server Script,Script Type,Skripttyp DocType: Print Settings,Enable Print Server,Aktivieren Sie den Druckserver -apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} weeks ago,vor {0} Woche(n) DocType: Email Account,Footer,Fußzeile apps/frappe/frappe/config/integrations.py,Authentication,Authentifizierung apps/frappe/frappe/utils/verified_command.py,Invalid Link,Ungültige Verknüpfung @@ -3444,7 +3438,6 @@ apps/frappe/frappe/integrations/doctype/ldap_settings/ldap_settings.py,Please In apps/frappe/frappe/core/doctype/data_import/log_details.html,Row Status,Zeilenstatus DocType: S3 Backup Settings,sa-east-1,sa-east-1 DocType: Workflow Transition,Workflow Transition,Workflow-Übergang -apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} months ago,vor {0} Monate(n) apps/frappe/frappe/custom/doctype/custom_field/custom_field.py,Custom Fields can only be added to a standard DocType.,Benutzerdefinierte Felder können nur zu einem Standard-DocType hinzugefügt werden. apps/frappe/frappe/public/js/frappe/list/list_sidebar_group_by.js,Created By,Erstellt von apps/frappe/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py,Dropbox Setup,Dropbox-Setup @@ -3500,7 +3493,6 @@ apps/frappe/frappe/website/doctype/website_theme/website_theme.js,Theme Colors,T apps/frappe/frappe/printing/page/print_format_builder/print_format_builder.js,Select a DocType to make a new format,"DocType auswählen, um ein neues Format zu erstellen" apps/frappe/frappe/desk/page/user_profile/user_profile.js,User does not exist,Benutzer existiert nicht apps/frappe/frappe/automation/doctype/auto_repeat/auto_repeat.py,'Recipients' not specified,"Keine ""Empfänger"" angegeben" -apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,just now,gerade eben apps/frappe/frappe/public/js/frappe/ui/filters/edit_filter.html,Apply,Anwenden DocType: Footer Item,Policy,Politik apps/frappe/frappe/integrations/utils.py,{0} Settings not found,{0} Einstellungen nicht gefunden @@ -3592,7 +3584,6 @@ DocType: Auto Email Report,Period,Periode apps/frappe/frappe/core/doctype/data_import_beta/data_import_beta.js,About {0} minute remaining,Noch ungefähr {0} Minuten apps/frappe/frappe/www/login.py,Invalid Login Token,Invalid Login Token apps/frappe/frappe/public/js/frappe/chat.js,Discard,Verwerfen -apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,1 hour ago,vor 1 Stunde DocType: Website Settings,Home Page,Startseite DocType: Error Snapshot,Parent Error Snapshot,Momentaufnahme des übergeordneten Fehlers apps/frappe/frappe/public/js/frappe/data_import/import_preview.js,Map columns from {0} to fields in {1},Ordnen Sie Spalten von {0} Feldern in {1} zu. @@ -3804,7 +3795,6 @@ DocType: Webhook,on_update_after_submit,on_update_after_submit DocType: System Settings,Allow Login using User Name,Login mit Benutzernamen zulassen apps/frappe/frappe/core/doctype/report/report.js,Enable Report,Bericht aktivieren DocType: DocField,Display Depends On,Anzeige ist abhängig von -apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,> {0} year(s) ago,> {0} Jahr (e) her DocType: Social Login Key,API Endpoint,API-Endpunkt DocType: Web Page,Insert Code,Code einfügen DocType: Data Migration Run,Current Mapping Type,Aktueller Kartentyp @@ -4188,3 +4178,23 @@ DocType: DocField,Ignore User Permissions,Ignorieren von Benutzerberechtigungen apps/frappe/frappe/public/js/frappe/web_form/web_form.js,Saved Successfully,Erfolgreich gespeichert apps/frappe/frappe/core/doctype/user/user.py,Please ask your administrator to verify your sign-up,Bitte fragen Sie Ihren Administrator Ihre Anmeldung bis zum überprüfen DocType: Domain Settings,Active Domains,Aktive Domains +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,Now,Jetzt +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} m,{0} m +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} h,{0} h +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} D,{0} T +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} W,{0} W +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} M,{0} M +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} Y,{0} J +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,Just now,Gerade eben +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,1 minute ago,Vor einer Minute +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} minutes ago,Vor {0} Minuten +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,1 hour ago,Vor einer Stunde +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} hours ago,Vor {0} Stunden +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,Yesterday,Gestern +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} days ago,Vor {0} Tagen +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,1 week ago,Vor einer Woche +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} weeks ago,Vor {0} Wochen +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,1 month ago,Vor einem Monat +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} months ago,Vor {0} Monaten +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,1 year ago,Vor einem Jahr +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} years ago,Vor {0} Jahren \ No newline at end of file diff --git a/frappe/twofactor.py b/frappe/twofactor.py index e60113215b..253636764f 100644 --- a/frappe/twofactor.py +++ b/frappe/twofactor.py @@ -30,7 +30,7 @@ def two_factor_is_enabled(user=None): if bypass_two_factor_auth and user: user_doc = frappe.get_doc("User", user) restrict_ip_list = user_doc.get_restricted_ip_list() #can be None or one or more than one ip address - if restrict_ip_list: + if restrict_ip_list and frappe.local.request_ip: for ip in restrict_ip_list: if frappe.local.request_ip.startswith(ip): enabled = False @@ -374,11 +374,11 @@ def delete_qrimage(user, check_expiry=False): def delete_all_barcodes_for_users(): '''Task to delete all barcodes for user.''' - if not two_factor_is_enabled(): - return users = frappe.get_all('User', {'enabled':1}) for user in users: + if not two_factor_is_enabled(user=user.name): + continue delete_qrimage(user.name, check_expiry=True) def should_remove_barcode_image(barcode): diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 649d3bf72c..34432839bb 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -81,13 +81,29 @@ def validate_phone_number(phone_number, throw=False): return False phone_number = phone_number.strip() - match = re.match("([0-9\ \+\_\-\,\.\*\#\(\)]){1,20}$", phone_number) + match = re.match(r"([0-9\ \+\_\-\,\.\*\#\(\)]){1,20}$", phone_number) if not match and throw: frappe.throw(frappe._("{0} is not a valid Phone Number").format(phone_number), frappe.InvalidPhoneNumberError) return bool(match) +def validate_name(name, throw=False): + """Returns True if the name is valid + valid names may have unicode and ascii characters, dash, quotes, numbers + anything else is considered invalid + """ + if not name: + return False + + name = name.strip() + match = re.match(r"^[\w][\w\'\-]*([ \w][\w\'\-]+)*$", name) + + if not match and throw: + frappe.throw(frappe._("{0} is not a valid Name").format(name), frappe.InvalidNameError) + + return bool(match) + def validate_email_address(email_str, throw=False): """Validates the email string""" email = email_str = (email_str or "").strip() diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py index 03f063e058..4b37e850f0 100755 --- a/frappe/utils/background_jobs.py +++ b/frappe/utils/background_jobs.py @@ -73,7 +73,7 @@ def enqueue(method, queue='default', timeout=None, event=None, def enqueue_doc(doctype, name=None, method=None, queue='default', timeout=300, now=False, **kwargs): '''Enqueue a method to be run on a document''' - enqueue('frappe.utils.background_jobs.run_doc_method', doctype=doctype, name=name, + return enqueue('frappe.utils.background_jobs.run_doc_method', doctype=doctype, name=name, doc_method=method, queue=queue, timeout=timeout, now=now, **kwargs) def run_doc_method(doctype, name, doc_method, **kwargs): diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index 984e28db5e..a4c2c4bb70 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -23,8 +23,9 @@ class BackupGenerator: If specifying db_file_name, also append ".sql.gz" """ def __init__(self, db_name, user, password, backup_path_db=None, backup_path_files=None, - backup_path_private_files=None, db_host="localhost"): + backup_path_private_files=None, db_host="localhost", db_port=3306): self.db_host = db_host + self.db_port = db_port or 3306 self.db_name = db_name self.user = user self.password = password @@ -108,10 +109,10 @@ class BackupGenerator: import frappe.utils # escape reserved characters - args = dict([item[0], frappe.utils.esc(item[1], '$ ')] + args = dict([item[0], frappe.utils.esc(str(item[1]), '$ ')] for item in self.__dict__.copy().items()) - cmd_string = """mysqldump --single-transaction --quick --lock-tables=false -u %(user)s -p%(password)s %(db_name)s -h %(db_host)s | gzip > %(backup_path_db)s """ % args + cmd_string = """mysqldump --single-transaction --quick --lock-tables=false -u %(user)s -p%(password)s %(db_name)s -h %(db_host)s -P %(db_port)s | gzip > %(backup_path_db)s """ % args err, out = frappe.utils.execute_in_shell(cmd_string) def send_email(self): @@ -171,7 +172,8 @@ def new_backup(older_than=6, ignore_files=False, backup_path_db=None, backup_pat frappe.conf.db_password, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, - db_host = frappe.db.host) + db_host = frappe.db.host, + db_port = frappe.db.port) odb.get_backup(older_than, ignore_files, force=force) return odb diff --git a/frappe/utils/change_log.py b/frappe/utils/change_log.py index c94a247796..776fb825c2 100644 --- a/frappe/utils/change_log.py +++ b/frappe/utils/change_log.py @@ -174,9 +174,12 @@ def parse_latest_non_beta_release(response): Returns json : json object pertaining to the latest non-beta release """ - for release in response: - if release['prerelease'] == True: continue - return release + version_list = [release.get('tag_name').strip('v') for release in response if not release.get('prerelease')] + + if version_list: + return sorted(version_list, key=Version, reverse=True)[0] + + return None def check_release_on_github(app): # Check if repo remote is on github @@ -199,12 +202,11 @@ def check_release_on_github(app): org_name = remote_url.split('/')[3] r = requests.get('https://api.github.com/repos/{}/{}/releases'.format(org_name, app)) - if r.status_code == 200 and r.json(): + if r.ok: lastest_non_beta_release = parse_latest_non_beta_release(r.json()) - return Version(lastest_non_beta_release['tag_name'].strip('v')), org_name - else: - # In case of an improper response or if there are no releases - return None + return Version(lastest_non_beta_release), org_name + # In case of an improper response or if there are no releases + return None def add_message_to_redis(update_json): # "update-message" will store the update message string diff --git a/frappe/utils/data.py b/frappe/utils/data.py index dc4d3c5e53..9ee683b12f 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -653,6 +653,45 @@ def is_image(filepath): filepath = filepath.split('?')[0] return (guess_type(filepath)[0] or "").startswith("image/") +def get_thumbnail_base64_for_image(src): + from PIL import Image + from frappe.core.doctype.file.file import get_local_image + from frappe import safe_decode, cache + + if not src: + frappe.throw('Invalid source for image: {0}'.format(src)) + + if not src.startswith('/files'): + return + + def _get_base64(): + try: + image, unused_filename, extn = get_local_image(src) + except IOError: + return + + original_size = image.size + size = 50, 50 + image.thumbnail(size, Image.ANTIALIAS) + + base64_string = image_to_base64(image, extn) + return { + 'base64': 'data:image/{0};base64,{1}'.format(extn, safe_decode(base64_string)), + 'width': original_size[0], + 'height': original_size[1] + } + + return cache().hget('thumbnail_base64', src, generator=_get_base64) + +def image_to_base64(image, extn): + import base64 + from io import BytesIO + + buffered = BytesIO() + image.save(buffered, extn) + img_str = base64.b64encode(buffered.getvalue()) + return img_str + # from Jinja2 code _striptags_re = re.compile(r'(|<[^>]*>)') @@ -661,6 +700,9 @@ def strip_html(text): return _striptags_re.sub("", text) def escape_html(text): + if not isinstance(text, string_types): + return text + html_escape_table = { "&": "&", '"': """, @@ -1091,3 +1133,6 @@ def get_source_value(source, key): def is_subset(list_a, list_b): '''Returns whether list_a is a subset of list_b''' return len(list(set(list_a) & set(list_b))) == len(list_a) + +def generate_hash(*args, **kwargs): + return frappe.generate_hash(*args, **kwargs) diff --git a/frappe/utils/jinja.py b/frappe/utils/jinja.py index bc26490422..5a8f66b054 100644 --- a/frappe/utils/jinja.py +++ b/frappe/utils/jinja.py @@ -11,12 +11,20 @@ def get_jenv(): from jinja2.sandbox import SandboxedEnvironment # frappe will be loaded last, so app templates will get precedence - jenv = SandboxedEnvironment(loader = get_jloader(), - undefined=DebugUndefined) + jenv = SandboxedEnvironment( + loader=get_jloader(), + undefined=DebugUndefined + ) set_filters(jenv) jenv.globals.update(get_safe_globals()) jenv.globals.update(get_jenv_customization('methods')) + jenv.globals.update({ + 'component': component, + 'c': component, + 'resolve_class': resolve_class, + 'inspect': inspect + }) frappe.local.jenv = jenv @@ -156,3 +164,68 @@ def get_jenv_customization(customization_type): out[fn_name] = frappe.get_attr(fn_string) return out + + +def component(name, **kwargs): + from jinja2 import TemplateNotFound + + template_name = 'templates/components/' + name + '.html' + jenv = get_jenv() + + try: + source = jenv.loader.get_source(jenv, template_name)[0] + except TemplateNotFound: + return '
    Component "{0}" not found
    '.format(name) + + attributes, html = parse_front_matter_attrs_and_html(source) + context = {} + context.update(attributes) + context.update(kwargs) + + if 'class' in context: + context['class'] = resolve_class(context['class']) + else: + context['class'] = '' + + return get_jenv().from_string(html).render(context) + +def resolve_class(classes): + import frappe + + if classes is None: + return '' + + if isinstance(classes, frappe.string_types): + return classes + + if isinstance(classes, (list, tuple)): + return ' '.join([resolve_class(c) for c in classes]).strip() + + if isinstance(classes, dict): + return ' '.join([classname for classname in classes if classes[classname]]).strip() + + return classes + +def parse_front_matter_attrs_and_html(source): + from frappe.website.router import get_frontmatter + + html = source + attributes = {} + + if not source.startswith('---'): + return attributes, html + + res = get_frontmatter(source) + if res['attributes']: + attributes = res['attributes'] + html = res['body'] + + return attributes, html + +def inspect(var, render=True): + context = { "var": var } + if render: + html = "
    {{ var | pprint | e }}
    " + else: + html = "" + return get_jenv().from_string(html).render(context) diff --git a/frappe/utils/logger.py b/frappe/utils/logger.py index d40e2565cb..5a77434cde 100755 --- a/frappe/utils/logger.py +++ b/frappe/utils/logger.py @@ -5,7 +5,7 @@ from logging.handlers import RotatingFileHandler from six import text_type default_log_level = logging.DEBUG -LOG_FILENAME = '../logs/frappe.log' +LOG_FILENAME = '../logs/{}-frappe.log'.format(frappe.local.site) def get_logger(module, with_more_info=True): if module in frappe.loggers: @@ -57,4 +57,3 @@ def set_log_level(level): '''Use this method to set log level to something other than the default DEBUG''' frappe.log_level = getattr(logging, (level or '').upper(), None) or default_log_level frappe.loggers = {} - diff --git a/frappe/utils/password.py b/frappe/utils/password.py index da5cdecc55..b939607b19 100644 --- a/frappe/utils/password.py +++ b/frappe/utils/password.py @@ -131,9 +131,9 @@ def create_auth_table(): frappe.db.create_auth_table() def encrypt(pwd): - if len(pwd) > 100: - # encrypting > 100 chars will lead to truncation - frappe.throw(_('Password cannot be more than 100 characters long')) + if len(pwd) > 127: + # encrypting > 127 chars will lead to truncation + frappe.throw(_('Password cannot be more than 127 characters long')) cipher_suite = Fernet(encode(get_encryption_key())) cipher_text = cstr(cipher_suite.encrypt(encode(pwd))) diff --git a/frappe/utils/response.py b/frappe/utils/response.py index 1dfbbe5516..fe7af072cf 100644 --- a/frappe/utils/response.py +++ b/frappe/utils/response.py @@ -218,6 +218,6 @@ def send_private_file(path): def handle_session_stopped(): frappe.respond_as_web_page(_("Updating"), - _("Your system is being updated. Please refresh again after a few moments"), + _("Your system is being updated. Please refresh again after a few moments."), http_status_code=503, indicator_color='orange', fullpage = True, primary_action=None) return frappe.website.render.render("message", http_status_code=503) diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index 62d0286e03..f80d819907 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -11,14 +11,14 @@ from frappe.website.utils import (get_shade, get_toc, get_next_link) from frappe.modules import scrub from frappe.www.printview import get_visible_columns import frappe.exceptions +import frappe.integrations.utils class ServerScriptNotEnabled(frappe.PermissionError): pass def safe_exec(script, _globals=None, _locals=None): # script reports must be enabled via site_config.json if not frappe.conf.server_script_enabled: - frappe.msgprint('Please Enable Server Scripts') - raise ServerScriptNotEnabled + frappe.throw('Please Enable Server Scripts', ServerScriptNotEnabled) # build globals exec_globals = get_safe_globals() @@ -79,6 +79,8 @@ def get_safe_globals(): user=user, csrf_token=frappe.local.session.data.csrf_token if getattr(frappe.local, "session", None) else '' ), + make_get_request = frappe.integrations.utils.make_get_request, + make_post_request = frappe.integrations.utils.make_post_request, socketio_port=frappe.conf.socketio_port, get_hooks=frappe.get_hooks, ), diff --git a/frappe/website/context.py b/frappe/website/context.py index dcef22af43..5663199545 100644 --- a/frappe/website/context.py +++ b/frappe/website/context.py @@ -36,6 +36,8 @@ def get_context(path, args=None): if frappe.conf.developer_mode: context._context_dict = context + context.developer_mode = frappe.conf.developer_mode + return context def update_controller_context(context, controller): @@ -221,24 +223,24 @@ def add_metatags(context): tags = frappe._dict(context.get("metatags") or {}) if tags: - if not "twitter:card" in tags: - tags["twitter:card"] = "summary_large_image" - - if not "og:type" in tags: + if "og:type" not in tags: tags["og:type"] = "article" - if tags.get("name"): - tags["og:title"] = tags["twitter:title"] = tags["name"] + name = tags.get('name') or tags.get('title') + if name: + tags["og:title"] = tags["twitter:title"] = name - if tags.get("title"): - tags["og:title"] = tags["twitter:title"] = tags["title"] - - if tags.get("description"): - tags["og:description"] = tags["twitter:description"] = tags["description"] + description = tags.get("description") or context.description + if description: + tags['description'] = tags["og:description"] = tags["twitter:description"] = description image = tags.get('image', context.image or None) if image: tags["og:image"] = tags["twitter:image:src"] = tags["image"] = frappe.utils.get_url(image) + tags['twitter:card'] = "summary_large_image" + + if context.author or tags.get('author'): + tags['author'] = context.author or tags.get('author') if context.path: tags['og:url'] = tags['url'] = frappe.utils.get_url(context.path) @@ -246,11 +248,6 @@ def add_metatags(context): if context.published_on: tags['datePublished'] = context.published_on - if context.author: - tags['author'] = context.author - - if context.description: - tags['description'] = context.description tags['language'] = frappe.local.lang or 'en' diff --git a/frappe/website/doctype/blog_post/blog_post.js b/frappe/website/doctype/blog_post/blog_post.js index e1b8341139..adc03ca77e 100644 --- a/frappe/website/doctype/blog_post/blog_post.js +++ b/frappe/website/doctype/blog_post/blog_post.js @@ -3,6 +3,40 @@ frappe.ui.form.on('Blog Post', { refresh: function(frm) { - + generate_google_search_preview(frm); + }, + title: function(frm) { + generate_google_search_preview(frm); + }, + meta_description: function(frm) { + generate_google_search_preview(frm); + }, + blog_intro: function(frm) { + generate_google_search_preview(frm); } }); + +function generate_google_search_preview(frm) { + let google_preview = frm.get_field("google_preview"); + let seo_title = (frm.doc.title).slice(0, 60); + let seo_description = (frm.doc.meta_description || frm.doc.blog_intro || "").slice(0, 160); + let date = frm.doc.published_on ? new frappe.datetime.datetime(frm.doc.published_on).moment.format('ll') + ' - ' : ''; + let route_array = frm.doc.route.split('/'); + route_array.pop(); + + google_preview.html(` + +
    + + ${frappe.boot.sitename} + › ${route_array.join(' › ')} + +
    + ${ seo_title } +
    +

    + ${ date } ${ seo_description } +

    +
    + `); +} diff --git a/frappe/website/doctype/blog_post/blog_post.json b/frappe/website/doctype/blog_post/blog_post.json index 9944cbf4b2..04e349a2b0 100644 --- a/frappe/website/doctype/blog_post/blog_post.json +++ b/frappe/website/doctype/blog_post/blog_post.json @@ -21,7 +21,13 @@ "content", "content_md", "content_html", - "email_sent" + "email_sent", + "meta_tags", + "meta_description", + "column_break_18", + "meta_image", + "section_break_20", + "google_preview" ], "fields": [ { @@ -123,6 +129,36 @@ "fieldname": "disable_comments", "fieldtype": "Check", "label": "Disable Comments" + }, + { + "fieldname": "meta_description", + "fieldtype": "Small Text", + "label": "Meta Description" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "fieldname": "meta_image", + "fieldtype": "Attach Image", + "label": "Meta Image" + }, + { + "fieldname": "section_break_20", + "fieldtype": "Section Break" + }, + { + "description": "This is an example Google SERP Preview.", + "fieldname": "google_preview", + "fieldtype": "HTML", + "label": "Google Snippet Preview", + "read_only": 1 + }, + { + "fieldname": "meta_tags", + "fieldtype": "Section Break", + "label": "Meta Tags" } ], "has_web_view": 1, @@ -131,7 +167,7 @@ "is_published_field": "published", "links": [], "max_attachments": 5, - "modified": "2020-04-08 19:58:13.672332", + "modified": "2020-04-29 17:32:41.055883", "modified_by": "Administrator", "module": "Website", "name": "Blog Post", diff --git a/frappe/website/doctype/blog_post/blog_post.py b/frappe/website/doctype/blog_post/blog_post.py index 148ba15be7..f306c49c0a 100644 --- a/frappe/website/doctype/blog_post/blog_post.py +++ b/frappe/website/doctype/blog_post/blog_post.py @@ -65,16 +65,18 @@ class BlogPost(WebsiteGenerator): context.content = get_html_content_based_on_type(self, 'content', self.content_type) - context.description = self.blog_intro or strip_html_tags(context.content[:140]) + + #if meta description is not present, then blog intro or first 140 characters of the blog will be set as description + context.description = self.meta_description or self.blog_intro or strip_html_tags(context.content[:140]) context.metatags = { "name": self.title, "description": context.description, } + #if meta image is not present, then first image inside the blog will be set as the meta image image = find_first_image(context.content) - if image: - context.metatags["image"] = image + context.metatags["image"] = self.meta_image or image or None self.load_comments(context) @@ -95,7 +97,6 @@ class BlogPost(WebsiteGenerator): else: context.comment_text = _('{0} comments').format(len(context.comment_list)) - def get_list_context(context=None): list_context = frappe._dict( template = "templates/includes/blog/blog.html", @@ -106,7 +107,7 @@ def get_list_context(context=None): title = _('Blog') ) - category = sanitize_html(frappe.local.form_dict.blog_category or frappe.local.form_dict.category) + category = frappe.utils.escape_html(frappe.local.form_dict.blog_category or frappe.local.form_dict.category) if category: category_title = get_blog_category(category) list_context.sub_title = _("Posts filed under {0}").format(category_title) @@ -149,7 +150,7 @@ def get_blog_category(route): def get_blog_list(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by=None): conditions = [] - category = filters.blog_category or sanitize_html(frappe.local.form_dict.blog_category or frappe.local.form_dict.category) + category = filters.blog_category or frappe.utils.escape_html(frappe.local.form_dict.blog_category or frappe.local.form_dict.category) if filters: if filters.blogger: conditions.append('t1.blogger=%s' % frappe.db.escape(filters.blogger)) diff --git a/frappe/website/doctype/blog_post/templates/blog_post.html b/frappe/website/doctype/blog_post/templates/blog_post.html index 285223a2af..ab3c2afa1a 100644 --- a/frappe/website/doctype/blog_post/templates/blog_post.html +++ b/frappe/website/doctype/blog_post/templates/blog_post.html @@ -15,7 +15,7 @@

    - {{ description }} + {{ blog_intro }}

    {{ content }} diff --git a/frappe/website/doctype/blogger/blogger.json b/frappe/website/doctype/blogger/blogger.json index be4eb6399e..b8165a5908 100644 --- a/frappe/website/doctype/blogger/blogger.json +++ b/frappe/website/doctype/blogger/blogger.json @@ -1,274 +1,108 @@ { - "allow_copy": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "field:short_name", - "beta": 0, - "creation": "2013-03-25 16:00:51", - "custom": 0, - "description": "User ID of a Blogger", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "actions": [], + "allow_import": 1, + "autoname": "field:short_name", + "creation": "2013-03-25 16:00:51", + "description": "User ID of a Blogger", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "disabled", + "short_name", + "full_name", + "user", + "bio", + "avatar", + "posts" + ], "fields": [ { - "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_list_view": 0, - "in_standard_filter": 0, - "label": "Disabled", - "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 - }, + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Will be used in url (usually first name).", - "fieldname": "short_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Short 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 - }, + "description": "Will be used in url (usually first name).", + "fieldname": "short_name", + "fieldtype": "Data", + "label": "Short Name", + "reqd": 1, + "unique": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "full_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Full 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 - }, + "fieldname": "full_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Full Name", + "reqd": 1 + }, { - "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": 0, - "in_standard_filter": 0, - "label": "User", - "length": 0, - "no_copy": 0, - "options": "User", - "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 - }, + "fieldname": "user", + "fieldtype": "Link", + "label": "User", + "options": "User" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "bio", - "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": "Bio", - "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 - }, + "fieldname": "bio", + "fieldtype": "Small Text", + "label": "Bio" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "avatar", - "fieldtype": "Attach", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Avatar", - "length": 0, - "no_copy": 0, - "options": "", - "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 - }, + "fieldname": "avatar", + "fieldtype": "Attach", + "label": "Avatar" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "posts", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Posts", - "length": 0, - "no_copy": 1, - "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 + "fieldname": "posts", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Posts", + "no_copy": 1, + "read_only": 1 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-user", - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 1, - "modified": "2018-10-10 14:40:40.407657", - "modified_by": "Administrator", - "module": "Website", - "name": "Blogger", - "owner": "Administrator", + ], + "icon": "fa fa-user", + "idx": 1, + "links": [ + { + "link_doctype": "Blog Post", + "link_fieldname": "blogger" + } + ], + "max_attachments": 1, + "modified": "2020-04-19 08:21:09.684300", + "modified_by": "Administrator", + "module": "Website", + "name": "Blogger", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 1, - "is_custom": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Website Manager", - "set_user_permissions": 1, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Website Manager", + "set_user_permissions": 1, + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 0, - "role": "Blogger", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "email": 1, + "print": 1, + "read": 1, + "role": "Blogger", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "title_field": "full_name", - "track_changes": 1, - "track_seen": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "full_name", + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/website/doctype/color/__init__.py b/frappe/website/doctype/color/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/website/doctype/css_class/css_class.js b/frappe/website/doctype/color/color.js similarity index 82% rename from frappe/website/doctype/css_class/css_class.js rename to frappe/website/doctype/color/color.js index 4544e249bf..78b3f773d1 100644 --- a/frappe/website/doctype/css_class/css_class.js +++ b/frappe/website/doctype/color/color.js @@ -1,7 +1,7 @@ // Copyright (c) 2020, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on('CSS Class', { +frappe.ui.form.on('Color', { // refresh: function(frm) { // } diff --git a/frappe/website/doctype/color/color.json b/frappe/website/doctype/color/color.json new file mode 100644 index 0000000000..f42898da12 --- /dev/null +++ b/frappe/website/doctype/color/color.json @@ -0,0 +1,44 @@ +{ + "actions": [], + "autoname": "Prompt", + "creation": "2020-04-19 02:25:37.010180", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "color" + ], + "fields": [ + { + "fieldname": "color", + "fieldtype": "Color", + "in_list_view": 1, + "label": "Color", + "reqd": 1 + } + ], + "links": [], + "modified": "2020-04-19 02:25:47.417772", + "modified_by": "Administrator", + "module": "Website", + "name": "Color", + "owner": "Administrator", + "permissions": [ + { + "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", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/website/doctype/web_view_item/web_view_item.py b/frappe/website/doctype/color/color.py similarity index 89% rename from frappe/website/doctype/web_view_item/web_view_item.py rename to frappe/website/doctype/color/color.py index cc440305c0..245b9e9165 100644 --- a/frappe/website/doctype/web_view_item/web_view_item.py +++ b/frappe/website/doctype/color/color.py @@ -6,5 +6,5 @@ from __future__ import unicode_literals # import frappe from frappe.model.document import Document -class WebViewItem(Document): +class Color(Document): pass diff --git a/frappe/website/doctype/color/test_color.py b/frappe/website/doctype/color/test_color.py new file mode 100644 index 0000000000..2f2be331ad --- /dev/null +++ b/frappe/website/doctype/color/test_color.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestColor(unittest.TestCase): + pass diff --git a/frappe/website/doctype/web_page/templates/web_page.html b/frappe/website/doctype/web_page/templates/web_page.html index 274b8abe32..84fb83ee6b 100644 --- a/frappe/website/doctype/web_page/templates/web_page.html +++ b/frappe/website/doctype/web_page/templates/web_page.html @@ -15,6 +15,9 @@ {% endblock %} {% block page_content %} +{%- if content_type == 'Page Builder' -%} + {{ page_builder_html }} +{%- else -%}
    {% include "templates/includes/slideshow.html" %}
    @@ -26,6 +29,7 @@ {% include 'templates/includes/comments/comments.html' %} {%- endif %}
    +{%- endif -%} {% endblock %} {% block style %} @@ -35,5 +39,11 @@ {% endblock %} {% block script %} - + {%- if script -%} + + {%- endif -%} + + {%- for script in page_builder_scripts -%} + + {%- endfor -%} {% endblock %} diff --git a/frappe/website/doctype/web_page/web_page.js b/frappe/website/doctype/web_page/web_page.js index ed6b25fe4d..c0a3bcdc20 100644 --- a/frappe/website/doctype/web_page/web_page.js +++ b/frappe/website/doctype/web_page/web_page.js @@ -1,7 +1,10 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // MIT License. See license.txt -frappe.ui.form.on("Web Page", { +frappe.ui.form.on('Web Page', { + onload: function() { + frappe.require('/assets/frappe/js/frappe/utils/web_page_block.js'); + }, title: function(frm) { if (frm.doc.title && !frm.doc.route) { frm.set_value('route', frappe.scrub(frm.doc.title, '-')); @@ -26,7 +29,7 @@ frappe.ui.form.on("Web Page", { frm.events.layout(frm); } }, - published: function (frm) { + published: function(frm) { // If current date is before end date, // and web page is manually unpublished, // set end date to current date. @@ -35,13 +38,13 @@ frappe.ui.form.on("Web Page", { // Set date a few seconds in the future to avoid throwing // start and end date validation error - end_date.setSeconds(end_date.getSeconds() + 5) + end_date.setSeconds(end_date.getSeconds() + 5); - frm.set_value("end_date", end_date); + frm.set_value('end_date', end_date); } }, set_meta_tags(frm) { frappe.utils.set_meta_tag(frm.doc.route); } -}) +}); diff --git a/frappe/website/doctype/web_page/web_page.json b/frappe/website/doctype/web_page/web_page.json index 645d83e155..4766c24045 100644 --- a/frappe/website/doctype/web_page/web_page.json +++ b/frappe/website/doctype/web_page/web_page.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_guest_to_view": 1, "allow_import": 1, "creation": "2013-03-28 10:35:30", @@ -13,6 +14,7 @@ "slideshow", "cb1", "published", + "full_width", "show_title", "start_date", "end_date", @@ -22,6 +24,7 @@ "main_section", "main_section_md", "main_section_html", + "page_blocks", "custom_javascript", "insert_code", "javascript", @@ -39,6 +42,10 @@ "sb2", "header", "breadcrumbs", + "metatags_section", + "meta_title", + "meta_description", + "meta_image", "set_meta_tags" ], "fields": [ @@ -105,7 +112,7 @@ "fieldname": "content_type", "fieldtype": "Select", "label": "Content Type", - "options": "Rich Text\nMarkdown\nHTML" + "options": "Rich Text\nMarkdown\nHTML\nPage Builder" }, { "depends_on": "eval:doc.content_type==='Rich Text'", @@ -217,7 +224,7 @@ "depends_on": "eval:!doc.__islocal", "fieldname": "sb2", "fieldtype": "Section Break", - "label": "Header, Breadcrumbs and Meta Tags" + "label": "Header and Breadcrumbs" }, { "description": "HTML for header section. Optional", @@ -235,21 +242,56 @@ { "fieldname": "set_meta_tags", "fieldtype": "Button", - "label": "Set Meta Tags" + "label": "Add Custom Tags" }, { "default": "0", "fieldname": "dynamic_template", "fieldtype": "Check", "label": "Dynamic Template" + }, + { + "depends_on": "eval:doc.content_type=='Page Builder'", + "fieldname": "page_blocks", + "fieldtype": "Table", + "label": "Page Building Blocks", + "options": "Web Page Block" + }, + { + "default": "0", + "fieldname": "full_width", + "fieldtype": "Check", + "label": "Full Width" + }, + { + "collapsible": 1, + "fieldname": "metatags_section", + "fieldtype": "Section Break", + "label": "Meta Tags" + }, + { + "fieldname": "meta_title", + "fieldtype": "Data", + "label": "Title" + }, + { + "fieldname": "meta_description", + "fieldtype": "Small Text", + "label": "Description" + }, + { + "fieldname": "meta_image", + "fieldtype": "Attach Image", + "label": "Image" } ], "has_web_view": 1, "icon": "fa fa-file-alt", "idx": 1, "is_published_field": "published", + "links": [], "max_attachments": 20, - "modified": "2019-10-02 13:58:50.825481", + "modified": "2020-04-25 20:40:39.253548", "modified_by": "Administrator", "module": "Website", "name": "Web Page", diff --git a/frappe/website/doctype/web_page/web_page.py b/frappe/website/doctype/web_page/web_page.py index ffa836e3c5..722c81fce9 100644 --- a/frappe/website/doctype/web_page/web_page.py +++ b/frappe/website/doctype/web_page/web_page.py @@ -12,7 +12,7 @@ from jinja2.exceptions import TemplateSyntaxError import frappe from frappe import _ from frappe.utils import get_datetime, now, strip_html -from frappe.utils.jinja import render_template +from frappe.utils.jinja import render_template, component from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow from frappe.website.router import resolve_route from frappe.website.utils import (extract_title, find_first_image, get_comment_list, @@ -36,6 +36,7 @@ class WebPage(WebsiteGenerator): def get_context(self, context): context.main_section = get_html_content_based_on_type(self, 'main_section', self.content_type) + context.source_content_type = self.content_type self.render_dynamic(context) # if static page, get static content @@ -59,6 +60,7 @@ class WebPage(WebsiteGenerator): self.set_metatags(context) self.set_breadcrumbs(context) self.set_title_and_header(context) + self.set_page_blocks(context) return context @@ -67,8 +69,7 @@ class WebPage(WebsiteGenerator): is_jinja = context.dynamic_template or "" in context.main_section if is_jinja or ("{{" in context.main_section): try: - context["main_section"] = render_template(context.main_section, - context) + context["main_section"] = render_template(context.main_section, context) if not "" in context.main_section: context["no_cache"] = 1 except TemplateSyntaxError: @@ -109,6 +110,13 @@ class WebPage(WebsiteGenerator): if not context.title and context.header: context.title = strip_html(context.header) + def set_page_blocks(self, context): + if self.content_type != 'Page Builder': + return + out = get_web_blocks_html(self.page_blocks) + context.page_builder_html = out.html + context.page_builder_scripts = out.scripts + def add_hero(self, context): """Add a hero element if specified in content or hooks. Hero elements get full page width.""" @@ -127,13 +135,11 @@ class WebPage(WebsiteGenerator): def set_metatags(self, context): context.metatags = { - "name": context.title + "name": self.meta_title or self.title, + "description": self.meta_description, + "image": self.meta_image or find_first_image(context.main_section or "") } - image = find_first_image(context.main_section or "") - if image: - context.metatags["image"] = image - def validate_dates(self): if self.end_date: if self.start_date and get_datetime(self.end_date) < get_datetime(self.start_date): @@ -192,3 +198,29 @@ def check_broken_links(): cnt += 1 print("{0} links broken".format(cnt)) + +def get_web_blocks_html(blocks): + '''Converts a list of blocks into Raw HTML and extracts out their scripts for deduplication''' + + out = frappe._dict(html='', scripts=[]) + extracted_scripts = [] + for block in blocks: + rendered_html = component('web_block', web_block=block) + html, scripts = extract_script_tags(rendered_html) + out.html += html + if block.web_template not in extracted_scripts: + out.scripts += scripts + extracted_scripts.append(block.web_template) + + # de-duplicate scripts + out.scripts = list(set(out.scripts)) + return out + +def extract_script_tags(html): + from bs4 import BeautifulSoup + soup = BeautifulSoup(html, "html.parser") + scripts = [] + for script in soup.find_all('script'): + scripts.append(script.text) + script.extract() + return str(soup), scripts diff --git a/frappe/website/doctype/web_page_block/__init__.py b/frappe/website/doctype/web_page_block/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/website/doctype/web_page_block/web_page_block.json b/frappe/website/doctype/web_page_block/web_page_block.json new file mode 100644 index 0000000000..77f2e25469 --- /dev/null +++ b/frappe/website/doctype/web_page_block/web_page_block.json @@ -0,0 +1,85 @@ +{ + "actions": [], + "creation": "2020-04-16 22:57:13.729460", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "web_template", + "edit_values", + "web_template_values", + "css_class", + "column_break_5", + "add_container", + "add_padding", + "add_shade", + "hide_block" + ], + "fields": [ + { + "fieldname": "web_template", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Web Template", + "options": "Web Template" + }, + { + "fieldname": "edit_values", + "fieldtype": "Button", + "in_list_view": 1, + "label": "Edit Values" + }, + { + "fieldname": "web_template_values", + "fieldtype": "Code", + "hidden": 1, + "label": "Web Template Values", + "options": "JSON" + }, + { + "fieldname": "css_class", + "fieldtype": "Small Text", + "label": "CSS Class" + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "default": "1", + "fieldname": "add_padding", + "fieldtype": "Check", + "label": "Add Padding" + }, + { + "default": "0", + "fieldname": "add_shade", + "fieldtype": "Check", + "label": "Add Gray Background" + }, + { + "default": "1", + "fieldname": "add_container", + "fieldtype": "Check", + "label": "Add Container" + }, + { + "default": "0", + "fieldname": "hide_block", + "fieldtype": "Check", + "label": "Hide Block" + } + ], + "istable": 1, + "links": [], + "modified": "2020-04-29 15:08:25.976179", + "modified_by": "Administrator", + "module": "Website", + "name": "Web Page Block", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/website/doctype/web_page_block/web_page_block.py b/frappe/website/doctype/web_page_block/web_page_block.py new file mode 100644 index 0000000000..57978e7484 --- /dev/null +++ b/frappe/website/doctype/web_page_block/web_page_block.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals + +import frappe +from frappe.model.document import Document +from frappe.website.doctype.web_template.web_template import get_rendered_template + + +class WebPageBlock(Document): + def render(self): + values = self.web_template_values or '{}' + values = frappe.parse_json(values) + rendered_html = get_rendered_template(self.web_template, values) + return rendered_html diff --git a/frappe/website/doctype/web_page_view/__init__.py b/frappe/website/doctype/web_page_view/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/website/doctype/web_page_view/test_web_page_view.py b/frappe/website/doctype/web_page_view/test_web_page_view.py new file mode 100644 index 0000000000..d51727ec68 --- /dev/null +++ b/frappe/website/doctype/web_page_view/test_web_page_view.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestWebPageView(unittest.TestCase): + pass diff --git a/frappe/website/doctype/web_page_view/web_page_view.js b/frappe/website/doctype/web_page_view/web_page_view.js new file mode 100644 index 0000000000..77a047e408 --- /dev/null +++ b/frappe/website/doctype/web_page_view/web_page_view.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Web Page View', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/website/doctype/web_page_view/web_page_view.json b/frappe/website/doctype/web_page_view/web_page_view.json new file mode 100644 index 0000000000..7a1a210d62 --- /dev/null +++ b/frappe/website/doctype/web_page_view/web_page_view.json @@ -0,0 +1,75 @@ +{ + "actions": [], + "creation": "2020-04-15 22:54:46.009703", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "path", + "referrer", + "browser", + "browser_version", + "date" + ], + "fields": [ + { + "fieldname": "path", + "fieldtype": "Data", + "label": "Path", + "set_only_once": 1 + }, + { + "fieldname": "referrer", + "fieldtype": "Data", + "label": "Referrer", + "search_index": 1, + "set_only_once": 1 + }, + { + "fieldname": "browser", + "fieldtype": "Data", + "label": "Browser", + "search_index": 1, + "set_only_once": 1 + }, + { + "fieldname": "browser_version", + "fieldtype": "Data", + "label": "Browser Version", + "set_only_once": 1 + }, + { + "fieldname": "date", + "fieldtype": "Datetime", + "label": "Date", + "set_only_once": 1 + } + ], + "in_create": 1, + "links": [], + "modified": "2020-04-15 23:31:27.517793", + "modified_by": "Administrator", + "module": "Website", + "name": "Web Page 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 + } + ], + "quick_entry": 1, + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "path", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/website/doctype/web_page_view/web_page_view.py b/frappe/website/doctype/web_page_view/web_page_view.py new file mode 100644 index 0000000000..93cf1d7bb8 --- /dev/null +++ b/frappe/website/doctype/web_page_view/web_page_view.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class WebPageView(Document): + pass + + +@frappe.whitelist(allow_guest=True) +def make_view_log(path, referrer=None, browser=None, version=None, url=None, user_tz=None): + request_dict = frappe.request.__dict__ + user_agent = request_dict.get('environ', {}).get('HTTP_USER_AGENT') + + is_unique = True + if referrer.startswith(url): + is_unique = False + + if path != "/" and path.startswith('/'): + path = path[1:] + + if is_tracking_enabled(): + view = frappe.new_doc("Web Page View") + view.path = path + view.referrer = referrer + view.browser = browser + view.browser_version = version + view.time_zone = user_tz + view.user_agent = user_agent + view.is_unique = is_unique + view.insert(ignore_permissions=True) + + return + +@frappe.whitelist() +def get_page_view_count(path): + return frappe.db.count("Web Page View", filters={'path': path}) + +def is_tracking_enabled(): + return frappe.db.get_value("Website Settings", "Website Settings", "enable_view_tracking") \ No newline at end of file diff --git a/frappe/website/doctype/web_template/__init__.py b/frappe/website/doctype/web_template/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/website/doctype/web_template/test_web_template.py b/frappe/website/doctype/web_template/test_web_template.py new file mode 100644 index 0000000000..a6d887cc24 --- /dev/null +++ b/frappe/website/doctype/web_template/test_web_template.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest +from bs4 import BeautifulSoup +from frappe.utils import set_request +from frappe.website.render import render + + +class TestWebTemplate(unittest.TestCase): + def test_render_web_template_with_values(self): + doc = frappe.get_doc("Web Template", "Hero with Right Image") + values = { + "title": "Test Hero", + "subtitle": "Test subtitle content", + "primary_action": "/test", + "primary_action_label": "Test Button", + } + html = doc.render(values) + + soup = BeautifulSoup(html, "html.parser") + heading = soup.find("h1") + self.assertTrue("Test Hero" in heading.text) + + subtitle = soup.find("p") + self.assertTrue("Test subtitle content" in subtitle.text) + + button = soup.find("a") + self.assertTrue("Test Button" in button.text) + self.assertTrue("/test" == button.attrs["href"]) + + def test_web_page_with_page_builder(self): + self.create_web_page() + + set_request(method="GET", path="test-web-template") + response = render() + + self.assertEqual(response.status_code, 200) + + html = frappe.safe_decode(response.get_data()) + + soup = BeautifulSoup(html, "html.parser") + sections = soup.find("main").find_all("section") + + self.assertEqual(len(sections), 2) + self.assertEqual(sections[0].find("h2").text, "Test Title") + self.assertEqual(sections[0].find("p").text, "test lorem ipsum") + self.assertEqual(len(sections[1].find_all("a")), 3) + + def test_tailwind_styles_in_developer_mode(self): + self.create_web_page() + theme = self.create_tailwind_theme() + theme.set_as_default() + + frappe.conf.developer_mode = 1 + + set_request(method="GET", path="test-web-template") + response = render() + self.assertEqual(response.status_code, 200) + html = frappe.safe_decode(response.get_data()) + + soup = BeautifulSoup(html, "html.parser") + stylesheet = soup.select_one('link[rel="stylesheet"]') + + self.assertEqual(stylesheet.attrs['href'], '/assets/css/tailwind.css') + + frappe.get_doc('Website Theme', 'Standard').set_as_default() + + def test_tailwind_styles_in_production(self): + self.create_web_page() + theme = self.create_tailwind_theme() + theme.set_as_default() + + frappe.conf.developer_mode = 0 + + set_request(method="GET", path="test-web-template") + response = render() + self.assertEqual(response.status_code, 200) + html = frappe.safe_decode(response.get_data()) + + soup = BeautifulSoup(html, "html.parser") + style = soup.select_one("style[data-tailwind]") + + self.assertTrue(style) + self.assertTrue('py-20' in style.text) + self.assertTrue('text-gray-900' in style.text) + + frappe.get_doc('Website Theme', 'Standard').set_as_default() + + def create_web_page(self): + if not frappe.db.exists("Web Page", "test-web-template"): + frappe.get_doc( + { + "doctype": "Web Page", + "title": "test-web-template", + "name": "test-web-template", + "published": 1, + "route": "test-web-template", + "content_type": "Page Builder", + "page_blocks": [ + { + "web_template": "Section with Image", + "web_template_values": frappe.as_json( + {"title": "Test Title", "subtitle": "test lorem ipsum"} + ), + }, + { + "web_template": "Section with Cards", + "web_template_values": frappe.as_json( + { + "title": "Test Title", + "subtitle": "test lorem ipsum", + "card_size": "Medium", + "card_1_title": "Card 1 Title", + "card_1_content": "Card 1 Content", + "card_1_url": "/card1url", + "card_2_title": "Card 2 Title", + "card_2_content": "Card 2 Content", + "card_2_url": "/card2url", + "card_3_title": "Card 3 Title", + "card_3_content": "Card 3 Content", + "card_3_url": "/card3url", + } + ), + }, + ], + } + ).insert() + + def create_tailwind_theme(self): + if not frappe.db.exists('Website Theme', 'Tailwind'): + theme = frappe.get_doc({ + 'doctype': 'Website Theme', + 'theme': 'Tailwind', + 'based_on': 'Tailwind' + }).insert() + else: + theme = frappe.get_doc('Website Theme', 'Tailwind') + return theme diff --git a/frappe/website/doctype/web_template/web_template.js b/frappe/website/doctype/web_template/web_template.js new file mode 100644 index 0000000000..aacfd485bb --- /dev/null +++ b/frappe/website/doctype/web_template/web_template.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Web Template', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/website/doctype/css_class/css_class.json b/frappe/website/doctype/web_template/web_template.json similarity index 55% rename from frappe/website/doctype/css_class/css_class.json rename to frappe/website/doctype/web_template/web_template.json index 2a7e1e010e..9c0ef37b1c 100644 --- a/frappe/website/doctype/css_class/css_class.json +++ b/frappe/website/doctype/web_template/web_template.json @@ -1,44 +1,44 @@ { "actions": [], - "allow_import": 1, "allow_rename": 1, "autoname": "Prompt", - "creation": "2020-03-17 15:03:31.431344", + "creation": "2020-04-17 12:12:52.145708", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "is_global", - "is_dynamic", - "css" + "standard", + "template", + "fields" ], "fields": [ { - "default": "0", - "fieldname": "is_global", - "fieldtype": "Check", - "label": "Is Global?" - }, - { - "fieldname": "css", + "depends_on": "eval:!doc.standard", + "fieldname": "template", "fieldtype": "Code", "in_list_view": 1, - "label": "CSS", + "label": "Template", + "options": "HTML" + }, + { + "fieldname": "fields", + "fieldtype": "Table", + "label": "Fields", + "options": "Web Template Field", "reqd": 1 }, { "default": "0", - "description": "Website Theme elements are accessible as Jinja variables. Example: \"{{ primary_color }}\"", - "fieldname": "is_dynamic", + "fieldname": "standard", "fieldtype": "Check", - "label": "Is Dynamic?" + "label": "Standard" } ], "links": [], - "modified": "2020-03-17 17:01:14.874631", + "modified": "2020-04-17 14:05:28.499020", "modified_by": "Administrator", "module": "Website", - "name": "CSS Class", + "name": "Web Template", "owner": "Administrator", "permissions": [ { @@ -49,11 +49,12 @@ "print": 1, "read": 1, "report": 1, - "role": "Website Manager", + "role": "System Manager", "share": 1, "write": 1 } ], + "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 diff --git a/frappe/website/doctype/web_template/web_template.py b/frappe/website/doctype/web_template/web_template.py new file mode 100644 index 0000000000..cd5e19bbf2 --- /dev/null +++ b/frappe/website/doctype/web_template/web_template.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import os +from frappe.model.document import Document +from frappe.modules.export_file import export_to_files, create_folder, get_module_path, scrub_dt_dn + + +class WebTemplate(Document): + def validate(self): + for field in self.fields: + if not field.fieldname: + field.fieldname = frappe.scrub(field.label) + + def on_update(self): + if self.standard and frappe.conf.developer_mode: + export_to_files(record_list=[["Web Template", self.name]], create_init=True) + self.create_template_file() + + def create_template_file(self): + if self.standard: + folder = create_folder("Website", self.doctype, self.name, False) + path = os.path.join(folder, frappe.scrub(self.name) + '.html') + if not os.path.exists(path): + open(path, 'w').close() + + def render(self, values): + return get_rendered_template(self.name, values) + + +def get_rendered_template(web_template, values): + standard = frappe.db.get_value("Web Template", web_template, "standard") + if standard: + module_path = get_module_path("Website") + dt, dn = scrub_dt_dn("Web Template", web_template) + scrubbed = frappe.scrub(web_template) + full_path = os.path.join("frappe", module_path, dt, dn, scrubbed + ".html") + root_app_path = os.path.abspath(os.path.join(frappe.get_app_path('frappe'), '..')) + template = os.path.relpath(full_path, root_app_path) + else: + template = frappe.db.get_value("Web Template", web_template, "template") + + context = values or {} + context.update({'values': values}) + return frappe.render_template(template, context) diff --git a/frappe/website/doctype/web_template_field/__init__.py b/frappe/website/doctype/web_template_field/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/website/doctype/web_template_field/test_web_template_field.py b/frappe/website/doctype/web_template_field/test_web_template_field.py new file mode 100644 index 0000000000..40f5d7a1cc --- /dev/null +++ b/frappe/website/doctype/web_template_field/test_web_template_field.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestWebTemplateField(unittest.TestCase): + pass diff --git a/frappe/website/doctype/web_template_field/web_template_field.js b/frappe/website/doctype/web_template_field/web_template_field.js new file mode 100644 index 0000000000..909a05483a --- /dev/null +++ b/frappe/website/doctype/web_template_field/web_template_field.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Web Template Field', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/website/doctype/web_template_field/web_template_field.json b/frappe/website/doctype/web_template_field/web_template_field.json new file mode 100644 index 0000000000..900655e207 --- /dev/null +++ b/frappe/website/doctype/web_template_field/web_template_field.json @@ -0,0 +1,61 @@ +{ + "actions": [], + "creation": "2020-04-17 12:12:31.857277", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "label", + "fieldname", + "fieldtype", + "reqd", + "options" + ], + "fields": [ + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "reqd": 1 + }, + { + "fieldname": "fieldname", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Fieldname" + }, + { + "default": "Data", + "fieldname": "fieldtype", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Fieldtype", + "options": "Attach Image\nCheck\nData\nInt\nSelect\nSmall Text\nText\nMarkdown Editor\nSection Break\nColumn Break", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "reqd", + "fieldtype": "Check", + "label": "Mandatory" + }, + { + "fieldname": "options", + "fieldtype": "Small Text", + "label": "Options" + } + ], + "istable": 1, + "links": [], + "modified": "2020-04-29 14:53:23.192395", + "modified_by": "Administrator", + "module": "Website", + "name": "Web Template 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/frappe/website/doctype/web_template_field/web_template_field.py b/frappe/website/doctype/web_template_field/web_template_field.py new file mode 100644 index 0000000000..8c25b619b6 --- /dev/null +++ b/frappe/website/doctype/web_template_field/web_template_field.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class WebTemplateField(Document): + pass diff --git a/frappe/website/doctype/web_view/templates/web_view.html b/frappe/website/doctype/web_view/templates/web_view.html deleted file mode 100644 index bf993c05fb..0000000000 --- a/frappe/website/doctype/web_view/templates/web_view.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "templates/web.html" %} - -{% block page_content %} - {% include "frappe/website/doctype/web_view/templates/web_view_content.html" %} -{% endblock %} - - \ No newline at end of file diff --git a/frappe/website/doctype/web_view/templates/web_view_content.html b/frappe/website/doctype/web_view/templates/web_view_content.html deleted file mode 100644 index f2e750a472..0000000000 --- a/frappe/website/doctype/web_view/templates/web_view_content.html +++ /dev/null @@ -1,77 +0,0 @@ -{%- if css_rules or css -%} - - -{%- endif -%} - -{%- macro render_element(element) -%} - {%- if element.element_type=='Content' -%} -
    - {{ element.web_content_html }} -
    - {%- elif element.element_type=='Image' -%} - - {%- endif -%} -{%- endmacro -%} - -{%- macro element_class(element) -%} - {{ element.element_class or "" }} -{%- endmacro -%} - -{%- macro element_style(element) -%} - {%- if element.element_style -%} - style = "{{ element.element_style }}" - {%- endif -%} -{%- endmacro -%} - - -{%- for section in sections -%} -
    -
    - {%- if section.section_intro -%} - -
    {{ section.section_intro }}
    - {%- endif -%} - - {%- if section.section_type == 'List' -%} - {%- for element in section.elements -%} - {{ render_element(element) }} - {%- endfor -%} - - {%- elif section.section_type == 'Grid' -%} -
    - {%- for element in section.elements -%} -
    - {{ render_element(element) }} -
    - {%- endfor -%} -
    - - {%- elif section.section_type == 'Tabbed' -%} - -
    - {%- for element in section.elements -%} -
    - {{ render_element(element) }} -
    - {%- endfor -%} -
    - - {%- endif -%} -
    -
    -{%- endfor -%} \ No newline at end of file diff --git a/frappe/website/doctype/web_view/templates/web_view_row.html b/frappe/website/doctype/web_view/templates/web_view_row.html deleted file mode 100644 index 2b999819cb..0000000000 --- a/frappe/website/doctype/web_view/templates/web_view_row.html +++ /dev/null @@ -1,4 +0,0 @@ - - \ No newline at end of file diff --git a/frappe/website/doctype/web_view/test_web_view.py b/frappe/website/doctype/web_view/test_web_view.py deleted file mode 100644 index 67b353844d..0000000000 --- a/frappe/website/doctype/web_view/test_web_view.py +++ /dev/null @@ -1,93 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies and Contributors -# See license.txt -from __future__ import unicode_literals - -import frappe -import unittest - -from frappe.website.doctype.web_page.test_web_page import get_page_content - -test_dependencies = ['Web Page'] # for test - -class TestWebView(unittest.TestCase): - @classmethod - def setUpClass(cls): - frappe.delete_doc_if_exists('Web View', 'test-web-view') - frappe.delete_doc_if_exists('CSS Class', 'test-css-class') - - frappe.get_doc(dict( - doctype = 'CSS Class', - name = 'test-css-class', - css = '.test-class { color: red; }' - )).insert() - - frappe.get_doc(dict( - doctype = 'Web View', - title = 'Test Web View', - route = 'test-web-view', - published = 1, - items = [ - dict( - element_type = 'Section', - section_type = 'List' - ), - dict( - element_type = 'Content', - web_content_type = 'Markdown', - web_content_markdown = '## Heading\n\nBody' - ), - dict( - element_type = 'Content', - web_content_type = 'HTML', - web_content_html = '
    Here is some HTML
    ' - ), - dict( - element_type = 'Section', - section_type = 'Grid' - ), - dict( - element_type = 'Content', - element_class = 'test-css-class', - web_content_type = 'Markdown', - web_content_markdown = 'Column 1' - ), - dict( - element_type = 'Content', - web_content_type = 'Markdown', - web_content_markdown = 'Column 2' - ), - ] - )).insert() - - def test_web_view(self): - html = get_page_content('test-web-view') - #print(html) - self.assert_web_view_in_html(html) - - def assert_web_view_in_html(self, html): - self.assertTrue('

    Heading

    ' in html) - self.assertTrue('
    Here is some HTML
    ' in html) - self.assertTrue('Column 1' in html) - self.assertTrue('Column 2' in html) - self.assertTrue('.test-class { color: red; }' in html) - - def test_web_view_in_footer(self): - website_settings = frappe.get_single("Website Settings") - website_settings.footer_type = 'Web View' - website_settings.footer_web_view = 'test-web-view' - website_settings.save() - - html = get_page_content('test-web-page-1') - - website_settings.footer_type = 'Standard' - website_settings.footer_web_view = '' - website_settings.save() - - # web view should still come as footer - self.assert_web_view_in_html(html) - - html_without_footer = get_page_content('test-web-page-1') - - # no more footer - self.assertFalse('Column 1' in html_without_footer) diff --git a/frappe/website/doctype/web_view/web_view.json b/frappe/website/doctype/web_view/web_view.json deleted file mode 100644 index 6d957fd0d9..0000000000 --- a/frappe/website/doctype/web_view/web_view.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "actions": [], - "allow_guest_to_view": 1, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:title", - "beta": 1, - "creation": "2020-03-16 15:28:03.828741", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "title", - "route", - "published", - "items", - "css" - ], - "fields": [ - { - "fieldname": "items", - "fieldtype": "Table", - "label": "Items", - "options": "Web View Item", - "reqd": 1 - }, - { - "fieldname": "title", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Title", - "reqd": 1, - "unique": 1 - }, - { - "fieldname": "route", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Route", - "reqd": 1 - }, - { - "default": "0", - "fieldname": "published", - "fieldtype": "Check", - "label": "Published" - }, - { - "fieldname": "css", - "fieldtype": "Code", - "label": "CSS" - } - ], - "has_web_view": 1, - "is_published_field": "published", - "links": [], - "modified": "2020-04-15 23:58:12.208049", - "modified_by": "Administrator", - "module": "Website", - "name": "Web 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 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Website Manager", - "share": 1, - "write": 1 - } - ], - "route": "route", - "sort_field": "creation", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/frappe/website/doctype/web_view/web_view.py b/frappe/website/doctype/web_view/web_view.py deleted file mode 100644 index 6828057fe1..0000000000 --- a/frappe/website/doctype/web_view/web_view.py +++ /dev/null @@ -1,74 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -# import frappe -from frappe.website.website_generator import WebsiteGenerator -from frappe.utils import markdown -import frappe - -class WebView(WebsiteGenerator): - def get_context(self, context): - # group items into sections - context.sections = [] - context.css_rules = [] - for item in self.items: - if not context.sections and item.element_type!='Section': - self.add_default_section(context) - - if item.element_type=='Section': - self.add_section(context, item) - else: - self.add_item(context, item) - - self.add_css_class(context, item) - - return context - - def add_section(self, context, item): - item.elements = [] - context.sections.append(item) - - if item.section_intro: - item.section_intro = markdown(item.section_intro) - - def add_item(self, context, item): - if item.hide: - return - - if item.web_content_type == 'Markdown': - item.web_content_html = markdown(item.web_content_markdown) - - if item.title: - item.element_id = frappe.scrub(item.title) - - context.sections[-1].elements.append(item) - - def add_css_class(self, context, item): - # add css class definitions selected by the user - if item.element_class and not item.hide: - css, is_dynamic = frappe.db.get_value('CSS Class', item.element_class, ['css', 'is_dynamic']) - if is_dynamic: - css = frappe.render_template(css, self.get_theme()) - context.css_rules.append(css) - - def render_content(self): - # webview can be rendered as an object (see footer) - return frappe.render_template("frappe/website/doctype/web_view/templates/web_view_content.html", self.get_context(self.as_dict())) - - def get_theme(self): - # get theme properties - if not hasattr(self, '_theme'): - default_theme = frappe.db.get_value("Website Settings", "Website Settings", "website_theme") - self._theme = frappe.get_value('Website Theme', default_theme, '*') - return self._theme - - def add_default_section(self, context): - # add a default section if not added - context.sections.append(frappe._dict( - element_type='Section', - section_type='List', - title='Default Section', - elements=[] - )) diff --git a/frappe/website/doctype/web_view_item/web_view_item.json b/frappe/website/doctype/web_view_item/web_view_item.json deleted file mode 100644 index 0d33cbb8ce..0000000000 --- a/frappe/website/doctype/web_view_item/web_view_item.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "actions": [], - "creation": "2020-03-16 15:25:17.530296", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "element_type", - "title", - "hide", - "column_break_3", - "columns", - "element_class", - "element_style", - "section_break_5", - "section_type", - "web_content_type", - "web_content_html", - "web_content_markdown", - "image_url", - "section_intro" - ], - "fields": [ - { - "fieldname": "element_type", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Element Type", - "options": "Section\nContent\nParagraph\nWeb List\nWeb Form", - "reqd": 1 - }, - { - "depends_on": "eval:doc.element_type==='Section'", - "fieldname": "section_type", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Section Type", - "options": "\nList\nTabbed\nGrid" - }, - { - "depends_on": "eval:doc.element_type==='Content'", - "fieldname": "web_content_type", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Web Content Type", - "options": "\nHTML\nMarkdown" - }, - { - "depends_on": "eval:doc.web_content_type==='HTML'", - "fieldname": "web_content_html", - "fieldtype": "HTML Editor", - "label": "Web Content HTML" - }, - { - "depends_on": "eval:doc.web_content_type==='Markdown'", - "fieldname": "web_content_markdown", - "fieldtype": "Markdown Editor", - "label": "Web Content Markdown" - }, - { - "fieldname": "title", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Title" - }, - { - "fieldname": "columns", - "fieldtype": "Int", - "label": "Columns" - }, - { - "fieldname": "element_class", - "fieldtype": "Link", - "label": "Element Class", - "options": "CSS Class" - }, - { - "fieldname": "section_break_5", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break_3", - "fieldtype": "Column Break" - }, - { - "depends_on": "eval:doc.element_type==='Image'", - "fieldname": "image_url", - "fieldtype": "Small Text", - "label": "Image URL" - }, - { - "depends_on": "eval:doc.element_type==='Section'", - "fieldname": "section_intro", - "fieldtype": "Markdown Editor", - "label": "Section Intro" - }, - { - "default": "0", - "fieldname": "hide", - "fieldtype": "Check", - "label": "Hide" - }, - { - "fieldname": "element_style", - "fieldtype": "Small Text", - "label": "Element Style" - } - ], - "istable": 1, - "links": [], - "modified": "2020-03-28 14:21:50.014823", - "modified_by": "Administrator", - "module": "Website", - "name": "Web View 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/frappe/website/doctype/website_settings/website_settings.js b/frappe/website/doctype/website_settings/website_settings.js index 38e1ff993a..be294258f4 100644 --- a/frappe/website/doctype/website_settings/website_settings.js +++ b/frappe/website/doctype/website_settings/website_settings.js @@ -56,6 +56,10 @@ frappe.ui.form.on('Website Settings', { }); }, + enable_view_tracking: function(frm) { + frappe.boot.website_tracking_enabled = frm.doc.enable_view_tracking; + }, + set_parent_options: function(frm, doctype, name) { var item = frappe.get_doc(doctype, name); if(item.parentfield === "top_bar_items") { diff --git a/frappe/website/doctype/website_settings/website_settings.json b/frappe/website/doctype/website_settings/website_settings.json index f9ed247c0d..b2d765b81f 100644 --- a/frappe/website/doctype/website_settings/website_settings.json +++ b/frappe/website/doctype/website_settings/website_settings.json @@ -19,20 +19,17 @@ "set_banner_from_image", "favicon", "top_bar", - "top_bar_type", - "top_bar_web_view", "navbar_search", "top_bar_items", "banner", "banner_html", "footer", - "footer_type", - "footer_web_view", "copyright", "address", "footer_items", "hide_footer_signup", "integrations", + "enable_view_tracking", "enable_google_indexing", "authorize_api_indexing_access", "indexing_refresh_token", @@ -134,13 +131,11 @@ }, { "default": "0", - "depends_on": "eval:doc.top_bar_type==='Standard'", "fieldname": "navbar_search", "fieldtype": "Check", "label": "Include Search in Top Bar" }, { - "depends_on": "eval:doc.top_bar_type==='Standard'", "fieldname": "top_bar_items", "fieldtype": "Table", "label": "Top Bar Items", @@ -196,7 +191,7 @@ "collapsible": 1, "fieldname": "integrations", "fieldtype": "Section Break", - "label": "Google Integrations" + "label": "Integrations" }, { "description": "Add Google Analytics ID: eg. UA-89XXX57-1. Please search help on Google Analytics for more information.", @@ -331,32 +326,10 @@ "label": "Authorize API Indexing Access" }, { - "default": "Standard", - "fieldname": "footer_type", - "fieldtype": "Select", - "label": "Footer Type", - "options": "Standard\nWeb View" - }, - { - "depends_on": "eval:doc.footer_type==='Web View'", - "fieldname": "footer_web_view", - "fieldtype": "Link", - "label": "Footer Web View", - "options": "Web View" - }, - { - "default": "Standard", - "fieldname": "top_bar_type", - "fieldtype": "Select", - "label": "Top Bar Type", - "options": "Standard\nWeb View" - }, - { - "depends_on": "eval:doc.top_bar_type==='Web View'", - "fieldname": "top_bar_web_view", - "fieldtype": "Link", - "label": "Top Bar Web View", - "options": "Web View" + "default": "0", + "fieldname": "enable_view_tracking", + "fieldtype": "Check", + "label": "Enable In App Website Tracking" } ], "icon": "fa fa-cog", @@ -364,7 +337,7 @@ "issingle": 1, "links": [], "max_attachments": 10, - "modified": "2020-04-21 16:46:59.947403", + "modified": "2020-04-30 12:37:44.070662", "modified_by": "Administrator", "module": "Website", "name": "Website Settings", diff --git a/frappe/website/doctype/website_settings/website_settings.py b/frappe/website/doctype/website_settings/website_settings.py index 49b93fae1d..ff94bd15a3 100644 --- a/frappe/website/doctype/website_settings/website_settings.py +++ b/frappe/website/doctype/website_settings/website_settings.py @@ -118,7 +118,7 @@ def get_website_settings(): for k in ["banner_html", "brand_html", "copyright", "twitter_share_via", "facebook_share", "google_plus_one", "twitter_share", "linked_in_share", "disable_signup", "hide_footer_signup", "head_html", "title_prefix", - "navbar_search"]: + "navbar_search", "enable_view_tracking"]: if hasattr(settings, k): context[k] = settings.get(k) @@ -149,7 +149,6 @@ def get_website_settings(): context[key] = context[key][-1] add_website_theme(context) - add_webviews(context, settings) if not context.get("favicon"): context["favicon"] = "/assets/frappe/images/favicon.png" @@ -159,17 +158,6 @@ def get_website_settings(): return context -def add_webviews(context, settings): - # render footer as webview, not standard view - # see base.html for how this is handled - if settings.footer_type=='Web View' and settings.footer_web_view: - context.footer_content = frappe.get_doc('Web View', - settings.footer_web_view).render_content() - - if settings.top_bar_type=='Web View' and settings.top_bar_web_view: - context.navbar_content = frappe.get_doc('Web View', - settings.top_bar_web_view).render_content() - def get_items(parentfield): all_top_items = frappe.db.sql("""\ select * from `tabTop Bar Item` diff --git a/frappe/website/doctype/website_theme/custom_theme.css b/frappe/website/doctype/website_theme/custom_theme.css new file mode 100644 index 0000000000..dee4720d31 --- /dev/null +++ b/frappe/website/doctype/website_theme/custom_theme.css @@ -0,0 +1,17 @@ +{% if google_font %} +@import url('https://fonts.googleapis.com/css2?family={{ google_font.replace(' ', '+') }}:wght@400;500;600;700;900&display=swap'); +{% endif -%} + +:root { + {%- if google_font %} + --font-family: "{{ google_font }}"; + {% endif -%} +} + +html, body { + {%- if font_size %} + font-size: {{ font_size }}; + {% endif -%} +} + +{{ custom_css or '' }} diff --git a/frappe/website/doctype/website_theme/website_theme.js b/frappe/website/doctype/website_theme/website_theme.js index 75ecbe15e3..28b18a1bcd 100644 --- a/frappe/website/doctype/website_theme/website_theme.js +++ b/frappe/website/doctype/website_theme/website_theme.js @@ -2,6 +2,9 @@ // MIT License. See license.txt frappe.ui.form.on('Website Theme', { + onload: function() { + frappe.require('/assets/frappe/js/frappe/utils/web_page_block.js'); + }, refresh(frm) { frm.clear_custom_buttons(); frm.toggle_display(["module", "custom"], !frappe.boot.developer_mode); diff --git a/frappe/website/doctype/website_theme/website_theme.json b/frappe/website/doctype/website_theme/website_theme.json index f7c6a9a1f2..10d018aa05 100644 --- a/frappe/website/doctype/website_theme/website_theme.json +++ b/frappe/website/doctype/website_theme/website_theme.json @@ -8,14 +8,17 @@ "engine": "InnoDB", "field_order": [ "theme", + "based_on", "module", "custom", - "configuration_section", + "bootstrap_theme_section", "google_font", "font_size", "font_properties", - "use_full_width", - "column_break_7", + "button_rounded_corners", + "button_shadows", + "button_gradients", + "column_break_11", "primary_color", "text_color", "light_color", @@ -24,8 +27,14 @@ "stylesheet_section", "theme_scss", "custom_scss", - "theme_json", "theme_url", + "tailwind_theme_section", + "custom_css", + "theme_css", + "navbar_section", + "navbar", + "footer_section", + "footer", "custom_js_section", "js" ], @@ -78,16 +87,60 @@ "options": "JS" }, { - "fieldname": "theme_json", - "fieldtype": "Code", - "hidden": 1, - "label": "Theme JSON", - "options": "JSON" + "default": "Bootstrap 4", + "fieldname": "based_on", + "fieldtype": "Select", + "label": "Theme Based On", + "options": "Bootstrap 4\nTailwind" }, { - "fieldname": "configuration_section", + "depends_on": "eval:doc.based_on == 'Tailwind'", + "fieldname": "navbar_section", "fieldtype": "Section Break", - "label": "Configuration" + "label": "Navbar" + }, + { + "depends_on": "eval:doc.based_on=='Tailwind'", + "fieldname": "tailwind_theme_section", + "fieldtype": "Section Break", + "label": "Tailwind Theme" + }, + { + "fieldname": "bootstrap_theme_section", + "fieldtype": "Section Break", + "label": "Theme Configuration" + }, + { + "depends_on": "eval:doc.based_on == 'Tailwind'", + "fieldname": "footer_section", + "fieldtype": "Section Break", + "label": "Footer" + }, + { + "fieldname": "navbar", + "fieldtype": "Table", + "label": "Navbar", + "options": "Web Page Block" + }, + { + "fieldname": "footer", + "fieldtype": "Table", + "label": "Footer", + "options": "Web Page Block" + }, + { + "fieldname": "custom_css", + "fieldtype": "Code", + "label": "Custom CSS", + "options": "CSS" + }, + { + "fieldname": "theme_css", + "fieldtype": "Code", + "hidden": 1, + "label": "Theme CSS", + "options": "CSS", + "read_only": 1 }, { "fieldname": "google_font", @@ -99,31 +152,32 @@ "fieldtype": "Data", "label": "Font Size" }, - { - "fieldname": "column_break_7", - "fieldtype": "Column Break" - }, { "fieldname": "primary_color", - "fieldtype": "Color", - "label": "Primary Color" + "fieldtype": "Link", + "label": "Primary Color", + "options": "Color" }, { "fieldname": "text_color", - "fieldtype": "Color", - "label": "Text Color" + "fieldtype": "Link", + "label": "Text Color", + "options": "Color" }, { "fieldname": "dark_color", - "fieldtype": "Color", - "label": "Dark Color" + "fieldtype": "Link", + "label": "Dark Color", + "options": "Color" }, { "fieldname": "background_color", - "fieldtype": "Color", - "label": "Background Color" + "fieldtype": "Link", + "label": "Background Color", + "options": "Color" }, { + "depends_on": "eval:doc.based_on == 'Bootstrap 4'", "fieldname": "stylesheet_section", "fieldtype": "Section Break", "label": "Stylesheet" @@ -135,8 +189,9 @@ }, { "fieldname": "light_color", - "fieldtype": "Color", - "label": "Light Color" + "fieldtype": "Link", + "label": "Light Color", + "options": "Color" }, { "default": "300,600", @@ -145,14 +200,30 @@ "label": "Font Properties" }, { - "description": "Content will not be inside a \"container\" class, you will have to add your own containers for different sections.", - "fieldname": "use_full_width", - "fieldtype": "Data", - "label": "Use Full Width" + "default": "1", + "fieldname": "button_rounded_corners", + "fieldtype": "Check", + "label": "Button Rounded Corners" + }, + { + "default": "0", + "fieldname": "button_shadows", + "fieldtype": "Check", + "label": "Button Shadows" + }, + { + "default": "0", + "fieldname": "button_gradients", + "fieldtype": "Check", + "label": "Button Gradients" + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" } ], "links": [], - "modified": "2020-03-19 09:46:48.750150", + "modified": "2020-04-25 00:00:23.347879", "modified_by": "Administrator", "module": "Website", "name": "Website Theme", diff --git a/frappe/website/doctype/website_theme/website_theme.py b/frappe/website/doctype/website_theme/website_theme.py index d0ce582482..ac7637d6c7 100644 --- a/frappe/website/doctype/website_theme/website_theme.py +++ b/frappe/website/doctype/website_theme/website_theme.py @@ -25,7 +25,7 @@ class WebsiteTheme(Document): def is_standard_and_not_valid_user(self): return (not self.custom and not frappe.local.conf.get('developer_mode') - and not (frappe.flags.in_import or frappe.flags.in_test)) + and not (frappe.flags.in_import or frappe.flags.in_test or frappe.flags.in_migrate)) def on_trash(self): if self.is_standard_and_not_valid_user(): @@ -41,9 +41,13 @@ class WebsiteTheme(Document): def validate_theme(self): '''Generate theme css if theme_scss has changed''' - doc_before_save = self.get_doc_before_save() - if doc_before_save is None or get_scss(self) != get_scss(doc_before_save): - self.generate_bootstrap_theme() + if self.based_on == 'Bootstrap 4': + doc_before_save = self.get_doc_before_save() + if doc_before_save is None or get_scss(self) != get_scss(doc_before_save): + self.generate_bootstrap_theme() + + if self.based_on == 'Tailwind': + self.theme_css = frappe.render_template('frappe/website/doctype/website_theme/custom_theme.css', self.as_dict(), is_path=True) def export_doc(self): """Export to standard folder `[module]/website_theme/[name]/[name].json`.""" @@ -61,10 +65,13 @@ class WebsiteTheme(Document): from subprocess import Popen, PIPE folder_path = join_path(frappe.utils.get_bench_path(), 'sites', 'assets', 'css') - self.delete_old_theme_files(folder_path) + + if not self.custom: + self.delete_old_theme_files(folder_path) # add a random suffix - file_name = frappe.scrub(self.name) + '_' + frappe.generate_hash('Website Theme', 8) + '.css' + suffix = frappe.generate_hash('Website Theme', 8) if self.custom else 'style' + file_name = frappe.scrub(self.name) + '_' + suffix + '.css' output_path = join_path(folder_path, file_name) content = get_scss(self) @@ -110,7 +117,7 @@ def add_website_theme(context): if not context.disable_website_theme: website_theme = get_active_theme() - context.theme = website_theme and website_theme.as_dict() or frappe._dict() + context.theme = website_theme or frappe._dict() def get_active_theme(): website_theme = frappe.db.get_value("Website Settings", "Website Settings", "website_theme") diff --git a/frappe/website/doctype/website_theme/website_theme_template.scss b/frappe/website/doctype/website_theme/website_theme_template.scss index e1728eee36..1bb4685b98 100644 --- a/frappe/website/doctype/website_theme/website_theme_template.scss +++ b/frappe/website/doctype/website_theme/website_theme_template.scss @@ -1,21 +1,23 @@ {% if google_font %} @import url('https://fonts.googleapis.com/css?family={{ google_font.replace(' ', '+') }}:{{ font_properties }}&display=swap'); + $font-family-sans-serif: "{{ google_font }}", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -{% endif %} -{% if primary_color %}$primary: {{ primary_color }};{% endif %} -{% if dark_color %}$dark: {{ dark_color }};{% endif %} -{% if text_color %}$body-color: {{ text_color }};{% endif %} -{% if background_color %}$body-bg: {{ background_color }};{% endif %} +{% endif -%} -$enable-shadows: {{ enable_shadows and "true" or "false" }}; -$enable-gradients: {{ enable_gradients and "true" or "false" }}; -$enable-rounded: {{ enable_rounded and "true" or "false" }}; +{% if primary_color %}$primary: {{ frappe.db.get_value('Color', primary_color, 'color') }};{% endif -%} +{% if dark_color %}$dark: {{ frappe.db.get_value('Color', dark_color, 'color') }};{% endif -%} +{% if text_color %}$body-color: {{ frappe.db.get_value('Color', text_color, 'color') }};{% endif -%} +{% if background_color %}$body-bg: {{ frappe.db.get_value('Color', background_color, 'color') }};{% endif -%} + +$enable-shadows: {{ button_shadows and "true" or "false" }}; +$enable-gradients: {{ button_gradients and "true" or "false" }}; +$enable-rounded: {{ button_rounded_corners and "true" or "false" }}; @import "frappe/public/scss/website"; +{% if font_size -%} body { - {% if font_size %} font-size: {{ font_size }}; - {% endif %} } +{%- endif %} diff --git a/frappe/website/render.py b/frappe/website/render.py index 7b8db59e4e..601a4e7eb2 100644 --- a/frappe/website/render.py +++ b/frappe/website/render.py @@ -202,6 +202,9 @@ def build_page(path): if '{next}' in html: html = html.replace('{next}', get_next_link(context.route)) + if '' in html and not frappe.conf.developer_mode: + html = add_processed_tailwind_css(html) + # html = frappe.get_template(context.base_template_path).render(context) if can_cache(context.no_cache): @@ -350,3 +353,20 @@ def raise_if_disabled(path): _path = r.route.lstrip('/') if path == _path and not r.enabled: raise frappe.PermissionError + +def add_processed_tailwind_css(html): + from subprocess import Popen, PIPE + + replace_string = '' + command = ['node', 'purgecss.js', 'css/tailwind.css', html] + process = Popen(command, cwd=frappe.get_app_path('frappe', '..'), stdout=PIPE, stderr=PIPE) + + stdout, stderr = process.communicate() + if stderr: + stderr = frappe.safe_decode(stderr) + print(stderr) + else: + css = frappe.safe_decode(stdout) + html = html.replace(replace_string, ''.format(css)) + + return html diff --git a/frappe/website/web_template/__init__.py b/frappe/website/web_template/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/website/web_template/footer_horizontal/__init__.py b/frappe/website/web_template/footer_horizontal/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/website/web_template/footer_horizontal/footer_horizontal.html b/frappe/website/web_template/footer_horizontal/footer_horizontal.html new file mode 100644 index 0000000000..10af7a7a34 --- /dev/null +++ b/frappe/website/web_template/footer_horizontal/footer_horizontal.html @@ -0,0 +1,34 @@ +
    + {%- if brand_image -%} +
    + {{ brand_label or 'Brand Logo' }} +
    + {%- endif -%} +
    +
    + {%- for index in ['1', '2', '3', '4', '5', '6', '7', '8'] -%} + + {%- set label = values['item_' + index + '_label'] -%} + {%- set url = values['item_' + index + '_url'] -%} + + {%- if label and url -%} + {{ c('navbar_link', label=label, url=url) }} + {%- endif -%} + + {%- endfor -%} +
    + {%- if content -%} +
    + {{ content }} +
    + {%- endif -%} +
    +
    +
    + {{ address or '' }} +
    +
    + {{ copyright or '' }} +
    +
    +
    diff --git a/frappe/website/web_template/footer_horizontal/footer_horizontal.json b/frappe/website/web_template/footer_horizontal/footer_horizontal.json new file mode 100644 index 0000000000..5e17f80eb1 --- /dev/null +++ b/frappe/website/web_template/footer_horizontal/footer_horizontal.json @@ -0,0 +1,103 @@ +{ + "creation": "2020-04-19 15:01:23.344637", + "docstatus": 0, + "doctype": "Web Template", + "fields": [ + { + "fieldname": "brand_image", + "fieldtype": "Attach Image", + "label": "Brand Image", + "reqd": 0 + }, + { + "fieldname": "brand_label", + "fieldtype": "Data", + "label": "Brand Label", + "reqd": 0 + }, + { + "fieldname": "address", + "fieldtype": "Small Text", + "label": "Address", + "reqd": 0 + }, + { + "fieldname": "copyright", + "fieldtype": "Small Text", + "label": "Copyright", + "reqd": 0 + }, + { + "fieldname": "item_1_label", + "fieldtype": "Data", + "label": "Item 1 Label", + "reqd": 0 + }, + { + "fieldname": "item_1_url", + "fieldtype": "Data", + "label": "Item 1 URL", + "reqd": 0 + }, + { + "fieldname": "item_2_label", + "fieldtype": "Data", + "label": "Item 2 Label", + "reqd": 0 + }, + { + "fieldname": "item_2_url", + "fieldtype": "Data", + "label": "Item 2 URL", + "reqd": 0 + }, + { + "fieldname": "item_3_label", + "fieldtype": "Data", + "label": "Item 3 Label", + "reqd": 0 + }, + { + "fieldname": "item_3_url", + "fieldtype": "Data", + "label": "Item 3 URL", + "reqd": 0 + }, + { + "fieldname": "item_4_label", + "fieldtype": "Data", + "label": "Item 4 Label", + "reqd": 0 + }, + { + "fieldname": "item_4_url", + "fieldtype": "Data", + "label": "Item 4 URL", + "reqd": 0 + }, + { + "fieldname": "item_5_label", + "fieldtype": "Data", + "label": "Item 5 Label", + "reqd": 0 + }, + { + "fieldname": "item_5_url", + "fieldtype": "Data", + "label": "Item 5 URL", + "reqd": 0 + }, + { + "fieldname": "content", + "fieldtype": "Markdown Editor", + "label": "Content", + "reqd": 0 + } + ], + "idx": 0, + "modified": "2020-04-26 15:14:15.892042", + "modified_by": "Administrator", + "name": "Footer Horizontal", + "owner": "Administrator", + "standard": 1 +} \ No newline at end of file diff --git a/frappe/website/web_template/full_width_image/__init__.py b/frappe/website/web_template/full_width_image/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/website/web_template/full_width_image/full_width_image.html b/frappe/website/web_template/full_width_image/full_width_image.html new file mode 100644 index 0000000000..19bd17801a --- /dev/null +++ b/frappe/website/web_template/full_width_image/full_width_image.html @@ -0,0 +1 @@ +{{ c('image_with_blur', src=url, alt=description, class="w-full") }} diff --git a/frappe/website/web_template/full_width_image/full_width_image.json b/frappe/website/web_template/full_width_image/full_width_image.json new file mode 100644 index 0000000000..08e04e2d3f --- /dev/null +++ b/frappe/website/web_template/full_width_image/full_width_image.json @@ -0,0 +1,26 @@ +{ + "creation": "2020-04-17 16:03:35.676241", + "docstatus": 0, + "doctype": "Web Template", + "fields": [ + { + "fieldname": "url", + "fieldtype": "Attach Image", + "label": "Image", + "reqd": 0 + }, + { + "fieldname": "description", + "fieldtype": "Data", + "label": "Description", + "reqd": 0 + } + ], + "idx": 0, + "modified": "2020-04-26 15:09:18.173851", + "modified_by": "Administrator", + "name": "Full Width Image", + "owner": "Administrator", + "standard": 1, + "template": "" +} \ No newline at end of file diff --git a/frappe/website/web_template/hero/__init__.py b/frappe/website/web_template/hero/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/website/web_template/hero/hero.html b/frappe/website/web_template/hero/hero.html new file mode 100644 index 0000000000..e7b69ab782 --- /dev/null +++ b/frappe/website/web_template/hero/hero.html @@ -0,0 +1,21 @@ +
    +

    + {{ title }} +

    + {%- if subtitle -%} +

    + {{ subtitle }} +

    + {%- endif -%} + {%- if primary_action or secondary_action -%} +
    + {%- if primary_action -%} + {{ c('button', label=primary_action_label, url=primary_action, variant="primary", size="large") }} + {%- endif -%} + {%- if secondary_action -%} + {{ c('button', label=secondary_action_label, url=secondary_action, variant="secondary", size="large", class="ml-4") }} + {%- endif -%} +
    + {%- endif -%} +
    diff --git a/frappe/website/web_template/hero/hero.json b/frappe/website/web_template/hero/hero.json new file mode 100644 index 0000000000..37e06b802d --- /dev/null +++ b/frappe/website/web_template/hero/hero.json @@ -0,0 +1,57 @@ +{ + "creation": "2020-04-19 15:26:23.140620", + "docstatus": 0, + "doctype": "Web Template", + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "reqd": 0 + }, + { + "fieldname": "subtitle", + "fieldtype": "Text", + "label": "Subtitle", + "reqd": 0 + }, + { + "fieldname": "primary_action_label", + "fieldtype": "Data", + "label": "Primary Action Label", + "reqd": 0 + }, + { + "fieldname": "primary_action", + "fieldtype": "Data", + "label": "Primary Action", + "reqd": 0 + }, + { + "fieldname": "secondary_action_label", + "fieldtype": "Data", + "label": "Secondary Action Label", + "reqd": 0 + }, + { + "fieldname": "secondary_action", + "fieldtype": "Data", + "label": "Secondary Action", + "reqd": 0 + }, + { + "fieldname": "align", + "fieldtype": "Select", + "label": "Align", + "options": "Left\nCenter", + "reqd": 0 + } + ], + "idx": 0, + "modified": "2020-04-20 12:27:45.760391", + "modified_by": "Administrator", + "name": "Hero", + "owner": "Administrator", + "standard": 1, + "template": "" +} \ No newline at end of file diff --git a/frappe/website/web_template/hero_with_right_image/__init__.py b/frappe/website/web_template/hero_with_right_image/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/website/web_template/hero_with_right_image/hero_with_right_image.html b/frappe/website/web_template/hero_with_right_image/hero_with_right_image.html new file mode 100644 index 0000000000..015bf8b222 --- /dev/null +++ b/frappe/website/web_template/hero_with_right_image/hero_with_right_image.html @@ -0,0 +1,37 @@ +
    +
    +
    +

    + {{ title }} +

    + {%- if subtitle -%} +

    + {{ subtitle }} +

    + {%- endif -%} +
    + {%- if primary_action -%} + {{ c('button', label=primary_action_label, url=primary_action, variant="primary", size="large") }} + {%- endif -%} + {%- if secondary_action -%} + {{ c('button', label=secondary_action_label, url=secondary_action, variant="secondary", size="large", class="ml-4") }} + {%- endif -%} +
    +
    +
    + {%- if image -%} + {{ c('image_with_blur', + class=["hidden md:block max-h-144 object-contain", "w-full md:w-6/12" if contain_image else "md:max-w-md lg:max-w-lg xl:max-w-xl xxl:max-w-2xl"], + src=image, + alt="") + }} + {%- endif -%} +
    + +{%- if not contain_image -%} + +{%- endif -%} diff --git a/frappe/website/web_template/hero_with_right_image/hero_with_right_image.json b/frappe/website/web_template/hero_with_right_image/hero_with_right_image.json new file mode 100644 index 0000000000..3cb4701e7c --- /dev/null +++ b/frappe/website/web_template/hero_with_right_image/hero_with_right_image.json @@ -0,0 +1,62 @@ +{ + "creation": "2020-04-17 12:18:04.376273", + "docstatus": 0, + "doctype": "Web Template", + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "reqd": 0 + }, + { + "fieldname": "subtitle", + "fieldtype": "Text", + "label": "Subtitle", + "reqd": 0 + }, + { + "fieldname": "image", + "fieldtype": "Attach Image", + "label": "Image", + "reqd": 0 + }, + { + "fieldname": "contain_image", + "fieldtype": "Check", + "label": "Restrict Image inside Container", + "reqd": 0 + }, + { + "fieldname": "primary_action_label", + "fieldtype": "Data", + "label": "Primary Action Label", + "reqd": 0 + }, + { + "fieldname": "primary_action", + "fieldtype": "Data", + "label": "Primary Action", + "reqd": 0 + }, + { + "fieldname": "secondary_action_label", + "fieldtype": "Data", + "label": "Secondary Action Label", + "reqd": 0 + }, + { + "fieldname": "secondary_action", + "fieldtype": "Data", + "label": "Secondary Action", + "reqd": 0 + } + ], + "idx": 0, + "modified": "2020-04-29 14:12:31.613545", + "modified_by": "Administrator", + "name": "Hero with Right Image", + "owner": "Administrator", + "standard": 1, + "template": "" +} \ No newline at end of file diff --git a/frappe/website/web_template/markdown/__init__.py b/frappe/website/web_template/markdown/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/website/web_template/markdown/markdown.html b/frappe/website/web_template/markdown/markdown.html new file mode 100644 index 0000000000..b086dd327c --- /dev/null +++ b/frappe/website/web_template/markdown/markdown.html @@ -0,0 +1,3 @@ +
    + {{ frappe.utils.md_to_html(content) }} +
    diff --git a/frappe/website/web_template/markdown/markdown.json b/frappe/website/web_template/markdown/markdown.json new file mode 100644 index 0000000000..34db1d1aab --- /dev/null +++ b/frappe/website/web_template/markdown/markdown.json @@ -0,0 +1,19 @@ +{ + "creation": "2020-04-19 15:56:20.833205", + "docstatus": 0, + "doctype": "Web Template", + "fields": [ + { + "fieldname": "content", + "fieldtype": "Markdown Editor", + "label": "Content", + "reqd": 0 + } + ], + "idx": 0, + "modified": "2020-04-19 15:56:20.833205", + "modified_by": "Administrator", + "name": "Markdown", + "owner": "Administrator", + "standard": 1 +} \ No newline at end of file diff --git a/frappe/website/web_template/navbar_with_links_on_right/__init__.py b/frappe/website/web_template/navbar_with_links_on_right/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/website/web_template/navbar_with_links_on_right/navbar_with_links_on_right.html b/frappe/website/web_template/navbar_with_links_on_right/navbar_with_links_on_right.html new file mode 100644 index 0000000000..01652f8c80 --- /dev/null +++ b/frappe/website/web_template/navbar_with_links_on_right/navbar_with_links_on_right.html @@ -0,0 +1,56 @@ +
    +
    +
    + + Brand Logo + +
    +
    + + + + +
    + +
    +
    diff --git a/frappe/website/web_template/navbar_with_links_on_right/navbar_with_links_on_right.json b/frappe/website/web_template/navbar_with_links_on_right/navbar_with_links_on_right.json new file mode 100644 index 0000000000..8b3200d19e --- /dev/null +++ b/frappe/website/web_template/navbar_with_links_on_right/navbar_with_links_on_right.json @@ -0,0 +1,97 @@ +{ + "creation": "2020-04-19 13:49:33.971237", + "docstatus": 0, + "doctype": "Web Template", + "fields": [ + { + "fieldname": "brand_image", + "fieldtype": "Attach Image", + "label": "Brand Image", + "reqd": 0 + }, + { + "fieldname": "brand_label", + "fieldtype": "Data", + "label": "Brand Label", + "reqd": 0 + }, + { + "fieldname": "cta_label", + "fieldtype": "Data", + "label": "CTA Label", + "reqd": 0 + }, + { + "fieldname": "cta_url", + "fieldtype": "Data", + "label": "CTA URL", + "reqd": 0 + }, + { + "fieldname": "item_1_label", + "fieldtype": "Data", + "label": "Item 1 Label", + "reqd": 0 + }, + { + "fieldname": "item_1_url", + "fieldtype": "Data", + "label": "Item 1 URL", + "reqd": 0 + }, + { + "fieldname": "item_2_label", + "fieldtype": "Data", + "label": "Item 2 Label", + "reqd": 0 + }, + { + "fieldname": "item_2_url", + "fieldtype": "Data", + "label": "Item 2 URL", + "reqd": 0 + }, + { + "fieldname": "item_3_label", + "fieldtype": "Data", + "label": "Item 3 Label", + "reqd": 0 + }, + { + "fieldname": "item_3_url", + "fieldtype": "Data", + "label": "Item 3 URL", + "reqd": 0 + }, + { + "fieldname": "item_4_label", + "fieldtype": "Data", + "label": "Item 4 Label", + "reqd": 0 + }, + { + "fieldname": "item_4_url", + "fieldtype": "Data", + "label": "Item 4 URL", + "reqd": 0 + }, + { + "fieldname": "item_5_label", + "fieldtype": "Data", + "label": "Item 5 Label", + "reqd": 0 + }, + { + "fieldname": "item_5_url", + "fieldtype": "Data", + "label": "Item 5 URL", + "reqd": 0 + } + ], + "idx": 0, + "modified": "2020-04-19 13:51:26.391098", + "modified_by": "Administrator", + "name": "Navbar with Links on Right", + "owner": "Administrator", + "standard": 1 +} \ No newline at end of file diff --git a/frappe/website/web_template/section_with_cards/__init__.py b/frappe/website/web_template/section_with_cards/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/website/web_template/section_with_cards/section_with_cards.html b/frappe/website/web_template/section_with_cards/section_with_cards.html new file mode 100644 index 0000000000..d408275b44 --- /dev/null +++ b/frappe/website/web_template/section_with_cards/section_with_cards.html @@ -0,0 +1,47 @@ +{%- macro card(title, content, url) -%} +{%- set card_classes = resolve_class({ + 'p-6': card_size == 'Small', + 'p-7': card_size == 'Medium', + 'p-8': card_size == 'Large' +}) -%} +{%- set title_classes = resolve_class({ + 'text-base font-semibold': card_size == 'Small', + 'text-lg md:text-xl font-semibold': card_size == 'Medium', + 'text-xl md:text-2xl font-bold': card_size == 'Large' +}) -%} +{%- set content_classes = resolve_class({ + 'text-sm': card_size == 'Small', + 'text-base': card_size == 'Medium', + 'text-base xxl:text-lg': card_size == 'Large' +}) -%} + + +

    {{ title }}

    +

    {{ content or '' }}

    +
    +{%- endmacro -%} + +{%- if title -%} +

    {{ title }}

    +{%- endif -%} +{%- if subtitle -%} +

    {{ subtitle }}

    +{%- endif -%} +{%- set card_size = card_size or 'Small' -%} +{%- set classes = resolve_class({ + 'mt-8': title, + 'sm:grid-cols-2 lg:grid-cols-4': card_size == 'Small', + 'sm:grid-cols-2 lg:grid-cols-3': card_size == 'Medium', + 'sm:grid-cols-2': card_size == 'Large', +}) -%} +
    + {%- for index in ['1', '2', '3', '4', '5', '6', '7', '8'] -%} + {%- set title = values['card_' + index + '_title'] -%} + {%- set content = values['card_' + index + '_content'] -%} + {%- set url = values['card_' + index + '_url'] -%} + {%- if title -%} + {{ card(title, content, url) }} + {%- endif -%} + {%- endfor -%} +
    diff --git a/frappe/website/web_template/section_with_cards/section_with_cards.json b/frappe/website/web_template/section_with_cards/section_with_cards.json new file mode 100644 index 0000000000..9ec430ae60 --- /dev/null +++ b/frappe/website/web_template/section_with_cards/section_with_cards.json @@ -0,0 +1,224 @@ +{ + "creation": "2020-04-21 20:55:01.740525", + "docstatus": 0, + "doctype": "Web Template", + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "reqd": 0 + }, + { + "fieldname": "subtitle", + "fieldtype": "Data", + "label": "Subtitle", + "reqd": 0 + }, + { + "fieldname": "card_size", + "fieldtype": "Select", + "label": "Card Size", + "options": "Small\nMedium\nLarge", + "reqd": 0 + }, + { + "fieldname": "card_1", + "fieldtype": "Section Break", + "label": "Card 1", + "reqd": 0 + }, + { + "fieldname": "card_1_title", + "fieldtype": "Data", + "label": "Title", + "reqd": 0 + }, + { + "fieldname": "card_1_content", + "fieldtype": "Small Text", + "label": "Content", + "reqd": 0 + }, + { + "fieldname": "card_1_url", + "fieldtype": "Data", + "label": "URL", + "reqd": 0 + }, + { + "fieldname": "card_2", + "fieldtype": "Section Break", + "label": "Card 2", + "reqd": 0 + }, + { + "fieldname": "card_2_title", + "fieldtype": "Data", + "label": "Title", + "reqd": 0 + }, + { + "fieldname": "card_2_content", + "fieldtype": "Small Text", + "label": "Content", + "reqd": 0 + }, + { + "fieldname": "card_2_url", + "fieldtype": "Data", + "label": "URL", + "reqd": 0 + }, + { + "fieldname": "card_3", + "fieldtype": "Section Break", + "label": "Card 3", + "reqd": 0 + }, + { + "fieldname": "card_3_title", + "fieldtype": "Data", + "label": "Title", + "reqd": 0 + }, + { + "fieldname": "card_3_content", + "fieldtype": "Small Text", + "label": "Content", + "reqd": 0 + }, + { + "fieldname": "card_3_url", + "fieldtype": "Data", + "label": "URL", + "reqd": 0 + }, + { + "fieldname": "card_4", + "fieldtype": "Section Break", + "label": "Card 4", + "reqd": 0 + }, + { + "fieldname": "card_4_title", + "fieldtype": "Data", + "label": "Title", + "reqd": 0 + }, + { + "fieldname": "card_4_content", + "fieldtype": "Small Text", + "label": "Content", + "reqd": 0 + }, + { + "fieldname": "card_4_url", + "fieldtype": "Data", + "label": "URL", + "reqd": 0 + }, + { + "fieldname": "card_5", + "fieldtype": "Section Break", + "label": "Card 5", + "reqd": 0 + }, + { + "fieldname": "card_5_title", + "fieldtype": "Data", + "label": "Title", + "reqd": 0 + }, + { + "fieldname": "card_5_content", + "fieldtype": "Small Text", + "label": "Content", + "reqd": 0 + }, + { + "fieldname": "card_5_url", + "fieldtype": "Data", + "label": "URL", + "reqd": 0 + }, + { + "fieldname": "card_6", + "fieldtype": "Section Break", + "label": "Card 6", + "reqd": 0 + }, + { + "fieldname": "card_6_title", + "fieldtype": "Data", + "label": "Title", + "reqd": 0 + }, + { + "fieldname": "card_6_content", + "fieldtype": "Small Text", + "label": "Content", + "reqd": 0 + }, + { + "fieldname": "card_6_url", + "fieldtype": "Data", + "label": "URL", + "reqd": 0 + }, + { + "fieldname": "card_7", + "fieldtype": "Section Break", + "label": "Card 7", + "reqd": 0 + }, + { + "fieldname": "card_7_title", + "fieldtype": "Data", + "label": "Title", + "reqd": 0 + }, + { + "fieldname": "card_7_content", + "fieldtype": "Small Text", + "label": "Content", + "reqd": 0 + }, + { + "fieldname": "card_7_url", + "fieldtype": "Data", + "label": "URL", + "reqd": 0 + }, + { + "fieldname": "card_8", + "fieldtype": "Section Break", + "label": "Card 8", + "reqd": 0 + }, + { + "fieldname": "card_8_title", + "fieldtype": "Data", + "label": "Title", + "reqd": 0 + }, + { + "fieldname": "card_8_content", + "fieldtype": "Small Text", + "label": "Content", + "reqd": 0 + }, + { + "fieldname": "card_8_url", + "fieldtype": "Data", + "label": "URL", + "reqd": 0 + } + ], + "idx": 0, + "modified": "2020-04-29 22:40:03.362229", + "modified_by": "Administrator", + "name": "Section with Cards", + "owner": "Administrator", + "standard": 1 +} \ No newline at end of file diff --git a/frappe/website/web_template/section_with_cta/__init__.py b/frappe/website/web_template/section_with_cta/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/website/web_template/section_with_cta/section_with_cta.html b/frappe/website/web_template/section_with_cta/section_with_cta.html new file mode 100644 index 0000000000..415392f751 --- /dev/null +++ b/frappe/website/web_template/section_with_cta/section_with_cta.html @@ -0,0 +1,21 @@ +
    +
    +

    {{ title }}

    +

    {{ subtitle }}

    +

    + {{ c('button', label=cta_label, url=cta_url, variant="primary", size="large") }} +

    + {%- if cta_description -%} +
    + {{ cta_description }} +
    + {%- endif -%} +
    + {%- if show_confetti -%} +
    +
    +
    +
    +
    + {%- endif -%} +
    diff --git a/frappe/website/web_template/section_with_cta/section_with_cta.json b/frappe/website/web_template/section_with_cta/section_with_cta.json new file mode 100644 index 0000000000..b1de1c991f --- /dev/null +++ b/frappe/website/web_template/section_with_cta/section_with_cta.json @@ -0,0 +1,49 @@ +{ + "creation": "2020-04-17 19:57:40.534914", + "docstatus": 0, + "doctype": "Web Template", + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "reqd": 0 + }, + { + "fieldname": "subtitle", + "fieldtype": "Small Text", + "label": "Subtitle", + "reqd": 0 + }, + { + "fieldname": "cta_label", + "fieldtype": "Data", + "label": "CTA Label", + "reqd": 0 + }, + { + "fieldname": "cta_url", + "fieldtype": "Data", + "label": "CTA URL", + "reqd": 0 + }, + { + "fieldname": "cta_description", + "fieldtype": "Small Text", + "label": "CTA Description", + "reqd": 0 + }, + { + "fieldname": "show_confetti", + "fieldtype": "Check", + "label": "Show Confetti", + "reqd": 0 + } + ], + "idx": 0, + "modified": "2020-04-24 22:01:17.098158", + "modified_by": "Administrator", + "name": "Section with CTA", + "owner": "Administrator", + "standard": 1 +} \ No newline at end of file diff --git a/frappe/website/web_template/section_with_image/__init__.py b/frappe/website/web_template/section_with_image/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/website/web_template/section_with_image/section_with_image.html b/frappe/website/web_template/section_with_image/section_with_image.html new file mode 100644 index 0000000000..9a7753cc4a --- /dev/null +++ b/frappe/website/web_template/section_with_image/section_with_image.html @@ -0,0 +1,5 @@ +

    {{ title }}

    +

    {{ subtitle }}

    + +{{ c('image_with_blur', src=image, alt=image_description, class="w-full mt-8 rounded-xl") }} + diff --git a/frappe/website/web_template/section_with_image/section_with_image.json b/frappe/website/web_template/section_with_image/section_with_image.json new file mode 100644 index 0000000000..5f610e5e2f --- /dev/null +++ b/frappe/website/web_template/section_with_image/section_with_image.json @@ -0,0 +1,33 @@ +{ + "creation": "2020-04-17 19:29:21.186193", + "docstatus": 0, + "doctype": "Web Template", + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title" + }, + { + "fieldname": "subtitle", + "fieldtype": "Small Text", + "label": "Subtitle" + }, + { + "fieldname": "image", + "fieldtype": "Attach Image", + "label": "Image" + }, + { + "fieldname": "image_description", + "fieldtype": "Data", + "label": "Image Description" + } + ], + "idx": 0, + "modified": "2020-04-17 19:31:33.474017", + "modified_by": "Administrator", + "name": "Section with Image", + "owner": "Administrator", + "standard": 1 +} \ No newline at end of file diff --git a/frappe/website/web_template/section_with_left_image/__init__.py b/frappe/website/web_template/section_with_left_image/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/website/web_template/section_with_left_image/section_with_left_image.html b/frappe/website/web_template/section_with_left_image/section_with_left_image.html new file mode 100644 index 0000000000..d2d1568708 --- /dev/null +++ b/frappe/website/web_template/section_with_left_image/section_with_left_image.html @@ -0,0 +1,9 @@ +
    +
    + {{ title }} +
    +
    +

    {{ title }}

    +

    {{ content }}

    +
    +
    diff --git a/frappe/website/web_template/section_with_left_image/section_with_left_image.json b/frappe/website/web_template/section_with_left_image/section_with_left_image.json new file mode 100644 index 0000000000..bae672d312 --- /dev/null +++ b/frappe/website/web_template/section_with_left_image/section_with_left_image.json @@ -0,0 +1,29 @@ +{ + "creation": "2020-04-18 12:58:28.670489", + "docstatus": 0, + "doctype": "Web Template", + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title" + }, + { + "fieldname": "content", + "fieldtype": "Small Text", + "label": "Content" + }, + { + "fieldname": "image", + "fieldtype": "Attach Image", + "label": "Image" + } + ], + "idx": 0, + "modified": "2020-04-18 12:58:28.670489", + "modified_by": "Administrator", + "name": "Section with Left Image", + "owner": "Administrator", + "standard": 1, + "template": "" +} \ No newline at end of file diff --git a/frappe/website/web_template/section_with_tabs/__init__.py b/frappe/website/web_template/section_with_tabs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/website/web_template/section_with_tabs/section_with_tabs.html b/frappe/website/web_template/section_with_tabs/section_with_tabs.html new file mode 100644 index 0000000000..67b4ac9426 --- /dev/null +++ b/frappe/website/web_template/section_with_tabs/section_with_tabs.html @@ -0,0 +1,111 @@ +

    {{ title }}

    +

    {{ subtitle }}

    + +
    +
    + {% set ns = namespace(tabs=[]) %} + + {%- for index in ['1', '2', '3', '4', '5', '6'] -%} + + {%- set buttonid = 'id-' + frappe.utils.generate_hash('TabButton', 12) -%} + {%- set panelid = 'id-' + frappe.utils.generate_hash('TabPanel', 12) -%} + + {%- set tab = { + 'title': values['tab_' + index + '_title'], + 'content': values['tab_' + index + '_content'], + 'buttonid': buttonid, + 'panelid': panelid, } + -%} + + {%- if tab.title and tab.content -%} + {%- set ns.tabs = ns.tabs + [tab] -%} + {%- endif -%} + + {%- endfor -%} +
    + {%- for tab in ns.tabs -%} + {%- set first_tab = true if loop.index0 == 0 else false -%} + + {%- endfor -%} +
    + {%- for tab in ns.tabs -%} + {%- set first_tab = true if loop.index0 == 0 else false -%} +
    +
    + {{ frappe.utils.md_to_html(tab.content) }} +
    +
    + {%- endfor -%} +
    +
    + + diff --git a/frappe/website/web_template/section_with_tabs/section_with_tabs.json b/frappe/website/web_template/section_with_tabs/section_with_tabs.json new file mode 100644 index 0000000000..1ff205839a --- /dev/null +++ b/frappe/website/web_template/section_with_tabs/section_with_tabs.json @@ -0,0 +1,97 @@ +{ + "creation": "2020-04-17 16:34:09.726205", + "docstatus": 0, + "doctype": "Web Template", + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "reqd": 0 + }, + { + "fieldname": "subtitle", + "fieldtype": "Data", + "label": "Subtitle", + "reqd": 0 + }, + { + "fieldname": "tab_1_title", + "fieldtype": "Data", + "label": "Tab 1 Title", + "reqd": 0 + }, + { + "fieldname": "tab_1_content", + "fieldtype": "Markdown Editor", + "label": "Tab 1 Content", + "reqd": 0 + }, + { + "fieldname": "tab_2_title", + "fieldtype": "Data", + "label": "Tab 2 Title", + "reqd": 0 + }, + { + "fieldname": "tab_2_content", + "fieldtype": "Markdown Editor", + "label": "Tab 2 Content", + "reqd": 0 + }, + { + "fieldname": "tab_3_title", + "fieldtype": "Data", + "label": "Tab 3 Title", + "reqd": 0 + }, + { + "fieldname": "tab_3_content", + "fieldtype": "Markdown Editor", + "label": "Tab 3 Content", + "reqd": 0 + }, + { + "fieldname": "tab_4_title", + "fieldtype": "Data", + "label": "Tab 4 Title", + "reqd": 0 + }, + { + "fieldname": "tab_4_content", + "fieldtype": "Markdown Editor", + "label": "Tab 4 Content", + "reqd": 0 + }, + { + "fieldname": "tab_5_title", + "fieldtype": "Data", + "label": "Tab 5 Title", + "reqd": 0 + }, + { + "fieldname": "tab_5_content", + "fieldtype": "Markdown Editor", + "label": "Tab 5 Content", + "reqd": 0 + }, + { + "fieldname": "tab_6_title", + "fieldtype": "Data", + "label": "Tab 6 Title", + "reqd": 0 + }, + { + "fieldname": "tab_6_content", + "fieldtype": "Markdown Editor", + "label": "Tab 6 Content", + "reqd": 0 + } + ], + "idx": 0, + "modified": "2020-04-23 21:22:26.011780", + "modified_by": "Administrator", + "name": "Section with Tabs", + "owner": "Administrator", + "standard": 1 +} \ No newline at end of file diff --git a/frappe/website/website_theme/standard/standard.json b/frappe/website/website_theme/standard/standard.json index e610d82582..1729e4616a 100644 --- a/frappe/website/website_theme/standard/standard.json +++ b/frappe/website/website_theme/standard/standard.json @@ -1,26 +1,22 @@ { - "apply_style": 0, - "apply_text_styles": 0, - "creation": "2015-02-19 13:37:33.925909", - "css": ".navbar-header {\n display: none;\n}", - "custom": 0, - "docstatus": 0, - "doctype": "Website Theme", - "font_size": "14px", - "footer_color": "", - "footer_text_color": "", - "heading_style": "", - "heading_webfont": "", - "idx": 26, - "link_color": "", - "modified": "2016-12-29 05:40:17.289226", - "modified_by": "Administrator", - "module": "Website", - "name": "Standard", - "owner": "Administrator", - "text_color": "", - "text_webfont": "", - "theme": "Standard", - "top_bar_color": "", - "top_bar_text_color": "" + "based_on": "Bootstrap 4", + "button_gradients": 0, + "button_rounded_corners": 1, + "button_shadows": 0, + "creation": "2015-02-19 13:37:33.925909", + "custom": 0, + "docstatus": 0, + "doctype": "Website Theme", + "font_properties": "300,600", + "footer": [], + "idx": 26, + "modified": "2020-04-29 12:26:48.399125", + "modified_by": "Administrator", + "module": "Website", + "name": "Standard", + "navbar": [], + "owner": "Administrator", + "theme": "Standard", + "theme_scss": "$enable-shadows: false;\n$enable-gradients: false;\n$enable-rounded: true;\n\n@import \"frappe/public/scss/website\";\n\n", + "theme_url": "/assets/css/standard_style.css" } \ No newline at end of file diff --git a/frappe/www/login.html b/frappe/www/login.html index 65e85db296..ebbff748ec 100644 --- a/frappe/www/login.html +++ b/frappe/www/login.html @@ -31,7 +31,7 @@ - + {{ _('Show') }}