diff --git a/.codacy.yml b/.codacy.yml deleted file mode 100644 index 4754a63e7e..0000000000 --- a/.codacy.yml +++ /dev/null @@ -1,2 +0,0 @@ -exclude_paths: - - '**.sql' \ No newline at end of file diff --git a/.deepsource.toml b/.deepsource.toml deleted file mode 100644 index f5edb47a13..0000000000 --- a/.deepsource.toml +++ /dev/null @@ -1,17 +0,0 @@ -version = 1 - -test_patterns = [ - "**/test_*.py" -] - -exclude_patterns = [ - "frappe/patches/**", - "*.min.js" -] - -[[analyzers]] -name = "python" -enabled = true - - [analyzers.meta] - runtime_version = "3.x.x" diff --git a/.eslintignore b/.eslintignore index 2fd65d307d..baf9bb2cc5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,5 +5,4 @@ frappe/core/doctype/doctype/boilerplate/* frappe/core/doctype/report/boilerplate/* frappe/public/js/frappe/class.js frappe/templates/includes/* -frappe/tests/testcafe/* -frappe/www/website_script.js \ No newline at end of file +frappe/www/website_script.js diff --git a/.eslintrc b/.eslintrc index a2538feab5..d123023a68 100644 --- a/.eslintrc +++ b/.eslintrc @@ -147,6 +147,7 @@ "context": true, "before": true, "beforeEach": true, - "qz": true + "qz": true, + "localforage": true } } diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000000..399b176e1d --- /dev/null +++ b/.flake8 @@ -0,0 +1,32 @@ +[flake8] +ignore = + E121, + E126, + E127, + E128, + E203, + E225, + E226, + E231, + E241, + E251, + E261, + E265, + E302, + E303, + E305, + E402, + E501, + E741, + W291, + W292, + W293, + W391, + W503, + W504, + F403, + B007, + B950, + W191, + +max-line-length = 200 \ No newline at end of file diff --git a/.travis/consumer_db/mariadb.json b/.github/helper/consumer_db/mariadb.json similarity index 89% rename from .travis/consumer_db/mariadb.json rename to .github/helper/consumer_db/mariadb.json index fb5b3bc976..2e32157e1a 100644 --- a/.travis/consumer_db/mariadb.json +++ b/.github/helper/consumer_db/mariadb.json @@ -1,5 +1,6 @@ { - "db_host": "localhost", + "db_host": "127.0.0.1", + "db_port": 3306, "db_name": "test_frappe_consumer", "db_password": "test_frappe", "allow_tests": true, diff --git a/.travis/consumer_db/postgres.json b/.github/helper/consumer_db/postgres.json similarity index 90% rename from .travis/consumer_db/postgres.json rename to .github/helper/consumer_db/postgres.json index fed9fdfde2..9532670029 100644 --- a/.travis/consumer_db/postgres.json +++ b/.github/helper/consumer_db/postgres.json @@ -1,5 +1,6 @@ { - "db_host": "localhost", + "db_host": "127.0.0.1", + "db_port": 5432, "db_name": "test_frappe_consumer", "db_password": "test_frappe", "db_type": "postgres", diff --git a/.github/helper/install.sh b/.github/helper/install.sh new file mode 100644 index 0000000000..f6f0cad31a --- /dev/null +++ b/.github/helper/install.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +set -e + +cd ~ || exit + +pip install frappe-bench + +bench init frappe-bench --skip-assets --python "$(which python)" --frappe-path "${GITHUB_WORKSPACE}" + +mkdir ~/frappe-bench/sites/test_site +cp "${GITHUB_WORKSPACE}/.github/helper/consumer_db/$DB.json" ~/frappe-bench/sites/test_site/site_config.json + +if [ "$TYPE" == "server" ]; then + mkdir ~/frappe-bench/sites/test_site_producer; + cp "${GITHUB_WORKSPACE}/.github/helper/producer_db/$DB.json" ~/frappe-bench/sites/test_site_producer/site_config.json; +fi + +if [ "$DB" == "mariadb" ];then + mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"; + mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"; + + mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE DATABASE test_frappe_consumer"; + mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE USER 'test_frappe_consumer'@'localhost' IDENTIFIED BY 'test_frappe_consumer'"; + mysql --host 127.0.0.1 --port 3306 -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe_consumer\`.* TO 'test_frappe_consumer'@'localhost'"; + + mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE DATABASE test_frappe_producer"; + mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE USER 'test_frappe_producer'@'localhost' IDENTIFIED BY 'test_frappe_producer'"; + mysql --host 127.0.0.1 --port 3306 -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe_producer\`.* TO 'test_frappe_producer'@'localhost'"; + + mysql --host 127.0.0.1 --port 3306 -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'"; + mysql --host 127.0.0.1 --port 3306 -u root -e "FLUSH PRIVILEGES"; + fi + +if [ "$DB" == "postgres" ];then + echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE DATABASE test_frappe_consumer" -U postgres; + echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE USER test_frappe_consumer WITH PASSWORD 'test_frappe'" -U postgres; + + echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE DATABASE test_frappe_producer" -U postgres; + echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE USER test_frappe_producer WITH PASSWORD 'test_frappe'" -U postgres; +fi + +cd ./frappe-bench || exit + +sed -i 's/^watch:/# watch:/g' Procfile +sed -i 's/^schedule:/# schedule:/g' Procfile + +if [ "$TYPE" == "server" ]; then sed -i 's/^socketio:/# socketio:/g' Procfile; fi +if [ "$TYPE" == "server" ]; then sed -i 's/^redis_socketio:/# redis_socketio:/g' Procfile; fi + +if [ "$TYPE" == "ui" ]; then bench setup requirements --node; fi + +# install node-sass which is required for website theme test +cd ./apps/frappe || exit +yarn add node-sass@4.13.1 +cd ../.. + +bench start & +bench --site test_site reinstall --yes +if [ "$TYPE" == "server" ]; then bench --site test_site_producer reinstall --yes; fi +bench build --app frappe \ No newline at end of file diff --git a/.github/helper/install_dependencies.sh b/.github/helper/install_dependencies.sh new file mode 100644 index 0000000000..9be8519d85 --- /dev/null +++ b/.github/helper/install_dependencies.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -e + +# python "${GITHUB_WORKSPACE}/.github/helper/roulette.py" +# if [[ $? != 2 ]];then +# exit; +# fi + + # install wkhtmltopdf +wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz +tar -xf /tmp/wkhtmltox.tar.xz -C /tmp +sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf +sudo chmod o+x /usr/local/bin/wkhtmltopdf + +# install cups +sudo apt-get install libcups2-dev + +# install redis +sudo apt-get install redis-server + diff --git a/.travis/producer_db/mariadb.json b/.github/helper/producer_db/mariadb.json similarity index 89% rename from .travis/producer_db/mariadb.json rename to .github/helper/producer_db/mariadb.json index 988282a554..c1db0d765f 100644 --- a/.travis/producer_db/mariadb.json +++ b/.github/helper/producer_db/mariadb.json @@ -1,5 +1,6 @@ { - "db_host": "localhost", + "db_host": "127.0.0.1", + "db_port": 3306, "db_name": "test_frappe_producer", "db_password": "test_frappe", "allow_tests": true, diff --git a/.travis/producer_db/postgres.json b/.github/helper/producer_db/postgres.json similarity index 89% rename from .travis/producer_db/postgres.json rename to .github/helper/producer_db/postgres.json index 6426e99058..8b9d2a20fd 100644 --- a/.travis/producer_db/postgres.json +++ b/.github/helper/producer_db/postgres.json @@ -1,5 +1,6 @@ { - "db_host": "localhost", + "db_host": "127.0.0.1", + "db_port": 5432, "db_name": "test_frappe_producer", "db_password": "test_frappe", "db_type": "postgres", diff --git a/.travis/roulette.py b/.github/helper/roulette.py similarity index 93% rename from .travis/roulette.py rename to .github/helper/roulette.py index 4d83137199..ba775d6794 100644 --- a/.travis/roulette.py +++ b/.github/helper/roulette.py @@ -24,10 +24,12 @@ def is_docs(file): if __name__ == "__main__": build_type = os.environ.get("TYPE") - commit_range = os.environ.get("TRAVIS_COMMIT_RANGE") + before = os.environ.get("BEFORE") + after = os.environ.get("AFTER") + commit_range = before + '...' + after print("Build Type: {}".format(build_type)) print("Commit Range: {}".format(commit_range)) - + try: files_changed = get_output("git diff --name-only {}".format(commit_range), shell=False) except Exception: diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml new file mode 100644 index 0000000000..665e7b6c10 --- /dev/null +++ b/.github/workflows/ci-tests.yml @@ -0,0 +1,148 @@ +name: CI + +on: [pull_request, workflow_dispatch, push] + +jobs: + test: + runs-on: ubuntu-18.04 + + strategy: + fail-fast: false + matrix: + include: + - DB: "mariadb" + TYPE: "server" + JOB_NAME: "Python MariaDB" + RUN_COMMAND: bench --site test_site run-tests --coverage + + - DB: "postgres" + TYPE: "server" + JOB_NAME: "Python PostgreSQL" + RUN_COMMAND: bench --site test_site run-tests --coverage + + - DB: "mariadb" + TYPE: "ui" + JOB_NAME: "UI MariaDB" + RUN_COMMAND: bench --site test_site run-ui-tests frappe --headless + + name: ${{ matrix.JOB_NAME }} + + services: + mysql: + image: mariadb:10.3 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: YES + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + + postgres: + image: postgres:12.4 + env: + POSTGRES_PASSWORD: travis + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + steps: + - name: Clone + uses: actions/checkout@v2 + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.7 + + - uses: actions/setup-node@v2 + with: + node-version: '12' + check-latest: true + + - name: Add to Hosts + run: | + echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts + echo "127.0.0.1 test_site_producer" | sudo tee -a /etc/hosts + + - name: Cache pip + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Cache node modules + uses: actions/cache@v2 + env: + cache-name: cache-node-modules + with: + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v2 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Cache cypress binary + uses: actions/cache@v2 + with: + path: ~/.cache + key: ${{ runner.os }}-cypress- + restore-keys: | + ${{ runner.os }}-cypress- + ${{ runner.os }}- + + - name: Install Dependencies + run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh + env: + BEFORE: ${{ env.GITHUB_EVENT_PATH.before }} + AFTER: ${{ env.GITHUB_EVENT_PATH.after }} + TYPE: ${{ matrix.TYPE }} + + - name: Install + run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh + env: + DB: ${{ matrix.DB }} + TYPE: ${{ matrix.TYPE }} + + - name: Run Set-Up + if: matrix.TYPE == 'ui' + run: cd ~/frappe-bench/ && bench --site test_site execute frappe.utils.install.complete_setup_wizard + env: + DB: ${{ matrix.DB }} + TYPE: ${{ matrix.TYPE }} + + - name: Run Tests + run: cd ~/frappe-bench/ && ${{ matrix.RUN_COMMAND }} + env: + DB: ${{ matrix.DB }} + TYPE: ${{ matrix.TYPE }} + + - name: Coverage + if: matrix.TYPE == 'server' + run: | + cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE} + cd ${GITHUB_WORKSPACE} + pip install coveralls==2.2.0 + pip install coverage==4.5.4 + coveralls + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }} diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 0000000000..321dfb567b --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,22 @@ +name: Semgrep + +on: + pull_request: + branches: + - develop +jobs: + semgrep: + name: Frappe Linter + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup python3 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Run semgrep + run: | + python -m pip install -q semgrep + git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q + files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF) + if [ -f .semgrep.yml ]; then semgrep --config=.semgrep.yml --quiet --error $files; fi diff --git a/.mergify.yml b/.mergify.yml index 5b0ec71b1c..82f710a5a8 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -3,7 +3,9 @@ pull_request_rules: conditions: - status-success=Sider - status-success=Semantic Pull Request - - status-success=Travis CI - Pull Request + - status-success=Python MariaDB + - status-success=Python PostgreSQL + - status-success=UI MariaDB - status-success=security/snyk (frappe) - label!=dont-merge - label!=squash @@ -14,8 +16,9 @@ pull_request_rules: - name: Automatic squash on CI success and review conditions: - status-success=Sider - - status-success=Semantic Pull Request - - status-success=Travis CI - Pull Request + - status-success=Python MariaDB + - status-success=Python PostgreSQL + - status-success=UI MariaDB - status-success=security/snyk (frappe) - label!=dont-merge - label=squash diff --git a/.semgrep.yml b/.semgrep.yml new file mode 100644 index 0000000000..99d237251e --- /dev/null +++ b/.semgrep.yml @@ -0,0 +1,29 @@ +#Reference: https://semgrep.dev/docs/writing-rules/rule-syntax/ + +rules: +- id: eval + patterns: + - pattern-not: eval("...") + - pattern: eval(...) + message: | + Detected the use of eval(). eval() can be dangerous if used to evaluate + dynamic content. Avoid it or use safe_eval(). + languages: + - python + severity: ERROR + +# translations +- id: frappe-translation-syntax-python + pattern-either: + - pattern: _(f"...") # f-strings not allowed + - pattern: _("..." + "...") # concatenation not allowed + - pattern: _("") # empty string is meaningless + - pattern: _("..." % ...) # Only positional formatters are allowed. + - pattern: _("...".format(...)) # format should not be used before translating + - pattern: _("...") + ... + _("...") # don't split strings + message: | + Incorrect use of translation function detected. + Please refer: https://frappeframework.com/docs/user/en/translations + languages: + - python + severity: ERROR diff --git a/frappe/.stylelintrc b/.stylelintrc similarity index 100% rename from frappe/.stylelintrc rename to .stylelintrc diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 53ad56a948..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,129 +0,0 @@ -language: python -dist: bionic - -addons: - hosts: - - test_site - - test_site_producer - mariadb: 10.3 - postgresql: 9.5 - chrome: stable - -services: - - xvfb - - mysql - -git: - depth: 1 - -cache: - 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: - - name: "Python 3.7 MariaDB" - python: 3.7 - env: DB=mariadb TYPE=server - script: bench --verbose --site test_site run-tests --coverage - - - name: "Python 3.7 PostgreSQL" - python: 3.7 - env: DB=postgres TYPE=server - script: bench --verbose --site test_site run-tests --coverage - - - name: "Cypress" - python: 3.7 - env: DB=mariadb TYPE=ui - before_script: - - bench --site test_site execute frappe.utils.install.complete_setup_wizard - script: bench --site test_site run-ui-tests frappe --headless - -before_install: - # do we really want to run travis? - - | - python ./.travis/roulette.py - if [[ $? != 2 ]];then - exit; - fi - - # install wkhtmltopdf - - wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz - - tar -xf /tmp/wkhtmltox.tar.xz -C /tmp - - sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf - - sudo chmod o+x /usr/local/bin/wkhtmltopdf - - # install cups - - sudo apt-get install libcups2-dev - -install: - - cd ~ - - source ./.nvm/nvm.sh - - nvm install 12 - - - pip install frappe-bench - - - bench init frappe-bench --skip-assets --python $(which python) --frappe-path $TRAVIS_BUILD_DIR - - - mkdir ~/frappe-bench/sites/test_site - - cp $TRAVIS_BUILD_DIR/.travis/consumer_db/$DB.json ~/frappe-bench/sites/test_site/site_config.json - - - if [ $TYPE == "server" ]; then - mkdir ~/frappe-bench/sites/test_site_producer; - cp $TRAVIS_BUILD_DIR/.travis/producer_db/$DB.json ~/frappe-bench/sites/test_site_producer/site_config.json; - fi - - - if [ $DB == "mariadb" ];then - mysql -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"; - mysql -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"; - - mysql -u root -e "CREATE DATABASE test_frappe_consumer"; - mysql -u root -e "CREATE USER 'test_frappe_consumer'@'localhost' IDENTIFIED BY 'test_frappe_consumer'"; - mysql -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe_consumer\`.* TO 'test_frappe_consumer'@'localhost'"; - - mysql -u root -e "CREATE DATABASE test_frappe_producer"; - mysql -u root -e "CREATE USER 'test_frappe_producer'@'localhost' IDENTIFIED BY 'test_frappe_producer'"; - mysql -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe_producer\`.* TO 'test_frappe_producer'@'localhost'"; - - mysql -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'"; - mysql -u root -e "FLUSH PRIVILEGES"; - fi - - - if [ $DB == "postgres" ];then - psql -c "CREATE DATABASE test_frappe_consumer" -U postgres; - psql -c "CREATE USER test_frappe_consumer WITH PASSWORD 'test_frappe'" -U postgres; - - psql -c "CREATE DATABASE test_frappe_producer" -U postgres; - psql -c "CREATE USER test_frappe_producer WITH PASSWORD 'test_frappe'" -U postgres; - fi - - - cd ./frappe-bench - - - sed -i 's/^watch:/# watch:/g' Procfile - - sed -i 's/^schedule:/# schedule:/g' Procfile - - - if [ $TYPE == "server" ]; then sed -i 's/^socketio:/# socketio:/g' Procfile; fi - - if [ $TYPE == "server" ]; then sed -i 's/^redis_socketio:/# redis_socketio:/g' Procfile; fi - - - if [ $TYPE == "ui" ]; then bench setup requirements --node; fi - - # install node-sass which is required for website theme test - - cd ./apps/frappe - - yarn add node-sass@4.13.1 - - cd ../.. - - - bench start & - - bench --site test_site reinstall --yes - - if [ $TYPE == "server" ]; then bench --site test_site_producer reinstall --yes; fi - - bench build --app frappe - -after_script: - - pip install coverage==4.5.4 - - pip install python-coveralls - - coveralls -b apps/frappe -d ../../sites/.coverage diff --git a/CODEOWNERS b/CODEOWNERS index 1afa3f72e3..92723ab035 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -8,10 +8,10 @@ website/ @prssanna web_form/ @prssanna templates/ @surajshetty3416 www/ @surajshetty3416 -integrations/ @nextchamp-saqib +integrations/ @leela patches/ @surajshetty3416 dashboard/ @prssanna -email/ @saurabh6790 +email/ @leela event_streaming/ @ruchamahabal data_import* @netchampfaris core/ @surajshetty3416 diff --git a/README.md b/README.md index f99988ae79..b00d291b96 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@
- - + + diff --git a/ci/fix-mariadb.sh b/ci/fix-mariadb.sh deleted file mode 100755 index 886ec5e0d0..0000000000 --- a/ci/fix-mariadb.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -# stolen from http://cgit.drupalcode.org/octopus/commit/?id=db4f837 -includedir=`mysql_config --variable=pkgincludedir` -thiscwd=`pwd` -_THIS_DB_VERSION=`mysql -V 2>&1 | tr -d "\n" | cut -d" " -f6 | awk '{ print $1}' | cut -d"-" -f1 | awk '{ print $1}' | sed "s/[\,']//g"` -if [ "$_THIS_DB_VERSION" = "5.5.40" ] && [ ! -e "$includedir-$_THIS_DB_VERSION-fixed.log" ] ; then - cd $includedir - sudo patch -p1 < $thiscwd/ci/my_config.h.patch &> /dev/null - sudo touch $includedir-$_THIS_DB_VERSION-fixed.log -fi diff --git a/ci/my_config.h.patch b/ci/my_config.h.patch deleted file mode 100644 index 5247b5b39b..0000000000 --- a/ci/my_config.h.patch +++ /dev/null @@ -1,22 +0,0 @@ -diff -burp a/my_config.h b/my_config.h ---- a/my_config.h 2014-10-09 19:32:46.000000000 -0400 -+++ b/my_config.h 2014-10-09 19:35:12.000000000 -0400 -@@ -641,17 +641,4 @@ - #define SIZEOF_TIME_T 8 - /* #undef TIME_T_UNSIGNED */ - --/* -- stat structure (from ) is conditionally defined -- to have different layout and size depending on the defined macros. -- The correct macro is defined in my_config.h, which means it MUST be -- included first (or at least before - so, practically, -- before including any system headers). -- -- __GLIBC__ is defined in --*/ --#ifdef __GLIBC__ --#error MUST be included first! --#endif -- - #endif - diff --git a/cypress/integration/control_rating.js b/cypress/integration/control_rating.js index 31c036d240..592ed87004 100644 --- a/cypress/integration/control_rating.js +++ b/cypress/integration/control_rating.js @@ -18,7 +18,7 @@ context('Control Rating', () => { get_dialog_with_rating().as('dialog'); cy.get('div.rating') - .children('i.fa') + .children('svg') .first() .click() .should('have.class', 'star-click'); @@ -33,11 +33,11 @@ context('Control Rating', () => { get_dialog_with_rating(); cy.get('div.rating') - .children('i.fa') + .children('svg') .first() .invoke('trigger', 'mouseenter') .should('have.class', 'star-hover') .invoke('trigger', 'mouseleave') .should('not.have.class', 'star-hover'); }); -}); \ No newline at end of file +}); diff --git a/cypress/integration/control_select.js b/cypress/integration/control_select.js new file mode 100644 index 0000000000..0bc719b4a7 --- /dev/null +++ b/cypress/integration/control_select.js @@ -0,0 +1,36 @@ +context('Control Select', () => { + before(() => { + cy.login(); + cy.visit('/app/website'); + }); + + function get_dialog_with_select() { + return cy.dialog({ + title: 'Select', + fields: [{ + 'fieldname': 'select_control', + 'fieldtype': 'Select', + 'placeholder': 'Select an Option', + 'options': ['', 'Option 1', 'Option 2', 'Option 2'], + }] + }); + } + + it('toggles placholder on clicking an option', () => { + get_dialog_with_select().as('dialog'); + + cy.get('.frappe-control[data-fieldname=select_control] .control-input').as('control'); + cy.get('.frappe-control[data-fieldname=select_control] .control-input select').as('select'); + cy.get('@control').get('.select-icon').should('exist'); + cy.get('@control').get('.placeholder').should('have.css', 'display', 'block'); + cy.get('@select').select('Option 1'); + cy.get('@control').get('.placeholder').should('have.css', 'display', 'none'); + cy.get('@select').invoke('val', ''); + cy.get('@control').get('.placeholder').should('have.css', 'display', 'block'); + + + cy.get('@dialog').then(dialog => { + dialog.hide(); + }); + }); +}); diff --git a/cypress/integration/recorder.js b/cypress/integration/recorder.js index 7236200741..d30cc3568c 100644 --- a/cypress/integration/recorder.js +++ b/cypress/integration/recorder.js @@ -3,6 +3,16 @@ context('Recorder', () => { cy.login(); }); + beforeEach(() => { + cy.visit('/app/recorder'); + return cy.window().its('frappe').then(frappe => { + // reset recorder + return frappe.xcall("frappe.recorder.stop").then(() => { + return frappe.xcall("frappe.recorder.delete"); + }); + }); + }); + it('Navigate to Recorder', () => { cy.visit('/app'); cy.awesomebar('recorder'); @@ -11,7 +21,6 @@ context('Recorder', () => { }); it('Recorder Empty State', () => { - cy.visit('/app/recorder'); cy.get('.title-text').should('contain', 'Recorder'); cy.get('.indicator-pill').should('contain', 'Inactive').should('have.class', 'red'); @@ -24,7 +33,6 @@ context('Recorder', () => { }); it('Recorder Start', () => { - cy.visit('/app/recorder'); cy.get('.primary-action').should('contain', 'Start').click(); cy.get('.indicator-pill').should('contain', 'Active').should('have.class', 'green'); @@ -40,15 +48,9 @@ context('Recorder', () => { cy.visit('/app/recorder'); cy.get('.title-text').should('contain', 'Recorder'); cy.get('.result-list').should('contain', '/api/method/frappe.desk.reportview.get'); - - cy.get('#page-recorder .primary-action').should('contain', 'Stop').click(); - cy.wait(500); - cy.get('#page-recorder .btn-secondary').should('contain', 'Clear').click(); - cy.get('.msg-box').should('contain', 'Inactive'); }); - it('Recorder View Request', () => { - cy.visit('/app/recorder'); + it.only('Recorder View Request', () => { cy.get('.primary-action').should('contain', 'Start').click(); cy.visit('/app/List/DocType/List'); @@ -64,9 +66,5 @@ context('Recorder', () => { cy.url().should('include', '/recorder/request'); cy.get('form').should('contain', '/api/method/frappe'); - - cy.get('#page-recorder .primary-action').should('contain', 'Stop').click(); - cy.wait(200); - cy.get('#page-recorder .btn-secondary').should('contain', 'Clear').click(); }); -}); \ No newline at end of file +}); diff --git a/cypress/integration/table_multiselect.js b/cypress/integration/table_multiselect.js index 8b83a0d914..bdcf5d1ff0 100644 --- a/cypress/integration/table_multiselect.js +++ b/cypress/integration/table_multiselect.js @@ -8,7 +8,7 @@ context('Table MultiSelect', () => { it('select value from multiselect dropdown', () => { cy.new_form('Assignment Rule'); cy.fill_field('__newname', name); - cy.fill_field('document_type', 'ToDo'); + cy.fill_field('document_type', 'Blog Post'); cy.fill_field('assign_condition', 'status=="Open"', 'Code'); cy.get('input[data-fieldname="users"]').focus().as('input'); cy.get('input[data-fieldname="users"] + ul').should('be.visible'); @@ -45,6 +45,6 @@ context('Table MultiSelect', () => { cy.get(`.list-subject:contains("table multiselect")`).last().find('a').click(); cy.get('.frappe-control[data-fieldname="users"] .form-control .tb-selected-value').as('existing_value'); cy.get('@existing_value').find('.btn-link-to-form').click(); - cy.location('pathname').should('contain', '/user/test@erpnext.com'); + cy.location('pathname').should('contain', '/user/test%40erpnext.com'); }); }); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 7f0afdf035..1964b96d70 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -275,7 +275,7 @@ Cypress.Commands.add('get_open_dialog', () => { }); Cypress.Commands.add('hide_dialog', () => { - cy.wait(200); + cy.wait(300); cy.get_open_dialog().find('.btn-modal-close').click(); cy.get('.modal:visible').should('not.exist'); }); @@ -312,7 +312,6 @@ Cypress.Commands.add('add_filter', () => { cy.get('.filter-section .filter-button').click(); cy.wait(300); cy.get('.filter-popover').should('exist'); - cy.get('.filter-popover').find('.add-filter').click(); }); Cypress.Commands.add('clear_filters', () => { diff --git a/frappe/__init__.py b/frappe/__init__.py index 95d9c782a4..871d1b9e92 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1,8 +1,14 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt """ -globals attached to frappe module -+ some utility functions that should probably be moved +Frappe - Low Code Open Source Framework in Python and JS + +Frappe, pronounced fra-pay, is a full stack, batteries-included, web +framework written in Python and Javascript with MariaDB as the database. +It is the framework which powers ERPNext. It is pretty generic and can +be used to build database driven apps. + +Read the documentation: https://frappeframework.com/docs """ from __future__ import unicode_literals, print_function @@ -11,11 +17,15 @@ from werkzeug.local import Local, release_local import os, sys, importlib, inspect, json from past.builtins import cmp import click -from faker import Faker -# public +# Local application imports from .exceptions import * from .utils.jinja import (get_jenv, get_template, render_template, get_email_from_template, get_jloader) +from .utils.lazy_loader import lazy_import + +# Lazy imports +faker = lazy_import('faker') + # Harmless for Python 3 # For Python 2 set default encoding to utf-8 @@ -190,17 +200,20 @@ def init(site, sites_path=None, new_site=False): local.initialised = True -def connect(site=None, db_name=None): +def connect(site=None, db_name=None, set_admin_as_user=True): """Connect to site database instance. :param site: If site is given, calls `frappe.init`. - :param db_name: Optional. Will use from `site_config.json`.""" + :param db_name: Optional. Will use from `site_config.json`. + :param set_admin_as_user: Set Administrator as current user. + """ from frappe.database import get_db if site: init(site) local.db = get_db(user=db_name or local.conf.db_name) - set_user("Administrator") + if set_admin_as_user: + set_user("Administrator") def connect_replica(): from frappe.database import get_db @@ -462,8 +475,8 @@ def get_request_header(key, default=None): def sendmail(recipients=[], sender="", subject="No Subject", message="No Message", as_markdown=False, delayed=True, reference_doctype=None, reference_name=None, - unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None, - attachments=None, content=None, doctype=None, name=None, reply_to=None, + unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None, add_unsubscribe_link=1, + attachments=None, content=None, doctype=None, name=None, reply_to=None, queue_separately=False, cc=[], bcc=[], message_id=None, in_reply_to=None, send_after=None, expose_recipients=None, send_priority=1, communication=None, retry=1, now=None, read_receipt=None, is_notification=False, inline_images=None, template=None, args=None, header=None, print_letterhead=False, with_container=False): @@ -510,10 +523,10 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message from frappe.email import queue queue.send(recipients=recipients, sender=sender, subject=subject, message=message, text_content=text_content, - reference_doctype = doctype or reference_doctype, reference_name = name or reference_name, + reference_doctype = doctype or reference_doctype, reference_name = name or reference_name, add_unsubscribe_link=add_unsubscribe_link, unsubscribe_method=unsubscribe_method, unsubscribe_params=unsubscribe_params, unsubscribe_message=unsubscribe_message, attachments=attachments, reply_to=reply_to, cc=cc, bcc=bcc, message_id=message_id, in_reply_to=in_reply_to, - send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority, + send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority, queue_separately=queue_separately, communication=communication, now=now, read_receipt=read_receipt, is_notification=is_notification, inline_images=inline_images, header=header, print_letterhead=print_letterhead, with_container=with_container) @@ -1189,10 +1202,10 @@ def make_property_setter(args, ignore_validate=False, validate_fields_for_doctyp ps.validate_fieldtype_change() ps.insert() -def import_doc(path, ignore_links=False, ignore_insert=False, insert=False): +def import_doc(path): """Import a file using Data Import.""" from frappe.core.doctype.data_import.data_import import import_doc - import_doc(path, ignore_links=ignore_links, ignore_insert=ignore_insert, insert=insert) + import_doc(path) def copy_doc(doc, ignore_no_copy=True): """ No_copy fields also get copied.""" @@ -1742,12 +1755,12 @@ def parse_json(val): def mock(type, size=1, locale='en'): results = [] - faker = Faker(locale) - if not type in dir(faker): + fake = faker.Faker(locale) + if type not in dir(fake): raise ValueError('Not a valid mock type.') else: for i in range(size): - data = getattr(faker, type)() + data = getattr(fake, type)() results.append(data) from frappe.chat.util import squashify diff --git a/frappe/app.py b/frappe/app.py index adf2bfa8c9..607479ad52 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -128,6 +128,8 @@ def init_request(request): if frappe.local.conf.get('maintenance_mode'): frappe.connect() raise frappe.SessionStopped('Session Stopped') + else: + frappe.connect(set_admin_as_user=False) make_form_dict(request) @@ -152,10 +154,10 @@ def process_response(response): def set_cors_headers(response): origin = frappe.request.headers.get('Origin') - if not origin: + allow_cors = frappe.conf.allow_cors + if not (origin and allow_cors): return - allow_cors = frappe.conf.allow_cors if allow_cors != "*": if not isinstance(allow_cors, list): allow_cors = [allow_cors] @@ -181,6 +183,9 @@ def make_form_dict(request): else: args = request.form or request.args + if not isinstance(args, dict): + frappe.throw("Invalid request arguments") + try: frappe.local.form_dict = frappe._dict({ k:v[0] if isinstance(v, (list, tuple)) else v \ for k, v in iteritems(args) }) diff --git a/frappe/auth.py b/frappe/auth.py index 2e0ec681d2..946a8c52d5 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -207,23 +207,44 @@ class LoginManager: if frappe.session.user != "Guest": clear_sessions(frappe.session.user, keep_current=True) - def authenticate(self, user=None, pwd=None): + def authenticate(self, user: str = None, pwd: str = None): + from frappe.core.doctype.user.user import User + if not (user and pwd): user, pwd = frappe.form_dict.get('usr'), frappe.form_dict.get('pwd') if not (user and pwd): self.fail(_('Incomplete login details'), user=user) - if cint(frappe.db.get_value("System Settings", "System Settings", "allow_login_using_mobile_number")): - user = frappe.db.get_value("User", filters={"mobile_no": user}, fieldname="name") or user + # Ignore password check if tmp_id is set, 2FA takes care of authentication. + validate_password = not bool(frappe.form_dict.get('tmp_id')) + user = User.find_by_credentials(user, pwd, validate_password=validate_password) - if cint(frappe.db.get_value("System Settings", "System Settings", "allow_login_using_user_name")): - user = frappe.db.get_value("User", filters={"username": user}, fieldname="name") or user + if not user: + self.fail('Invalid login credentials') - self.check_if_enabled(user) - if not frappe.form_dict.get('tmp_id'): - self.user = self.check_password(user, pwd) + sys_settings = frappe.get_doc("System Settings") + track_login_attempts = (sys_settings.allow_consecutive_login_attempts >0) + + tracker_kwargs = {} + if track_login_attempts: + tracker_kwargs['lock_interval'] = sys_settings.allow_login_after_fail + tracker_kwargs['max_consecutive_login_attempts'] = sys_settings.allow_consecutive_login_attempts + + tracker = LoginAttemptTracker(user.name, **tracker_kwargs) + + if track_login_attempts and not tracker.is_user_allowed(): + frappe.throw(_("Your account has been locked and will resume after {0} seconds") + .format(sys_settings.allow_login_after_fail), frappe.SecurityException) + + if not user.is_authenticated: + tracker.add_failure_attempt() + self.fail('Invalid login credentials', user=user.name) + elif not (user.name == 'Administrator' or user.enabled): + tracker.add_failure_attempt() + self.fail('User disabled or missing', user=user.name) else: - self.user = user + tracker.add_success_attempt() + self.user = user.name def force_user_to_reset_password(self): if not self.user: @@ -245,23 +266,12 @@ class LoginManager: if last_pwd_reset_days > reset_pwd_after_days: return True - def check_if_enabled(self, user): - """raise exception if user not enabled""" - doc = frappe.get_doc("System Settings") - if cint(doc.allow_consecutive_login_attempts) > 0: - check_consecutive_login_attempts(user, doc) - - if user=='Administrator': return - if not cint(frappe.db.get_value('User', user, 'enabled')): - self.fail('User disabled or missing', user=user) - def check_password(self, user, pwd): """check password""" try: # returns user in correct case return check_password(user, pwd) except frappe.AuthenticationError: - self.update_invalid_login(user) self.fail('Incorrect password', user=user) def fail(self, message, user=None): @@ -272,15 +282,6 @@ class LoginManager: frappe.db.commit() raise frappe.AuthenticationError - def update_invalid_login(self, user): - last_login_tried = get_last_tried_login_data(user) - - failed_count = 0 - if last_login_tried > get_datetime(): - failed_count = get_login_failed_count(user) - - frappe.cache().hset('login_failed_count', user, failed_count + 1) - def run_trigger(self, event='on_login'): for method in frappe.get_hooks().get(event, []): frappe.call(frappe.get_attr(method), login_manager=self) @@ -383,38 +384,6 @@ def clear_cookies(): frappe.session.sid = "" frappe.local.cookie_manager.delete_cookie(["full_name", "user_id", "sid", "user_image", "system_user"]) -def get_last_tried_login_data(user, get_last_login=False): - locked_account_time = frappe.cache().hget('locked_account_time', user) - if get_last_login and locked_account_time: - return locked_account_time - - last_login_tried = frappe.cache().hget('last_login_tried', user) - if not last_login_tried or last_login_tried < get_datetime(): - last_login_tried = get_datetime() + datetime.timedelta(seconds=60) - - frappe.cache().hset('last_login_tried', user, last_login_tried) - - return last_login_tried - -def get_login_failed_count(user): - return cint(frappe.cache().hget('login_failed_count', user)) or 0 - -def check_consecutive_login_attempts(user, doc): - login_failed_count = get_login_failed_count(user) - last_login_tried = (get_last_tried_login_data(user, True) - + datetime.timedelta(seconds=doc.allow_login_after_fail)) - - if login_failed_count >= cint(doc.allow_consecutive_login_attempts): - locked_account_time = frappe.cache().hget('locked_account_time', user) - if not locked_account_time: - frappe.cache().hset('locked_account_time', user, get_datetime()) - - if last_login_tried > get_datetime(): - frappe.throw(_("Your account has been locked and will resume after {0} seconds") - .format(doc.allow_login_after_fail), frappe.SecurityException) - else: - delete_login_failed_cache(user) - def validate_ip_address(user): """check if IP Address is valid""" user = frappe.get_cached_doc("User", user) if not frappe.flags.in_test else frappe.get_doc("User", user) @@ -436,3 +405,87 @@ def validate_ip_address(user): return frappe.throw(_("Access not allowed from this IP Address"), frappe.AuthenticationError) + + +class LoginAttemptTracker(object): + """Track login attemts of a user. + + Lock the account for s number of seconds if there have been n consecutive unsuccessful attempts to log in. + """ + def __init__(self, user_name: str, max_consecutive_login_attempts: int=3, lock_interval:int = 5*60): + """ Initialize the tracker. + + :param user_name: Name of the loggedin user + :param max_consecutive_login_attempts: Maximum allowed consecutive failed login attempts + :param lock_interval: Locking interval incase of maximum failed attempts + """ + self.user_name = user_name + self.lock_interval = datetime.timedelta(seconds=lock_interval) + self.max_failed_logins = max_consecutive_login_attempts + + @property + def login_failed_count(self): + return frappe.cache().hget('login_failed_count', self.user_name) + + @login_failed_count.setter + def login_failed_count(self, count): + frappe.cache().hset('login_failed_count', self.user_name, count) + + @login_failed_count.deleter + def login_failed_count(self): + frappe.cache().hdel('login_failed_count', self.user_name) + + @property + def login_failed_time(self): + """First failed login attempt time within lock interval. + + For every user we track only First failed login attempt time within lock interval of time. + """ + return frappe.cache().hget('login_failed_time', self.user_name) + + @login_failed_time.setter + def login_failed_time(self, timestamp): + frappe.cache().hset('login_failed_time', self.user_name, timestamp) + + @login_failed_time.deleter + def login_failed_time(self): + frappe.cache().hdel('login_failed_time', self.user_name) + + def add_failure_attempt(self): + """ Log user failure attempts into the system. + + Increase the failure count if new failure is with in current lock interval time period, if not reset the login failure count. + """ + login_failed_time = self.login_failed_time + login_failed_count = self.login_failed_count # Consecutive login failure count + current_time = get_datetime() + + if not (login_failed_time and login_failed_count): + login_failed_time, login_failed_count = current_time, 0 + + if login_failed_time + self.lock_interval > current_time: + login_failed_count += 1 + else: + login_failed_time, login_failed_count = current_time, 1 + + self.login_failed_time = login_failed_time + self.login_failed_count = login_failed_count + + def add_success_attempt(self): + """Reset login failures. + """ + del self.login_failed_count + del self.login_failed_time + + def is_user_allowed(self) -> bool: + """Is user allowed to login + + User is not allowed to login if login failures are greater than threshold within in lock interval from first login failure. + """ + login_failed_time = self.login_failed_time + login_failed_count = self.login_failed_count or 0 + current_time = get_datetime() + + if login_failed_time and login_failed_time + self.lock_interval > current_time and login_failed_count > self.max_failed_logins: + return False + return True diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.js b/frappe/automation/doctype/assignment_rule/assignment_rule.js index ee1a076465..97bed4f8f3 100644 --- a/frappe/automation/doctype/assignment_rule/assignment_rule.js +++ b/frappe/automation/doctype/assignment_rule/assignment_rule.js @@ -9,6 +9,16 @@ frappe.ui.form.on('Assignment Rule', { frm.events.rule(frm); }, + setup: function(frm) { + frm.set_query("document_type", () => { + return { + filters: { + name: ["!=", "ToDo"] + } + }; + }); + }, + document_type: function(frm) { frm.trigger('set_options'); }, diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.py b/frappe/automation/doctype/assignment_rule/assignment_rule.py index d20398d564..c673d5ceeb 100644 --- a/frappe/automation/doctype/assignment_rule/assignment_rule.py +++ b/frappe/automation/doctype/assignment_rule/assignment_rule.py @@ -18,6 +18,8 @@ class AssignmentRule(Document): if not len(set(assignment_days)) == len(assignment_days): repeated_days = get_repeated(assignment_days) frappe.throw(_("Assignment Day {0} has been repeated.").format(frappe.bold(repeated_days))) + if self.document_type == 'ToDo': + frappe.throw(_('Assignment Rule is not allowed on {0} document type').format(frappe.bold("ToDo"))) def on_update(self): clear_assignment_rule_cache(self) @@ -298,4 +300,4 @@ def get_repeated(values): def clear_assignment_rule_cache(rule): frappe.cache_manager.clear_doctype_map('Assignment Rule', rule.document_type) - frappe.cache_manager.clear_doctype_map('Assignment Rule', 'due_date_rules_for_' + rule.document_type) \ No newline at end of file + frappe.cache_manager.clear_doctype_map('Assignment Rule', 'due_date_rules_for_' + rule.document_type) diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index 830af68de7..281e699640 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -15,6 +15,8 @@ from frappe.model.document import Document from frappe.core.doctype.communication.email import make from frappe.utils.background_jobs import get_jobs from frappe.automation.doctype.assignment_rule.assignment_rule import get_repeated +from frappe.contacts.doctype.contact.contact import get_contacts_linked_from +from frappe.contacts.doctype.contact.contact import get_contacts_linking_to month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12} week_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6} @@ -328,13 +330,8 @@ class AutoRepeat(Document): def fetch_linked_contacts(self): if self.reference_doctype and self.reference_document: - res = frappe.db.get_all('Contact', - fields=['email_id'], - filters=[ - ['Dynamic Link', 'link_doctype', '=', self.reference_doctype], - ['Dynamic Link', 'link_name', '=', self.reference_document] - ]) - + res = get_contacts_linking_to(self.reference_doctype, self.reference_document, fields=['email_id']) + res += get_contacts_linked_from(self.reference_doctype, self.reference_document, fields=['email_id']) email_ids = list(set([d.email_id for d in res])) if not email_ids: frappe.msgprint(_('No contacts linked to document'), alert=True) diff --git a/frappe/boot.py b/frappe/boot.py index 8cf75e02bb..0dfcb8d1b4 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -21,7 +21,7 @@ from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabl from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points from frappe.model.base_document import get_controller from frappe.social.doctype.post.post import frequently_visited_links -from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings +from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings, get_app_logo def get_bootinfo(): """build and return boot info""" @@ -62,6 +62,7 @@ def get_bootinfo(): doclist.extend(get_meta_bundle("Page")) bootinfo.home_folder = frappe.db.get_value("File", {"is_home_folder": 1}) bootinfo.navbar_settings = get_navbar_settings() + bootinfo.notification_settings = get_notification_settings() # ipinfo if frappe.session.data.get('ipinfo'): @@ -90,6 +91,7 @@ def get_bootinfo(): bootinfo.link_preview_doctypes = get_link_preview_doctypes() bootinfo.additional_filters_config = get_additional_filters_from_hooks() bootinfo.desk_settings = get_desk_settings() + bootinfo.app_logo_url = get_app_logo() return bootinfo @@ -323,4 +325,7 @@ def get_desk_settings(): for key in desk_properties: desk_settings[key] = desk_settings.get(key) or role.get(key) - return desk_settings \ No newline at end of file + return desk_settings + +def get_notification_settings(): + return frappe.get_cached_doc('Notification Settings', frappe.session.user) diff --git a/frappe/build.py b/frappe/build.py index f47a7cb32b..baedb633b6 100644 --- a/frappe/build.py +++ b/frappe/build.py @@ -15,7 +15,7 @@ import frappe from frappe.utils.minify import JavascriptMinify import click -from requests import get +import psutil from six import iteritems, text_type from six.moves.urllib.parse import urlparse @@ -26,6 +26,8 @@ sites_path = os.path.abspath(os.getcwd()) def download_file(url, prefix): + from requests import get + filename = urlparse(url).path.split("/")[-1] local_filename = os.path.join(prefix, filename) with get(url, stream=True, allow_redirects=True) as r: @@ -225,7 +227,7 @@ def bundle(no_compress, app=None, make_copy=False, restore=False, verbose=False, frappe_app_path = os.path.abspath(os.path.join(app_paths[0], "..")) check_yarn() - frappe.commands.popen(command, cwd=frappe_app_path) + frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env()) def watch(no_compress): @@ -237,13 +239,32 @@ def watch(no_compress): frappe_app_path = os.path.abspath(os.path.join(app_paths[0], "..")) check_yarn() frappe_app_path = frappe.get_app_path("frappe", "..") - frappe.commands.popen("{pacman} run watch".format(pacman=pacman), cwd=frappe_app_path) + frappe.commands.popen("{pacman} run watch".format(pacman=pacman), + cwd=frappe_app_path, env=get_node_env()) def check_yarn(): if not find_executable("yarn"): print("Please install yarn using below command and try again.\nnpm install -g yarn") +def get_node_env(): + node_env = { + "NODE_OPTIONS": f"--max_old_space_size={get_safe_max_old_space_size()}" + } + return node_env + +def get_safe_max_old_space_size(): + safe_max_old_space_size = 0 + try: + total_memory = psutil.virtual_memory().total / (1024 * 1024) + # reference for the safe limit assumption + # https://nodejs.org/api/cli.html#cli_max_old_space_size_size_in_megabytes + # set minimum value 1GB + safe_max_old_space_size = max(1024, int(total_memory * 0.75)) + except Exception: + pass + + return safe_max_old_space_size def make_asset_dirs(make_copy=False, restore=False): # don't even think of making assets_path absolute - rm -rf ahead. diff --git a/frappe/commands/__init__.py b/frappe/commands/__init__.py index b7294fff77..b9ae02e112 100644 --- a/frappe/commands/__init__.py +++ b/frappe/commands/__init__.py @@ -11,6 +11,7 @@ import frappe.utils import subprocess # nosec from functools import wraps from six import StringIO +from os import environ click.disable_unicode_literals_warning = True @@ -53,16 +54,20 @@ def get_site(context, raise_err=True): return None def popen(command, *args, **kwargs): - output = kwargs.get('output', True) - cwd = kwargs.get('cwd') - shell = kwargs.get('shell', True) + output = kwargs.get('output', True) + cwd = kwargs.get('cwd') + shell = kwargs.get('shell', True) raise_err = kwargs.get('raise_err') + env = kwargs.get('env') + if env: + env = dict(environ, **env) proc = subprocess.Popen(command, - stdout = None if output else subprocess.PIPE, - stderr = None if output else subprocess.PIPE, - shell = shell, - cwd = cwd + stdout=None if output else subprocess.PIPE, + stderr=None if output else subprocess.PIPE, + shell=shell, + cwd=cwd, + env=env ) return_ = proc.wait() diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 4a631be3ac..0fadf2a294 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -9,7 +9,6 @@ import click import frappe from frappe.commands import get_site, pass_context from frappe.exceptions import SiteNotSpecifiedError -from frappe.installer import _new_site @click.command('new-site') @@ -31,6 +30,8 @@ def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin verbose=False, install_apps=None, source_sql=None, force=None, no_mariadb_socket=False, install_app=None, db_name=None, db_password=None, db_type=None, db_host=None, db_port=None): "Create a new site" + from frappe.installer import _new_site + frappe.init(site=site, new_site=True) _new_site(db_name, site, mariadb_root_username=mariadb_root_username, @@ -57,6 +58,7 @@ def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_password=None, db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None, with_private_files=None): "Restore site database from an sql file" from frappe.installer import ( + _new_site, extract_sql_from_archive, extract_files, is_downgrade, @@ -145,6 +147,8 @@ def reinstall(context, admin_password=None, mariadb_root_username=None, mariadb_ _reinstall(site, admin_password, mariadb_root_username, mariadb_root_password, yes, verbose=context.verbose) def _reinstall(site, admin_password=None, mariadb_root_username=None, mariadb_root_password=None, yes=False, verbose=False): + from frappe.installer import _new_site + if not yes: click.confirm('This will wipe your database. Are you sure you want to reinstall?', abort=True) try: diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index e9fa7217a8..5ff66171fc 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -293,7 +293,7 @@ def import_doc(context, path, force=False): try: frappe.init(site=site) frappe.connect() - import_doc(path, overwrite=context.force) + import_doc(path) finally: frappe.destroy() if not context.sites: @@ -483,7 +483,6 @@ def console(context): @click.option('--doctype', help="For DocType") @click.option('--doctype-list-path', help="Path to .txt file for list of doctypes. Example erpnext/tests/server/agriculture.txt") @click.option('--test', multiple=True, help="Specific test") -@click.option('--driver', help="For Travis") @click.option('--ui-tests', is_flag=True, default=False, help="Run UI Tests") @click.option('--module', help="Run tests in a module") @click.option('--profile', is_flag=True, default=False) @@ -493,9 +492,9 @@ def console(context): @click.option('--junit-xml-output', help="Destination file path for junit xml report") @click.option('--failfast', is_flag=True, default=False) @pass_context -def run_tests(context, app=None, module=None, doctype=None, test=(), - driver=None, profile=False, coverage=False, junit_xml_output=False, ui_tests = False, - doctype_list_path=None, skip_test_records=False, skip_before_tests=False, failfast=False): +def run_tests(context, app=None, module=None, doctype=None, test=(), profile=False, + coverage=False, junit_xml_output=False, ui_tests = False, doctype_list_path=None, + skip_test_records=False, skip_before_tests=False, failfast=False): "Run tests" import frappe.test_runner @@ -535,8 +534,8 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), cov.start() ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests, - force=context.force, profile=profile, junit_xml_output=junit_xml_output, - ui_tests=ui_tests, doctype_list_path=doctype_list_path, failfast=failfast) + force=context.force, profile=profile, junit_xml_output=junit_xml_output, + ui_tests=ui_tests, doctype_list_path=doctype_list_path, failfast=failfast) if coverage: cov.stop() @@ -578,7 +577,7 @@ def run_ui_tests(context, app, headless=False): frappe.commands.popen("yarn add cypress@^6 cypress-file-upload@^5 --no-lockfile") # run for headless mode - run_or_open = 'run --browser chrome --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open' + run_or_open = 'run --browser firefox --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open' command = '{site_env} {password_env} {cypress} {run_or_open}' formatted_command = command.format(site_env=site_env, password_env=password_env, cypress=cypress_path, run_or_open=run_or_open) diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index 987ba7d3d6..b3d4c6fc5c 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -97,11 +97,16 @@ class Contact(Document): if len([email.email_id for email in self.email_ids if email.is_primary]) > 1: frappe.throw(_("Only one {0} can be set as primary.").format(frappe.bold("Email ID"))) + primary_email_exists = False for d in self.email_ids: if d.is_primary == 1: + primary_email_exists = True self.email_id = d.email_id.strip() break + if not primary_email_exists: + self.email_id = "" + def set_primary(self, fieldname): # Used to set primary mobile and phone no. if len(self.phone_nos) == 0: @@ -115,11 +120,16 @@ class Contact(Document): if len(is_primary) > 1: frappe.throw(_("Only one {0} can be set as primary.").format(frappe.bold(frappe.unscrub(fieldname)))) + primary_number_exists = False for d in self.phone_nos: if d.get(field_name) == 1: + primary_number_exists = True setattr(self, fieldname, d.phone) break + if not primary_number_exists: + setattr(self, fieldname, "") + def get_default_contact(doctype, name): '''Returns default contact for the given doctype, name''' out = frappe.db.sql('''select parent, @@ -256,3 +266,27 @@ def get_contact_with_phone_number(number): def get_contact_name(email_id): contact = frappe.get_list("Contact Email", filters={"email_id": email_id}, fields=["parent"], limit=1) return contact[0].parent if contact else None + +def get_contacts_linking_to(doctype, docname, fields=None): + """Return a list of contacts containing a link to the given document.""" + return frappe.get_list('Contact', fields=fields, filters=[ + ['Dynamic Link', 'link_doctype', '=', doctype], + ['Dynamic Link', 'link_name', '=', docname] + ]) + +def get_contacts_linked_from(doctype, docname, fields=None): + """Return a list of contacts that are contained in (linked from) the given document.""" + link_fields = frappe.get_meta(doctype).get('fields', { + 'fieldtype': 'Link', + 'options': 'Contact' + }) + if not link_fields: + return [] + + contact_names = frappe.get_value(doctype, docname, fieldname=[f.fieldname for f in link_fields]) + if not contact_names: + return [] + + return frappe.get_list('Contact', fields=fields, filters={ + 'name': ('in', contact_names) + }) diff --git a/frappe/contacts/report/addresses_and_contacts/test_addresses_and_contacts.py b/frappe/contacts/report/addresses_and_contacts/test_addresses_and_contacts.py index 2db395102a..9e98dcf6f6 100644 --- a/frappe/contacts/report/addresses_and_contacts/test_addresses_and_contacts.py +++ b/frappe/contacts/report/addresses_and_contacts/test_addresses_and_contacts.py @@ -103,7 +103,7 @@ class TestAddressesAndContacts(unittest.TestCase): create_linked_contact(links_list, d) report_data = get_data({"reference_doctype": "Test Custom Doctype"}) for idx, link in enumerate(links_list): - test_item = [link, 'test address line 1', 'test address line 2', 'Milan', None, None, 'Italy', 0, '_Test First Name', '_Test Last Name', '_Test Address-Billing', '+91 0000000000', None, 'test_contact@example.com', 1] + test_item = [link, 'test address line 1', 'test address line 2', 'Milan', None, None, 'Italy', 0, '_Test First Name', '_Test Last Name', '_Test Address-Billing', '+91 0000000000', '', 'test_contact@example.com', 1] self.assertListEqual(test_item, report_data[idx]) def tearDown(self): diff --git a/frappe/core/doctype/activity_log/test_activity_log.py b/frappe/core/doctype/activity_log/test_activity_log.py index 4dbfd6700e..bd0ea08cc7 100644 --- a/frappe/core/doctype/activity_log/test_activity_log.py +++ b/frappe/core/doctype/activity_log/test_activity_log.py @@ -77,6 +77,10 @@ class TestActivityLog(unittest.TestCase): self.assertRaises(frappe.AuthenticationError, LoginManager) self.assertRaises(frappe.AuthenticationError, LoginManager) self.assertRaises(frappe.AuthenticationError, LoginManager) + + # REMOVE ME: current logic allows allow_consecutive_login_attempts+1 attempts + # before raising security exception, remove below line when that is fixed. + self.assertRaises(frappe.AuthenticationError, LoginManager) self.assertRaises(frappe.SecurityException, LoginManager) time.sleep(5) self.assertRaises(frappe.AuthenticationError, LoginManager) diff --git a/frappe/core/doctype/data_import/data_import.py b/frappe/core/doctype/data_import/data_import.py index 72de092461..a9761c3430 100644 --- a/frappe/core/doctype/data_import/data_import.py +++ b/frappe/core/doctype/data_import/data_import.py @@ -2,16 +2,16 @@ # Copyright (c) 2019, Frappe Technologies and contributors # For license information, please see license.txt -from __future__ import unicode_literals import os -import frappe -from frappe.model.document import Document -from frappe.core.doctype.data_import.importer import Importer +import frappe +from frappe import _ from frappe.core.doctype.data_import.exporter import Exporter +from frappe.core.doctype.data_import.importer import Importer +from frappe.model.document import Document +from frappe.modules.import_file import import_file_by_path from frappe.utils.background_jobs import enqueue from frappe.utils.csvutils import validate_google_sheets_url -from frappe import _ class DataImport(Document): @@ -173,15 +173,7 @@ def import_file( ############## -def import_doc( - path, - overwrite=False, - ignore_links=False, - ignore_insert=False, - insert=False, - submit=False, - pre_process=None, -): +def import_doc(path, pre_process=None): if os.path.isdir(path): files = [os.path.join(path, f) for f in os.listdir(path)] else: @@ -190,30 +182,21 @@ def import_doc( for f in files: if f.endswith(".json"): frappe.flags.mute_emails = True - frappe.modules.import_file.import_file_by_path( - f, data_import=True, force=True, pre_process=pre_process, reset_permissions=True + import_file_by_path( + f, + data_import=True, + force=True, + pre_process=pre_process, + reset_permissions=True ) frappe.flags.mute_emails = False frappe.db.commit() elif f.endswith(".csv"): - import_file_by_path( - f, - ignore_links=ignore_links, - overwrite=overwrite, - submit=submit, - pre_process=pre_process, - ) + validate_csv_import_file(f) frappe.db.commit() -def import_file_by_path( - path, - ignore_links=False, - overwrite=False, - submit=False, - pre_process=None, - no_email=True, -): +def validate_csv_import_file(path): if path.endswith(".csv"): print() print("This method is deprecated.") diff --git a/frappe/core/doctype/data_import/exporter.py b/frappe/core/doctype/data_import/exporter.py index 66e32a1270..acaa294a6f 100644 --- a/frappe/core/doctype/data_import/exporter.py +++ b/frappe/core/doctype/data_import/exporter.py @@ -2,13 +2,15 @@ # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt +import typing + import frappe from frappe.model import ( display_fieldtypes, no_value_fields, table_fields as table_fieldtypes, ) -from frappe.utils import flt, format_duration +from frappe.utils import flt, format_duration, groupby_metric from frappe.utils.csvutils import build_csv_response from frappe.utils.xlsxutils import build_xlsx_response @@ -116,7 +118,6 @@ class Exporter: def get_data_to_export(self): frappe.permissions.can_export(self.doctype, raise_exception=True) - data_to_export = [] table_fields = [f for f in self.exportable_fields if f != self.doctype] data = self.get_data_as_docs() @@ -128,14 +129,13 @@ class Exporter: if table_fields: # add child table data for f in table_fields: - for i, child_row in enumerate(doc[f]): + for i, child_row in enumerate(doc.get(f, [])): table_df = self.meta.get_field(f) child_doctype = table_df.options rows = self.add_data_row(child_doctype, child_row.parentfield, child_row, rows, i) - data_to_export += rows - - return data_to_export + for row in rows: + yield row def add_data_row(self, doctype, parentfield, doc, rows, row_idx): if len(rows) < row_idx + 1: @@ -204,17 +204,13 @@ class Exporter: ) child_data[key] = data - return self.merge_data(parent_data, child_data) - - def merge_data(self, parent_data, child_data): + # Group children data by parent name + grouped_children_data = self.group_children_data_by_parent(child_data) for doc in parent_data: - for table_field, table_rows in child_data.items(): - doc[table_field] = [row for row in table_rows if row.parent == doc.name] - - return parent_data + related_children_docs = grouped_children_data.get(doc.name, {}) + yield {**doc, **related_children_docs} def add_header(self): - header = [] for df in self.fields: is_parent = not df.is_child_table_field @@ -261,3 +257,6 @@ class Exporter: def build_xlsx_response(self): build_xlsx_response(self.get_csv_array_for_export(), self.doctype) + + def group_children_data_by_parent(self, children_data: typing.Dict[str, list]): + return groupby_metric(children_data, key='parent') diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index dde3dfaee9..388d9389f2 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -472,32 +472,6 @@ class ImportFile: doc = parent_doc - if self.import_type == INSERT: - # check if there is atleast one row for mandatory table fields - meta = frappe.get_meta(self.doctype) - mandatory_table_fields = [ - df - for df in meta.fields - if df.fieldtype in table_fieldtypes - and df.reqd - and len(doc.get(df.fieldname, [])) == 0 - ] - if len(mandatory_table_fields) == 1: - self.warnings.append( - { - "row": first_row.row_number, - "message": _("There should be atleast one row for {0} table").format( - frappe.bold(mandatory_table_fields[0].label) - ), - } - ) - elif mandatory_table_fields: - fields_string = ", ".join([df.label for df in mandatory_table_fields]) - message = _("There should be atleast one row for the following tables: {0}").format( - fields_string - ) - self.warnings.append({"row": first_row.row_number, "message": message}) - return doc, rows, data[len(rows) :] def get_warnings(self): @@ -626,7 +600,6 @@ class Row: new_doc.update(doc) doc = new_doc - self.check_mandatory_fields(doctype, doc, table_df) return doc def validate_value(self, value, col): @@ -727,66 +700,6 @@ class Row: pass return value - def check_mandatory_fields(self, doctype, doc, table_df=None): - """If import type is Insert: - Check for mandatory fields (except table fields) in doc - if import type is Update: - Check for name field or autoname field in doc - """ - meta = frappe.get_meta(doctype) - if self.import_type == UPDATE: - if meta.istable: - # when updating records with table rows, - # there are two scenarios: - # 1. if row 'name' is provided in the template - # the table row will be updated - # 2. if row 'name' is not provided - # then a new row will be added - # so we dont need to check for mandatory - return - - # for update, only ID (name) field is mandatory - id_field = get_id_field(doctype) - if doc.get(id_field.fieldname) in INVALID_VALUES: - self.warnings.append( - { - "row": self.row_number, - "message": _("{0} is a mandatory field").format(id_field.label), - } - ) - return - - fields = [ - df - for df in meta.fields - if df.fieldtype not in table_fieldtypes - and df.reqd - and doc.get(df.fieldname) in INVALID_VALUES - ] - - if not fields: - return - - def get_field_label(df): - return "{0}{1}".format(df.label, " ({})".format(table_df.label) if table_df else "") - - if len(fields) == 1: - field_label = get_field_label(fields[0]) - self.warnings.append( - { - "row": self.row_number, - "message": _("{0} is a mandatory field").format(frappe.bold(field_label)), - } - ) - else: - fields_string = ", ".join([frappe.bold(get_field_label(df)) for df in fields]) - self.warnings.append( - { - "row": self.row_number, - "message": _("{0} are mandatory fields").format(fields_string), - } - ) - def get_values(self, indexes): return [self.data[i] for i in indexes] diff --git a/frappe/core/doctype/data_import/test_importer.py b/frappe/core/doctype/data_import/test_importer.py index b083b9eaaa..f76d4504a4 100644 --- a/frappe/core/doctype/data_import/test_importer.py +++ b/frappe/core/doctype/data_import/test_importer.py @@ -13,7 +13,7 @@ doctype_name = 'DocType for Import' class TestImporter(unittest.TestCase): @classmethod def setUpClass(cls): - create_doctype_if_not_exists(doctype_name) + create_doctype_if_not_exists(doctype_name,) def test_data_import_from_file(self): import_file = get_import_file('sample_import_file') @@ -59,18 +59,18 @@ class TestImporter(unittest.TestCase): def test_data_import_without_mandatory_values(self): import_file = get_import_file('sample_import_file_without_mandatory') data_import = self.get_importer(doctype_name, import_file) + frappe.local.message_log = [] data_import.start_import() data_import.reload() - warnings = frappe.parse_json(data_import.template_warnings) + import_log = frappe.parse_json(data_import.import_log) + self.assertEqual(import_log[0]['row_indexes'], [2,3]) + expected_error = "Error: Child 1 of DocType for Import Row #1: Value missing for: Child Title" + self.assertEqual(frappe.parse_json(import_log[0]['messages'][0])['message'], expected_error) + expected_error = "Error: Child 1 of DocType for Import Row #2: Value missing for: Child Title" + self.assertEqual(frappe.parse_json(import_log[0]['messages'][1])['message'], expected_error) - self.assertEqual(warnings[0]['row'], 2) - self.assertEqual(warnings[0]['message'], "Child Title (Table Field 1) is a mandatory field") - - self.assertEqual(warnings[1]['row'], 3) - self.assertEqual(warnings[1]['message'], "Child Title (Table Field 1 Again) is a mandatory field") - - self.assertEqual(warnings[2]['row'], 4) - self.assertEqual(warnings[2]['message'], "Title is a mandatory field") + self.assertEqual(import_log[1]['row_indexes'], [4]) + self.assertEqual(frappe.parse_json(import_log[1]['messages'][0])['message'], "Title is required") def test_data_import_update(self): existing_doc = frappe.get_doc( @@ -104,6 +104,8 @@ class TestImporter(unittest.TestCase): data_import.reference_doctype = doctype data_import.import_file = import_file.file_url data_import.insert() + # Commit so that the first import failure does not rollback the Data Import insert. + frappe.db.commit() return data_import diff --git a/frappe/core/doctype/data_import_legacy/data_import_legacy.py b/frappe/core/doctype/data_import_legacy/data_import_legacy.py index df3a3edd3a..63f806d75b 100644 --- a/frappe/core/doctype/data_import_legacy/data_import_legacy.py +++ b/frappe/core/doctype/data_import_legacy/data_import_legacy.py @@ -2,20 +2,22 @@ # Copyright (c) 2017, Frappe Technologies and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe, os -from frappe import _ +import os + +import frappe import frappe.modules.import_file -from frappe.model.document import Document -from frappe.utils.data import format_datetime +from frappe import _ from frappe.core.doctype.data_import_legacy.importer import upload +from frappe.model.document import Document +from frappe.modules.import_file import import_file_by_path as _import_file_by_path from frappe.utils.background_jobs import enqueue +from frappe.utils.data import format_datetime class DataImportLegacy(Document): def autoname(self): if not self.name: - self.name = "Import on " +format_datetime(self.creation) + self.name = "Import on " + format_datetime(self.creation) def validate(self): if not self.import_file: @@ -33,6 +35,7 @@ class DataImportLegacy(Document): def get_importable_doctypes(): return frappe.cache().hget("can_import", frappe.session.user) + @frappe.whitelist() def import_data(data_import): frappe.db.set_value("Data Import Legacy", data_import, "import_status", "In Progress", update_modified=False) @@ -57,7 +60,7 @@ def import_doc(path, overwrite=False, ignore_links=False, ignore_insert=False, for f in files: if f.endswith(".json"): frappe.flags.mute_emails = True - frappe.modules.import_file.import_file_by_path(f, data_import=True, force=True, pre_process=pre_process, reset_permissions=True) + _import_file_by_path(f, data_import=True, force=True, pre_process=pre_process, reset_permissions=True) frappe.flags.mute_emails = False frappe.db.commit() elif f.endswith(".csv"): @@ -69,7 +72,7 @@ def import_file_by_path(path, ignore_links=False, overwrite=False, submit=False, from frappe.utils.csvutils import read_csv_content print("Importing " + path) with open(path, "r") as infile: - upload(rows = read_csv_content(infile.read()), ignore_links=ignore_links, no_email=no_email, overwrite=overwrite, + upload(rows=read_csv_content(infile.read()), ignore_links=ignore_links, no_email=no_email, overwrite=overwrite, submit_after_import=submit, pre_process=pre_process) diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index 92d05869a0..276ce7bee7 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -562,7 +562,7 @@ }, { "group": "Customization", - "link_doctype": "Custom Script", + "link_doctype": "Client Script", "link_fieldname": "dt" }, { @@ -616,7 +616,7 @@ "link_fieldname": "reference_doctype" } ], - "modified": "2020-12-23 23:48:33.752219", + "modified": "2021-02-17 20:18:06.212232", "modified_by": "Administrator", "module": "Core", "name": "DocType", diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index b32afb891b..a95d00f9b8 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -659,7 +659,7 @@ class DocType(Document): flags = {"flags": re.ASCII} if six.PY3 else {} # a DocType name should not start or end with an empty space - if re.match("^[ \t\n\r]+|[ \t\n\r]+$", name, **flags): + if re.search("^[ \t\n\r]+|[ \t\n\r]+$", name, **flags): frappe.throw(_("DocType's name should not start or end with whitespace"), frappe.NameError) # a DocType's name should not start with a number or underscore diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 445ca1184d..c237b8e436 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -75,7 +75,7 @@ class File(Document): self.add_comment_in_reference_doc('Attachment', _('Added {0}').format("{file_name}{icon}".format(**{ "icon": ' ' if self.is_private else "", - "file_url": quote(self.file_url) if self.file_url else self.file_name, + "file_url": quote(frappe.safe_encode(self.file_url)) if self.file_url else self.file_name, "file_name": self.file_name or self.file_url }))) diff --git a/frappe/core/doctype/navbar_settings/navbar_settings.py b/frappe/core/doctype/navbar_settings/navbar_settings.py index db510981a4..2244bc9e4e 100644 --- a/frappe/core/doctype/navbar_settings/navbar_settings.py +++ b/frappe/core/doctype/navbar_settings/navbar_settings.py @@ -25,7 +25,7 @@ class NavbarSettings(Document): @frappe.whitelist(allow_guest=True) def get_app_logo(): - app_logo = frappe.db.get_single_value('Navbar Settings', 'app_logo') + app_logo = frappe.db.get_single_value('Navbar Settings', 'app_logo', cache=True) if not app_logo: app_logo = frappe.get_hooks('app_logo_url')[-1] 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 0d6aa3d7d1..92493a593a 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -3,6 +3,7 @@ # For license information, please see license.txt from __future__ import unicode_literals +from typing import Dict, List import frappe, json from frappe.model.document import Document @@ -11,12 +12,13 @@ from datetime import datetime from croniter import croniter from frappe.utils.background_jobs import enqueue, get_jobs + class ScheduledJobType(Document): def autoname(self): - self.name = '.'.join(self.method.split('.')[-2:]) + self.name = ".".join(self.method.split(".")[-2:]) def validate(self): - if self.frequency != 'All': + if self.frequency != "All": # force logging for all events other than continuous ones (ALL) self.create_log = 1 @@ -84,7 +86,7 @@ class ScheduledJobType(Document): def log_status(self, status): # log file - frappe.logger("scheduler").info('Scheduled Job {0}: {1} for {2}'.format(status, self.method, frappe.local.site)) + frappe.logger("scheduler").info(f"Scheduled Job {status}: {self.method} for {frappe.local.site}") self.update_scheduler_log(status) def update_scheduler_log(self, status): @@ -111,28 +113,29 @@ class ScheduledJobType(Document): @frappe.whitelist() -def execute_event(doc): - frappe.only_for('System Manager') +def execute_event(doc: str): + frappe.only_for("System Manager") doc = json.loads(doc) - frappe.get_doc('Scheduled Job Type', doc.get('name')).enqueue(force=True) + frappe.get_doc("Scheduled Job Type", doc.get("name")).enqueue(force=True) + return doc -def run_scheduled_job(job_type): - '''This is a wrapper function that runs a hooks.scheduler_events method''' +def run_scheduled_job(job_type: str): + """This is a wrapper function that runs a hooks.scheduler_events method""" try: - frappe.get_doc('Scheduled Job Type', dict(method=job_type)).execute() + frappe.get_doc("Scheduled Job Type", dict(method=job_type)).execute() except Exception: print(frappe.get_traceback()) -def sync_jobs(hooks=None): +def sync_jobs(hooks: Dict = None): frappe.reload_doc("core", "doctype", "scheduled_job_type") scheduler_events = hooks or frappe.get_hooks("scheduler_events") all_events = insert_events(scheduler_events) clear_events(all_events) -def insert_events(scheduler_events): +def insert_events(scheduler_events: Dict) -> List: cron_jobs, event_jobs = [], [] for event_type in scheduler_events: events = scheduler_events.get(event_type) @@ -144,7 +147,7 @@ def insert_events(scheduler_events): return cron_jobs + event_jobs -def insert_cron_jobs(events): +def insert_cron_jobs(events: Dict) -> List: cron_jobs = [] for cron_format in events: for event in events.get(cron_format): @@ -153,25 +156,29 @@ def insert_cron_jobs(events): return cron_jobs -def insert_event_jobs(events, event_type): +def insert_event_jobs(events: List, event_type: str) -> List: event_jobs = [] for event in events: event_jobs.append(event) - frequency = event_type.replace('_', ' ').title() + frequency = event_type.replace("_", " ").title() insert_single_event(frequency, event) return event_jobs -def insert_single_event(frequency, event, cron_format=None): +def insert_single_event(frequency: str, event: str, cron_format: str = None): cron_expr = {"cron_format": cron_format} if cron_format else {} - doc = frappe.get_doc({ - "doctype": "Scheduled Job Type", - "method": event, - "cron_format": cron_format, - "frequency": frequency - }) + doc = frappe.get_doc( + { + "doctype": "Scheduled Job Type", + "method": event, + "cron_format": cron_format, + "frequency": frequency, + } + ) - if not frappe.db.exists("Scheduled Job Type", {"method": event, "frequency": frequency, **cron_expr }): + if not frappe.db.exists( + "Scheduled Job Type", {"method": event, "frequency": frequency, **cron_expr} + ): try: doc.insert() except frappe.DuplicateEntryError: @@ -179,7 +186,12 @@ def insert_single_event(frequency, event, cron_format=None): doc.insert() -def clear_events(all_events): - for event in frappe.get_all("Scheduled Job Type", ("name", "method")): - if event.method not in all_events: +def clear_events(all_events: List): + for event in frappe.get_all( + "Scheduled Job Type", fields=["name", "method", "server_script"] + ): + is_server_script = event.server_script + is_defined_in_hooks = event.method in all_events + + if not (is_defined_in_hooks or is_server_script): frappe.delete_doc("Scheduled Job Type", event.name) diff --git a/frappe/core/doctype/server_script/server_script.js b/frappe/core/doctype/server_script/server_script.js index a317d69166..95a63780f8 100644 --- a/frappe/core/doctype/server_script/server_script.js +++ b/frappe/core/doctype/server_script/server_script.js @@ -6,46 +6,11 @@ frappe.ui.form.on('Server Script', { frm.trigger('setup_help'); }, 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(); - - }); + if (frm.doc.script_type != 'Scheduler Event') { + frm.dashboard.hide(); } }, - 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 - } - }); - }, - setup_help(frm) { frm.get_field('help_html').html(`

DocType Event

diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json index 9aa7b5afe5..b7e49673f8 100644 --- a/frappe/core/doctype/server_script/server_script.json +++ b/frappe/core/doctype/server_script/server_script.json @@ -8,6 +8,7 @@ "field_order": [ "script_type", "reference_doctype", + "event_frequency", "doctype_event", "api_method", "allow_guest", @@ -84,11 +85,24 @@ { "fieldname": "help_html", "fieldtype": "HTML" + }, + { + "depends_on": "eval:doc.script_type == \"Scheduler Event\"", + "fieldname": "event_frequency", + "fieldtype": "Select", + "label": "Event Frequency", + "mandatory_depends_on": "eval:doc.script_type == \"Scheduler Event\"", + "options": "All\nHourly\nDaily\nWeekly\nMonthly\nYearly\nHourly Long\nDaily Long\nWeekly Long\nMonthly Long" } ], "index_web_pages_for_search": 1, - "links": [], - "modified": "2021-01-03 18:50:14.767595", + "links": [ + { + "link_doctype": "Scheduled Job Type", + "link_fieldname": "server_script" + } + ], + "modified": "2021-02-18 12:36:19.803425", "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 88d68dba14..8838d9e954 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import ast +from typing import Dict, List import frappe from frappe.model.document import Document @@ -14,67 +15,146 @@ from frappe import _ class ServerScript(Document): def validate(self): - frappe.only_for('Script Manager', True) + frappe.only_for("Script Manager", True) + self.validate_script() + self.sync_scheduled_jobs() + self.clear_scheduled_events() + + def on_update(self): + frappe.cache().delete_value("server_script_map") + self.sync_scheduler_events() + + def on_trash(self): + if self.script_type == "Scheduler Event": + for job in self.scheduled_jobs: + frappe.delete_doc("Scheduled Job Type", job.name) + + @property + def scheduled_jobs(self) -> List[Dict[str, str]]: + return frappe.get_all( + "Scheduled Job Type", + filters={"server_script": self.name}, + fields=["name", "stopped"], + ) + + def validate_script(self): + """Utilizes the ast module to check for syntax errors + """ ast.parse(self.script) - @staticmethod - def on_update(): - frappe.cache().delete_value('server_script_map') + def sync_scheduled_jobs(self): + """Sync Scheduled Job Type statuses if Server Script's disabled status is changed + """ + if self.script_type != "Scheduler Event" or not self.has_value_changed("disabled"): + return - def execute_method(self): - if self.script_type == 'API': - # validate if guest is allowed - if frappe.session.user == 'Guest' and not self.allow_guest: - raise frappe.PermissionError - _globals, _locals = safe_exec(self.script) - return _globals.frappe.flags # output can be stored in flags - else: - # wrong report type! + for scheduled_job in self.scheduled_jobs: + if bool(scheduled_job.stopped) != bool(self.disabled): + job = frappe.get_doc("Scheduled Job Type", scheduled_job.name) + job.stopped = self.disabled + job.save() + + def sync_scheduler_events(self): + """Create or update Scheduled Job Type documents for Scheduler Event Server Scripts + """ + if not self.disabled and self.event_frequency and self.script_type == "Scheduler Event": + setup_scheduler_events(script_name=self.name, frequency=self.event_frequency) + + def clear_scheduled_events(self): + """Deletes existing scheduled jobs by Server Script if self.event_frequency has changed + """ + if self.script_type == "Scheduler Event" and self.has_value_changed("event_frequency"): + for scheduled_job in self.scheduled_jobs: + frappe.delete_doc("Scheduled Job Type", scheduled_job.name) + + def execute_method(self) -> Dict: + """Specific to API endpoint Server Scripts + + Raises: + frappe.DoesNotExistError: If self.script_type is not API + frappe.PermissionError: If self.allow_guest is unset for API accessed by Guest user + + Returns: + dict: Evaluates self.script with frappe.utils.safe_exec.safe_exec and returns the flags set in it's safe globals + """ + # wrong report type! + if self.script_type != "API": raise frappe.DoesNotExistError - def execute_doc(self, doc): - # execute event - safe_exec(self.script, None, dict(doc = doc)) + # validate if guest is allowed + if frappe.session.user == "Guest" and not self.allow_guest: + raise frappe.PermissionError + + # output can be stored in flags + _globals, _locals = safe_exec(self.script) + return _globals.frappe.flags + + def execute_doc(self, doc: Document): + """Specific to Document Event triggered Server Scripts + + Args: + doc (Document): Executes script with for a certain document's events + """ + safe_exec(self.script, _locals={"doc": doc}) def execute_scheduled_method(self): - if self.script_type == 'Scheduler Event': - safe_exec(self.script) - else: - # wrong report type! + """Specific to Scheduled Jobs via Server Scripts + + Raises: + frappe.DoesNotExistError: If script type is not a scheduler event + """ + if self.script_type != "Scheduler Event": raise frappe.DoesNotExistError - def get_permission_query_conditions(self, user): + safe_exec(self.script) + + def get_permission_query_conditions(self, user: str) -> List[str]: + """Specific to Permission Query Server Scripts + + Args: + user (str): Takes user email to execute script and return list of conditions + + Returns: + list: Returns list of conditions defined by rules in self.script + """ locals = {"user": user, "conditions": ""} safe_exec(self.script, None, locals) if locals["conditions"]: return locals["conditions"] + @frappe.whitelist() def setup_scheduler_events(script_name, frequency): - method = frappe.scrub('{0}-{1}'.format(script_name, frequency)) - scheduled_script = frappe.db.get_value('Scheduled Job Type', - dict(method=method)) + """Creates or Updates Scheduled Job Type documents based on the specified script name and frequency + + Args: + script_name (str): Name of the Server Script document + frequency (str): Event label compatible with the Frappe scheduler + """ + method = frappe.scrub(f"{script_name}-{frequency}") + scheduled_script = frappe.db.get_value("Scheduled Job Type", {"method": method}) if not scheduled_script: - doc = frappe.get_doc(dict( - doctype = 'Scheduled Job Type', - method = method, - frequency = frequency, - server_script = script_name - )) + frappe.get_doc( + { + "doctype": "Scheduled Job Type", + "method": method, + "frequency": frequency, + "server_script": script_name, + } + ).insert() - doc.insert() - - frappe.msgprint(_('Enabled scheduled execution for script {0}').format(script_name)) + 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 = frappe.get_doc("Scheduled Job Type", scheduled_script) + + if doc.frequency == frequency: + return + + doc.frequency = frequency doc.save() - frappe.msgprint(_('Scheduled execution for script {0} has updated').format(script_name)) + frappe.msgprint( + _("Scheduled execution for script {0} has updated").format(script_name) + ) diff --git a/frappe/core/doctype/sms_settings/sms_settings.json b/frappe/core/doctype/sms_settings/sms_settings.json index 3bb89604af..073fb88bc7 100755 --- a/frappe/core/doctype/sms_settings/sms_settings.json +++ b/frappe/core/doctype/sms_settings/sms_settings.json @@ -202,7 +202,7 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-11-01 12:57:20.943845", + "modified": "2021-03-02 18:06:00.868688", "modified_by": "Administrator", "module": "Core", "name": "SMS Settings", @@ -233,6 +233,6 @@ "read_only": 0, "read_only_onload": 0, "show_name_in_global_search": 0, - "track_changes": 0, + "track_changes": 1, "track_seen": 0 -} \ No newline at end of file +} diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py index 77b61f22bb..8a8071423e 100644 --- a/frappe/core/doctype/user/test_user.py +++ b/frappe/core/doctype/user/test_user.py @@ -2,7 +2,7 @@ # MIT License. See license.txt from __future__ import unicode_literals -import frappe, unittest +import frappe, unittest, uuid from frappe.model.delete_doc import delete_doc from frappe.utils.data import today, add_to_date @@ -11,6 +11,7 @@ from frappe.utils import get_url from frappe.core.doctype.user.user import get_total_users from frappe.core.doctype.user.user import MaxUsersReachedError, test_password_strength from frappe.core.doctype.user.user import extract_mentions +from frappe.frappeclient import FrappeClient test_records = frappe.get_test_records('User') @@ -229,16 +230,45 @@ class TestUser(unittest.TestCase): self.assertEqual(extract_mentions(comment)[1], "test.again@example1.com") def test_rate_limiting_for_reset_password(self): - from frappe.utils.password import delete_password_reset_cache - delete_password_reset_cache() - + # Allow only one reset request for a day frappe.db.set_value("System Settings", "System Settings", "password_reset_limit", 1) + frappe.db.commit() - user = frappe.get_doc("User", "testperm@example.com") - link = user.reset_password() - self.assertRegex(link, "\/update-password\?key=[A-Za-z0-9]*") + url = get_url() + data={'cmd': 'frappe.core.doctype.user.user.reset_password', 'user': 'test@test.com'} - self.assertRaises(frappe.ValidationError, user.reset_password, False) + # Clear rate limit tracker to start fresh + key = f"rl:{data['cmd']}:{data['user']}" + frappe.cache().delete(key) + + c = FrappeClient(url) + res1 = c.session.post(url, data=data, verify=c.verify, headers=c.headers) + res2 = c.session.post(url, data=data, verify=c.verify, headers=c.headers) + self.assertEqual(res1.status_code, 200) + self.assertEqual(res2.status_code, 417) + + def test_user_rollback(self): + """ """ + frappe.db.commit() + frappe.db.begin() + user_id = str(uuid.uuid4()) + email = f'{user_id}@example.com' + try: + frappe.flags.in_import = True # disable throttling + frappe.get_doc(dict( + doctype='User', + email=email, + first_name=user_id, + )).insert() + finally: + frappe.flags.in_import = False + + # Check user has been added + self.assertIsNotNone(frappe.db.get("User", {"email": email})) + + # Check that rollback works + frappe.db.rollback() + self.assertIsNone(frappe.db.get("User", {"email": email})) def delete_contact(user): diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 99d1deeb03..747ace5de6 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -302,7 +302,7 @@ "no_copy": 1 }, { - "default": "0", + "default": "1", "fieldname": "logout_all_sessions", "fieldtype": "Check", "label": "Logout From All Devices After Changing Password" @@ -669,7 +669,7 @@ } ], "max_attachments": 5, - "modified": "2021-01-02 11:21:50.507786", + "modified": "2021-02-01 16:11:06.037543", "modified_by": "Administrator", "module": "Core", "name": "User", diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 5a35907ccf..c103ad7e4a 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -2,21 +2,25 @@ # MIT License. See license.txt from __future__ import unicode_literals, print_function + +from bs4 import BeautifulSoup + import frappe +import frappe.share +import frappe.defaults +import frappe.permissions from frappe.model.document import Document from frappe.utils import cint, flt, has_gravatar, escape_html, format_datetime, now_datetime, get_formatted_email, today from frappe import throw, msgprint, _ -from frappe.utils.password import update_password as _update_password +from frappe.utils.password import update_password as _update_password, check_password, get_password_reset_limit from frappe.desk.notifications import clear_notifications -from frappe.desk.doctype.notification_settings.notification_settings import create_notification_settings +from frappe.desk.doctype.notification_settings.notification_settings import create_notification_settings, toggle_notifications from frappe.utils.user import get_system_managers -from bs4 import BeautifulSoup -import frappe.permissions -import frappe.share -import frappe.defaults from frappe.website.utils import is_signup_enabled +from frappe.rate_limiter import rate_limit from frappe.utils.background_jobs import enqueue + STANDARD_USERS = ("Guest", "Administrator") @@ -146,6 +150,9 @@ class User(Document): if not cint(self.enabled) and getattr(frappe.local, "login_manager", None): frappe.local.login_manager.logout(user=self.name) + # toggle notifications based on the user's status + toggle_notifications(self.name, enable=cint(self.enabled)) + def add_system_manager_role(self): # if adding system manager, do nothing if not cint(self.enabled) or ("System Manager" in [user_role.role for user_role in @@ -238,11 +245,6 @@ class User(Document): def reset_password(self, send_email=False, password_expired=False): from frappe.utils import random_string, get_url - rate_limit = frappe.db.get_single_value("System Settings", "password_reset_limit") - - if rate_limit: - check_password_reset_limit(self.name, rate_limit) - key = random_string(32) self.db_set("reset_password_key", key) @@ -254,7 +256,6 @@ class User(Document): if send_email: self.password_reset_mail(link) - update_password_reset_limit(self.name) return link def get_other_system_managers(self): @@ -358,6 +359,9 @@ class User(Document): set `user`=null where `user`=%s""", (self.name)) + # delete notification settings + frappe.delete_doc("Notification Settings", self.name, ignore_permissions=True) + def before_rename(self, old_name, new_name, merge=False): self.check_demo() @@ -527,6 +531,27 @@ class User(Document): return [i.strip() for i in self.restrict_ip.split(",")] + @classmethod + def find_by_credentials(cls, user_name: str, password: str, validate_password: bool = True): + """Find the user by credentials. + """ + login_with_mobile = cint(frappe.db.get_value("System Settings", "System Settings", "allow_login_using_mobile_number")) + filter = {"mobile_no": user_name} if login_with_mobile else {"name": user_name} + + user = frappe.db.get_value("User", filters=filter, fieldname=['name', 'enabled'], as_dict=True) or {} + if not user: + return + + user['is_authenticated'] = True + if validate_password: + try: + check_password(user_name, password) + except frappe.AuthenticationError: + user['is_authenticated'] = False + + return user + + @frappe.whitelist() def get_timezones(): import pytz @@ -562,6 +587,10 @@ def get_perm_info(role): @frappe.whitelist(allow_guest=True) def update_password(new_password, logout_all_sessions=0, key=None, old_password=None): + #validate key to avoid key input like ['like', '%'], '', ['in', ['']] + if key and not isinstance(key, str): + frappe.throw(_('Invalid key type')) + result = test_password_strength(new_password, key, old_password) feedback = result.get("feedback", None) @@ -812,6 +841,7 @@ def sign_up(email, full_name, redirect_to): return 2, _("Please ask your administrator to verify your sign-up") @frappe.whitelist(allow_guest=True) +@rate_limit(key='user', limit=get_password_reset_limit, seconds = 24*60*60, methods=['POST']) def reset_password(user): if user=="Administrator": return 'not allowed' @@ -1143,16 +1173,3 @@ def generate_keys(user): def switch_theme(theme): if theme in ["Dark", "Light"]: frappe.db.set_value("User", frappe.session.user, "desk_theme", theme) - -def update_password_reset_limit(user): - generated_link_count = get_generated_link_count(user) - generated_link_count += 1 - frappe.cache().hset("password_reset_link_count", user, generated_link_count) - -def check_password_reset_limit(user, rate_limit): - generated_link_count = get_generated_link_count(user) - if generated_link_count >= rate_limit: - frappe.throw(_("You have reached the hourly limit for generating password reset links. Please try again later.")) - -def get_generated_link_count(user): - return cint(frappe.cache().hget("password_reset_link_count", user)) or 0 diff --git a/frappe/core/page/background_jobs/background_jobs.js b/frappe/core/page/background_jobs/background_jobs.js index cabe91375f..0b4d6792dc 100644 --- a/frappe/core/page/background_jobs/background_jobs.js +++ b/frappe/core/page/background_jobs/background_jobs.js @@ -28,6 +28,16 @@ class BackgroundJobs { } }); + // add a "Remove Failed Jobs button" + this.remove_failed_button = this.page.add_inner_button(__("Remove Failed Jobs"), () => { + frappe.call({ + method: 'frappe.core.page.background_jobs.background_jobs.remove_failed_jobs', + callback: () => { + this.refresh_jobs(); + } + }); + }); + $(frappe.render_template('background_jobs_outer')).appendTo(this.page.body); this.content = $(this.page.body).find('.table-area'); } @@ -62,4 +72,4 @@ class BackgroundJobs { } }); } -} \ No newline at end of file +} diff --git a/frappe/core/page/background_jobs/background_jobs.py b/frappe/core/page/background_jobs/background_jobs.py index 4a94de4ace..847b23bd3e 100644 --- a/frappe/core/page/background_jobs/background_jobs.py +++ b/frappe/core/page/background_jobs/background_jobs.py @@ -1,58 +1,88 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt -from __future__ import unicode_literals -import frappe +import json +from typing import TYPE_CHECKING, Dict, List from rq import Queue, Worker -from frappe.utils.background_jobs import get_redis_conn -from frappe.utils import format_datetime, cint, convert_utc_to_user_timezone -from frappe.utils.scheduler import is_scheduler_inactive -from frappe import _ -colors = { +import frappe +from frappe import _ +from frappe.utils import convert_utc_to_user_timezone, format_datetime +from frappe.utils.background_jobs import get_redis_conn +from frappe.utils.scheduler import is_scheduler_inactive + +if TYPE_CHECKING: + from rq.job import Job + +JOB_COLORS = { 'queued': 'orange', 'failed': 'red', 'started': 'blue', 'finished': 'green' } + @frappe.whitelist() -def get_info(show_failed=False): +def get_info(show_failed=False) -> List[Dict]: + if isinstance(show_failed, str): + show_failed = json.loads(show_failed) + conn = get_redis_conn() queues = Queue.all(conn) workers = Worker.all(conn) jobs = [] - def add_job(j, name): - 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)), - 'color': colors[j.get_status()] - }) - if j.exc_info: - jobs[-1]['exc_info'] = j.exc_info + def add_job(job: 'Job', name: str) -> None: + if job.kwargs.get('site') == frappe.local.site: + job_info = { + 'job_name': job.kwargs.get('kwargs', {}).get('playbook_method') + or job.kwargs.get('kwargs', {}).get('job_type') + or str(job.kwargs.get('job_name')), + 'status': job.get_status(), + 'queue': name, + 'creation': format_datetime(convert_utc_to_user_timezone(job.created_at)), + 'color': JOB_COLORS[job.get_status()] + } - for w in workers: - j = w.get_current_job() - if j: - add_job(j, w.name) + if job.exc_info: + job_info['exc_info'] = job.exc_info - for q in queues: - if q.name != 'failed': - for j in q.get_jobs(): add_job(j, q.name) + jobs.append(job_info) - if cint(show_failed): - for q in queues: - if q.name == 'failed': - for j in q.get_jobs()[:10]: add_job(j, q.name) + # show worker jobs + for worker in workers: + job = worker.get_current_job() + if job: + add_job(job, worker.name) + + for queue in queues: + # show active queued jobs + if queue.name != 'failed': + for job in queue.jobs: + add_job(job, queue.name) + + # show failed jobs, if requested + if show_failed: + fail_registry = queue.failed_job_registry + for job_id in fail_registry.get_job_ids(): + job = queue.fetch_job(job_id) + add_job(job, queue.name) return jobs + +@frappe.whitelist() +def remove_failed_jobs(): + conn = get_redis_conn() + queues = Queue.all(conn) + for queue in queues: + fail_registry = queue.failed_job_registry + for job_id in fail_registry.get_job_ids(): + job = queue.fetch_job(job_id) + fail_registry.remove(job, delete_job=True) + + @frappe.whitelist() def get_scheduler_status(): if is_scheduler_inactive(): diff --git a/frappe/core/page/dashboard_view/dashboard_view.js b/frappe/core/page/dashboard_view/dashboard_view.js index 686d11c6bf..e8e9cc9502 100644 --- a/frappe/core/page/dashboard_view/dashboard_view.js +++ b/frappe/core/page/dashboard_view/dashboard_view.js @@ -36,17 +36,17 @@ class Dashboard { } else { // last opened if (frappe.last_dashboard) { - frappe.set_route('dashboard-view', frappe.last_dashboard); + frappe.set_re_route('dashboard-view', frappe.last_dashboard); } else { // default dashboard frappe.db.get_list('Dashboard', {filters: {is_default: 1}}).then(data => { if (data && data.length) { - frappe.set_route('dashboard-view', data[0].name); + frappe.set_re_route('dashboard-view', data[0].name); } else { // no default, get the latest one frappe.db.get_list('Dashboard', {limit: 1}).then(data => { if (data && data.length) { - frappe.set_route('dashboard-view', data[0].name); + frappe.set_re_route('dashboard-view', data[0].name); } else { // create a new dashboard! frappe.new_doc('Dashboard'); diff --git a/frappe/core/page/recorder/recorder.js b/frappe/core/page/recorder/recorder.js index 4d6d6aa84c..b75ea6a41c 100644 --- a/frappe/core/page/recorder/recorder.js +++ b/frappe/core/page/recorder/recorder.js @@ -22,6 +22,7 @@ class Recorder { } show() { - + if (!this.view || this.view.$route.name == "recorder-detail") return; + this.view.$router.replace({name: "recorder-detail"}); } } diff --git a/frappe/core/workspace/build/build.json b/frappe/core/workspace/build/build.json index c4bde55d7f..aefda698b1 100644 --- a/frappe/core/workspace/build/build.json +++ b/frappe/core/workspace/build/build.json @@ -11,6 +11,7 @@ "hide_custom": 0, "icon": "tool", "idx": 0, + "is_default": 0, "is_standard": 1, "label": "Build", "links": [ @@ -163,8 +164,8 @@ { "hidden": 0, "is_query_report": 0, - "label": "Custom Script", - "link_to": "Custom Script", + "label": "Client Script", + "link_to": "Client Script", "link_type": "DocType", "onboard": 0, "only_for": "", @@ -181,7 +182,7 @@ "type": "Link" } ], - "modified": "2021-01-02 14:03:15.029699", + "modified": "2021-02-04 13:48:48.493146", "modified_by": "Administrator", "module": "Core", "name": "Build", diff --git a/frappe/custom/doctype/custom_script/README.md b/frappe/custom/doctype/client_script/README.md similarity index 100% rename from frappe/custom/doctype/custom_script/README.md rename to frappe/custom/doctype/client_script/README.md diff --git a/frappe/custom/doctype/custom_script/__init__.py b/frappe/custom/doctype/client_script/__init__.py similarity index 100% rename from frappe/custom/doctype/custom_script/__init__.py rename to frappe/custom/doctype/client_script/__init__.py diff --git a/frappe/custom/doctype/client_script/client_script.js b/frappe/custom/doctype/client_script/client_script.js new file mode 100644 index 0000000000..27d11af4d1 --- /dev/null +++ b/frappe/custom/doctype/client_script/client_script.js @@ -0,0 +1,93 @@ +// Copyright (c) 2016, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Client Script', { + refresh(frm) { + if (frm.doc.dt && frm.doc.script) { + frm.add_custom_button(__('Go to {0}', [frm.doc.dt]), + () => frappe.set_route('List', frm.doc.dt, 'List')); + } + + if (frm.doc.view == 'Form') { + frm.add_custom_button(__('Add script for Child Table'), () => { + frappe.model.with_doctype(frm.doc.dt, () => { + const child_tables = frappe.meta.get_docfields(frm.doc.dt, null, { + fieldtype: 'Table' + }).map(df => df.options); + + const d = new frappe.ui.Dialog({ + title: __('Select Child Table'), + fields: [ + { + label: __('Select Child Table'), + fieldtype: 'Link', + fieldname: 'cdt', + options: 'DocType', + get_query: () => { + return { + filters: { + istable: 1, + name: ['in', child_tables] + } + }; + } + } + ], + primary_action: ({ cdt }) => { + cdt = d.get_field('cdt').value; + frm.events.add_script_for_doctype(frm, cdt); + d.hide(); + } + }); + + d.show(); + }); + }); + } + + frm.set_query('dt', { + filters: { + istable: 0 + } + }); + }, + + dt(frm) { + frm.toggle_display('view', !frappe.boot.single_types.includes(frm.doc.dt)); + + if (!frm.doc.script) { + frm.events.add_script_for_doctype(frm, frm.doc.dt); + } + + if (frm.doc.script && !frm.doc.script.includes(frm.doc.dt)) { + frm.doc.script = ''; + frm.events.add_script_for_doctype(frm, frm.doc.dt); + } + }, + + view(frm) { + let has_form_boilerplate = frm.doc.script.includes('frappe.ui.form.on') + if (frm.doc.view === 'List' && has_form_boilerplate) { + frm.set_value('script', ''); + } + if (frm.doc.view === 'Form' && !has_form_boilerplate) { + frm.trigger('dt'); + } + }, + + add_script_for_doctype(frm, doctype) { + if (!doctype) return; + let boilerplate = ` +frappe.ui.form.on('${doctype}', { + refresh(frm) { + // your code here + } +}) + `.trim(); + let script = (frm.doc.script || ''); + if (script) { + script += '\n\n'; + } + frm.set_value('script', script + boilerplate); + } +}); diff --git a/frappe/custom/doctype/custom_script/custom_script.json b/frappe/custom/doctype/client_script/client_script.json similarity index 85% rename from frappe/custom/doctype/custom_script/custom_script.json rename to frappe/custom/doctype/client_script/client_script.json index 328b247c49..db02d8d4bc 100644 --- a/frappe/custom/doctype/custom_script/custom_script.json +++ b/frappe/custom/doctype/client_script/client_script.json @@ -2,12 +2,13 @@ "actions": [], "allow_import": 1, "creation": "2013-01-10 16:34:01", - "description": "Adds a client custom script to a DocType", + "description": "Adds a custom client script to a DocType", "doctype": "DocType", "document_type": "Document", "engine": "InnoDB", "field_order": [ "dt", + "view", "enabled", "script", "sample" @@ -23,8 +24,7 @@ "oldfieldtype": "Link", "options": "DocType", "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "set_only_once": 1 }, { "fieldname": "script", @@ -32,35 +32,37 @@ "label": "Script", "oldfieldname": "script", "oldfieldtype": "Code", - "options": "JS", - "show_days": 1, - "show_seconds": 1 + "options": "JS" }, { "fieldname": "sample", "fieldtype": "HTML", "label": "Sample", - "options": "

Custom Script Help

\n

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

\n
\n\n// fetch local_tax_no on selection of customer \n// cur_frm.add_fetch(link_field,  source_fieldname,  target_fieldname); \ncur_frm.add_fetch('customer',  'local_tax_no',  'local_tax_no');\n\n// additional validation on dates \nfrappe.ui.form.on('Task',  'validate',  function(frm) {\n    if (frm.doc.from_date < get_today()) {\n        msgprint('You can not select past date in From Date');\n        validated = false;\n    } \n});\n\n// make a field read-only after saving \nfrappe.ui.form.on('Task',  {\n    refresh: function(frm) {\n        // use the __islocal value of doc,  to check if the doc is saved or not\n        frm.set_df_property('myfield',  'read_only',  frm.doc.__islocal ? 0 : 1);\n    } \n});\n\n// additional permission check\nfrappe.ui.form.on('Task',  {\n    validate: function(frm) {\n        if(user=='user1@example.com' && frm.doc.purpose!='Material Receipt') {\n            msgprint('You are only allowed Material Receipt');\n            validated = false;\n        }\n    } \n});\n\n// calculate sales incentive\nfrappe.ui.form.on('Sales Invoice',  {\n    validate: function(frm) {\n        // calculate incentives for each person on the deal\n        total_incentive = 0\n        $.each(frm.doc.sales_team,  function(i,  d) {\n            // calculate incentive\n            var incentive_percent = 2;\n            if(frm.doc.base_grand_total > 400) incentive_percent = 4;\n            // actual incentive\n            d.incentives = flt(frm.doc.base_grand_total) * incentive_percent / 100;\n            total_incentive += flt(d.incentives)\n        });\n        frm.doc.total_incentive = total_incentive;\n    } \n})\n\n
", - "show_days": 1, - "show_seconds": 1 + "options": "

Client Script Help

\n

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

\n
\n\n// fetch local_tax_no on selection of customer \n// cur_frm.add_fetch(link_field,  source_fieldname,  target_fieldname); \ncur_frm.add_fetch('customer',  'local_tax_no',  'local_tax_no');\n\n// additional validation on dates \nfrappe.ui.form.on('Task',  'validate',  function(frm) {\n    if (frm.doc.from_date < get_today()) {\n        msgprint('You can not select past date in From Date');\n        validated = false;\n    } \n});\n\n// make a field read-only after saving \nfrappe.ui.form.on('Task',  {\n    refresh: function(frm) {\n        // use the __islocal value of doc,  to check if the doc is saved or not\n        frm.set_df_property('myfield',  'read_only',  frm.doc.__islocal ? 0 : 1);\n    } \n});\n\n// additional permission check\nfrappe.ui.form.on('Task',  {\n    validate: function(frm) {\n        if(user=='user1@example.com' && frm.doc.purpose!='Material Receipt') {\n            msgprint('You are only allowed Material Receipt');\n            validated = false;\n        }\n    } \n});\n\n// calculate sales incentive\nfrappe.ui.form.on('Sales Invoice',  {\n    validate: function(frm) {\n        // calculate incentives for each person on the deal\n        total_incentive = 0\n        $.each(frm.doc.sales_team,  function(i,  d) {\n            // calculate incentive\n            var incentive_percent = 2;\n            if(frm.doc.base_grand_total > 400) incentive_percent = 4;\n            // actual incentive\n            d.incentives = flt(frm.doc.base_grand_total) * incentive_percent / 100;\n            total_incentive += flt(d.incentives)\n        });\n        frm.doc.total_incentive = total_incentive;\n    } \n})\n\n
" }, { "default": "0", "fieldname": "enabled", "fieldtype": "Check", - "label": "Enabled", - "show_days": 1, - "show_seconds": 1 + "label": "Enabled" + }, + { + "default": "Form", + "fieldname": "view", + "fieldtype": "Select", + "label": "Apply To", + "options": "List\nForm", + "set_only_once": 1 } ], "icon": "fa fa-glass", "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-08-24 21:56:07.719579", + "modified": "2021-03-16 20:33:51.400191", "modified_by": "Administrator", "module": "Custom", - "name": "Custom Script", + "name": "Client Script", "owner": "Administrator", "permissions": [ { @@ -86,6 +88,7 @@ "write": 1 } ], + "sort_field": "modified", "sort_order": "ASC", "track_changes": 1 } \ No newline at end of file diff --git a/frappe/custom/doctype/client_script/client_script.py b/frappe/custom/doctype/client_script/client_script.py new file mode 100644 index 0000000000..049f979263 --- /dev/null +++ b/frappe/custom/doctype/client_script/client_script.py @@ -0,0 +1,31 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt +from __future__ import unicode_literals +import frappe + +from frappe import _ +from frappe.model.document import Document + + +class ClientScript(Document): + def autoname(self): + self.name = f"{self.dt}-{self.view}" + + def validate(self): + if not self.is_new(): + return + + exists = frappe.db.exists( + "Client Script", {"dt": self.dt, "view": self.view} + ) + if exists: + frappe.throw( + _("Client Script for {0} {1} already exists").format(frappe.bold(self.dt), self.view), + frappe.DuplicateEntryError, + ) + + def on_update(self): + frappe.clear_cache(doctype=self.dt) + + def on_trash(self): + frappe.clear_cache(doctype=self.dt) diff --git a/frappe/custom/doctype/custom_script/test_custom_script.py b/frappe/custom/doctype/client_script/test_client_script.py similarity index 65% rename from frappe/custom/doctype/custom_script/test_custom_script.py rename to frappe/custom/doctype/client_script/test_client_script.py index 6947e6060d..de113c1ce7 100644 --- a/frappe/custom/doctype/custom_script/test_custom_script.py +++ b/frappe/custom/doctype/client_script/test_client_script.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe import unittest -# test_records = frappe.get_test_records('Custom Script') +# test_records = frappe.get_test_records('Client Script') -class TestCustomScript(unittest.TestCase): +class TestClientScript(unittest.TestCase): pass diff --git a/frappe/custom/doctype/custom_script/custom_script.js b/frappe/custom/doctype/custom_script/custom_script.js deleted file mode 100644 index 711e7d1796..0000000000 --- a/frappe/custom/doctype/custom_script/custom_script.js +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) 2016, Frappe Technologies and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Custom Script', { - refresh(frm) { - if (frm.doc.dt && frm.doc.script) { - frm.add_custom_button(__('Go to {0}', [frm.doc.dt]), - () => frappe.set_route('List', frm.doc.dt, 'List')); - } - - frm.add_custom_button(__('Add script for Child Table'), () => { - frappe.model.with_doctype(frm.doc.dt, () => { - const child_tables = frappe.meta.get_docfields(frm.doc.dt, null, { - fieldtype: 'Table' - }).map(df => df.options); - - const d = new frappe.ui.Dialog({ - title: __('Select Child Table'), - fields: [ - { - label: __('Select Child Table'), - fieldtype: 'Link', - fieldname: 'cdt', - options: 'DocType', - get_query: () => { - return { - filters: { - istable: 1, - name: ['in', child_tables] - } - }; - } - } - ], - primary_action: ({ cdt }) => { - cdt = d.get_field('cdt').value; - frm.events.add_script_for_doctype(frm, cdt); - d.hide(); - } - }); - - d.show(); - }); - }); - - frm.set_query('dt', { - filters: { - istable: 0 - } - }); - }, - - dt(frm) { - if (!frm.doc.script) { - frm.events.add_script_for_doctype(frm, frm.doc.dt); - } - - if (frm.doc.script && !frm.doc.script.includes(frm.doc.dt)) { - frm.doc.script = ''; - frm.events.add_script_for_doctype(frm, frm.doc.dt); - } - }, - - add_script_for_doctype(frm, doctype) { - let boilerplate = ` -frappe.ui.form.on('${doctype}', { - refresh(frm) { - // your code here - } -}) - `.trim(); - let script = (frm.doc.script || ''); - if (script) { - script += '\n\n'; - } - frm.set_value('script', script + boilerplate); - } -}); diff --git a/frappe/custom/doctype/custom_script/custom_script.py b/frappe/custom/doctype/custom_script/custom_script.py deleted file mode 100644 index e15819de65..0000000000 --- a/frappe/custom/doctype/custom_script/custom_script.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt -from __future__ import unicode_literals -import frappe - -from frappe.model.document import Document - -class CustomScript(Document): - def autoname(self): - self.name = self.dt + "-Client" - - def on_update(self): - frappe.clear_cache(doctype=self.dt) - - def on_trash(self): - frappe.clear_cache(doctype=self.dt) - diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index 79978a49d7..d9d8ae196e 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -76,6 +76,7 @@ frappe.ui.form.on("Customize Form", { frm.trigger("setup_sortable"); } } + localStorage["customize_doctype"] = frm.doc.doc_type; } }); } else { diff --git a/frappe/custom/workspace/customization/customization.json b/frappe/custom/workspace/customization/customization.json index 3631914249..cdc3b73366 100644 --- a/frappe/custom/workspace/customization/customization.json +++ b/frappe/custom/workspace/customization/customization.json @@ -10,6 +10,7 @@ "hide_custom": 0, "icon": "customization", "idx": 0, + "is_default": 0, "is_standard": 1, "label": "Customization", "links": [ @@ -81,8 +82,8 @@ "dependencies": "", "hidden": 0, "is_query_report": 0, - "label": "Custom Script", - "link_to": "Custom Script", + "label": "Client Script", + "link_to": "Client Script", "link_type": "DocType", "onboard": 0, "type": "Link" @@ -115,7 +116,7 @@ "type": "Link" } ], - "modified": "2020-12-01 13:38:39.843773", + "modified": "2021-02-04 13:50:35.750463", "modified_by": "Administrator", "module": "Custom", "name": "Customization", @@ -134,8 +135,14 @@ "type": "DocType" }, { - "label": "Custom Script", - "link_to": "Custom Script", + "label": "Client Script", + "link_to": "Client Script", + "type": "DocType" + }, + { + "doc_view": "", + "label": "Server Script", + "link_to": "Server Script", "type": "DocType" } ] diff --git a/frappe/database/database.py b/frappe/database/database.py index 179206a4af..4fcf10efda 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -16,7 +16,6 @@ import frappe.model.meta from frappe import _ from time import time from frappe.utils import now, getdate, cast_fieldtype, get_datetime -from frappe.utils.background_jobs import execute_job, get_queue from frappe.model.utils.link_count import flush_local_link_count from frappe.utils import cint @@ -1032,6 +1031,8 @@ class Database(object): insert_list = [] def enqueue_jobs_after_commit(): + from frappe.utils.background_jobs import execute_job, get_queue + if frappe.flags.enqueue_after_commit and len(frappe.flags.enqueue_after_commit) > 0: for job in frappe.flags.enqueue_after_commit: q = get_queue(job.get("queue"), is_async=job.get("is_async")) diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py index 3cbb2e4f0e..f9997d1526 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -8,8 +8,7 @@ from pymysql.times import TimeDelta from pymysql.constants import ER, FIELD_TYPE from pymysql.converters import conversions -from frappe.utils import get_datetime, cstr -from markdown2 import UnicodeWithAttrs +from frappe.utils import get_datetime, cstr, UnicodeWithAttrs from frappe.database.database import Database from six import PY2, binary_type, text_type, string_types from frappe.database.mariadb.schema import MariaDBTable diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index 0ded8e0717..5b6e2fdd21 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -601,8 +601,8 @@ def merge_cards_based_on_label(cards): for card in cards: label = card.get('label') if label in cards_dict: - links = loads(cards_dict[label].links) + loads(card.links) - cards_dict[label].update(dict(links=dumps(links))) + links = cards_dict[label].links + card.links + cards_dict[label].update(dict(links=links)) cards_dict[label] = cards_dict.pop(label) else: cards_dict[label] = card diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index b19f6cf9f0..48b34e6cd9 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -171,7 +171,6 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date): doctype = chart.document_type datefield = chart.based_on - aggregate_function = get_aggregate_function(chart.chart_type) value_field = chart.value_based_on or '1' from_date = from_date.strftime('%Y-%m-%d') to_date = to_date @@ -183,7 +182,8 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date): doctype, fields = [ '{} as _unit'.format(datefield), - '{aggregate_function}({value_field})'.format(aggregate_function=aggregate_function, value_field=value_field), + 'SUM({})'.format(value_field), + 'COUNT(*)' ], filters = filters, group_by = '_unit', @@ -192,7 +192,7 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date): ignore_ifnull = True ) - result = get_result(data, timegrain, from_date, to_date) + result = get_result(data, timegrain, from_date, to_date, chart.chart_type) chart_config = { "labels": [get_period(r[0], timegrain) for r in result], @@ -288,15 +288,21 @@ def get_aggregate_function(chart_type): }[chart_type] -def get_result(data, timegrain, from_date, to_date): +def get_result(data, timegrain, from_date, to_date, chart_type): dates = get_dates_from_timegrain(from_date, to_date, timegrain) result = [[date, 0] for date in dates] data_index = 0 if data: for i, d in enumerate(result): + count = 0 while data_index < len(data) and getdate(data[data_index][0]) <= d[0]: d[1] += data[data_index][1] + count += data[data_index][2] data_index += 1 + if chart_type == 'Average' and not count == 0: + d[1] = d[1]/count + if chart_type == 'Count': + d[1] = count return result diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py index 3c37ad4a09..72ab18385d 100644 --- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py @@ -212,19 +212,52 @@ class TestDashboardChart(unittest.TestCase): frappe.db.rollback() -def insert_test_records(): - create_new_communication(datetime(2018, 12, 30), 50) - create_new_communication(datetime(2019, 1, 4), 100) - create_new_communication(datetime(2019, 1, 6), 200) - create_new_communication(datetime(2019, 1, 7), 400) - create_new_communication(datetime(2019, 1, 8), 300) - create_new_communication(datetime(2019, 1, 10), 100) + def test_avg_dashboard_chart(self): + insert_test_records() -def create_new_communication(date, rating): + if frappe.db.exists('Dashboard Chart', 'Test Average Dashboard Chart'): + frappe.delete_doc('Dashboard Chart', 'Test Average Dashboard Chart') + + frappe.get_doc(dict( + doctype = 'Dashboard Chart', + chart_name = 'Test Average Dashboard Chart', + chart_type = 'Average', + document_type = 'Communication', + based_on = 'communication_date', + value_based_on = 'rating', + timespan = 'Select Date Range', + time_interval = 'Weekly', + from_date = datetime(2018, 12, 30), + to_date = datetime(2019, 1, 15), + filters_json = '[]', + timeseries = 1 + )).insert() + + result = get(chart_name='Test Average Dashboard Chart', refresh = 1) + + self.assertEqual(result.get('datasets')[0].get('values'), [50.0, 150.0, 266.6666666666667, 0.0]) + self.assertEqual( + result.get('labels'), + ['30-12-18', '06-01-19', '13-01-19', '20-01-19'] + ) + + frappe.db.rollback() + +def insert_test_records(): + create_new_communication('Communication 1', datetime(2018, 12, 30), 50) + create_new_communication('Communication 2', datetime(2019, 1, 4), 100) + create_new_communication('Communication 3', datetime(2019, 1, 6), 200) + create_new_communication('Communication 4', datetime(2019, 1, 7), 400) + create_new_communication('Communication 5', datetime(2019, 1, 8), 300) + create_new_communication('Communication 6', datetime(2019, 1, 10), 100) + +def create_new_communication(subject, date, rating): communication = { 'doctype': 'Communication', - 'subject': 'Test Communication', + 'subject': subject, 'rating': rating, 'communication_date': date } - frappe.get_doc(communication).insert() + comm = frappe.get_doc(communication) + if not frappe.db.exists("Communication", {'subject' : comm.subject}): + comm.insert() diff --git a/frappe/desk/doctype/kanban_board/kanban_board.py b/frappe/desk/doctype/kanban_board/kanban_board.py index f1ad41db6c..a655e9e1da 100644 --- a/frappe/desk/doctype/kanban_board/kanban_board.py +++ b/frappe/desk/doctype/kanban_board/kanban_board.py @@ -17,6 +17,10 @@ class KanbanBoard(Document): def on_update(self): frappe.clear_cache(doctype=self.reference_doctype) + def before_insert(self): + for column in self.columns: + column.order = get_order_for_column(self, column.column_name) + def validate_column_name(self): for column in self.columns: if not column.column_name: @@ -125,6 +129,53 @@ def update_order(board_name, order): board.save() return board, updated_cards +@frappe.whitelist() +def update_order_for_single_card(board_name, docname, from_colname, to_colname, old_index, new_index): + '''Save the order of cards in columns''' + board = frappe.get_doc('Kanban Board', board_name) + doctype = board.reference_doctype + fieldname = board.field_name + old_index = frappe.parse_json(old_index) + new_index = frappe.parse_json(new_index) + + # save current order and index of columns to be updated + from_col_order, from_col_idx = get_kanban_column_order_and_index(board, from_colname) + to_col_order, to_col_idx = get_kanban_column_order_and_index(board, to_colname) + + if from_colname == to_colname: + from_col_order = to_col_order + + to_col_order.insert(new_index, from_col_order.pop((old_index))) + + # save updated order + board.columns[from_col_idx].order = frappe.as_json(from_col_order) + board.columns[to_col_idx].order = frappe.as_json(to_col_order) + board.save() + + # update changed value in doc + frappe.set_value(doctype, docname, fieldname, to_colname) + + return board + +def get_kanban_column_order_and_index(board, colname): + for i, col in enumerate(board.columns): + if col.column_name == colname: + col_order = frappe.parse_json(col.order) + col_idx = i + + return col_order, col_idx + +@frappe.whitelist() +def add_card(board_name, docname, colname): + board = frappe.get_doc('Kanban Board', board_name) + + col_order, col_idx = get_kanban_column_order_and_index(board, colname) + col_order.insert(0, docname) + + board.columns[col_idx].order = frappe.as_json(col_order) + + board.save() + return board @frappe.whitelist() def quick_kanban_board(doctype, board_name, field_name, project=None): @@ -133,6 +184,13 @@ def quick_kanban_board(doctype, board_name, field_name, project=None): doc = frappe.new_doc('Kanban Board') meta = frappe.get_meta(doctype) + doc.kanban_board_name = board_name + doc.reference_doctype = doctype + doc.field_name = field_name + + if project: + doc.filters = '[["Task","project","=","{0}"]]'.format(project) + options = '' for field in meta.fields: if field.fieldname == field_name: @@ -149,12 +207,6 @@ def quick_kanban_board(doctype, board_name, field_name, project=None): column_name=column )) - doc.kanban_board_name = board_name - doc.reference_doctype = doctype - doc.field_name = field_name - - if project: - doc.filters = '[["Task","project","=","{0}"]]'.format(project) if doctype in ['Note', 'ToDo']: doc.private = 1 @@ -162,6 +214,12 @@ def quick_kanban_board(doctype, board_name, field_name, project=None): doc.save() return doc +def get_order_for_column(board, colname): + filters = [[board.reference_doctype, board.field_name, '=', colname]] + if board.filters: + filters.append(frappe.parse_json(board.filters)[0]) + + return frappe.as_json(frappe.get_list(board.reference_doctype, filters=filters, pluck='name')) @frappe.whitelist() def update_column_order(board_name, order): diff --git a/frappe/desk/doctype/notification_settings/notification_settings.py b/frappe/desk/doctype/notification_settings/notification_settings.py index 9b124cd6f4..4ab40bffe9 100644 --- a/frappe/desk/doctype/notification_settings/notification_settings.py +++ b/frappe/desk/doctype/notification_settings/notification_settings.py @@ -42,7 +42,11 @@ def create_notification_settings(user): _doc = frappe.new_doc('Notification Settings') _doc.name = user _doc.insert(ignore_permissions=True) - frappe.db.commit() + + +def toggle_notifications(user, enable=False): + if frappe.db.exists("Notification Settings", user): + frappe.db.set_value("Notification Settings", user, 'enabled', enable) @frappe.whitelist() @@ -76,4 +80,4 @@ def get_permission_query_conditions(user): @frappe.whitelist() def set_seen_value(value, user): - frappe.db.set_value('Notification Settings', user, 'seen', value, update_modified=False) \ No newline at end of file + frappe.db.set_value('Notification Settings', user, 'seen', value, update_modified=False) diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py index 6bddd09fc7..7d1a697f6b 100644 --- a/frappe/desk/doctype/number_card/number_card.py +++ b/frappe/desk/doctype/number_card/number_card.py @@ -86,7 +86,7 @@ def get_result(doc, filters, to_date=None): filters = frappe.parse_json(filters) if not filters: - filters = [] + filters = [] if to_date: filters.append([doc.document_type, 'creation', '<', to_date]) @@ -107,9 +107,13 @@ def get_percentage_difference(doc, filters, result): return previous_result = calculate_previous_result(doc, filters) - difference = (result - previous_result)/100.0 - - return difference + if previous_result == 0: + return None + else: + if result == previous_result: + return 0 + else: + return ((result/previous_result)-1)*100.0 def calculate_previous_result(doc, filters): @@ -197,4 +201,4 @@ def add_card_to_dashboard(args): card.save() dashboard.append('cards', dashboard_link) - dashboard.save() \ No newline at end of file + dashboard.save() diff --git a/frappe/desk/form/document_follow.py b/frappe/desk/form/document_follow.py index f5ace4d732..f5e5c0ca9b 100644 --- a/frappe/desk/form/document_follow.py +++ b/frappe/desk/form/document_follow.py @@ -147,6 +147,8 @@ def get_version(doctype, doc_name, frequency, user): return timeline def get_comments(doctype, doc_name, frequency, user): + from html2text import html2text + timeline = [] filters = get_filters("reference_name", doc_name, frequency, user) comments = frappe.get_all("Comment", @@ -166,7 +168,7 @@ def get_comments(doctype, doc_name, frequency, user): "time": comment.modified, "data": { "time": time, - "comment": frappe.utils.html2text(str(comment.content)), + "comment": html2text(str(comment.content)), "by": by }, "doctype": doctype, @@ -197,6 +199,8 @@ def get_follow_users(doctype, doc_name): ) def get_row_changed(row_changed, time, doctype, doc_name, v): + from html2text import html2text + items = [] for d in row_changed: d[2] = d[2] if d[2] else ' ' @@ -209,8 +213,8 @@ def get_row_changed(row_changed, time, doctype, doc_name, v): "table_field": d[0], "row": str(d[1]), "field": d[3][0][0], - "from": frappe.utils.html2text(str(d[3][0][1])), - "to": frappe.utils.html2text(str(d[3][0][2])) + "from": html2text(str(d[3][0][1])), + "to": html2text(str(d[3][0][2])) }, "doctype": doctype, "doc_name": doc_name, @@ -236,6 +240,8 @@ def get_added_row(added, time, doctype, doc_name, v): return items def get_field_changed(changed, time, doctype, doc_name, v): + from html2text import html2text + items = [] for d in changed: d[1] = d[1] if d[1] else ' ' @@ -246,8 +252,8 @@ def get_field_changed(changed, time, doctype, doc_name, v): "data": { "time": time, "field": d[0], - "from": frappe.utils.html2text(str(d[1])), - "to": frappe.utils.html2text(str(d[2])) + "from": html2text(str(d[1])), + "to": html2text(str(d[2])) }, "doctype": doctype, "doc_name": doc_name, diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py index 733ee1774c..a62e2837d5 100644 --- a/frappe/desk/form/linked_with.py +++ b/frappe/desk/form/linked_with.py @@ -79,28 +79,30 @@ def get_submitted_linked_docs(doctype, name, docs=None, visited=None): @frappe.whitelist() def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=[]): """ - Cancel all linked doctype + Cancel all linked doctype, optionally ignore doctypes specified in a list. Arguments: - docs (str) - It contains all list of dictionaries of a linked documents. + docs (json str) - It contains list of dictionaries of a linked documents. + ignore_doctypes_on_cancel_all (list) - List of doctypes to ignore while cancelling. """ docs = json.loads(docs) if isinstance(ignore_doctypes_on_cancel_all, string_types): ignore_doctypes_on_cancel_all = json.loads(ignore_doctypes_on_cancel_all) for i, doc in enumerate(docs, 1): - if validate_linked_doc(doc, ignore_doctypes_on_cancel_all) is True: - frappe.publish_progress(percent=i * 100 / ((len(docs) - len(ignore_doctypes_on_cancel_all))), title=_("Cancelling documents")) + if validate_linked_doc(doc, ignore_doctypes_on_cancel_all): linked_doc = frappe.get_doc(doc.get("doctype"), doc.get("name")) linked_doc.cancel() + frappe.publish_progress(percent=i/len(docs) * 100, title=_("Cancelling documents")) def validate_linked_doc(docinfo, ignore_doctypes_on_cancel_all=[]): """ Validate a document to be submitted and non-exempted from auto-cancel. - Args: - docs (dict): The document to check for submitted and non-exempt from auto-cancel + Arguments: + docinfo (dict): The document to check for submitted and non-exempt from auto-cancel + ignore_doctypes_on_cancel_all (list) - List of doctypes to ignore while cancelling. Returns: bool: True if linked document passes all validations, else False diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py index d5428b1da2..e637f4969a 100644 --- a/frappe/desk/form/meta.py +++ b/frappe/desk/form/meta.py @@ -63,7 +63,7 @@ class FormMeta(Meta): "__linked_with", "__messages", "__print_formats", "__workflow_docs", "__form_grid_templates", "__listview_template", "__tree_js", "__dashboard", "__kanban_column_fields", '__templates', - '__custom_js'): + '__custom_js', '__custom_list_js'): d[k] = self.get(k) # d['fields'] = d.get('fields', []) @@ -130,9 +130,23 @@ class FormMeta(Meta): def add_custom_script(self): """embed all require files""" # custom script - custom = frappe.db.get_value("Custom Script", {"dt": self.name, "enabled": 1}, "script") or "" + client_scripts = frappe.db.get_all("Client Script", + filters={"dt": self.name, "enabled": 1}, + fields=["script", "view"], + order_by="creation asc" + ) or "" - self.set("__custom_js", custom) + list_script = '' + form_script = '' + for script in client_scripts: + if script.view == 'List': + list_script += script.script + + if script.view == 'Form': + form_script += script.script + + self.set("__custom_js", form_script) + self.set("__custom_list_js", list_script) def add_search_fields(self): """add search fields found in the doctypes indicated by link fields' options""" diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py index 4c3bab2e23..395d2b9571 100644 --- a/frappe/desk/form/utils.py +++ b/frappe/desk/form/utils.py @@ -47,7 +47,7 @@ def validate_link(): except Exception as e: error_message = str(e).split("Unknown column '") fieldname = None if len(error_message)<=1 else error_message[1].split("'")[0] - frappe.msgprint(_("Wrong fieldname {0} in add_fetch configuration of custom script").format(fieldname)) + frappe.msgprint(_("Wrong fieldname {0} in add_fetch configuration of custom client script").format(fieldname)) frappe.errprint(frappe.get_traceback()) if fetch_value: diff --git a/frappe/desk/leaderboard.py b/frappe/desk/leaderboard.py index 8d00ea9bc2..2a981f061b 100644 --- a/frappe/desk/leaderboard.py +++ b/frappe/desk/leaderboard.py @@ -16,8 +16,18 @@ def get_leaderboards(): @frappe.whitelist() def get_energy_point_leaderboard(date_range, company = None, field = None, limit = None): + all_users = frappe.db.get_all('User', + filters = { + 'name': ['not in', ['Administrator', 'Guest']], + 'enabled': 1, + 'user_type': ['!=', 'Website User'] + }, + order_by = 'name ASC') + all_users_list = list(map(lambda x: x['name'], all_users)) + filters = [ ['type', '!=', 'Review'], + ['user', 'in', all_users_list] ] if date_range: date_range = frappe.parse_json(date_range) @@ -28,15 +38,7 @@ def get_energy_point_leaderboard(date_range, company = None, field = None, limit group_by = 'user', order_by = 'value desc' ) - all_users = frappe.db.get_all('User', - filters = { - 'name': ['not in', ['Administrator', 'Guest']], - 'enabled': 1, - 'user_type': ['!=', 'Website User'] - }, - order_by = 'name ASC') - all_users_list = list(map(lambda x: x['name'], all_users)) energy_point_users_list = list(map(lambda x: x['name'], energy_point_users)) for user in all_users_list: if user not in energy_point_users_list: diff --git a/frappe/desk/page/leaderboard/leaderboard.js b/frappe/desk/page/leaderboard/leaderboard.js index 825e1d959b..b3fccf84f9 100644 --- a/frappe/desk/page/leaderboard/leaderboard.js +++ b/frappe/desk/page/leaderboard/leaderboard.js @@ -141,7 +141,7 @@ class Leaderboard { } create_date_range_field() { - let timespan_field = $(this.parent).find(`.frappe-control[data-original-title='Timespan']`); + let timespan_field = $(this.parent).find(`.frappe-control[data-original-title=${__('Timespan')}]`); this.date_range_field = $(`
`).insertAfter(timespan_field).hide(); let date_field = frappe.ui.form.make_control({ diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js index c39e7f52c0..f44a57e339 100644 --- a/frappe/desk/page/setup_wizard/setup_wizard.js +++ b/frappe/desk/page/setup_wizard/setup_wizard.js @@ -201,7 +201,7 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { this.abort_setup(r.message.fail); } }, - error: this.abort_setup("Error in setup", true) + error: () => this.abort_setup("Error in setup") }); } diff --git a/frappe/desk/page/user_profile/user_profile_controller.js b/frappe/desk/page/user_profile/user_profile_controller.js index 61f8ec3c06..c1a89f316e 100644 --- a/frappe/desk/page/user_profile/user_profile_controller.js +++ b/frappe/desk/page/user_profile/user_profile_controller.js @@ -151,32 +151,30 @@ class UserProfile { // eslint-disable-next-line no-unused-vars render_percentage_chart(field, title) { - // REDESIGN-TODO: chart seems to be broken. Enable this once fixed. - this.wrapper.find('.percentage-chart-container').hide(); - // frappe.xcall('frappe.desk.page.user_profile.user_profile.get_energy_points_percentage_chart_data', { - // user: this.user_id, - // field: field - // }).then(chart => { - // if (chart.labels.length) { - // this.percentage_chart = new frappe.Chart('.performance-percentage-chart', { - // type: 'percentage', - // data: { - // labels: chart.labels, - // datasets: chart.datasets - // }, - // truncateLegends: 1, - // barOptions: { - // height: 11, - // depth: 1 - // }, - // height: 200, - // maxSlices: 8, - // colors: ['purple', 'blue', 'cyan', 'teal', 'pink', 'red', 'orange', 'yellow'], - // }); - // } else { - // this.wrapper.find('.percentage-chart-container').hide(); - // } - // }); + frappe.xcall('frappe.desk.page.user_profile.user_profile.get_energy_points_percentage_chart_data', { + user: this.user_id, + field: field + }).then(chart => { + if (chart.labels.length) { + this.percentage_chart = new frappe.Chart('.performance-percentage-chart', { + type: 'percentage', + data: { + labels: chart.labels, + datasets: chart.datasets + }, + truncateLegends: 1, + barOptions: { + height: 11, + depth: 1 + }, + height: 200, + maxSlices: 8, + colors: ['purple', 'blue', 'cyan', 'teal', 'pink', 'red', 'orange', 'yellow'], + }); + } else { + this.wrapper.find('.percentage-chart-container').hide(); + } + }); } create_line_chart_filters() { @@ -452,4 +450,4 @@ class UserProfileTimeline extends BaseTimeline { } frappe.provide('frappe.ui'); -frappe.ui.UserProfile = UserProfile; \ No newline at end of file +frappe.ui.UserProfile = UserProfile; diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 3008cf0e61..22d47d1120 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -164,10 +164,14 @@ def get_script(report_name): module = report.module or frappe.db.get_value( "DocType", report.ref_doctype, "module" ) - module_path = get_module_path(module) - report_folder = os.path.join(module_path, "report", scrub(report.name)) - script_path = os.path.join(report_folder, scrub(report.name) + ".js") - print_path = os.path.join(report_folder, scrub(report.name) + ".html") + + is_custom_module = frappe.get_cached_value("Module Def", module, "custom") + + # custom modules are virtual modules those exists in DB but not in disk. + module_path = '' if is_custom_module else get_module_path(module) + report_folder = module_path and os.path.join(module_path, "report", scrub(report.name)) + script_path = report_folder and os.path.join(report_folder, scrub(report.name) + ".js") + print_path = report_folder and os.path.join(report_folder, scrub(report.name) + ".html") script = None if os.path.exists(script_path): diff --git a/frappe/desk/search.py b/frappe/desk/search.py index f4e6543844..6faa827dde 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -80,13 +80,15 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0, is_whitelisted(frappe.get_attr(query)) frappe.response["values"] = frappe.call(query, doctype, txt, searchfield, start, page_length, filters, as_dict=as_dict) - except Exception as e: + except frappe.exceptions.PermissionError as e: if frappe.local.conf.developer_mode: raise e else: frappe.respond_as_web_page(title='Invalid Method', html='Method not found', indicator_color='red', http_status_code=404) return + except Exception as e: + raise e elif not query and doctype in standard_queries: # from standard queries search_widget(doctype, txt, standard_queries[doctype][0], diff --git a/frappe/desk/utils.py b/frappe/desk/utils.py index c19e531739..01b47ac106 100644 --- a/frappe/desk/utils.py +++ b/frappe/desk/utils.py @@ -7,12 +7,13 @@ def validate_route_conflict(doctype, name): ''' Raises exception if name clashes with routes from other documents for /app routing ''' - if frappe.flags.ignore_route_conflict_validation: - return all_names = [] for _doctype in ['Page', 'Workspace', 'DocType']: - all_names.extend([slug(d) for d in frappe.get_all(_doctype, pluck='name') if (doctype != _doctype and d != name)]) + try: + all_names.extend([slug(d) for d in frappe.get_all(_doctype, pluck='name') if (doctype != _doctype and d != name)]) + except frappe.db.TableMissingError: + pass if slug(name) in all_names: frappe.msgprint(frappe._('Name already taken, please set a new name')) diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index ca4dbb83e2..4869c5a9bf 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -90,6 +90,29 @@ class EmailAccount(Document): if self.append_to not in valid_doctypes: frappe.throw(_("Append To can be one of {0}").format(comma_or(valid_doctypes))) + def before_save(self): + messages = [] + as_list = 1 + if not self.enable_incoming and self.default_incoming: + self.default_incoming = False + messages.append(_("{} has been disabled. It can only be enabled if {} is checked.") + .format( + frappe.bold(_('Default Incoming')), + frappe.bold(_('Enable Incoming')) + ) + ) + if not self.enable_outgoing and self.default_outgoing: + self.default_outgoing = False + messages.append(_("{} has been disabled. It can only be enabled if {} is checked.") + .format( + frappe.bold(_('Default Outgoing')), + frappe.bold(_('Enable Outgoing')) + ) + ) + if messages: + if len(messages) == 1: (as_list, messages) = (0, messages[0]) + frappe.msgprint(messages, as_list= as_list, indicator='orange', title=_("Defaults Updated")) + def on_update(self): """Check there is only one default of each type.""" from frappe.core.doctype.user.user import setup_user_email_inbox diff --git a/frappe/email/doctype/newsletter/newsletter.json b/frappe/email/doctype/newsletter/newsletter.json index 1dd6115b43..dcd19ed33c 100644 --- a/frappe/email/doctype/newsletter/newsletter.json +++ b/frappe/email/doctype/newsletter/newsletter.json @@ -19,9 +19,12 @@ "message", "message_md", "message_html", + "section_break_13", "send_unsubscribe_link", "send_attachments", + "column_break_9", "published", + "send_webview_link", "route", "test_the_newsletter", "test_email_id", @@ -160,6 +163,21 @@ "fieldtype": "Check", "label": "Schedule Sending", "read_only_depends_on": "eval: doc.email_sent" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "published", + "fieldname": "send_webview_link", + "fieldtype": "Check", + "label": "Send Web View Link" + }, + { + "fieldname": "section_break_13", + "fieldtype": "Section Break" } ], "has_web_view": 1, @@ -169,7 +187,7 @@ "is_published_field": "published", "links": [], "max_attachments": 3, - "modified": "2020-08-24 19:59:37.262500", + "modified": "2021-02-22 14:33:56.095380", "modified_by": "Administrator", "module": "Email", "name": "Newsletter", diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index 2791ebb75b..ad985ee20e 100755 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -68,13 +68,17 @@ class Newsletter(WebsiteGenerator): except IOError: frappe.throw(_("Unable to find attachment {0}").format(file.name)) - send(recipients=self.recipients, sender=sender, - subject=self.subject, message=self.get_message(), + args = { + "message": self.get_message(), + "name": self.name + } + frappe.sendmail(recipients=self.recipients, sender=sender, + subject=self.subject, message=self.get_message(), template="newsletter", reference_doctype=self.doctype, reference_name=self.name, add_unsubscribe_link=self.send_unsubscribe_link, attachments=attachments, unsubscribe_method="/unsubscribe", unsubscribe_params={"name": self.name}, - send_priority=0, queue_separately=True) + send_priority=0, queue_separately=True, args=args) if not frappe.flags.in_test: frappe.db.auto_commit_on_many_writes = False diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index 7aa70830e7..3fb1dfa0da 100755 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -297,8 +297,9 @@ def inline_style_in_html(html): for app in apps: path = 'assets/{0}/css/email.css'.format(app) - if os.path.exists(os.path.abspath(path)): - css_files.append(path) + css_files.append(path) + + css_files = [css_file for css_file in css_files if os.path.exists(os.path.abspath(css_file))] p = Premailer(html=html, external_styles=css_files, strip_important=False) diff --git a/frappe/email/test_email_body.py b/frappe/email/test_email_body.py index 9b0b5e41d7..3fcabb9495 100644 --- a/frappe/email/test_email_body.py +++ b/frappe/email/test_email_body.py @@ -17,7 +17,7 @@ class TestEmailBody(unittest.TestCase):

Hey John Doe!

This is embedded image you asked for

- +
''' email_text = ''' @@ -25,7 +25,7 @@ Hey John Doe! This is the text version of this email ''' - img_path = os.path.abspath('assets/frappe/images/favicon.png') + img_path = os.path.abspath('assets/frappe/images/frappe-favicon.svg') with open(img_path, 'rb') as f: img_content = f.read() img_base64 = base64.b64encode(img_content).decode() @@ -77,12 +77,11 @@ This is the text version of this email def test_image(self): img_signature = ''' -Content-Type: image/png +Content-Type: image/svg+xml MIME-Version: 1.0 Content-Transfer-Encoding: base64 -Content-Disposition: inline; filename="favicon.png" +Content-Disposition: inline; filename="frappe-favicon.svg" ''' - self.assertTrue(img_signature in self.email_string) self.assertTrue(self.img_base64 in self.email_string) @@ -117,7 +116,7 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> def test_replace_filename_with_cid(self): original_message = '''
- test + test
''' diff --git a/frappe/exceptions.py b/frappe/exceptions.py index 82fbff7a90..ab65e6e006 100644 --- a/frappe/exceptions.py +++ b/frappe/exceptions.py @@ -33,6 +33,9 @@ class PermissionError(Exception): class DoesNotExistError(ValidationError): http_status_code = 404 +class PageDoesNotExistError(ValidationError): + http_status_code = 404 + class NameError(Exception): http_status_code = 409 diff --git a/frappe/geo/country_info.json b/frappe/geo/country_info.json index e4c4e278b0..1e0ae161bc 100644 --- a/frappe/geo/country_info.json +++ b/frappe/geo/country_info.json @@ -2729,11 +2729,11 @@ }, "Zimbabwe": { "code": "zw", - "currency": "ZWD", - "currency_fraction": "Thebe", + "currency": "ZWL", + "currency_fraction": "Cent", "currency_fraction_units": 100, "currency_name": "Zimbabwe Dollar", - "currency_symbol": "P", + "currency_symbol": "ZWL$", "number_format": "# ###.##", "timezones": [ "Africa/Harare" diff --git a/frappe/hooks.py b/frappe/hooks.py index 97a8b70953..177ac13a45 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -58,6 +58,11 @@ website_route_rules = [ {"from_route": "/kb/", "to_route": "Help Article"}, {"from_route": "/newsletters", "to_route": "Newsletter"}, {"from_route": "/profile", "to_route": "me"}, + {"from_route": "/app/", "to_route": "app"}, +] + +website_redirects = [ + {"source": r"/desk(.*)", "target": r"/app\1"}, ] base_template = "templates/base.html" @@ -202,8 +207,7 @@ scheduler_events = { "frappe.deferred_insert.save_to_db", "frappe.desk.form.document_follow.send_hourly_updates", "frappe.integrations.doctype.google_calendar.google_calendar.sync", - "frappe.email.doctype.newsletter.newsletter.send_scheduled_email", - "frappe.utils.password.delete_password_reset_cache" + "frappe.email.doctype.newsletter.newsletter.send_scheduled_email" ], "daily": [ "frappe.email.queue.set_expiry_for_email_queue", @@ -286,61 +290,70 @@ before_migrate = ['frappe.patches.v11_0.sync_user_permission_doctype_before_migr after_migrate = ['frappe.website.doctype.website_theme.website_theme.after_migrate'] otp_methods = ['OTP App','Email','SMS'] -user_privacy_documents = [ - { - 'doctype': 'File', - 'match_field': 'attached_to_name', - 'personal_fields': ['file_name', 'file_url'], - 'applies_to_website_user': 1 - }, - { - 'doctype': 'Email Group Member', - 'match_field': 'email', - }, - { - 'doctype': 'Email Unsubscribe', - 'match_field': 'email', - }, - { - 'doctype': 'Email Queue', - 'match_field': 'sender', - }, - { - 'doctype': 'Email Queue Recipient', - 'match_field': 'recipient', - }, - { - 'doctype': 'Contact', - 'match_field': 'email_id', - 'personal_fields': ['first_name', 'last_name', 'phone', 'mobile_no'], - }, - { - 'doctype': 'Contact Email', - 'match_field': 'email_id', - }, - { - 'doctype': 'Address', - 'match_field': 'email_id', - 'personal_fields': ['address_title', 'address_line1', 'address_line2', 'city', 'county', 'state', 'pincode', - 'phone', 'fax'], - }, - { - 'doctype': 'Communication', - 'match_field': 'sender', - 'personal_fields': ['sender_full_name', 'phone_no', 'content'], - }, - { - 'doctype': 'Communication', - 'match_field': 'recipients', - }, - { - 'doctype': 'User', - 'match_field': 'name', - 'personal_fields': ['email', 'username', 'first_name', 'middle_name', 'last_name', 'full_name', 'birth_date', - 'user_image', 'phone', 'mobile_no', 'location', 'banner_image', 'interest', 'bio', 'email_signature'], - 'applies_to_website_user': 1 - }, +user_data_fields = [ + {"doctype": "Access Log", "strict": True}, + {"doctype": "Activity Log", "strict": True}, + {"doctype": "Comment", "strict": True}, + { + "doctype": "Contact", + "filter_by": "email_id", + "redact_fields": ["first_name", "last_name", "phone", "mobile_no"], + "rename": True, + }, + {"doctype": "Contact Email", "filter_by": "email_id"}, + { + "doctype": "Address", + "filter_by": "email_id", + "redact_fields": [ + "address_title", + "address_line1", + "address_line2", + "city", + "county", + "state", + "pincode", + "phone", + "fax", + ], + }, + { + "doctype": "Communication", + "filter_by": "sender", + "redact_fields": ["sender_full_name", "phone_no", "content"], + }, + {"doctype": "Communication", "filter_by": "recipients"}, + {"doctype": "Email Group Member", "filter_by": "email"}, + {"doctype": "Email Unsubscribe", "filter_by": "email", "partial": True}, + {"doctype": "Email Queue", "filter_by": "sender"}, + {"doctype": "Email Queue Recipient", "filter_by": "recipient"}, + { + "doctype": "File", + "filter_by": "attached_to_name", + "redact_fields": ["file_name", "file_url"], + }, + { + "doctype": "User", + "filter_by": "name", + "redact_fields": [ + "email", + "username", + "first_name", + "middle_name", + "last_name", + "full_name", + "birth_date", + "user_image", + "phone", + "mobile_no", + "location", + "banner_image", + "interest", + "bio", + "email_signature", + ], + }, + {"doctype": "Version", "strict": True}, ] global_search_doctypes = { diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index 6faa542a60..b4304f6ee8 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -108,13 +108,8 @@ class TestConnectedApp(unittest.TestCase): session = requests.Session() - # first login of a new user on a new site fails with "401 UNAUTHORIZED" - # when anybody fixes that, the two lines below can be removed first_login = login() - self.assertEqual(first_login.status_code, 401) - - second_login = login() - self.assertEqual(second_login.status_code, 200) + self.assertEqual(first_login.status_code, 200) authorization_url = self.connected_app.initiate_web_application_flow(user=self.user_name) diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py index 71445b44d7..09da1ecc42 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py @@ -131,12 +131,10 @@ def upload_from_folder(path, is_private, dropbox_folder, dropbox_client, did_not for f in frappe.get_all("File", filters={"is_folder": 0, "is_private": is_private, "uploaded_to_dropbox": 0}, fields=['file_url', 'name', 'file_name']): - if is_private: - filename = f.file_url.replace('/private/files/', '') - else: - if not f.file_url: - f.file_url = '/files/' + f.file_name; - filename = f.file_url.replace('/files/', '') + if not f.file_url: + continue + filename = f.file_url.rsplit('/', 1)[-1] + filepath = os.path.join(path, filename) if filename in ignore_list: diff --git a/frappe/integrations/doctype/social_login_key/social_login_key.py b/frappe/integrations/doctype/social_login_key/social_login_key.py index 1092c3240e..d84e6ef11d 100644 --- a/frappe/integrations/doctype/social_login_key/social_login_key.py +++ b/frappe/integrations/doctype/social_login_key/social_login_key.py @@ -124,7 +124,7 @@ class SocialLoginKey(Document): "provider_name": "Frappe", "enable_social_login": 1, "custom_base_url": 1, - "icon":"/assets/frappe/images/favicon.png", + "icon":"/assets/frappe/images/frappe-favicon.svg", "redirect_url": "/api/method/frappe.www.login.login_via_frappe", "api_endpoint": "/api/method/frappe.integrations.oauth2.openid_profile", "api_endpoint_args":None, diff --git a/frappe/integrations/doctype/webhook/test_webhook.py b/frappe/integrations/doctype/webhook/test_webhook.py index c5084bae2d..fa7f9534e1 100644 --- a/frappe/integrations/doctype/webhook/test_webhook.py +++ b/frappe/integrations/doctype/webhook/test_webhook.py @@ -30,6 +30,10 @@ class TestWebhook(unittest.TestCase): self.user.email = frappe.mock("email") self.user.save() + def tearDown(self) -> None: + self.user.delete() + super().tearDown() + def test_validate_doc_events(self): "Test creating a submit-related webhook for a non-submittable DocType" diff --git a/frappe/integrations/oauth2.py b/frappe/integrations/oauth2.py index 07db778a2d..c444964a16 100644 --- a/frappe/integrations/oauth2.py +++ b/frappe/integrations/oauth2.py @@ -163,10 +163,13 @@ def openid_profile(*args, **kwargs): first_name, last_name, avatar, name = frappe.db.get_value("User", frappe.session.user, ["first_name", "last_name", "user_image", "name"]) frappe_userid = frappe.db.get_value("User Social Login", {"parent":frappe.session.user, "provider": "frappe"}, "userid") request_url = urlparse(frappe.request.url) + base_url = frappe.db.get_value("Social Login Key", "frappe", "base_url") or None if avatar: if validate_url(avatar): picture = avatar + elif base_url: + picture = base_url + '/' + avatar else: picture = request_url.scheme + "://" + request_url.netloc + avatar diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index c39a73ccd7..af06696621 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -118,7 +118,7 @@ core_doctypes_list = ( 'Customize Form Field', 'Property Setter', 'Custom Field', - 'Custom Script' + 'Client Script' ) log_types = ( diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 44c012ace1..f9e5a1a6f7 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -12,11 +12,9 @@ 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 -from frappe.utils.password import get_decrypted_password, set_encrypted_password 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, @@ -69,13 +67,13 @@ def get_controller(doctype): if frappe.local.dev_server: return _get_controller() - + site_controllers = frappe.controllers.setdefault(frappe.local.site, {}) if doctype not in site_controllers: site_controllers[doctype] = _get_controller() - + return site_controllers[doctype] - + class BaseDocument(object): ignore_in_getter = ("doctype", "_meta", "meta", "_table_fields", "_valid_columns") @@ -94,6 +92,14 @@ class BaseDocument(object): return self._meta def update(self, d): + """ Update multiple fields of a doctype using a dictionary of key-value pairs. + + Example: + doc.update({ + "user": "admin", + "balance": 42000 + }) + """ if "doctype" in d: self.set("doctype", d.get("doctype")) @@ -159,6 +165,15 @@ class BaseDocument(object): del self.__dict__[key] def append(self, key, value=None): + """ Append an item to a child table. + + Example: + doc.append("childtable", { + "child_table_field": "value", + "child_table_int_field": 0, + ... + }) + """ if value==None: value={} if isinstance(value, (dict, BaseDocument)): @@ -402,25 +417,60 @@ class BaseDocument(object): doc.db_update() def show_unique_validation_message(self, e): - # TODO: Find a better way to extract fieldname if frappe.db.db_type != 'postgres': fieldname = str(e).split("'")[-2] label = None - # unique_first_fieldname_second_fieldname is the constraint name - # created using frappe.db.add_unique - if "unique_" in fieldname: - fieldname = fieldname.split("_", 1)[1] + # MariaDB gives key_name in error. Extracting fieldname from key name + try: + fieldname = self.get_field_name_by_key_name(fieldname) + except IndexError: + pass - df = self.meta.get_field(fieldname) - if df: - label = df.label + label = self.get_label_from_fieldname(fieldname) frappe.msgprint(_("{0} must be unique").format(label or fieldname)) # this is used to preserve traceback raise frappe.UniqueValidationError(self.doctype, self.name, e) + def get_field_name_by_key_name(self, key_name): + """MariaDB stores a mapping between `key_name` and `column_name`. + This function returns the `column_name` associated with the `key_name` passed + + Args: + key_name (str): The name of the database index. + + Raises: + IndexError: If the key is not found in the table. + + Returns: + str: The column name associated with the key. + """ + return frappe.db.sql(f""" + SHOW + INDEX + FROM + `tab{self.doctype}` + WHERE + key_name=%s + AND + Non_unique=0 + """, key_name, as_dict=True)[0].get("Column_name") + + def get_label_from_fieldname(self, fieldname): + """Returns the associated label for fieldname + + Args: + fieldname (str): The fieldname in the DocType to use to pull the label. + + Returns: + str: The label associated with the fieldname, if found, otherwise `None`. + """ + df = self.meta.get_field(fieldname) + if df: + return df.label + def update_modified(self): """Update modified timestamp""" self.set("modified", now()) @@ -707,6 +757,8 @@ class BaseDocument(object): - Ignore if 'Ignore XSS Filter' is checked or fieldtype is 'Code' """ + from bs4 import BeautifulSoup + if frappe.flags.in_install: return @@ -743,6 +795,8 @@ class BaseDocument(object): def _save_passwords(self): """Save password field values in __Auth table""" + from frappe.utils.password import set_encrypted_password + if self.flags.ignore_save_passwords is True: return @@ -757,6 +811,8 @@ class BaseDocument(object): self.set(df.fieldname, '*'*len(new_password)) def get_password(self, fieldname='password', raise_exception=True): + from frappe.utils.password import get_decrypted_password + if self.get(fieldname) and not self.is_dummy_password(self.get(fieldname)): return self.get(fieldname) @@ -803,6 +859,11 @@ class BaseDocument(object): from frappe.model.meta import get_default_df df = get_default_df(fieldname) + if not currency: + currency = self.get(df.get("options")) + if not frappe.db.exists('Currency', currency, cache=True): + currency = None + val = self.get(fieldname) if translated: diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 7b29692ad1..d0e0a6fb1a 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -68,7 +68,7 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa check_permission_and_not_submitted(doc) frappe.db.sql("delete from `tabCustom Field` where dt = %s", name) - frappe.db.sql("delete from `tabCustom Script` where dt = %s", name) + frappe.db.sql("delete from `tabClient Script` where dt = %s", name) frappe.db.sql("delete from `tabProperty Setter` where doc_type = %s", name) frappe.db.sql("delete from `tabReport` where ref_doctype=%s", name) frappe.db.sql("delete from `tabCustom DocPerm` where parent=%s", name) diff --git a/frappe/model/document.py b/frappe/model/document.py index a01f66759e..0a7d9b4a7d 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -6,7 +6,6 @@ import frappe import time from frappe import _, msgprint from frappe.utils import flt, cstr, now, get_datetime_str, file_lock, date_diff -from frappe.utils.background_jobs import enqueue from frappe.model.base_document import BaseDocument, get_controller from frappe.model.naming import set_new_name from six import iteritems, string_types @@ -1015,6 +1014,8 @@ class Document(BaseDocument): def notify_update(self): """Publish realtime that the current document is modified""" + if frappe.flags.in_patch: return + frappe.publish_realtime("doc_update", {"modified": self.modified, "doctype": self.doctype, "name": self.name}, doctype=self.doctype, docname=self.name, after_commit=True) @@ -1267,6 +1268,8 @@ class Document(BaseDocument): # call _submit instead of submit, so you can override submit to call # run_delayed based on some action # See: Stock Reconciliation + from frappe.utils.background_jobs import enqueue + if hasattr(self, '_' + action): action = '_' + action diff --git a/frappe/model/sync.py b/frappe/model/sync.py index e04d3d56b9..61983d322c 100644 --- a/frappe/model/sync.py +++ b/frappe/model/sync.py @@ -61,7 +61,7 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe for module_name in frappe.local.app_modules.get(app_name) or []: folder = os.path.dirname(frappe.get_module(app_name + "." + module_name).__file__) - get_doc_files(files, folder, force, sync_everything, verbose=verbose) + get_doc_files(files, folder) l = len(files) if l: @@ -77,7 +77,7 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe # print each progress bar on new line print() -def get_doc_files(files, start_path, force=0, sync_everything = False, verbose=False): +def get_doc_files(files, start_path): """walk and sync all doctypes and pages""" # load in sequence - warning for devs diff --git a/frappe/patches.txt b/frappe/patches.txt index 8e8644342e..6e94bf0adc 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -35,6 +35,7 @@ frappe.patches.v11_0.change_email_signature_fieldtype execute:frappe.reload_doc('core', 'doctype', 'activity_log') execute:frappe.reload_doc('core', 'doctype', 'deleted_document') execute:frappe.reload_doc('core', 'doctype', 'domain_settings') +frappe.patches.v13_0.rename_custom_client_script frappe.patches.v8_0.rename_page_role_to_has_role #2017-03-16 frappe.patches.v7_2.setup_custom_perms #2017-01-19 frappe.patches.v8_0.set_user_permission_for_page_and_report #2017-03-20 @@ -209,7 +210,7 @@ frappe.patches.v9_1.resave_domain_settings frappe.patches.v9_1.revert_domain_settings frappe.patches.v9_1.move_feed_to_activity_log execute:frappe.delete_doc('Page', 'data-import-tool', ignore_missing=True) -frappe.patches.v10_0.reload_countries_and_currencies # 14-10-2020 +frappe.patches.v10_0.reload_countries_and_currencies # 2021-02-03 frappe.patches.v10_0.refactor_social_login_keys frappe.patches.v10_0.enable_chat_by_default_within_system_settings frappe.patches.v10_0.remove_custom_field_for_disabled_domain @@ -327,5 +328,8 @@ execute:frappe.delete_doc_if_exists('Page', 'workspace') execute:frappe.delete_doc_if_exists('Page', 'dashboard', force=1) frappe.core.doctype.page.patches.drop_unused_pages execute:frappe.get_doc('Role', 'Guest').save() # remove desk access -frappe.patches.v13_0.rename_desk_page_to_workspace +frappe.patches.v13_0.rename_desk_page_to_workspace # 02.02.2021 frappe.patches.v13_0.delete_package_publish_tool +frappe.patches.v13_0.rename_list_view_setting_to_list_view_settings +frappe.patches.v13_0.remove_twilio_settings +frappe.patches.v12_0.rename_uploaded_files_with_proper_name diff --git a/frappe/patches/v12_0/rename_uploaded_files_with_proper_name.py b/frappe/patches/v12_0/rename_uploaded_files_with_proper_name.py new file mode 100644 index 0000000000..854a381e1c --- /dev/null +++ b/frappe/patches/v12_0/rename_uploaded_files_with_proper_name.py @@ -0,0 +1,31 @@ +import frappe +import os + +def execute(): + file_names_with_url = frappe.get_all("File", filters={ + "is_folder": 0, + "file_name": ["like", "%/%"] + }, fields=['name', 'file_name', 'file_url']) + + for f in file_names_with_url: + filename = f.file_name.rsplit('/', 1)[-1] + + if not f.file_url: + f.file_url = f.file_name + + try: + if not file_exists(f.file_url): + continue + frappe.db.set_value('File', f.name, { + "file_name": filename, + "file_url": f.file_url + }, update_modified=False) + except Exception: + continue + +def file_exists(file_path): + file_path = frappe.utils.get_files_path( + file_path.rsplit('/', 1)[-1], + is_private=file_path.startswith('/private') + ) + return os.path.exists(file_path) diff --git a/frappe/patches/v12_0/reset_home_settings.py b/frappe/patches/v12_0/reset_home_settings.py index db16c31f15..e4b9de6cb2 100644 --- a/frappe/patches/v12_0/reset_home_settings.py +++ b/frappe/patches/v12_0/reset_home_settings.py @@ -1,6 +1,7 @@ import frappe def execute(): + frappe.reload_doc('core', 'doctype', 'user') frappe.db.sql(''' UPDATE `tabUser` SET `home_settings` = '' diff --git a/frappe/patches/v13_0/enable_custom_script.py b/frappe/patches/v13_0/enable_custom_script.py index 92284e6dcc..edc242e700 100644 --- a/frappe/patches/v13_0/enable_custom_script.py +++ b/frappe/patches/v13_0/enable_custom_script.py @@ -5,9 +5,8 @@ from __future__ import unicode_literals import frappe def execute(): - """Enable all the existing custom script""" - frappe.reload_doc("Custom", "doctype", "Custom Script") + """Enable all the existing Client script""" frappe.db.sql(""" - UPDATE `tabCustom Script` SET enabled=1 + UPDATE `tabClient Script` SET enabled=1 """) \ No newline at end of file diff --git a/frappe/patches/v13_0/remove_twilio_settings.py b/frappe/patches/v13_0/remove_twilio_settings.py new file mode 100644 index 0000000000..363cbdd4b6 --- /dev/null +++ b/frappe/patches/v13_0/remove_twilio_settings.py @@ -0,0 +1,20 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +import frappe + + +def execute(): + """Add missing Twilio patch. + + While making Twilio as a standaone app, we missed to delete Twilio records from DB through migration. Adding the missing patch. + """ + frappe.delete_doc_if_exists('DocType', 'Twilio Number Group') + if twilio_settings_doctype_in_integrations(): + frappe.delete_doc_if_exists('DocType', 'Twilio Settings') + frappe.db.sql("delete from `tabSingles` where `doctype`=%s", 'Twilio Settings') + +def twilio_settings_doctype_in_integrations() -> bool: + """Check Twilio Settings doctype exists in integrations module or not. + """ + return frappe.db.exists("DocType", {'name': 'Twilio Settings', 'module': 'Integrations'}) diff --git a/frappe/patches/v13_0/rename_custom_client_script.py b/frappe/patches/v13_0/rename_custom_client_script.py new file mode 100644 index 0000000000..718f1f6a46 --- /dev/null +++ b/frappe/patches/v13_0/rename_custom_client_script.py @@ -0,0 +1,9 @@ +import frappe + + +def execute(): + if frappe.db.exists("DocType", "Client Script"): + return + + frappe.rename_doc("DocType", "Custom Script", "Client Script") + frappe.reload_doctype("Client Script", force=True) diff --git a/frappe/patches/v13_0/rename_desk_page_to_workspace.py b/frappe/patches/v13_0/rename_desk_page_to_workspace.py index 10308e8e53..6483fc380c 100644 --- a/frappe/patches/v13_0/rename_desk_page_to_workspace.py +++ b/frappe/patches/v13_0/rename_desk_page_to_workspace.py @@ -13,7 +13,9 @@ def execute(): rename_doc('DocType', 'Desk Chart', 'Workspace Chart', ignore_if_exists=True) rename_doc('DocType', 'Desk Shortcut', 'Workspace Shortcut', ignore_if_exists=True) - if frappe.db.exists('DocType', 'Desk Link'): - rename_doc('DocType', 'Desk Link', 'Workspace Link', ignore_if_exists=True) + rename_doc('DocType', 'Desk Link', 'Workspace Link', ignore_if_exists=True) - frappe.reload_doc('desk', 'doctype', 'workspace') + frappe.reload_doc('desk', 'doctype', 'workspace', force=True) + frappe.reload_doc('desk', 'doctype', 'workspace_link', force=True) + frappe.reload_doc('desk', 'doctype', 'workspace_chart', force=True) + frappe.reload_doc('desk', 'doctype', 'workspace_shortcut', force=True) diff --git a/frappe/patches/v13_0/rename_list_view_setting_to_list_view_settings.py b/frappe/patches/v13_0/rename_list_view_setting_to_list_view_settings.py new file mode 100644 index 0000000000..fcf8afc826 --- /dev/null +++ b/frappe/patches/v13_0/rename_list_view_setting_to_list_view_settings.py @@ -0,0 +1,20 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import frappe + + +def execute(): + if frappe.db.table_exists('List View Setting'): + existing_list_view_settings = frappe.get_all('List View Settings', as_list=True) + for list_view_setting in frappe.get_all('List View Setting', fields = ['disable_count', 'disable_sidebar_stats', 'disable_auto_refresh', 'name']): + name = list_view_setting.pop('name') + if name not in [x[0] for x in existing_list_view_settings]: + list_view_setting['doctype'] = 'List View Settings' + list_view_settings = frappe.get_doc(list_view_setting) + # setting name here is necessary because autoname is set as prompt + list_view_settings.name = name + list_view_settings.insert() + frappe.delete_doc("DocType", "List View Setting", force=True) + frappe.db.commit() diff --git a/frappe/patches/v13_0/update_icons_in_customized_desk_pages.py b/frappe/patches/v13_0/update_icons_in_customized_desk_pages.py index da7d054682..93bf5c766e 100644 --- a/frappe/patches/v13_0/update_icons_in_customized_desk_pages.py +++ b/frappe/patches/v13_0/update_icons_in_customized_desk_pages.py @@ -2,6 +2,8 @@ from __future__ import unicode_literals import frappe def execute(): + if not frappe.db.exists('Desk Page'): return + pages = frappe.get_all("Desk Page", filters={ "is_standard": False }, fields=["name", "extends", "for_user"]) default_icon = {} for page in pages: diff --git a/frappe/printing/doctype/letter_head/letter_head.py b/frappe/printing/doctype/letter_head/letter_head.py index 491d959755..3a3b14faad 100644 --- a/frappe/printing/doctype/letter_head/letter_head.py +++ b/frappe/printing/doctype/letter_head/letter_head.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import frappe from frappe.utils import is_image from frappe.model.document import Document +from frappe import _ class LetterHead(Document): def before_insert(self): @@ -13,14 +14,20 @@ class LetterHead(Document): def validate(self): self.set_image() - if not self.is_default: - if not frappe.db.sql("""select count(*) from `tabLetter Head` where ifnull(is_default,0)=1"""): + self.validate_disabled_and_default() + + def validate_disabled_and_default(self): + if self.disabled and self.is_default: + frappe.throw(_("Letter Head cannot be both disabled and default")) + + if not self.is_default and not self.disabled: + if not frappe.db.exists('Letter Head', dict(is_default=1)): self.is_default = 1 def set_image(self): if self.source=='Image': if self.image and is_image(self.image): - self.content = ''.format(self.image) + self.content = ''.format(self.image) frappe.msgprint(frappe._('Header HTML set from attachment {0}').format(self.image), alert = True) else: frappe.msgprint(frappe._('Please attach an image file to set HTML'), alert = True, indicator = 'orange') diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json index 92d4a67d14..4032cef209 100644 --- a/frappe/printing/doctype/print_format/print_format.json +++ b/frappe/printing/doctype/print_format/print_format.json @@ -171,7 +171,7 @@ "fieldname": "custom_html_help", "fieldtype": "HTML", "label": "Custom HTML Help", - "options": "

Custom CSS Help

\n\n

Notes:

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

Examples

\n\n

1. Left align integers

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

1. Add border to sections except the last section

\n\n
.section-break { padding: 30px 0px; border-bottom: 1px solid #eee; }\n.section-break:last-child { padding-bottom: 0px; border-bottom: 0px;  }
\n" + "options": "

Custom CSS Help

\n\n

Notes:

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

Examples

\n\n

1. Left align integers

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

1. Add border to sections except the last section

\n\n
.section-break { padding: 30px 0px; border-bottom: 1px solid #eee; }\n.section-break:last-child { padding-bottom: 0px; border-bottom: 0px;  }
\n" }, { "depends_on": "custom_format", @@ -211,7 +211,7 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-12-14 11:38:49.132061", + "modified": "2021-03-01 15:25:46.578863", "modified_by": "Administrator", "module": "Printing", "name": "Print Format", diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js index 7e1db1eddb..dfd93c4efa 100644 --- a/frappe/printing/page/print/print.js +++ b/frappe/printing/page/print/print.js @@ -269,6 +269,7 @@ frappe.ui.form.PrintView = class { based_on: data.based_on, }; frappe.set_route('print-format-builder'); + this.print_sel.val(data.print_format_name); }, __('New Custom Print Format'), __('Start') @@ -412,6 +413,12 @@ frappe.ui.form.PrintView = class { ` ); + if (frappe.utils.is_rtl(this.lang_code)) { + this.$print_format_body.find('head').append( + `` + ); + } + this.$print_format_body.find('body').html( `` ); @@ -635,10 +642,13 @@ frappe.ui.form.PrintView = class { refresh_print_options() { this.print_formats = frappe.meta.get_print_formats(this.frm.doctype); - return this.print_sel.empty().add_options([ + const print_format_select_val = this.print_sel.val(); + this.print_sel.empty().add_options([ this.get_default_option_for_select(__('Select Print Format')), ...this.print_formats ]); + return this.print_formats.includes(print_format_select_val) + && this.print_sel.val(print_format_select_val); } selected_format() { 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 eb87190ab5..7e58e295b5 100644 --- a/frappe/printing/page/print_format_builder/print_format_builder.js +++ b/frappe/printing/page/print_format_builder/print_format_builder.js @@ -784,6 +784,7 @@ frappe.PrintFormatBuilder = Class.extend({ btn: this.page.btn_primary, callback: function(r) { me.print_format = r.message; + locals['Print Format'][me.print_format.name] = r.message; frappe.show_alert({message: __("Saved"), indicator: 'green'}); } }); diff --git a/frappe/printing/page/print_format_builder/print_format_builder_sidebar.html b/frappe/printing/page/print_format_builder/print_format_builder_sidebar.html index 0cf8178f82..1ebb87ac31 100644 --- a/frappe/printing/page/print_format_builder/print_format_builder_sidebar.html +++ b/frappe/printing/page/print_format_builder/print_format_builder_sidebar.html @@ -13,7 +13,7 @@ - {%= __(f.label) %} + {%= __(f.label) || __(f.fieldname) %}
{% } %} diff --git a/frappe/public/build.json b/frappe/public/build.json index 35e9d62436..51a2f55a37 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -27,8 +27,6 @@ "public/js/frappe/microtemplate.js", "public/js/frappe/query_string.js", - "public/js/frappe/ui/dropzone.js", - "public/js/frappe/ui/upload.html", "public/js/frappe/upload.js", "public/js/frappe/model/meta.js", @@ -36,7 +34,6 @@ "public/js/frappe/model/perm.js", "website/js/website.js", - "public/js/frappe/utils/rating_icons.html", "public/js/frappe/socketio_client.js" ], "js/bootstrap-4-web.min.js": "website/js/bootstrap-4.js", @@ -62,7 +59,6 @@ ], "js/dialog.min.js": [ "public/js/frappe/dom.js", - "public/js/frappe/form/formatters.js", "public/js/frappe/form/layout.js", "public/js/frappe/ui/field_group.js", @@ -79,8 +75,6 @@ "public/css/octicons/octicons.css", "public/less/desk.less", "public/less/module.less", - "public/less/link_preview.less", - "public/less/form.less", "public/less/mobile.less", "public/less/controls.less", "public/less/chat.less", @@ -99,22 +93,22 @@ "public/scss/print.scss" ], "concat:js/libs.min.js": [ - "public/js/lib/awesomplete/awesomplete.min.js", "public/js/lib/Sortable.min.js", "public/js/lib/jquery/jquery.hotkeys.js", "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js", "node_modules/vue/dist/vue.min.js", "node_modules/moment/min/moment-with-locales.min.js", "node_modules/moment-timezone/builds/moment-timezone-with-data.min.js", - "public/js/lib/socket.io.min.js", + "node_modules/socket.io-client/dist/socket.io.slim.js", + "node_modules/localforage/dist/localforage.min.js", "public/js/lib/jSignature.min.js", - "public/js/frappe/translate.js", "public/js/lib/leaflet/leaflet.js", "public/js/lib/leaflet/leaflet.draw.js", "public/js/lib/leaflet/L.Control.Locate.js", "public/js/lib/leaflet/easy-button.js" ], "js/desk.min.js": [ + "public/js/frappe/translate.js", "public/js/frappe/class.js", "public/js/frappe/polyfill.js", "public/js/frappe/provide.js", @@ -152,7 +146,6 @@ "public/js/frappe/ui/dialog.js", "public/js/frappe/ui/capture.js", "public/js/frappe/ui/app_icon.js", - "public/js/frappe/ui/dropzone.js", "public/js/frappe/ui/theme_switcher.js", "public/js/frappe/model/model.js", @@ -179,7 +172,6 @@ "public/js/frappe/utils/preview_email.js", "public/js/frappe/utils/file_manager.js", - "public/js/frappe/ui/upload.html", "public/js/frappe/upload.js", "public/js/frappe/ui/tree.js", @@ -194,7 +186,6 @@ "public/js/frappe/ui/toolbar/search.js", "public/js/frappe/ui/toolbar/tag_utils.js", "public/js/frappe/ui/toolbar/search.html", - "public/js/frappe/ui/toolbar/search_header.html", "public/js/frappe/ui/toolbar/search_utils.js", "public/js/frappe/ui/toolbar/about.js", "public/js/frappe/ui/toolbar/navbar.html", @@ -210,11 +201,11 @@ "public/js/frappe/ui/sort_selector.js", "public/js/frappe/change_log.html", + "public/js/frappe/ui/workspace_loading_skeleton.html", "public/js/frappe/desk.js", "public/js/frappe/query_string.js", "public/js/frappe/ui/comment.js", - "public/js/frappe/utils/rating_icons.html", "public/js/frappe/chat.js", "public/js/frappe/utils/energy_point_utils.js", @@ -232,7 +223,6 @@ "public/js/frappe/form/form.js", "public/js/frappe/meta_tag.js" ], - "css/list.min.css": "public/less/gantt.less", "js/list.min.js": [ "public/js/frappe/ui/listing.html", @@ -299,11 +289,12 @@ "css/web_form.css": [ "website/css/web_form.css", "public/css/octicons/octicons.css", - "public/less/controls.less", + "public/scss/controls.scss", "node_modules/frappe-datatable/dist/frappe-datatable.css" ], "css/email.css": "public/scss/email.scss", "js/barcode_scanner.min.js": "public/js/frappe/barcode_scanner/quagga.js", "js/user_profile_controller.min.js": "desk/page/user_profile/user_profile_controller.js", - "css/login.css": "public/scss/login.scss" + "css/login.css": "public/scss/login.scss", + "js/data_import_tools.min.js": "public/js/frappe/data_import/index.js" } diff --git a/frappe/public/css/common.css b/frappe/public/css/common.css deleted file mode 100644 index b08be904a6..0000000000 --- a/frappe/public/css/common.css +++ /dev/null @@ -1,253 +0,0 @@ -/* the element that this class is applied to, should have a max width for this to work*/ -body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -} -a { - cursor: pointer; -} -a, -a:hover, -a:active, -a:focus, -.btn, -.btn:hover, -.btn:active, -.btn:focus { - outline: 0; -} -img { - max-width: 100%; -} -p { - margin: 10px 0px; -} -.text-color { - color: #36414C !important; -} -.text-muted { - color: #8D99A6 !important; -} -.text-extra-muted { - color: #d1d8dd !important; -} -a, -.badge { - -webkit-transition: 0.2s; - -o-transition: 0.2s; - transition: 0.2s; -} -.btn { - -webkit-transition: background-color 0.2s; - -o-transition: background-color 0.2s; - transition: background-color 0.2s; -} -a.disabled, -a.disabled:hover { - color: #888; - cursor: default; - text-decoration: none; -} -a.grey, -.sidebar-section a, -.control-value a, -.data-row a { - text-decoration: none; -} -a.grey:hover, -.sidebar-section a:hover, -.control-value a:hover, -.data-row a:hover, -a.grey:focus, -.sidebar-section a:focus, -.control-value a:focus, -.data-row a:focus { - text-decoration: underline; -} -a.text-muted, -a.text-extra-muted { - text-decoration: none; -} -.underline { - text-decoration: underline; -} -.inline-block { - display: inline-block; -} -.bold, -.strong { - font-weight: bold; -} -kbd { - color: inherit; - background-color: #F0F4F7; -} -.btn [class^="fa fa-"], -.nav [class^="fa fa-"], -.btn [class*="fa fa-"], -.nav [class*="fa fa-"] { - display: inline-block; -} -.dropdown-menu > li > a { - padding: 14px; - white-space: normal; -} -.dropdown-menu { - min-width: 200px; - padding: 0px; - font-size: 12px; - max-height: 400px; - overflow: auto; - border-radius: 0px 0px 4px 4px; -} -.dropdown-menu .dropdown-header { - padding: 3px 14px; - font-size: 11px; - font-weight: 200; - padding-top: 12px; -} -.dropdown-menu .divider { - margin: 0px; -} -a.badge-hover:hover .badge, -a.badge-hover:focus .badge, -a.badge-hover:active .badge { - background-color: #D8DFE5; -} -.msgprint { - word-wrap: break-word; -} -.msgprint pre { - text-align: left; -} -.centered { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - -webkit-transform: translate(-50%, -50%); -} -.border-top { - border-top: 1px solid #d1d8dd; -} -.border-bottom { - border-bottom: 1px solid #d1d8dd; -} -.border-left { - border-left: 1px solid #d1d8dd; -} -.border-right { - border-right: 1px solid #d1d8dd; -} -.border { - border: 1px solid #d1d8dd; -} -.close-inline { - font-size: 120%; - font-weight: bold; - line-height: 1; - cursor: pointer; - color: inherit; - display: inline-block; -} -.close-inline:hover, -.close-inline:focus { - text-decoration: none; -} -.middle { - vertical-align: middle; -} -.full-center-container { - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; -} -.full-center { - position: absolute; - top: 50%; - left: 50%; - width: 100%; - transform: translate(-50%, -50%); - -webkit-transform: translate(-50%, -50%); -} -#freeze { - z-index: 1020; - bottom: 0px; - opacity: 0; - background-color: #fafbfc; -} -#freeze .freeze-message-container { - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; -} -#freeze .freeze-message { - position: absolute; - top: 50%; - left: 50%; - width: 100%; - transform: translate(-50%, -50%); - -webkit-transform: translate(-50%, -50%); - text-align: center; - color: #36414C !important; -} -#freeze.dark { - background-color: #334143; -} -#freeze.in { - opacity: 0.5; -} -a.no-decoration { - text-decoration: none; - color: inherit; -} -a.no-decoration:hover, -a.no-decoration:focus, -a.no-decoration:active { - text-decoration: none; - color: inherit; -} -.padding { - padding: 15px; -} -.margin { - margin: 15px; -} -.margin-top { - margin-top: 15px; -} -.margin-bottom { - margin-bottom: 15px; -} -.margin-left { - margin-left: 15px; -} -.margin-right { - margin-right: 15px; -} -@media (max-width: 767px) { - .text-center-xs { - text-align: center; - } -} -.grayscale { - -webkit-filter: grayscale(100%); - filter: grayscale(100%); -} -.uppercase { - padding-bottom: 4px; - text-transform: uppercase; - font-size: 12px; - letter-spacing: 0.4px; - color: #8D99A6; -} -.ellipsis { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 100%; - vertical-align: middle; -} diff --git a/frappe/public/css/docs.css b/frappe/public/css/docs.css deleted file mode 100644 index 3c57d0bf45..0000000000 --- a/frappe/public/css/docs.css +++ /dev/null @@ -1,604 +0,0 @@ -/* the element that this class is applied to, should have a max width for this to work*/ -.navbar .dropdown-toggle { - padding-top: 8px; - padding-bottom: 8px; -} -.navbar-fixed-top { - left: 0px; - right: 0px; -} -.navbar a { - font-size: 12px; - font-weight: bold; -} -.navbar-icon-home { - vertical-align: middle; -} -.navbar-icon-home:hover, -.navbar-icon-home:focus, -.navbar-icon-home:active, -.navbar-icon-home-hover { - opacity: 1; - Filter: alpha(opacity=100); - /* For IE8 and earlier */ -} -.navbar-user-image { - width: 24px; - height: 24px; - margin-right: 3px; - border-radius: 4px; -} -@media (max-width: 991px) { - .navbar-desk { - width: 35% !important; - } - .navbar-desk ~ ul > li { - float: left; - } - .navbar-desk ~ ul > li a { - padding-left: 10px !important; - padding-right: 10px !important; - } - .navbar-desk ~ ul > li a .avatar { - margin-right: 0; - } - .dropdown-navbar-new-comments > a { - padding: 8px 0 !important; - margin-left: 0 !important; - } -} -@media (max-width: 767px) { - .navbar-desk { - width: 50% !important; - } -} -#search-modal .modal-dialog, -#search-modal .modal-content { - background: transparent; -} -#search-modal .modal-header { - background: #fff; - width: 100%; -} -#search-modal .modal-header form { - vertical-align: middle; -} -#search-modal .modal-header button { - line-height: 0; - position: absolute; - right: 0; - top: 0; - z-index: 9; - padding: 9px; -} -.dropdown-navbar-new-comments > a { - border: 0; -} -.dropdown-navbar-new-comments .dropdown-menu { - margin-top: 0; -} -.dropdown-help .dropdown-menu { - width: 350px !important; - max-height: 440px; - overflow: auto; -} -.dropdown-help .dropdown-menu .input-group { - width: 100%; - background-color: #f5f7fa; - padding: 8px 12px; -} -.dropdown-help .dropdown-menu input { - width: 100%; - padding: 5px 10px; - outline: none; - border-radius: 3px 0 0 3px; - border: 1px solid #d1d8dd; - opacity: 0.9; - line-height: 1.5; -} -.dropdown-help .dropdown-menu button { - border: 1px solid #d1d8dd; -} -@media (max-width: 767px) { - .dropdown-help .dropdown-menu { - position: fixed !important; - top: 40px; - width: 100% !important; - } -} -@media (max-width: 767px) { - .dropdown-mobile.open .dropdown-menu { - position: absolute; - border-top: 1px solid rgba(0, 0, 0, 0.14902); - box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); - background-color: #fff; - right: 0; - left: auto; - } - .dropdown-mobile.open .dropdown-menu > li > a { - padding: 12px; - } - .dropdown-help { - display: none !important; - } -} -.navbar-new-comments { - display: inline-block; - min-width: 24px; - height: 24px; - border-radius: 4px; - color: #fff; - text-align: center; - padding: 2px 5px; - background-color: #b8c2cc; -} -.navbar-new-comments-true { - background-color: #ff5858; -} -.navbar-form .awesomplete { - margin-left: -15px; - width: 300px; -} -@media (max-width: 1199px) { - .navbar-form .awesomplete { - width: 280px; - } -} -@media (max-width: 991px) { - .navbar-form .awesomplete { - width: 250px; - } -} -#navbar-search { - width: 100%; - background-color: rgba(255, 255, 255, 0.9); -} -.navbar .navbar-search-icon { - color: #6C7680; - font-size: inherit; - position: relative; - right: 24px; - top: 1px; -} -.navbar .badge { - font-weight: normal; -} -#navbar-search-results { - left: auto; - right: inherit; - margin-top: -1px; - max-height: 300px; - overflow-y: auto; - overflow-x: hidden; -} -.navbar-center { - float: left; - color: #6C7680; -} -#navbar-breadcrumbs > li > a:before { - font-family: FontAwesome; - font-weight: normal; - font-style: normal; - text-decoration: inherit; - -webkit-font-smoothing: antialiased; - *margin-right: 0.3em; - display: inline-block; - speak: none; - font-size: 24px; - transition: 0.2s; - position: relative; - top: 3px; - content: "\f105"; - margin-right: 10px; - color: #C0C9D2; -} -#navbar-breadcrumbs > li > a:hover:before, -#navbar-breadcrumbs > li > a:focus:before, -#navbar-breadcrumbs > li > a:active:before { - color: #36414C; -} -#navbar-breadcrumbs > li > a { - padding: 6px 15px 10px 0px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 170px; -} -@media (min-width: 991px) and (max-width: 1199px) { - #navbar-breadcrumbs > li > a { - max-width: 120px; - } -} -.toolbar-user-fullname { - max-width: 150px; - display: inline-block; -} -.navbar-brand > img { - display: inline-block; -} -.toggle-sidebar { - margin-right: 10px; -} -.navbar-default .navbar-nav > li > a, -.navbar-default .navbar-brand { - color: #8D99A6; -} -body { - font-size: 16px; - line-height: 1.65em; - color: #454e57; - -webkit-font-smoothing: antialiased; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -} -.container { - max-width: 870px; -} -img { - max-width: 100%; -} -.splash { - border-bottom: 1px solid #d1d8dd; -} -.splash .jumbotron { - background-color: transparent; - padding: 40px 0 60px 0; - text-align: center; -} -.splash .jumbotron h1 { - font-size: 48px; - font-weight: 400; - opacity: 0.9; - color: #2E3338; -} -.splash .jumbotron p { - font-size: 24px; - font-color: #8D99A6 !important; - letter-spacing: 0px; - opacity: 0.7; - margin-bottom: 90px; - font-weight: 300; - line-height: 1.4em; -} -.splash .section { - padding: 30px 0 0 0; -} -.page-container { - padding-top: 38px; - margin: 0 auto; - max-width: 870px; -} -.page-container .webpage-content ol > li, -.page-container .webpage-content ul > li { - margin: 13px auto; -} -.page-container .webpage-content ol > li li, -.page-container .webpage-content ul > li li { - margin: 4px auto; -} -.page-container .webpage-content ol li ol { - list-style-type: disc; -} -.page-container .webpage-content ul, -.page-container .webpage-content ol { - margin-bottom: 32px; -} -@media (min-width: 768px) { - .page-container .page-content { - width: 83%; - margin: 0 auto; - } -} -#page-index { - padding-top: 0; - width: 100%; - margin: 0; -} -#page-index .page-content { - width: 100%; - margin: 0; -} -body[data-path="index"] .navbar .toggle-sidebar i { - color: #fff; -} -code { - color: #e66a12; - background: #fff6df; -} -pre { - background: #fafbfc; - border: 1px solid #e1e9f0; - border-radius: 2px; -} -.hljs { - background: transparent; - border: none; - padding: 1.2em 1.5em 1.5em; - color: #454e57; -} -.hljs-keyword, -.hljs-tag, -.css .hljs-class, -.css .hljs-id, -.lisp .hljs-title, -.nginx .hljs-title, -.hljs-request, -.hljs-status, -.clojure .hljs-attribute { - color: #e66a12; -} -.diff .hljs-deletion, -.hljs-string, -.hljs-tag .hljs-value, -.hljs-preprocessor, -.hljs-pragma, -.hljs-built_in, -.hljs-javadoc, -.smalltalk .hljs-class, -.smalltalk .hljs-localvars, -.smalltalk .hljs-array, -.css .hljs-rules .hljs-value, -.hljs-attr_selector, -.hljs-pseudo, -.apache .hljs-cbracket, -.tex .hljs-formula, -.coffeescript .hljs-attribute { - color: #dd4a68; -} -.hljs-number, -.hljs-date, -.hljs-regexp, -.hljs-literal, -.hljs-hexcolor, -.smalltalk .hljs-symbol, -.smalltalk .hljs-char, -.go .hljs-constant, -.hljs-change, -.lasso .hljs-variable, -.makefile .hljs-variable, -.asciidoc .hljs-bullet, -.markdown .hljs-bullet, -.asciidoc .hljs-link_url, -.markdown .hljs-link_url { - color: #7575ff; -} -.hljs-shebang, -.diff .hljs-addition, -.hljs-comment, -.hljs-annotation, -.hljs-template_comment, -.hljs-pi, -.hljs-doctype { - color: #6a906a; -} -.dos .hljs-keyword, -.hljs-decorator, -.hljs-title, -.hljs-type, -.diff .hljs-header, -.ruby .hljs-class .hljs-parent, -.apache .hljs-tag, -.nginx .hljs-built_in, -.tex .hljs-command, -.hljs-prompt { - color: #4f4fa4; -} -.navbar { - background-color: #36414C !important; - margin-left: auto; - margin-right: auto; -} -.navbar .container { - max-width: 870px; -} -.navbar .brand-logo { - width: 30px; - margin-top: -4px; - margin-right: 7px; -} -.navbar a { - font-size: 16px; - font-weight: normal; - color: #fff !important; -} -.navbar a.navbar-brand { - font-weight: bold; -} -.navbar a.toggle-sidebar { - margin-top: 8px; -} -.sidebar a { - font-size: 14px; - padding-top: 14px !important; - padding-bottom: 14px !important; -} -.breadcrumb { - line-height: 1em; - color: #8D99A6; - background-color: transparent; - margin-bottom: 32px; - padding: 0px; - padding-left: 20px; - background: url('/assets/img/up.png') 0% 30% no-repeat; -} -.breadcrumb .icon { - display: none; -} -.breadcrumb a, -.breadcrumb a:hover, -.breadcrumb a:focus, -.breadcrumb a:visited { - color: #7575ff; - font-size: 16px; -} -.hero-and-content a, -.hero-and-content a:hover, -.hero-and-content a:focus, -.hero-and-content a:visited { - color: #5E64FF; -} -.hero-and-content a.btn { - color: inherit; -} -a.btn-primary { - color: #7575ff; -} -a.btn-primary:hover, -a.btn-primary:focus, -a.btn-primary:visited { - color: #7575ff; -} -.btn-next-wrapper { - margin-top: 32px; - text-align: right; -} -h2 { - margin-top: 48px; - font-size: 24px; -} -h3, -h4 { - margin-top: 48px; -} -p { - margin-bottom: 16px; -} -.hero-and-content > p { - max-width: 723px; - margin: 0 auto; -} -.navbar { - background-color: transparent; - border: none; - padding: 15px 0px; - border-radius: 0px; - border-bottom: 1px solid #d1d8dd; -} -.section { - padding: 64px 0 0 0; -} -.dev-header { - margin-bottom: 30px; -} -.docs-footer { - padding: 30px 0px 60px 0px; - border-top: 1px solid #d1d8dd; - max-width: 870px; - margin: 0 auto; - margin-top: 80px; - font-size: 14px; -} -.docs-footer h3 { - margin-top: 24px; - font-size: 16px; -} -.docs-footer img.frappe-bird { - width: 40px; - height: 40px; - background: #fff; - margin-bottom: 10px; - padding: 5px; -} -.docs-footer a { - color: #8D99A6; -} -.docs-footer li { - display: inline-block; - margin: 0 10px; -} -.docs-footer .built-with-frappe { - margin-top: -50px; -} -.browser-image { - min-height: 200px; - border: 1px solid #d1d8dd; - border-bottom: 0px; -} -.fake-browser-frame { - position: relative; - margin: 24px auto 0px; - box-shadow: 0px -6px 100px 1px rgba(0, 0, 0, 0.1), 0px -6px 50px 1px rgba(0, 0, 0, 0.4); -} -.fake-browser-frame::before { - content: ""; - height: 24px; - position: absolute; - top: -24px; - left: 0px; - right: 0px; - border: 1px solid #d1d8dd; - background: #f5f7fa; - border-bottom: none; - border-top-left-radius: 4px; - border-top-right-radius: 4px; -} -.fake-browser-frame::after { - content: '\f111 \00a0\00a0 \f111 \00a0\00a0 \f111'; - position: absolute; - color: #d1d8dd; - top: -15px; - left: 8px; - /* octicon */ - font: normal normal; - font-size: 8px; - font-family: 'FontAwesome'; - line-height: 1; - display: inline-block; - text-decoration: none; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} -.fake-iphone-frame { - position: relative; - padding: 40px 8px; - border: 1px solid #d1d8dd; - border-radius: 15px; -} -.fake-ipad-frame { - position: relative; - padding: 8px 40px; - border: 1px solid #d1d8dd; - border-radius: 15px; -} -hr { - margin: 48px 0px 30px; -} -.edit { - color: #8d99a6; -} -a.edit, -a.edit:hover, -a.edit:focus, -a.edit:visited, -.edit-container .icon { - color: #8d99a6; -} -.btn-next { - margin-top: 30px; - margin-bottom: 0px; -} -.btn-next:after { - content: " \2192"; -} -#current td { - font-weight: bold; -} -#current td code { - font-weight: normal; - background: transparent; - font-family: "Helvetica Neue", Helvetica, Arial, "Open Sans", sans-serif; - color: #454e57; - font-size: 16px; -} -.hero-and-content [data-html-block="hero"] { - overflow-y: hidden; -} -.page-content-wrapper > .row .col-sm-8 { - width: 100%; -} -.page-content-wrapper > .row .col-sm-4 { - display: none; -} diff --git a/frappe/public/css/fonts/inter/inter.css b/frappe/public/css/fonts/inter/inter.css index 8fba7cf2cf..e7c92eed4e 100644 --- a/frappe/public/css/fonts/inter/inter.css +++ b/frappe/public/css/fonts/inter/inter.css @@ -1,5 +1,6 @@ @font-face { font-family: 'Inter'; + font-display: swap; font-style: normal; font-weight: 100; src: url("/assets/frappe/css/fonts/inter/inter_thin.woff2") format("woff2"), @@ -7,6 +8,7 @@ } @font-face { font-family: 'Inter'; + font-display: swap; font-style: italic; font-weight: 100; src: url("/assets/frappe/css/fonts/inter/inter_thinitalic.woff2") format("woff2"), @@ -15,6 +17,7 @@ @font-face { font-family: 'Inter'; + font-display: swap; font-style: normal; font-weight: 200; src: url("/assets/frappe/css/fonts/inter/inter_extralight.woff2") format("woff2"), @@ -22,6 +25,7 @@ } @font-face { font-family: 'Inter'; + font-display: swap; font-style: italic; font-weight: 200; src: url("/assets/frappe/css/fonts/inter/inter_extralightitalic.woff2") format("woff2"), @@ -30,6 +34,7 @@ @font-face { font-family: 'Inter'; + font-display: swap; font-style: normal; font-weight: 300; src: url("/assets/frappe/css/fonts/inter/inter_light.woff2") format("woff2"), @@ -37,6 +42,7 @@ } @font-face { font-family: 'Inter'; + font-display: swap; font-style: italic; font-weight: 300; src: url("/assets/frappe/css/fonts/inter/inter_lightitalic.woff2") format("woff2"), @@ -45,6 +51,7 @@ @font-face { font-family: 'Inter'; + font-display: swap; font-style: normal; font-weight: 400; src: url("/assets/frappe/css/fonts/inter/inter_regular.woff2") format("woff2"), @@ -52,6 +59,7 @@ } @font-face { font-family: 'Inter'; + font-display: swap; font-style: italic; font-weight: 400; src: url("/assets/frappe/css/fonts/inter/inter_italic.woff2") format("woff2"), @@ -60,6 +68,7 @@ @font-face { font-family: 'Inter'; + font-display: swap; font-style: normal; font-weight: 500; src: url("/assets/frappe/css/fonts/inter/inter_medium.woff2") format("woff2"), @@ -67,6 +76,7 @@ } @font-face { font-family: 'Inter'; + font-display: swap; font-style: italic; font-weight: 500; src: url("/assets/frappe/css/fonts/inter/inter_mediumitalic.woff2") format("woff2"), @@ -75,6 +85,7 @@ @font-face { font-family: 'Inter'; + font-display: swap; font-style: normal; font-weight: 600; src: url("/assets/frappe/css/fonts/inter/inter_semibold.woff2") format("woff2"), @@ -82,6 +93,7 @@ } @font-face { font-family: 'Inter'; + font-display: swap; font-style: italic; font-weight: 600; src: url("/assets/frappe/css/fonts/inter/inter_semibolditalic.woff2") format("woff2"), @@ -90,6 +102,7 @@ @font-face { font-family: 'Inter'; + font-display: swap; font-style: normal; font-weight: 700; src: url("/assets/frappe/css/fonts/inter/inter_bold.woff2") format("woff2"), @@ -97,6 +110,7 @@ } @font-face { font-family: 'Inter'; + font-display: swap; font-style: italic; font-weight: 700; src: url("/assets/frappe/css/fonts/inter/inter_bolditalic.woff2") format("woff2"), @@ -105,6 +119,7 @@ @font-face { font-family: 'Inter'; + font-display: swap; font-style: normal; font-weight: 800; src: url("/assets/frappe/css/fonts/inter/inter_extrabold.woff2") format("woff2"), @@ -112,6 +127,7 @@ } @font-face { font-family: 'Inter'; + font-display: swap; font-style: italic; font-weight: 800; src: url("/assets/frappe/css/fonts/inter/inter_extrabolditalic.woff2") format("woff2"), @@ -120,6 +136,7 @@ @font-face { font-family: 'Inter'; + font-display: swap; font-style: normal; font-weight: 900; src: url("/assets/frappe/css/fonts/inter/inter_black.woff2") format("woff2"), @@ -127,8 +144,9 @@ } @font-face { font-family: 'Inter'; + font-display: swap; font-style: italic; font-weight: 900; src: url("/assets/frappe/css/fonts/inter/inter_blackitalic.woff2") format("woff2"), url("/assets/frappe/css/fonts/inter/inter_blackitalic.woff") format("woff"); -} \ No newline at end of file +} diff --git a/frappe/public/css/form.css b/frappe/public/css/form.css deleted file mode 100644 index baad12b495..0000000000 --- a/frappe/public/css/form.css +++ /dev/null @@ -1,729 +0,0 @@ -.form-print-wrapper { - border: 1px solid #d1d8dd; - border-top: none; -} -.print-preview-wrapper { - padding: 30px 0px; - background-color: #f5f7fa; -} -.print-toolbar { - margin: 0px; - padding: 10px 0px; - border-bottom: 1px solid #d1d8dd; -} -.print-toolbar > div { - padding-right: 0px; -} -.print-toolbar > div:last-child { - padding-right: 15px; -} -.form-inner-toolbar { - padding: 10px 15px 0px; - background-color: #fafbfc; - text-align: right; -} -.form-inner-toolbar .btn { - margin-bottom: 10px; -} -.form-clickable-section { - border-top: 1px solid #d1d8dd; - padding: 10px 15px; - background-color: #F7FAFC; -} -.form-page.second-page { - border-top: 1px solid #d1d8dd; -} -.form-message { - padding: 15px 30px; - border-bottom: 1px solid #d1d8dd; -} -.document-flow-wrapper { - padding: 40px 15px 30px; - font-size: 12px; - border-bottom: 1px solid #EBEFF2; -} -.document-flow-wrapper .document-flow { - display: inline-block; - position: relative; - left: 50%; - transform: translateX(-50%); -} -.document-flow-wrapper .document-flow .document-flow-link-wrapper { - width: 140px; - display: inline-block; -} -.document-flow-wrapper .document-flow .document-flow-link-wrapper:not(:last-child) { - border-top: 1px solid #b8c2cc; - margin-right: -4px; -} -.document-flow-wrapper .document-flow .document-flow-link-wrapper:last-child { - margin-right: -140px; -} -.document-flow-wrapper .document-flow .document-flow-link { - margin-top: -10px; - display: inline-block; -} -.document-flow-wrapper .document-flow .document-flow-link:not(.disabled):hover .document-flow-link-label, -.document-flow-wrapper .document-flow .document-flow-link:not(.disabled):focus .document-flow-link-label, -.document-flow-wrapper .document-flow .document-flow-link:not(.disabled):active .document-flow-link-label { - text-decoration: underline; -} -.document-flow-wrapper .document-flow .document-flow-link-label { - display: inline-block; - margin-left: -50%; - margin-top: 5px; -} -@media (max-width: 767px) { - .document-flow-wrapper { - display: none; - } -} -.form-dashboard { - background-color: #fafbfc; -} -.form-dashboard-wrapper { - margin: -15px 0px; -} -.form-documents h6 { - margin-top: 15px; -} -.form-dashboard-section { - margin: 0px -15px; - padding: 15px 30px; - border-bottom: 1px solid #EBEFF2; -} -.form-dashboard-section:first-child { - padding-top: 0px; -} -.form-dashboard-section:last-child { - border-bottom: none; -} -.form-heatmap .heatmap { - display: flex; - justify-content: center; -} -.form-heatmap .heatmap-message { - margin-top: 10px; -} -@media (max-width: 991px) { - .form-heatmap { - overflow: hidden; - overflow-x: scroll; - } -} -.inline-graph .inline-graph-half { - width: 48%; - display: inline-block; - position: relative; - height: 30px; -} -.inline-graph .inline-graph-half .inline-graph-count { - font-size: 10px; - position: absolute; - left: 0; - right: 0; - top: 3px; - padding: 0px 5px; - text-align: left; -} -.inline-graph .inline-graph-half .inline-graph-bar { - position: absolute; - left: 0; - right: 0; - top: 20px; -} -.inline-graph .inline-graph-half .inline-graph-bar-inner { - display: block; - float: left; - background-color: #d1d8dd; - height: 6px; - border-radius: 0px 3px 3px 0px; -} -.inline-graph .inline-graph-half .inline-graph-bar-inner.dark { - background-color: #36414C; -} -.inline-graph .inline-graph-half:first-child { - border-right: 1px solid #d1d8dd; - margin-right: -3px; -} -.inline-graph .inline-graph-half:first-child .inline-graph-count { - text-align: right; -} -.inline-graph .inline-graph-half:first-child .inline-graph-bar-inner { - float: right; - border-radius: 3px 0px 0px 3px; -} -.progress-area { - padding-top: 15px; - padding-bottom: 15px; -} -.form-links .document-link { - margin-bottom: 10px; - height: 22px; -} -.form-links .document-link:hover .badge-link { - text-decoration: underline; -} -.form-links .document-link:hover .badge-link[disabled='disabled'] { - text-decoration: none; -} -.form-links .count { - display: inline-block; - margin-left: 5px; - margin-right: 5px; -} -h6.uppercase, -.h6.uppercase { - font-size: 11px; - font-weight: normal; - letter-spacing: 0.4px; - text-transform: uppercase; - color: #8D99A6; -} -.form-section { - margin: 0px; - padding: 15px; -} -.form-section .form-section-description { - margin-bottom: 10px; -} -.form-section .form-section-heading { - margin: 10px 0px; -} -.form-section .section-head { - margin: 0px 0px 15px 15px; - cursor: pointer; -} -.form-section .section-head .collapse-indicator { - color: #d1d8dd; - margin-left: 10px; - position: relative; - bottom: -1px; -} -.form-section .section-head .collapse-indicator.octicon-chevron-up { - bottom: -2px; -} -.form-section .section-head.collapsed { - margin-bottom: 0px; -} -.form-section:not(:last-child), -.form-inner-toolbar { - border-bottom: 1px solid #d1d8dd; -} -.empty-section { - display: none !important; -} -.modal .form-layout { - margin: -15px; -} -.modal .form-grid .form-layout { - margin: 0px; -} -.modal .form-section { - padding: 15px 7px; -} -.help ol { - padding-left: 19px; -} -.field_description_top { - margin-bottom: 3px; -} -.user-actions { - margin-bottom: 15px; -} -.user-actions a { - font-weight: bold; -} -.badge-important { - background-color: #e74c3c; -} -.address-box { - background-color: #fafbfc; - padding: 0px 15px; - margin: 15px 0px; - border: 1px solid #d1d8dd; - border-radius: 3px; - font-size: 12px; -} -.timeline { - margin: 30px 0px; -} -.timeline .timeline-head .comment-input { - height: auto; -} -.timeline-item { - margin-top: 0px; -} -.timeline-item b { - color: #36414C !important; -} -.timeline-item blockquote { - font-size: inherit; -} -.timeline-item .btn-more { - margin-left: 65px; -} -.timeline-item .gmail_extra { - display: none; -} -.timeline-items { - position: relative; -} -.timeline { - position: relative; -} -.timeline::before { - content: " "; - border-left: 1px solid #d1d8dd; - position: absolute; - top: 0px; - bottom: -124px; - left: 43px; - z-index: 0; -} -.timeline.in-dialog::before { - bottom: 0px; -} -@media (max-width: 991px) { - .timeline::before { - bottom: -64px; - } -} -.timeline-item.user-content { - margin: 30px 0px 30px 27px; -} -.timeline-item.user-content .media-body { - border: 1px solid #d1d8dd; - border-radius: 3px; - margin-left: -7px; - position: relative; - max-width: calc(100% - 50px); - padding-right: 0px; - overflow: visible; -} -.timeline-item.user-content .avatar-medium { - margin-right: 10px; - height: 45px; - width: 45px; -} -.timeline-item.user-content .action-btns { - position: absolute; - right: 0; - padding: 8px 15px 0 5px; -} -.timeline-item.user-content .action-btns .edit-btn-container { - margin-right: 13px; -} -.timeline-item.user-content .comment-header { - background-color: #fafbfc; - padding: 10px 15px 8px 13px; - margin: 0px; - color: #8D99A6; - border-bottom: 1px solid #EBEFF2; -} -.timeline-item.user-content .comment-header.links-active { - padding-right: 77px; -} -.timeline-item.user-content .comment-header .asset-details { - display: inline-block; - width: 100%; -} -.timeline-item.user-content .comment-header .asset-details .btn-link { - border: 0; - border-radius: 0; - padding: 0; -} -.timeline-item.user-content .comment-header .asset-details .btn-link:hover { - text-decoration: none; -} -.timeline-item.user-content .comment-header .commented-on-small { - display: none; -} -.timeline-item.user-content .comment-header .octicon-heart { - color: #ff5858; - cursor: pointer; -} -.timeline-item.user-content .reply { - padding: 15px; - overflow: auto; -} -.timeline-item.user-content .reply > div > p:first-child { - margin-top: 0px; -} -.timeline-item.user-content .reply > div > p:last-child { - margin-bottom: 0px; -} -.timeline-item.user-content .reply hr { - margin: 10px 0px; -} -.timeline-item.user-content .close-btn-container .close { - color: inherit; - opacity: 1; - padding: 0; - font-size: 18px; -} -.timeline-item.user-content .edit-btn-container { - padding: 0; -} -.timeline-item.user-content .edit-btn-container .edit { - color: inherit; - font-size: 21px; - line-height: 1; -} -.timeline-item.user-content .edit-btn-container .edit .octicon-check { - font-size: 1em; -} -.timeline-item.user-content .edit-btn-container .edit:hover, -.timeline-item.user-content .edit-btn-container .edit:focus { - color: #000; -} -.timeline-item.user-content .comment-likes { - margin-left: 5px; -} -.timeline-item.user-content .media-body:after, -.timeline-item.user-content .media-body:before { - right: 100%; - top: 15px; - border: solid transparent; - content: " "; - height: 0; - width: 0; - position: absolute; - pointer-events: none; -} -.timeline-item.user-content .media-body:after { - border-color: rgba(136, 183, 213, 0); - border-right-color: #fafbfc; - border-width: 6px; - margin-top: -6px; -} -.timeline-item.user-content .media-body:before { - border-color: rgba(194, 225, 245, 0); - border-right-color: #d1d8dd; - border-width: 7px; - margin-top: -7px; -} -.timeline-item.notification-content { - padding-left: 30px; - margin: 30px 0px; - position: relative; - color: #8D99A6; -} -.timeline-item.notification-content * { - color: #8D99A6; -} -.timeline-item.notification-content .fa-fw { - margin-left: 36px; -} -.timeline-item.notification-content div.small { - padding-left: 40px; -} -.timeline-item.notification-content div.small .fa-fw { - margin-left: 0px; -} -.timeline-item.notification-content .octicon-heart { - color: #ff5858 !important; -} -.timeline-item.notification-content::before { - content: " "; - width: 7px; - height: 7px; - background-color: #d1d8dd; - position: absolute; - left: 40px; - border-radius: 50%; - top: 5px; -} -.timeline-item .reply-link { - margin-left: 15px; - font-size: 12px; -} -.timeline-head { - background-color: white; - border: 1px solid #d1d8dd; - border-radius: 3px; - position: relative; - z-index: 1; -} -.timeline-head .comment-input-header { - background-color: #fafbfc; - padding: 7px 15px; - border-bottom: 1px solid #EBEFF2; -} -.timeline-head .comment-input-container { - padding: 15px; -} -.timeline-head .comment-input-container .awesomplete > ul { - min-width: 200px; -} -.timeline-head .comment-input { - border-color: #EBEFF2; - max-width: 100%; -} -.timeline-head .comment-input:focus { - box-shadow: none; -} -@media (max-width: 767px) { - .timeline-head { - border-left: none; - border-right: none; - border-radius: 0px; - } -} -.signature-field { - min-height: 300px; - background: #fff; - border: 1px solid #d1d8dd; - border-radius: 3px; - position: relative; - margin-top: -10px; -} -.signature-display { - margin: 7px 0px; - background: #fff; -} -.signature-btn-row { - position: absolute; - bottom: 12px; - right: 12px; -} -.signature-reset { - z-index: 10; - height: 30px; - width: 30px; - padding: 4px 0px; -} -.signature-img { - background: #fff; - border-radius: 3px; - margin-top: 5px; - max-height: 150px; -} -.timeline-new-email { - margin: 30px 0px; - padding-left: 70px; - position: relative; -} -.timeline-new-email::before { - content: " "; - width: 7px; - height: 7px; - background-color: #d1d8dd; - position: absolute; - left: 40px; - border-radius: 50%; - top: 5px; -} -.form-footer h5 { - margin: 15px 0px; - font-weight: bold; -} -.control-label, -.grid-heading-row { - color: #8D99A6; - font-size: 12px; -} -.control-label { - margin-bottom: 5px; - font-weight: normal; -} -.like-disabled-input { - margin-bottom: 7px; - min-height: 30px; - font-weight: bold; - background-color: #f5f7fa; - padding: 5px 10px; - border-radius: 3px; -} -.disabled-check { - color: #f5f7fa; - margin-right: 5px; - margin-bottom: -2px; -} -.like-disabled-input.for-description { - font-weight: normal; - font-size: 12px; -} -.frappe-control { - margin-bottom: 10px; -} -.frappe-control .help-box { - margin-top: 3px; -} -.frappe-control pre { - white-space: pre-wrap; - background-color: inherit; - border: none; - padding: 0px; - margin: 0px; -} -.flex-justify-center { - display: flex; - justify-content: center; -} -.flex-justify-end { - display: flex; - justify-content: flex-end; -} -.hide-control { - display: none !important; -} -.shared-user { - margin-bottom: 10px; -} -.attach-missing-image, -.attach-image-display { - cursor: pointer; -} -select.form-control { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; -} -.form-control.bold { - color: #000; - font-weight: bold; - background-color: #fffdf4; -} -.form-control[data-fieldtype="Password"] { - position: inherit; -} -.password-strength-indicator { - float: right; - padding: 15px; - margin-top: -41px; - margin-right: -7px; -} -.password-strength-message { - margin-top: -10px; -} -.control-code, -.control-code.bold { - height: 400px; - font-family: Monaco, "Courier New", monospace; - color: #36414C; - font-size: 12px; - line-height: 1.7em; -} -.delivery-status-indicator { - display: inline-block; - margin-top: -3px; - margin-left: 1px; - font-weight: 500; - color: #8D99A6; -} -.attach-btn { - margin-top: 10px; -} -@media (min-width: 768px) { - .layout-main .form-column.col-sm-12 > form > .input-max-width { - max-width: 50%; - padding-right: 15px; - } - .col-sm-6 .form-grid .form-column.col-sm-12 > form > .input-max-width { - max-width: none; - padding-right: 0px; - } - .form-column.col-sm-6 textarea[data-fieldtype="Code"] { - height: 120px !important; - } -} -@media (max-width: 991px) { - .form-section .form-section-heading { - margin-top: 10px; - } -} -@media (max-width: 767px) { - .form-section .section-head { - padding: 15px 15px 15px 0px; - } - .form-section .section-body .form-column:first-child .radio, - .form-section .section-body .form-column:first-child .checkbox { - margin-top: 0; - } - .form-column { - border-bottom: 1px solid #EBEFF2; - padding-top: 15px; - padding-bottom: 15px; - } - .form-column:last-child { - border-bottom: 0px; - } - .form-section { - padding-left: 0px !important; - padding-right: 0px !important; - } - .form-grid { - margin-left: -17px; - margin-right: -17px; - border-left: none !important; - border-right: none !important; - border-radius: none; - } - .form-page .form-section { - padding: 0px 15px; - } - .frappe-control.form-page { - padding: 7px 15px; - border-bottom: 1px solid #EBEFF2; - margin: 0px -15px; - } - .frappe-control.form-page .link-btn { - top: -2px; - } - .frappe-control.form-page .like-disabled-input { - min-height: 0px !important; - } - .frappe-control.form-page:last-child { - margin-bottom: 0px; - } - .form-page .frappe-control:last-child { - border-bottom: 0px; - } - .form-page .frappe-control[data-fieldtype="Table"] { - padding: 0px 15px; - margin-top: -1px; - border-bottom: none; - } - .form-page .frappe-control[data-fieldtype="Table"] label { - margin-top: 7px; - } - .form-page .form-control { - border: none; - border-bottom: 1px solid #d1d8dd; - box-shadow: none; - background-color: inherit; - height: auto; - padding: 0px; - margin-bottom: 7px; - border-radius: 0px; - text-align: left !important; - } - .form-page .form-control:focus { - box-shadow: none; - } -} -/* goals */ -.goals-page-container { - background-color: #fafbfc; - padding-top: 1px; -} -.goals-page-container .goal-container { - background-color: #fff; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); - border-radius: 2px; - padding: 10px; - margin: 10px; -} -body[data-route^="Form/Communication"] textarea[data-fieldname="subject"] { - height: 80px !important; -} -.frappe-control[data-fieldtype="Attach"] .attached-file { - position: relative; - margin-top: 5px; -} -.frappe-control[data-fieldtype="Attach"] .attached-file .close { - position: absolute; - top: 0; - right: 0; -} diff --git a/frappe/public/css/mobile.css b/frappe/public/css/mobile.css deleted file mode 100644 index 1cf8bb011a..0000000000 --- a/frappe/public/css/mobile.css +++ /dev/null @@ -1,411 +0,0 @@ -/* the element that this class is applied to, should have a max width for this to work*/ -html { - min-height: 100%; -} -body { - height: 100%; - margin: 0px; - padding: 0px !important; -} -html, -body { - overflow-x: hidden; - overflow-y: overlay; -} -@media (max-width: 991px) { - .intro-area, - .footnote-area { - padding: 15px; - } - .grid-row-open { - top: 0; - } - .layout-main { - position: relative; - } - body[data-route^="Form"] .page-title h1 { - margin-top: 12px; - } - body[data-route^="Form"] .page-title h1.editable-title { - padding-right: 80px; - } - body[data-route^="Form"] .page-title .indicator { - display: inline-block; - margin-top: 12px; - } - body[data-route^="Form"] .page-actions { - padding-top: 20px !important; - padding-bottom: 0px !important; - padding-left: 0px !important; - } - body[data-route^="Form"] .page-head .sub-heading { - font-weight: normal; - font-size: 10px; - display: block; - position: absolute; - right: 140px; - min-width: 200px; - } - body[data-route^="Form"] .title-text { - margin-top: 10px; - } - .toggle-navbar-new-comments { - padding: 8px 0px !important; - } - .navbar > .container > .navbar-header { - float: left; - width: 80%; - } - .navbar > .container > .navbar-right { - float: right; - } - .module-item { - padding: 7px 0px !important; - } - .module-item h4 { - font-weight: normal; - } - #navbar-breadcrumbs { - margin: 0px; - display: inline-block; - max-width: 150px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - #navbar-breadcrumbs > li, - #navbar-breadcrumbs > li > a { - display: inline-block; - vertical-align: middle; - } - #navbar-breadcrumbs > li > a:before { - content: "\f104"; - margin-right: 10px; - color: #6C7680; - } - #navbar-breadcrumbs li:not(:nth-last-child(-n+1)) { - display: none; - } - .navbar-nav { - margin: 0px; - margin-right: -15px; - } - .sidebar .form-group { - margin-bottom: 0px; - } - #sidebar-search { - height: 27px; - } - .sidebar .navbar-search-icon { - float: right; - color: #6C7680; - font-size: inherit; - position: relative; - right: 7px; - top: -20px; - height: 0; - } - .sidebar form { - padding: 7px; - } - .sidebar .main-menu { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 41px; - overflow-y: auto; - } - .sidebar .user-menu { - padding: 9px 14px; - background-color: #f5f7fa; - position: absolute; - left: 0; - bottom: 0; - right: 0; - } - .sidebar .user-menu, - .sidebar .user-menu .octicon { - color: #6C7680; - } - .sidebar .user-menu img { - margin-top: -1px; - } - body[data-route^="Module"] .navbar-center { - display: block !important; - position: absolute; - top: 10px; - left: 25%; - right: 25%; - text-align: center; - } - body.no-breadcrumbs .navbar .navbar-home { - display: inline-block !important; - padding-left: 0px; - margin-left: 0px; - padding-top: 6px; - } - body.no-breadcrumbs .navbar .navbar-home:before { - font-family: FontAwesome; - font-weight: normal; - font-style: normal; - text-decoration: inherit; - -webkit-font-smoothing: antialiased; - *margin-right: 0.3em; - display: inline-block; - speak: none; - font-size: 24px; - transition: 0.2s; - position: relative; - top: 3px; - content: "\f104"; - margin-right: 10px; - color: #6C7680; - } - body.no-breadcrumbs .navbar .navbar-home:hover:before, - body.no-breadcrumbs .navbar .navbar-home:focus:before, - body.no-breadcrumbs .navbar .navbar-home:active:before { - color: #36414C !important; - } - body[data-route=""] .navbar .navbar-home, - body[data-route="desktop"] .navbar .navbar-home { - padding: 8px 10px; - } - body[data-route=""] .navbar .navbar-home:before, - body[data-route="desktop"] .navbar .navbar-home:before { - display: none; - } - body[data-route=""] .navbar .navbar-home img, - body[data-route="desktop"] .navbar .navbar-home img { - margin-top: 0; - } - body[data-route=""] .toggle-sidebar, - body[data-route="desktop"] .toggle-sidebar { - display: none !important; - } - body[data-sidebar="0"] .toggle-sidebar { - display: none !important; - } - body[data-sidebar="0"] #navbar-breadcrumbs, - body[data-sidebar="0"] .navbar-home { - margin-left: 15px !important; - } -} -@media (max-width: 991px) and (max-width: 480px) { - #navbar-breadcrumbs li > a { - width: 100px; - } -} -@media (max-width: 767px) { - .toggle-sidebar { - margin-right: 0; - } - body[data-route^="Form"] .page-title .title-text { - font-size: 16px; - width: calc(100% - 90px); - } - body[data-route^="Form"] .page-title .indicator { - float: left; - margin-top: 10px; - margin-right: 5px; - } - .modal .modal-dialog { - margin: 0px; - padding: 0px; - width: 100%; - background-color: #fff; - } - .modal .modal-content { - border-radius: 0px; - border: none; - height: 100%; - } - .modal .modal-body .form-layout { - margin: -15px; - } - .modal .file-upload .input-upload { - width: 100%; - text-align: center; - } - .modal .file-upload .input-upload .btn-browse { - width: 100%; - } - .modal .file-upload .web-link-wrapper { - display: block; - width: 100% !important; - text-align: center; - } - .modal .file-upload .web-link-wrapper .file-upload-or { - display: block; - margin: 15px 24px; - } - .modal .file-upload .web-link-wrapper .input-link { - width: 100% !important; - } - .layout-main-section-wrapper { - padding: 0px; - } - .layout-main-section { - border-left-color: transparent !important; - border-right-color: transparent !important; - } - .list-row { - padding: 13px 15px !important; - } - .doclist-row { - position: relative; - padding-right: 10px; - } - .doclist-row .list-id { - font-weight: normal; - } - .doclist-row .list-row-id { - left: 18px; - text-align: left; - margin-top: 3px; - } - .doclist-row.has-checkbox .list-row-id { - left: 40px; - } - .doclist-row .list-row-indicator { - position: absolute; - right: 0px; - top: -20px; - } - .doclist-row .list-row-modified { - margin-right: -10px; - } - .doclist-row .list-row-left { - z-index: 1; - } - .doclist-row .list-row-right { - float: right; - } - .doclist-row .list-row-right .list-row-indicator { - top: 4px; - } - .doclist-row .list-row-right .list-row-indicator .indicator::before, - .doclist-row .list-row-right .list-row-indicator .indicator::after { - height: 12px; - width: 12px; - border-radius: 12px; - } - .doclist-row .list-row-right.no-right-column { - position: absolute; - top: 0; - right: 10px; - left: -10px; - width: 100%; - } - body[data-route^="chat"] .navbar-center { - display: block !important; - position: absolute; - top: 10px; - left: 25%; - right: 25%; - text-align: center; - } - #page-chat .layout-side-section { - position: relative; - left: 0px; - border-right: 1px solid #d1d8dd; - padding-left: 0px; - float: left; - width: 76px; - } - #page-chat .layout-main-section-wrapper { - position: absolute; - left: 75px; - right: 0px; - border-left: 1px solid #d1d8dd; - float: left; - } - #page-chat .module-sidebar-item { - margin: 0px; - } - #page-chat .module-sidebar-item .chat-sidebar-link { - padding: 15px; - } - #page-chat .timeline-head { - padding: 15px 15px 7px; - } - #page-chat .list-row { - padding: 7px 0px; - } - #page-chat .message-row-right { - margin-top: 10px; - text-align: left; - } - body[data-route^="Form"] .page-head .sub-heading { - right: 90px; - } - .timeline::before { - content: none; - } - .timeline .timeline-new-email { - margin: 20px 0; - padding-left: 15px; - } - .timeline .timeline-new-email::before { - content: none; - } - .timeline .timeline-item.user-content { - margin: 20px 15px; - } - .timeline .timeline-item.user-content .media-body { - margin-left: 0; - max-width: 100%; - overflow: hidden; - } - .timeline .timeline-item.user-content .media-body:before { - content: none; - } - .timeline .timeline-item.user-content .action-btns { - padding: 7px 10px 2px 5px; - } - .timeline .timeline-item.user-content .action-btns .edit-btn-container { - margin-right: 0; - } - .timeline .timeline-item.user-content .comment-header { - padding: 7px 10px; - } - .timeline .timeline-item.user-content .comment-header .links-active { - padding-right: 10px; - } - .timeline .timeline-item.user-content .comment-header .reply-link { - margin-left: 0; - } - .timeline .timeline-item.user-content .comment-header .asset-details { - width: calc(100% - 30px); - } - .timeline .timeline-item.user-content .avatar-medium { - margin-right: 10px; - } - .timeline .timeline-item.user-content .reply { - padding: 10px; - } - .timeline .timeline-item.user-content .commented-on-small { - display: inline-block; - } - .timeline .timeline-item.user-content .commented-on-small { - display: inline-block; - } - .timeline .timeline-item.notification-content { - padding-left: 15px; - margin: 20px 0; - } - .timeline .timeline-item.notification-content::before { - content: none; - } - .timeline .timeline-item.notification-content .small { - padding-left: 0; - } - .timeline .timeline-item .delivery-status-indicator { - float: left; - margin: 0 5px 0 0; - } - .timeline .asset-details { - line-height: 24px; - /*Height of avtar image -36px to align text center vertically*/ - } -} diff --git a/frappe/public/css/variables.css b/frappe/public/css/variables.css deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/public/icons/timeless/icon-right-arrow.svg b/frappe/public/icons/timeless/icon-right-arrow.svg deleted file mode 100644 index 1e044d0e4d..0000000000 --- a/frappe/public/icons/timeless/icon-right-arrow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/frappe/public/icons/timeless/symbol-defs.svg b/frappe/public/icons/timeless/symbol-defs.svg index d6852c620f..2b0cc8b696 100644 --- a/frappe/public/icons/timeless/symbol-defs.svg +++ b/frappe/public/icons/timeless/symbol-defs.svg @@ -1,4 +1,4 @@ - + diff --git a/frappe/public/images/favicon.png b/frappe/public/images/favicon.png deleted file mode 100644 index d66d3920f8..0000000000 Binary files a/frappe/public/images/favicon.png and /dev/null differ diff --git a/frappe/public/js/docs.js b/frappe/public/js/docs.js deleted file mode 100644 index c216de67f9..0000000000 --- a/frappe/public/js/docs.js +++ /dev/null @@ -1,52 +0,0 @@ -// used in documenation site built via document generator - -$(function() { - if(window.hljs) { - $('pre code').each(function(i, block) { - hljs.highlightBlock(block); - }); - } - - // search - $('.sidebar-navbar-items .octicon-search, .navbar .octicon-search').parent().on("click", function() { - var modal = frappe.get_modal("Search", - '

\ -

Search via Google

'); - modal.find(".search-input").on("keyup", function(e) { - if(e.which===13) { - modal.find(".btn-search").trigger("click"); - } - if(e.which===9) { - e.preventDefault(); - modal.find(".btn-search").focus(); - return false; - } - var text = $(this).val(); - modal.find(".btn-search").attr("href", "https://google.com/search?q=" - + text + "+site:" + (window.docs_base_url || "")); - }); - modal.modal("show"); - return false; - }); - -}); - -frappe = { - get_modal: function(title, body_html) { - var modal = $('').appendTo(document.body); - - return modal; - }, -}; diff --git a/frappe/public/js/frappe/chat.js b/frappe/public/js/frappe/chat.js index f63005278d..fd440dcbbc 100644 --- a/frappe/public/js/frappe/chat.js +++ b/frappe/public/js/frappe/chat.js @@ -1305,8 +1305,6 @@ class { this.set_wrapper(selector ? selector : "body") this.set_options(options) - // Load Emojis. - frappe.chat.emoji() } /** @@ -2783,7 +2781,8 @@ frappe.chat.setup = () => { } } -$(document).on('ready toolbar_setup', () => -{ - frappe.chat.setup() -}) +// TODO: Re-enable after re-designing chat +// $(document).on('ready toolbar_setup', () => +// { +// frappe.chat.setup() +// }) diff --git a/frappe/public/js/frappe/data_import/import_preview.js b/frappe/public/js/frappe/data_import/import_preview.js index 477cfb0786..786692e552 100644 --- a/frappe/public/js/frappe/data_import/import_preview.js +++ b/frappe/public/js/frappe/data_import/import_preview.js @@ -188,8 +188,8 @@ frappe.data_import.ImportPreview = class ImportPreview { .join(','); this.datatable.style.setStyle(row_classes, { pointerEvents: 'none', - backgroundColor: frappe.ui.color.get_color_shade('white', 'light'), - color: frappe.ui.color.get_color_shade('black', 'extra-light') + backgroundColor: frappe.ui.color.get_color_shade('gray', 'extra-light'), + color: frappe.ui.color.get_color_shade('gray', 'dark') }); } diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index cac2e65885..250d308b7e 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -45,10 +45,7 @@ frappe.Application = Class.extend({ this.setup_frappe_vue(); this.load_bootinfo(); this.load_user_permissions(); - this.set_app_logo_url() - .then(() => { - this.make_nav_bar(); - }); + this.make_nav_bar(); this.set_favicon(); this.setup_analytics(); this.set_fullwidth_if_enabled(); @@ -82,8 +79,11 @@ frappe.Application = Class.extend({ } if (frappe.user_roles.includes('System Manager')) { - this.show_change_log(); - this.show_update_available(); + // delayed following requests to make boot faster + setTimeout(() => { + this.show_change_log(); + this.show_update_available(); + }, 1000); } if (!frappe.boot.developer_mode) { @@ -128,7 +128,7 @@ frappe.Application = Class.extend({ } // REDESIGN-TODO: Fix preview popovers - //this.link_preview = new frappe.ui.LinkPreview(); + this.link_preview = new frappe.ui.LinkPreview(); if (!frappe.boot.developer_mode) { setInterval(function() { @@ -173,6 +173,9 @@ frappe.Application = Class.extend({ frappe.router.route(); } frappe.after_ajax(() => frappe.flags.setting_original_route = false); + frappe.router.on('change', () => { + $(".tooltip").hide(); + }); }, setup_frappe_vue() { @@ -201,12 +204,13 @@ frappe.Application = Class.extend({ email_password_prompt: function(email_account,user,i) { var me = this; - var d = new frappe.ui.Dialog({ - title: __('Email Account setup please enter your password for: {0}', [email_account[i]["email_id"]]), + let d = new frappe.ui.Dialog({ + title: __('Password missing in Email Account'), fields: [ - { 'fieldname': 'password', + { + 'fieldname': 'password', 'fieldtype': 'Password', - 'label': 'Email Account Password', + 'label': __('Please enter the password for: {0}', [email_account[i]["email_id"]]), 'reqd': 1 }, { @@ -470,19 +474,6 @@ frappe.Application = Class.extend({ $('').appendTo("head"); $('').appendTo("head"); }, - - set_app_logo_url: function() { - return frappe.call('frappe.core.doctype.navbar_settings.navbar_settings.get_app_logo') - .then(r => { - frappe.app.logo_url = r.message; - if (window.cordova) { - let host = frappe.request.url; - host = host.slice(0, host.length - 1); - frappe.app.logo_url = host + frappe.app.logo_url; - } - }); - }, - trigger_primary_action: function() { if(window.cur_dialog && cur_dialog.display) { // trigger primary @@ -498,6 +489,7 @@ frappe.Application = Class.extend({ if (frappe.utils.is_rtl()) { var ls = document.createElement('link'); ls.rel="stylesheet"; + ls.type = "text/css"; ls.href= "assets/css/frappe-rtl.css"; document.getElementsByTagName('head')[0].appendChild(ls); $('body').addClass('frappe-rtl'); diff --git a/frappe/public/js/frappe/file_uploader/FilePreview.vue b/frappe/public/js/frappe/file_uploader/FilePreview.vue index 7161dc8dc5..cca7dfde2a 100644 --- a/frappe/public/js/frappe/file_uploader/FilePreview.vue +++ b/frappe/public/js/frappe/file_uploader/FilePreview.vue @@ -11,17 +11,18 @@
- + {{ file.name | file_name }} - +
{{ file.name | file_name }} - - - +
+
{{ file.file_obj.size | file_size }} @@ -30,7 +31,7 @@
- {{ __('Drag and drop files here or') }} + {{ __('Drag and drop files here or upload from') }}
-
+
{{ __('Link') }}
+
{{ upload_notes }} @@ -65,21 +73,13 @@
-
@@ -187,6 +187,9 @@ export default { return this.files.length > 0 && this.files.every( file => file.total !== 0 && file.progress === file.total); + }, + allow_take_photo() { + return window.navigator.mediaDevices; } }, methods: { @@ -206,15 +209,19 @@ export default { on_file_input(e) { this.add_files(this.$refs.file_input.files); }, - remove_file(i) { - this.files = this.files.filter((file, j) => i !== j); + remove_file(file) { + this.files = this.files.filter(f => f !== file); }, - toggle_private(i) { - this.files[i].private = !this.files[i].private; - }, - toggle_all_private(flag) { - if (flag == null) { - flag = this.files.every(file => file.private); + toggle_all_private() { + let flag; + let private_values = this.files.filter(file => file.private); + if (private_values.length < this.files.length) { + // there are some private and some public + // set all to private + flag = true; + } else { + // all are private, set all to public + flag = false; } this.files = this.files.map(file => { file.private = flag; @@ -416,7 +423,25 @@ export default { xhr.send(form_data); }); - } + }, + capture_image() { + const capture = new frappe.ui.Capture({ + animate: false, + error: true + }); + capture.show(); + capture.submit(data_url => { + let filename = `capture_${frappe.datetime.now_datetime().replaceAll(/[: -]/g, '_')}.png`; + this.url_to_file(data_url, filename, 'image/png').then((file) => + this.add_files([file]) + ); + }); + }, + url_to_file(url, filename, mime_type) { + return fetch(url) + .then(res => res.arrayBuffer()) + .then(buffer => new File([buffer], filename, { type: mime_type })); + }, } } diff --git a/frappe/public/js/frappe/file_uploader/index.js b/frappe/public/js/frappe/file_uploader/index.js index 0a822971e1..28ce96cd44 100644 --- a/frappe/public/js/frappe/file_uploader/index.js +++ b/frappe/public/js/frappe/file_uploader/index.js @@ -48,6 +48,13 @@ export default class FileUploader { this.uploader = this.$fileuploader.$children[0]; + this.uploader.$watch('files', (files) => { + let all_private = files.every(file => file.private); + if (this.dialog) { + this.dialog.set_secondary_action_label(all_private ? __('Set all public') : __('Set all private')); + } + }, { deep: true }); + if (files && files.length) { this.uploader.add_files(files); } @@ -66,8 +73,10 @@ export default class FileUploader { title: __('Upload'), primary_action_label: __('Upload'), primary_action: () => this.upload_files(), - secondary_action_label: __('Toggle Private'), - secondary_action: () => this.uploader.toggle_all_private() + secondary_action_label: __('Set all private'), + secondary_action: () => { + this.uploader.toggle_all_private(); + } }); this.wrapper = this.dialog.body; diff --git a/frappe/public/js/frappe/form/controls/attach.js b/frappe/public/js/frappe/form/controls/attach.js index fe662c1ada..604510bb52 100644 --- a/frappe/public/js/frappe/form/controls/attach.js +++ b/frappe/public/js/frappe/form/controls/attach.js @@ -1,6 +1,6 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({ make_input: function() { - var me = this; + let me = this; this.$input = $(' - -
`; + `; }, get_awesomplete_settings() { diff --git a/frappe/public/js/frappe/form/controls/rating.js b/frappe/public/js/frappe/form/controls/rating.js index 191db35538..8117bf24bf 100644 --- a/frappe/public/js/frappe/form/controls/rating.js +++ b/frappe/public/js/frappe/form/controls/rating.js @@ -1,22 +1,24 @@ frappe.ui.form.ControlRating = frappe.ui.form.ControlInt.extend({ make_input() { this._super(); + let stars = ''; + [1, 2, 3, 4, 5].forEach(i => { + stars += ` + + `; + }); const star_template = `
- - - - - + ${stars}
`; $(this.input_area).html(star_template); - $(this.input_area).find('i').hover((ev) => { + $(this.input_area).find('svg').hover((ev) => { const el = $(ev.currentTarget); let star_value = el.data('rating'); - el.parent().children('i.fa').each( function(e) { + el.parent().children('svg').each( function(e) { if (e < star_value) { $(this).addClass('star-hover'); } else { @@ -25,16 +27,16 @@ frappe.ui.form.ControlRating = frappe.ui.form.ControlInt.extend({ }); }, (ev) => { const el = $(ev.currentTarget); - el.parent().children('i.fa').each( function() { + el.parent().children('svg').each( function() { $(this).removeClass('star-hover'); }); }); - $(this.input_area).find('i').click((ev) => { + $(this.input_area).find('svg').click((ev) => { const el = $(ev.currentTarget); let star_value = el.data('rating'); - el.parent().children('i.fa').each( function(e) { - if (e < star_value){ + el.parent().children('svg').each( function(e) { + if (e < star_value) { $(this).addClass('star-click'); } else { $(this).removeClass('star-click'); @@ -50,8 +52,8 @@ frappe.ui.form.ControlRating = frappe.ui.form.ControlInt.extend({ return cint(this.value, null); }, set_formatted_input(value) { - let el = $(this.input_area).find('i'); - el.children('i.fa').prevObject.each( function(e) { + let el = $(this.input_area).find('svg'); + el.children('svg').prevObject.each( function(e) { if (e < value) { $(this).addClass('star-click'); } else { @@ -59,4 +61,4 @@ frappe.ui.form.ControlRating = frappe.ui.form.ControlInt.extend({ } }); } -}); \ No newline at end of file +}); diff --git a/frappe/public/js/frappe/form/controls/select.js b/frappe/public/js/frappe/form/controls/select.js index f1655502ae..0fcfadb47d 100644 --- a/frappe/public/js/frappe/form/controls/select.js +++ b/frappe/public/js/frappe/form/controls/select.js @@ -2,11 +2,41 @@ frappe.ui.form.ControlSelect = frappe.ui.form.ControlData.extend({ html_element: 'select', make_input: function() { this._super(); - this.$wrapper.find('.control-input') - .addClass('flex align-center') - .append(frappe.utils.icon('select')); + + const is_xs_input = this.df.input_class + && this.df.input_class.includes('input-xs'); + this.set_icon(is_xs_input); + this.df.placeholder && this.set_placeholder(is_xs_input); + + this.$input.addClass('ellipsis'); this.set_options(); }, + set_icon: function(is_xs_input) { + const select_icon_html = + `
+ ${frappe.utils.icon('select', is_xs_input ? 'xs' : 'sm')} +
`; + if (this.only_input) { + this.$wrapper.append(select_icon_html); + } else { + this.$wrapper.find('.control-input') + .addClass('flex align-center') + .append(select_icon_html); + } + }, + set_placeholder: function(is_xs_input) { + const placeholder_html = + `
+ ${this.df.placeholder} +
`; + if (this.only_input) { + this.$wrapper.append(placeholder_html); + } else { + this.$wrapper.find('.control-input').append(placeholder_html); + } + this.toggle_placeholder(); + this.$input && this.$input.on('select-change', () => this.toggle_placeholder()); + }, set_formatted_input: function(value) { // refresh options first - (new ones??) if(value==null) value = ''; @@ -64,6 +94,10 @@ frappe.ui.form.ControlSelect = frappe.ui.form.ControlData.extend({ this.set_description(__("Please attach a file first.")); return [""]; } + }, + toggle_placeholder: function() { + const input_set = Boolean(this.$input.find('option:selected').text()); + this.$wrapper.find('.placeholder').toggle(!input_set); } }); @@ -88,6 +122,7 @@ frappe.ui.form.ControlSelect = frappe.ui.form.ControlData.extend({ label = is_label_null ? __(value) : __(v.label); } } + $('
-
+