├── .git-blame-ignore-revs ├── .github ├── helper │ ├── install.sh │ ├── install_dependencies.sh │ ├── redisearch.so │ └── site_config.json └── workflows │ ├── ci.yml │ ├── linters.yml │ └── ui-tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── commitlint.config.js ├── cypress.config.js ├── cypress ├── e2e │ ├── wiki.cy.js │ └── wiki_sidebar.cy.js └── support │ ├── commands.js │ └── e2e.js ├── license.txt ├── package.json ├── pyproject.toml ├── wiki ├── __init__.py ├── config │ ├── __init__.py │ ├── desktop.py │ └── docs.py ├── fixtures │ └── role.json ├── hooks.py ├── install.py ├── modules.txt ├── patches.txt ├── public │ ├── images │ │ └── wiki-logo.png │ ├── js │ │ ├── editor.js │ │ ├── render_wiki.js │ │ ├── wiki.bundle.js │ │ └── wiki.js │ └── scss │ │ ├── all-contributions.scss │ │ ├── contributions.bundle.scss │ │ ├── contributions.scss │ │ ├── dropdowns.scss │ │ ├── footer.scss │ │ ├── general.scss │ │ ├── markdown.scss │ │ ├── modal.scss │ │ ├── navbar.scss │ │ ├── page-toc.scss │ │ ├── revisions.scss │ │ ├── sidebar.scss │ │ ├── variables.scss │ │ └── wiki.bundle.scss ├── search.py ├── templates │ ├── __init__.py │ └── pages │ │ └── __init__.py ├── utils.py ├── wiki │ ├── __init__.py │ ├── doctype │ │ ├── __init__.py │ │ ├── migrate_to_wiki │ │ │ ├── __init__.py │ │ │ ├── migrate_to_wiki.js │ │ │ ├── migrate_to_wiki.json │ │ │ ├── migrate_to_wiki.py │ │ │ └── test_migrate_to_wiki.py │ │ ├── wiki_app_switcher_list_table │ │ │ ├── __init__.py │ │ │ ├── wiki_app_switcher_list_table.json │ │ │ └── wiki_app_switcher_list_table.py │ │ ├── wiki_feedback │ │ │ ├── __init__.py │ │ │ ├── patches │ │ │ │ └── delete_wiki_feedback_item.py │ │ │ ├── test_wiki_feedback.py │ │ │ ├── wiki_feedback.js │ │ │ ├── wiki_feedback.json │ │ │ └── wiki_feedback.py │ │ ├── wiki_group_item │ │ │ ├── __init__.py │ │ │ ├── wiki_group_item.json │ │ │ └── wiki_group_item.py │ │ ├── wiki_page │ │ │ ├── __init__.py │ │ │ ├── patches │ │ │ │ ├── convert_wiki_content_to_markdown.py │ │ │ │ ├── delete_is_new.py │ │ │ │ ├── set_allow_guest.py │ │ │ │ ├── update_escaped_chars.py │ │ │ │ └── update_escaped_code_content.py │ │ │ ├── review_contributions.py │ │ │ ├── search.py │ │ │ ├── sqlite_search.py │ │ │ ├── templates │ │ │ │ ├── base.html │ │ │ │ ├── comment.html │ │ │ │ ├── editor.html │ │ │ │ ├── feedback.html │ │ │ │ ├── navbar_items.html │ │ │ │ ├── navbar_search.html │ │ │ │ ├── page_settings.html │ │ │ │ ├── revisions.html │ │ │ │ ├── show.html │ │ │ │ ├── web_sidebar.html │ │ │ │ ├── wiki_doc.html │ │ │ │ ├── wiki_navbar.html │ │ │ │ ├── wiki_page.html │ │ │ │ └── wiki_page_row.html │ │ │ ├── test_wiki_page.py │ │ │ ├── wiki_page.js │ │ │ ├── wiki_page.json │ │ │ ├── wiki_page.py │ │ │ └── wiki_renderer.py │ │ ├── wiki_page_patch │ │ │ ├── __init__.py │ │ │ ├── test_wiki_page_patch.py │ │ │ ├── wiki_page_patch.js │ │ │ ├── wiki_page_patch.json │ │ │ └── wiki_page_patch.py │ │ ├── wiki_page_revision │ │ │ ├── __init__.py │ │ │ ├── patches │ │ │ │ ├── __init__.py │ │ │ │ └── add_usernames.py │ │ │ ├── test_wiki_page_revision.py │ │ │ ├── wiki_page_revision.js │ │ │ ├── wiki_page_revision.json │ │ │ └── wiki_page_revision.py │ │ ├── wiki_page_revision_item │ │ │ ├── __init__.py │ │ │ ├── wiki_page_revision_item.json │ │ │ └── wiki_page_revision_item.py │ │ ├── wiki_settings │ │ │ ├── __init__.py │ │ │ ├── patches │ │ │ │ └── wiki_navbar_item_migration.py │ │ │ ├── test_wiki_settings.py │ │ │ ├── wiki_settings.js │ │ │ ├── wiki_settings.json │ │ │ └── wiki_settings.py │ │ ├── wiki_sidebar │ │ │ ├── __init__.py │ │ │ ├── test_wiki_sidebar.py │ │ │ ├── wiki_sidebar.js │ │ │ ├── wiki_sidebar.json │ │ │ └── wiki_sidebar.py │ │ └── wiki_space │ │ │ ├── __init__.py │ │ │ ├── patches │ │ │ ├── wiki_navbar_app_switcher_migration.py │ │ │ └── wiki_sidebar_migration.py │ │ │ ├── test_wiki_space.py │ │ │ ├── wiki_space.js │ │ │ ├── wiki_space.json │ │ │ └── wiki_space.py │ ├── report │ │ ├── __init__.py │ │ └── wiki_broken_links │ │ │ ├── __init__.py │ │ │ ├── test_broken_link_checker.py │ │ │ ├── wiki_broken_links.js │ │ │ ├── wiki_broken_links.json │ │ │ └── wiki_broken_links.py │ └── workspace │ │ └── wiki │ │ └── wiki.json ├── wiki_search.py └── www │ ├── __init__.py │ ├── __pycache__ │ └── __init__.py │ ├── app-icon.png │ ├── contributions.html │ ├── contributions.py │ ├── drafts.html │ ├── drafts.py │ ├── wiki.html │ └── wiki.py └── yarn.lock /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Since version 2.23 (released in August 2019), git-blame has a feature 2 | # to ignore or bypass certain commits. 3 | # 4 | # This file contains a list of commits that are not likely what you 5 | # are looking for in a blame, such as mass reformatting or renaming. 6 | # You can set this file as a default ignore file for blame by running 7 | # the following command. 8 | # 9 | # $ git config blame.ignoreRevsFile .git-blame-ignore-revs 10 | 11 | # Apply ruff autoformatting 12 | 7d5ca3b70bf57cded4e43e5211cf064a71d120aa 13 | -------------------------------------------------------------------------------- /.github/helper/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | cd ~ || exit 4 | 5 | echo "Setting Up Bench..." 6 | 7 | pip install frappe-bench 8 | bench -v init frappe-bench --skip-assets --python "$(which python)" 9 | cd ./frappe-bench || exit 10 | 11 | bench -v setup requirements 12 | 13 | echo "Setting Up Wiki App..." 14 | bench get-app wiki "${GITHUB_WORKSPACE}" 15 | 16 | echo "Setting Up Sites & Database..." 17 | 18 | mkdir ~/frappe-bench/sites/wiki.test 19 | cp "${GITHUB_WORKSPACE}/.github/helper/site_config.json" ~/frappe-bench/sites/wiki.test/site_config.json 20 | 21 | mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "SET GLOBAL character_set_server = 'utf8mb4'"; 22 | mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"; 23 | 24 | mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "CREATE DATABASE test_wiki"; 25 | mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "CREATE USER 'test_wiki'@'localhost' IDENTIFIED BY 'test_wiki'"; 26 | mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "GRANT ALL PRIVILEGES ON \`test_wiki\`.* TO 'test_wiki'@'localhost'"; 27 | 28 | mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "FLUSH PRIVILEGES"; 29 | 30 | 31 | echo "Setting Up Procfile..." 32 | 33 | sed -i 's/^watch:/# watch:/g' Procfile 34 | sed -i 's/^schedule:/# schedule:/g' Procfile 35 | 36 | echo "Setting up redisearch module..." 37 | echo "loadmodule ${GITHUB_WORKSPACE}/.github/helper/redisearch.so" >> ./config/redis_cache.conf 38 | chmod +x "${GITHUB_WORKSPACE}/.github/helper/redisearch.so" 39 | cat ./config/redis_cache.conf 40 | 41 | echo "Starting Bench..." 42 | 43 | bench start &> bench_start.log & 44 | 45 | CI=Yes bench build & 46 | build_pid=$! 47 | 48 | bench --site wiki.test reinstall --yes 49 | bench --site wiki.test install-app wiki 50 | bench --site wiki.test execute wiki.wiki.doctype.wiki_page.search.build_index 51 | 52 | # wait till assets are built succesfully 53 | wait $build_pid 54 | -------------------------------------------------------------------------------- /.github/helper/install_dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | echo "Setting Up System Dependencies..." 5 | 6 | # redis repository 7 | curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg 8 | echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list 9 | 10 | sudo apt update 11 | sudo apt remove mysql-server mysql-client 12 | sudo apt install libcups2-dev redis mariadb-client 13 | 14 | install_wkhtmltopdf() { 15 | wget -q https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6.1-2.jammy_amd64.deb 16 | sudo apt install ./wkhtmltox_0.12.6.1-2.jammy_amd64.deb 17 | } 18 | install_wkhtmltopdf & 19 | -------------------------------------------------------------------------------- /.github/helper/redisearch.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/wiki/af7de232b6a7e25e6e8984abcd5c4f985a17587e/.github/helper/redisearch.so -------------------------------------------------------------------------------- /.github/helper/site_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_host": "127.0.0.1", 3 | "db_port": 3306, 4 | "db_name": "test_wiki", 5 | "db_password": "test_wiki", 6 | "allow_tests": true, 7 | "enable_ui_tests": true, 8 | "db_type": "mariadb", 9 | "auto_email_id": "test@example.com", 10 | "mail_server": "smtp.example.com", 11 | "mail_login": "test@example.com", 12 | "mail_password": "test", 13 | "admin_password": "admin", 14 | "root_login": "root", 15 | "root_password": "123", 16 | "host_name": "http://wiki.test:8000", 17 | "monitor": 1, 18 | "server_script_enabled": true, 19 | "mute_emails": true 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | workflow_dispatch: 8 | concurrency: 9 | group: develop-wiki-${{ github.event.number }} 10 | cancel-in-progress: true 11 | jobs: 12 | tests: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | name: Server 17 | services: 18 | redis-cache: 19 | image: redis:alpine 20 | ports: 21 | - 13000:6379 22 | redis-queue: 23 | image: redis:alpine 24 | ports: 25 | - 11000:6379 26 | redis-socketio: 27 | image: redis:alpine 28 | ports: 29 | - 12000:6379 30 | mariadb: 31 | image: mariadb:10.6 32 | env: 33 | MYSQL_ROOT_PASSWORD: root 34 | ports: 35 | - 3306:3306 36 | options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 37 | steps: 38 | - name: Clone 39 | uses: actions/checkout@v3 40 | - name: Setup Python 41 | uses: actions/setup-python@v4 42 | with: 43 | python-version: '3.11' 44 | - name: Setup Node 45 | uses: actions/setup-node@v3 46 | with: 47 | node-version: 18 48 | check-latest: true 49 | - name: Cache pip 50 | uses: actions/cache@v3 51 | with: 52 | path: ~/.cache/pip 53 | key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py', '**/setup.cfg') }} 54 | restore-keys: | 55 | ${{ runner.os }}-pip- 56 | ${{ runner.os }}- 57 | - name: Get yarn cache directory path 58 | id: yarn-cache-dir-path 59 | run: 'echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT' 60 | - uses: actions/cache@v3 61 | id: yarn-cache 62 | with: 63 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 64 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 65 | restore-keys: | 66 | ${{ runner.os }}-yarn- 67 | - name: Setup 68 | run: | 69 | pip install frappe-bench 70 | bench init --skip-redis-config-generation --skip-assets --python "$(which python)" ~/frappe-bench 71 | mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'" 72 | mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" 73 | - name: Install 74 | working-directory: /home/runner/frappe-bench 75 | run: | 76 | bench get-app wiki $GITHUB_WORKSPACE 77 | bench setup requirements --dev 78 | bench new-site --db-root-password root --admin-password admin test_site 79 | bench --site test_site install-app wiki 80 | bench build 81 | env: 82 | CI: 'Yes' 83 | - name: Run Tests 84 | working-directory: /home/runner/frappe-bench 85 | run: | 86 | bench --site test_site set-config allow_tests true 87 | bench --site test_site run-tests --app wiki 88 | env: 89 | TYPE: server 90 | -------------------------------------------------------------------------------- /.github/workflows/linters.yml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | push: 7 | branches: [ master ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | concurrency: 13 | group: commitcheck-frappe-${{ github.event_name }}-${{ github.event.number }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | commit-lint: 18 | name: 'Semantic Commits' 19 | runs-on: ubuntu-latest 20 | if: github.event_name == 'pull_request' 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | with: 25 | fetch-depth: 200 26 | - uses: actions/setup-node@v3 27 | with: 28 | node-version: 16 29 | check-latest: true 30 | 31 | - name: Check commit titles 32 | run: | 33 | npm install @commitlint/cli @commitlint/config-conventional 34 | npx commitlint --verbose --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} 35 | 36 | linter: 37 | name: 'Frappe Linter' 38 | runs-on: ubuntu-latest 39 | if: github.event_name == 'pull_request' 40 | 41 | steps: 42 | - uses: actions/checkout@v3 43 | - uses: actions/setup-python@v4 44 | with: 45 | python-version: '3.11' 46 | - uses: pre-commit/action@v3.0.0 47 | 48 | deps-vulnerable-check: 49 | name: 'Vulnerable Dependency Check' 50 | runs-on: ubuntu-latest 51 | 52 | steps: 53 | - uses: actions/setup-python@v4 54 | with: 55 | python-version: '3.11' 56 | - uses: actions/checkout@v3 57 | - run: | 58 | pip install pip-audit 59 | pip-audit --desc on . 60 | -------------------------------------------------------------------------------- /.github/workflows/ui-tests.yml: -------------------------------------------------------------------------------- 1 | name: UI Test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [ master ] 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | if: ${{ github.repository_owner == 'frappe' }} 15 | timeout-minutes: 60 16 | 17 | strategy: 18 | fail-fast: false 19 | 20 | name: UI Tests (Cypress) 21 | 22 | services: 23 | mariadb: 24 | image: mariadb:10.6 25 | env: 26 | MARIADB_ROOT_PASSWORD: 123 27 | ports: 28 | - 3306:3306 29 | options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 30 | 31 | steps: 32 | - name: Clone 33 | uses: actions/checkout@v3 34 | 35 | - name: Setup Python 36 | uses: actions/setup-python@v4 37 | with: 38 | python-version: '3.11' 39 | 40 | - name: Check for valid Python & Merge Conflicts 41 | run: | 42 | python -m compileall -q -f "${GITHUB_WORKSPACE}" 43 | if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}" 44 | then echo "Found merge conflicts" 45 | exit 1 46 | fi 47 | - uses: actions/setup-node@v3 48 | with: 49 | node-version: 18 50 | check-latest: true 51 | 52 | - name: Add to Hosts 53 | run: | 54 | echo "127.0.0.1 wiki.test" | sudo tee -a /etc/hosts 55 | - name: Cache pip 56 | uses: actions/cache@v3 57 | with: 58 | path: ~/.cache/pip 59 | key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }} 60 | restore-keys: | 61 | ${{ runner.os }}-pip- 62 | ${{ runner.os }}- 63 | - name: Get yarn cache directory path 64 | id: yarn-cache-dir-path 65 | run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT 66 | 67 | - uses: actions/cache@v3 68 | id: yarn-cache 69 | with: 70 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 71 | key: ${{ runner.os }}-yarn-ui-${{ hashFiles('**/yarn.lock') }} 72 | restore-keys: | 73 | ${{ runner.os }}-yarn-ui- 74 | - name: Cache cypress binary 75 | uses: actions/cache@v3 76 | with: 77 | path: ~/.cache/Cypress 78 | key: ${{ runner.os }}-cypress 79 | 80 | - name: Install Dependencies 81 | run: | 82 | bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh 83 | bash ${GITHUB_WORKSPACE}/.github/helper/install.sh 84 | env: 85 | BEFORE: ${{ env.GITHUB_EVENT_PATH.before }} 86 | AFTER: ${{ env.GITHUB_EVENT_PATH.after }} 87 | 88 | - name: Site Setup 89 | run: | 90 | cd ~/frappe-bench/ 91 | bench --site wiki.test execute frappe.utils.install.complete_setup_wizard 92 | bench --site wiki.test execute frappe.tests.ui_test_helpers.create_test_user 93 | 94 | - name: cypress pre-requisites 95 | run: | 96 | cd ~/frappe-bench/apps/wiki 97 | yarn add cypress@^10 --no-lockfile 98 | 99 | - name: UI Tests 100 | run: cd ~/frappe-bench && bench --site wiki.test run-ui-tests wiki --headless 101 | env: 102 | CYPRESS_BASE_URL: http://wiki.test:8000 103 | CYPRESS_RECORD_KEY: 2e746aa8-d98b-4dac-8bc0-6e35ce9111d2 104 | 105 | - name: Stop server 106 | run: | 107 | ps -ef | grep "[f]rappe serve" | awk '{print $2}' | xargs kill -s SIGINT 108 | sleep 5 109 | - name: Show bench output 110 | if: ${{ always() }} 111 | run: cat ~/frappe-bench/bench_start.log || true 112 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.egg-info 4 | *.swp 5 | tags 6 | wiki/docs/current 7 | wiki/public/css 8 | wiki/public/dist 9 | dist/ 10 | css-rtl/ 11 | node_modules/* 12 | wiki/public/node_modules 13 | .cypress-coverage 14 | cypress/screenshots 15 | cypress/videos 16 | __pycache__/ 17 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: 'node_modules|.git' 2 | default_stages: [commit] 3 | fail_fast: false 4 | 5 | 6 | repos: 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: v4.6.0 9 | hooks: 10 | - id: trailing-whitespace 11 | files: "wiki.*" 12 | exclude: ".*json$|.*txt$|.*csv|.*md|.*svg" 13 | - id: check-yaml 14 | # - id: no-commit-to-branch 15 | # args: ['--branch', 'master'] 16 | - id: check-merge-conflict 17 | - id: check-ast 18 | - id: check-json 19 | - id: check-toml 20 | - id: check-yaml 21 | - id: debug-statements 22 | 23 | - repo: https://github.com/astral-sh/ruff-pre-commit 24 | rev: v0.5.7 25 | hooks: 26 | - id: ruff 27 | name: "Run ruff linter and apply fixes" 28 | args: ["--fix"] 29 | 30 | - id: ruff-format 31 | name: "Format Python code" 32 | 33 | - repo: https://github.com/pre-commit/mirrors-prettier 34 | rev: v4.0.0-alpha.8 35 | hooks: 36 | - id: prettier 37 | types_or: [javascript] 38 | # Ignore any files that might contain jinja / bundles 39 | exclude: | 40 | (?x)^( 41 | wiki/public/dist/.*| 42 | .*node_modules.*| 43 | .*boilerplate.*| 44 | wiki/www/website_script.js| 45 | wiki/templates/includes/.*| 46 | wiki/public/js/lib/.* 47 | )$ 48 | ci: 49 | autoupdate_schedule: weekly 50 | skip: [] 51 | submodules: false 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Frappe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | wiki 4 |

Frappe Wiki

5 | 6 | **Open Source Documentation Tool** 7 | 8 | [![Wiki](https://img.shields.io/endpoint?url=https://cloud.cypress.io/badge/simple/w2jgcb/master&style=flat&logo=cypress)](https://cloud.cypress.io/projects/w2jgcb/runs) 9 | [![CI](https://github.com/frappe/wiki/actions/workflows/ci.yml/badge.svg?event=push)](https://github.com/frappe/wiki/actions/workflows/ci.yml) 10 | 11 | Hero Image 12 | 13 |
14 |
15 | Website 16 | - 17 | Documentation 18 |
19 | 20 | ## Frappe Wiki 21 | 22 | Frappe Wiki is an Open Source Wiki app built on the Frappe Framework. It is well suited to serve dynamic, text-heavy content like documentation and knowledge base. It allows publishing small changes and even new pages on the fly without downtime. It also maintains revision history and has a change approval mechanism. 23 | 24 |
25 | Screenshots 26 | Screenshot 2025-01-13 at 2 13 54 PM 27 |
28 | 29 | 30 | ### Motivation 31 | 32 | Frappe Wiki, like many of our products was developed for our own needs. We were looking for a simple, clean, open source wiki to write documentation for ERPNext, but due to a lack of good options, we decided to write our own from scratch! 33 | 34 | Our goal was clear: create an open-source wiki that provides a delightful experience for writers and readers. Today, we use Frappe Wiki for all sorts of internal things – user manuals, company policies, you name it! The easy-to-use interface and simple editing features make it perfect for anyone on our team. 35 | 36 | 37 | ### Key Features 38 | 39 | - **Create Wiki Pages**: Easily create and organize wiki pages to manage and share knowledge systematically. 40 | Author Content in Markdown: Write and format content effortlessly using Markdown syntax, ensuring a clean and readable structure. 41 | - **Set-up Controlled Wiki Updates**: Implement workflows to review and approve edits before publishing, ensuring content accuracy and consistency. 42 | - **Add Attachments**: Attach relevant files and documents directly to wiki pages for better context and resource sharing. 43 | - **Table of Contents**: Automatically generate a navigable table of contents for enhanced readability and structure. 44 | - **Custom Script Support via Wiki Settings**: Customize wiki behavior and extend functionality using custom scripts configured in Wiki Settings. 45 | 46 | ### Under the Hood 47 | 48 | - [**Frappe Framework**](https://github.com/frappe/frappe): A full-stack web application framework. 49 | 50 | - [**Ace Editor**](https://github.com/ajaxorg/ace): Ace is an embeddable code editor written in JavaScript. 51 | 52 | - [**RedisSearch**](https://github.com/RediSearch/RediSearch): A powerful search and indexing engine built on top of Redis. 53 | 54 | ## Production Setup 55 | 56 | ### Managed Hosting 57 | 58 | You can try [Frappe Cloud](https://frappecloud.com), a simple, user-friendly and sophisticated [open-source](https://github.com/frappe/press) platform to host Frappe applications with peace of mind. 59 | 60 | It takes care of installation, setup, upgrades, monitoring, maintenance and support of your Frappe deployments. It is a fully featured developer platform with an ability to manage and control multiple Frappe deployments. 61 | 62 |
63 | 64 | 65 | 66 | Try on Frappe Cloud 67 | 68 | 69 |
70 | 71 | ## Development Setup 72 | 73 | 74 | ### Local 75 | 76 | To setup the repository locally follow the steps mentioned below: 77 | 78 | 1. Setup bench by following the [Installation Steps](https://frappeframework.com/docs/user/en/installation) and start the server 79 | 80 | ``` 81 | bench start 82 | ``` 83 | 84 | 2. In a separate terminal window, cd into `frappe-bench` directory and run the following commands: 85 | 86 | ``` 87 | # get app 88 | $ bench get-app https://github.com/frappe/wiki 89 | 90 | # install on site 91 | $ bench --site sitename install-app wiki 92 | 93 | ``` 94 | 95 | ## Learn and connect 96 | - [Telegram Public Group](https://t.me/frappewiki) 97 | - [Discuss Forum](https://discuss.frappe.io/c/wiki/72) 98 | - [Documentation](https://docs.frappe.io/wiki/) 99 | - [YouTube](https://www.youtube.com/@frappetech) 100 | 101 |
102 |
103 |
104 | 105 | 106 | 107 | Frappe Technologies 108 | 109 | 110 |
111 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserPreset: "conventional-changelog-conventionalcommits", 3 | rules: { 4 | "subject-empty": [2, "never"], 5 | "type-case": [2, "always", "lower-case"], 6 | "type-empty": [2, "never"], 7 | "type-enum": [ 8 | 2, 9 | "always", 10 | [ 11 | "build", 12 | "chore", 13 | "ci", 14 | "docs", 15 | "feat", 16 | "fix", 17 | "perf", 18 | "refactor", 19 | "revert", 20 | "style", 21 | "test", 22 | ], 23 | ], 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require("cypress"); 2 | const { webserver_port } = require("../../sites/common_site_config.json"); 3 | 4 | module.exports = defineConfig({ 5 | e2e: { 6 | baseUrl: `http://wiki.test:${webserver_port}`, 7 | projectId: "w2jgcb", 8 | adminPassword: "admin", 9 | viewportWidth: 1100, 10 | setupNodeEvents(on, config) { 11 | // implement node event listeners here 12 | }, 13 | retries: { 14 | runMode: 2, 15 | openMode: 0, 16 | }, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /cypress/e2e/wiki.cy.js: -------------------------------------------------------------------------------- 1 | context("Wiki", () => { 2 | beforeEach(() => { 3 | cy.login(); 4 | cy.visit("/wiki"); 5 | }); 6 | 7 | it("creates a new wiki page", () => { 8 | cy.get(".dropdown-toggle.wiki-options").click(); 9 | cy.get(".edit-wiki-btn").click(); 10 | 11 | cy.get(".doc-sidebar .sidebar-group:first-child .add-sidebar-page").click(); 12 | cy.get(".wiki-editor .wiki-title-input").clear().type("Test Wiki Page"); 13 | cy.get(".ace_text-input").first().focus().type("New Wiki Page"); 14 | cy.get('.btn:contains("Save"):visible').click(); 15 | 16 | cy.intercept("*/test-wiki-page").as("testWikiPage"); 17 | cy.wait("@testWikiPage"); 18 | 19 | cy.get(".sidebar-item.active").should("contain", "Test Wiki Page"); 20 | cy.get(".from-markdown h1").should("contain", "Test Wiki Page"); 21 | cy.get(".from-markdown p").should("contain", "New Wiki Page"); 22 | }); 23 | 24 | it("edits a wiki page", () => { 25 | cy.get(".doc-sidebar").contains("Test Wiki Page").click(); 26 | 27 | cy.get(".dropdown-toggle.wiki-options").click(); 28 | cy.get(".edit-wiki-btn").click(); 29 | 30 | cy.get(".wiki-editor .wiki-title-input").clear().type("Old Wiki Page"); 31 | cy.get(".ace_text-input").first().focus().type("Old wiki page"); 32 | cy.get('.btn:contains("Save"):visible').click(); 33 | 34 | cy.intercept("*/test-wiki-page").as("testWikiPage"); 35 | cy.wait("@testWikiPage"); 36 | 37 | cy.get(".sidebar-item.active").should("contain", "Old Wiki Page"); 38 | cy.get(".from-markdown h1").should("contain", "Old Wiki Page"); 39 | cy.get(".from-markdown p").should("contain", "Old wiki page"); 40 | }); 41 | 42 | it("deletes a wiki page", () => { 43 | cy.get(".doc-sidebar").contains("Old Wiki Page").click(); 44 | 45 | cy.get(".dropdown-toggle.wiki-options").click(); 46 | cy.get(".edit-wiki-btn").click(); 47 | 48 | cy.get(".doc-sidebar").contains("Old Wiki Page").parent().next().click(); 49 | cy.contains("Yes").click(); 50 | 51 | cy.get(".doc-sidebar .sidebar-item").should("not.contain", "Old Wiki Page"); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /cypress/e2e/wiki_sidebar.cy.js: -------------------------------------------------------------------------------- 1 | context("Wiki Sidebar", () => { 2 | beforeEach(() => { 3 | cy.login(); 4 | cy.visit("/wiki"); 5 | }); 6 | 7 | it("creates a new wiki sidebar group", () => { 8 | cy.get(".dropdown-toggle.wiki-options").click(); 9 | cy.get(".edit-wiki-btn").click(); 10 | 11 | cy.get(".doc-sidebar").contains("Add Group").click(); 12 | cy.get('input[name="title"]') 13 | .clear() 14 | .type("T") 15 | .clear() 16 | .type("Test Wiki Sidebar"); 17 | cy.contains("Submit").click(); 18 | 19 | cy.get(".doc-sidebar .sidebar-group:last-child .add-sidebar-page").click(); 20 | cy.get(".wiki-editor .wiki-title-input").clear().type("Test Wiki Page"); 21 | cy.get(".ace_text-input").first().focus().type("New Wiki Page"); 22 | cy.get('.btn:contains("Save"):visible').click(); 23 | 24 | cy.get(".sidebar-group").should("contain", "Test Wiki Sidebar"); 25 | }); 26 | 27 | it("deletes a wiki sidebar group when the group is empty", () => { 28 | cy.get(".dropdown-toggle.wiki-options").click(); 29 | cy.get(".edit-wiki-btn").click(); 30 | 31 | cy.get(".doc-sidebar").contains("Test Wiki Page").parent().next().click(); 32 | cy.get('.btn:contains("Yes"):visible').click(); 33 | 34 | cy.intercept("/*").as("testWikiSidebar"); 35 | cy.visit("/wiki"); 36 | cy.wait("@testWikiSidebar"); 37 | 38 | cy.get(".sidebar-items").should("not.contain", "Test Wiki Sidebar"); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 26 | 27 | Cypress.Commands.add("login", (email, password) => { 28 | if (!email) { 29 | email = Cypress.config("testUser") || "Administrator"; 30 | } 31 | if (!password) { 32 | password = Cypress.config("adminPassword"); 33 | } 34 | cy.request({ 35 | url: "/api/method/login", 36 | method: "POST", 37 | body: { usr: email, pwd: password }, 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import "./commands"; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | Cypress.on("uncaught:exception", (err, runnable) => { 22 | return false; 23 | }); 24 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | License: MIT -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@tiptap/core": "^2.0.2", 4 | "@tiptap/extension-code-block": "^2.0.2", 5 | "@tiptap/extension-code-block-lowlight": "^2.0.2", 6 | "@tiptap/extension-document": "^2.0.2", 7 | "@tiptap/extension-image": "^2.0.2", 8 | "@tiptap/extension-link": "^2.0.2", 9 | "@tiptap/extension-placeholder": "^2.0.2", 10 | "@tiptap/extension-table": "^2.0.2", 11 | "@tiptap/extension-table-cell": "^2.0.2", 12 | "@tiptap/extension-table-header": "^2.0.2", 13 | "@tiptap/extension-table-row": "^2.0.2", 14 | "@tiptap/extension-task-item": "^2.0.2", 15 | "@tiptap/extension-task-list": "^2.0.2", 16 | "@tiptap/extension-text-align": "^2.0.2", 17 | "@tiptap/pm": "^2.0.2", 18 | "@tiptap/starter-kit": "^2.0.2", 19 | "ace-builds": "^1.36.2", 20 | "htmldiff-js": "^1.0.5", 21 | "lowlight": "^2.8.1", 22 | "pre-commit": "^1.2.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "wiki" 3 | authors = [ 4 | { name = "Frappe Technologies Pvt Ltd", email = "developers@frappe.io"} 5 | ] 6 | description = "Simple Wiki App" 7 | requires-python = ">=3.10,<3.13" 8 | readme = "README.md" 9 | dynamic = ["version"] 10 | 11 | [project.urls] 12 | Homepage = "https://frappe.io/wiki" 13 | Repository = "https://github.com/frappe/wiki.git" 14 | "Bug Reports" = "https://github.com/frappe/wiki/issues" 15 | 16 | [build-system] 17 | requires = ["flit_core >=3.4,<4"] 18 | build-backend = "flit_core.buildapi" 19 | 20 | 21 | [tool.ruff] 22 | line-length = 110 23 | target-version = "py310" 24 | 25 | [tool.ruff.lint] 26 | select = [ 27 | "F", 28 | "E", 29 | "W", 30 | "I", 31 | "UP", 32 | "B", 33 | "RUF", 34 | ] 35 | ignore = [ 36 | "B017", # assertRaises(Exception) - should be more specific 37 | "B018", # useless expression, not assigned to anything 38 | "B023", # function doesn't bind loop variable - will have last iteration's value 39 | "B904", # raise inside except without from 40 | "E101", # indentation contains mixed spaces and tabs 41 | "E402", # module level import not at top of file 42 | "E501", # line too long 43 | "E741", # ambiguous variable name 44 | "F401", # "unused" imports 45 | "F403", # can't detect undefined names from * import 46 | "F405", # can't detect undefined names from * import 47 | "F722", # syntax error in forward type annotation 48 | "W191", # indentation contains tabs 49 | "RUF001", # string contains ambiguous unicode character 50 | ] 51 | typing-modules = ["frappe.types.DF"] 52 | 53 | [tool.ruff.format] 54 | quote-style = "double" 55 | indent-style = "tab" 56 | docstring-code-format = true 57 | -------------------------------------------------------------------------------- /wiki/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.0.0" 2 | -------------------------------------------------------------------------------- /wiki/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/wiki/af7de232b6a7e25e6e8984abcd5c4f985a17587e/wiki/config/__init__.py -------------------------------------------------------------------------------- /wiki/config/desktop.py: -------------------------------------------------------------------------------- 1 | from frappe import _ 2 | 3 | 4 | def get_data(): 5 | return [ 6 | { 7 | "module_name": "Wiki", 8 | "color": "grey", 9 | "icon": "octicon octicon-file-directory", 10 | "type": "module", 11 | "label": _("Wiki"), 12 | } 13 | ] 14 | -------------------------------------------------------------------------------- /wiki/config/docs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration for docs 3 | """ 4 | 5 | # source_link = "https://github.com/[org_name]/wiki" 6 | # docs_base_url = "https://[org_name].github.io/wiki" 7 | # headline = "App that does everything" 8 | # sub_heading = "Yes, you got that right the first time, everything" 9 | 10 | 11 | def get_context(context): 12 | context.brand_html = "Wiki" 13 | -------------------------------------------------------------------------------- /wiki/fixtures/role.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "bulk_actions": 1, 4 | "chat": 1, 5 | "dashboard": 1, 6 | "desk_access": 1, 7 | "disabled": 0, 8 | "docstatus": 0, 9 | "doctype": "Role", 10 | "form_sidebar": 1, 11 | "home_page": null, 12 | "is_custom": 0, 13 | "list_sidebar": 1, 14 | "modified": "2021-08-21 13:11:40.043575", 15 | "name": "Wiki Approver", 16 | "notifications": 1, 17 | "parent": null, 18 | "parentfield": null, 19 | "parenttype": null, 20 | "restrict_to_domain": null, 21 | "role_name": "Wiki Approver", 22 | "search_bar": 1, 23 | "timeline": 1, 24 | "two_factor_auth": 0, 25 | "view_switcher": 1 26 | } 27 | ] -------------------------------------------------------------------------------- /wiki/hooks.py: -------------------------------------------------------------------------------- 1 | app_name = "wiki" 2 | app_title = "Wiki" 3 | app_publisher = "Frappe" 4 | app_description = "Simple Wiki App" 5 | app_icon = "octicon octicon-file-directory" 6 | app_color = "grey" 7 | app_email = "developers@frappe.io" 8 | app_license = "MIT" 9 | 10 | add_to_apps_screen = [ 11 | { 12 | "name": "wiki", 13 | "logo": "/assets/wiki/images/wiki-logo.png", 14 | "title": "Wiki", 15 | "route": "/app/wiki", 16 | "has_permission": "wiki.utils.check_app_permission", 17 | } 18 | ] 19 | 20 | page_renderer = "wiki.wiki.doctype.wiki_page.wiki_renderer.WikiPageRenderer" 21 | 22 | website_route_rules = [ 23 | {"from_route": "//edit-wiki", "to_route": "/edit"}, 24 | {"from_route": "//new-wiki", "to_route": "/new"}, 25 | {"from_route": "//revisions", "to_route": "/revisions"}, 26 | ] 27 | 28 | # Includes in 29 | # ------------------ 30 | 31 | # include js, css files in header of desk.html 32 | # app_include_css = "/assets/wiki/css/wiki.css" 33 | # app_include_js = "/assets/wiki/js/wiki.js" 34 | 35 | # include js, css files in header of web template 36 | # web_include_css = "/assets/wiki/css/wiki.css" 37 | # web_include_js = "/assets/wiki/js/wiki.js" 38 | 39 | # include custom scss in every website theme (without file extension ".scss") 40 | # website_theme_scss = "wiki/public/scss/website" 41 | 42 | # include js, css files in header of web form 43 | # webform_include_js = {"doctype": "public/js/doctype.js"} 44 | # webform_include_css = {"doctype": "public/css/doctype.css"} 45 | 46 | # include js in page 47 | # page_js = {"page" : "public/js/file.js"} 48 | 49 | # include js in doctype views 50 | # doctype_js = {"doctype" : "public/js/doctype.js"} 51 | # doctype_list_js = {"doctype" : "public/js/doctype_list.js"} 52 | # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} 53 | # doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"} 54 | 55 | # Home Pages 56 | # ---------- 57 | 58 | # application home page (will override Website Settings) 59 | # home_page = "login" 60 | 61 | # website user home page (by Role) 62 | # role_home_page = { 63 | # "Role": "home_page" 64 | # } 65 | 66 | # Generators 67 | # ---------- 68 | 69 | # automatically create page for each record of this doctype 70 | # website_generators = ["Web Page"] 71 | 72 | # Installation 73 | # ------------ 74 | 75 | # before_install = "wiki.install.before_install" 76 | after_install = "wiki.install.after_install" 77 | 78 | after_migrate = ["wiki.wiki.doctype.wiki_page.search.build_index_in_background"] 79 | 80 | # Desk Notifications 81 | # ------------------ 82 | # See frappe.core.notifications.get_notification_config 83 | 84 | # notification_config = "wiki.notifications.get_notification_config" 85 | 86 | # Permissions 87 | # ----------- 88 | # Permissions evaluated in scripted ways 89 | 90 | # permission_query_conditions = { 91 | # "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions", 92 | # } 93 | # 94 | # has_permission = { 95 | # "Event": "frappe.desk.doctype.event.event.has_permission", 96 | # } 97 | 98 | # DocType Class 99 | # --------------- 100 | # Override standard doctype classes 101 | 102 | # override_doctype_class = { 103 | # "ToDo": "custom_app.overrides.CustomToDo" 104 | # } 105 | 106 | # Document Events 107 | # --------------- 108 | # Hook on document methods and events 109 | 110 | # doc_events = { 111 | # "*": { 112 | # "on_update": "method", 113 | # "on_cancel": "method", 114 | # "on_trash": "method" 115 | # } 116 | # } 117 | 118 | # Scheduled Tasks 119 | # --------------- 120 | 121 | scheduler_events = { 122 | "cron": { 123 | "*/15 * * * *": ["wiki.wiki.doctype.wiki_page.search.build_index_in_background"], 124 | }, 125 | } 126 | 127 | # scheduler_events = { 128 | # "all": [ 129 | # "wiki.tasks.all" 130 | # ], 131 | # "daily": [ 132 | # "wiki.tasks.daily" 133 | # ], 134 | # "hourly": [ 135 | # "wiki.tasks.hourly" 136 | # ], 137 | # "weekly": [ 138 | # "wiki.tasks.weekly" 139 | # ] 140 | # "monthly": [ 141 | # "wiki.tasks.monthly" 142 | # ] 143 | # } 144 | 145 | # Testing 146 | # ------- 147 | 148 | # before_tests = "wiki.install.before_tests" 149 | 150 | # Overriding Methods 151 | # ------------------------------ 152 | # 153 | # override_whitelisted_methods = { 154 | # "frappe.desk.doctype.event.event.get_events": "wiki.event.get_events" 155 | # } 156 | 157 | # whitelisted_paths = { 158 | # "/update-page": ["Wiki Page", "update"], 159 | # "/new-page": ["Wiki Page", "new"], 160 | # "/get-route": "wiki.wiki.doctype.wiki_page.wiki_page.get_route", 161 | # } 162 | 163 | # 164 | # each overriding function accepts a `data` argument; 165 | # generated from the base implementation of the doctype dashboard, 166 | # along with any modifications made in other Frappe apps 167 | # override_doctype_dashboards = { 168 | # "Task": "wiki.task.get_dashboard_data" 169 | # } 170 | 171 | # exempt linked doctypes from being automatically cancelled 172 | # 173 | # auto_cancel_exempted_doctypes = ["Auto Repeat"] 174 | -------------------------------------------------------------------------------- /wiki/install.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | import frappe 6 | 7 | 8 | def after_install(): 9 | # create the wiki homepage 10 | page = frappe.new_doc("Wiki Page") 11 | page.title = "Home" 12 | page.route = "wiki/home" 13 | page.content = "Welcome to the homepage of your wiki!" 14 | page.published = True 15 | page.insert() 16 | 17 | # create the wiki space 18 | space = frappe.new_doc("Wiki Space") 19 | space.route = "wiki" 20 | space.insert() 21 | 22 | # create the wiki sidebar 23 | sidebar = frappe.new_doc("Wiki Group Item") 24 | sidebar.wiki_page = page.name 25 | sidebar.parent_label = "Wiki" 26 | sidebar.parent = space.name 27 | sidebar.parenttype = "Wiki Space" 28 | sidebar.parentfield = "wiki_sidebars" 29 | sidebar.insert() 30 | -------------------------------------------------------------------------------- /wiki/modules.txt: -------------------------------------------------------------------------------- 1 | Wiki -------------------------------------------------------------------------------- /wiki/patches.txt: -------------------------------------------------------------------------------- 1 | [pre_model_sync] 2 | wiki.wiki.doctype.wiki_page.patches.set_allow_guest 3 | wiki.wiki.doctype.wiki_page.patches.delete_is_new 4 | wiki.wiki.doctype.wiki_page_revision.patches.add_usernames 5 | wiki.wiki.doctype.wiki_feedback.patches.delete_wiki_feedback_item 6 | 7 | [post_model_sync] 8 | wiki.wiki.doctype.wiki_space.patches.wiki_sidebar_migration 9 | wiki.wiki.doctype.wiki_settings.patches.wiki_navbar_item_migration 10 | wiki.wiki.doctype.wiki_page.patches.convert_wiki_content_to_markdown 11 | wiki.wiki.doctype.wiki_page.patches.update_escaped_code_content 12 | wiki.wiki.doctype.wiki_page.patches.update_escaped_chars 13 | wiki.wiki.doctype.wiki_space.patches.wiki_navbar_app_switcher_migration -------------------------------------------------------------------------------- /wiki/public/images/wiki-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/wiki/af7de232b6a7e25e6e8984abcd5c4f985a17587e/wiki/public/images/wiki-logo.png -------------------------------------------------------------------------------- /wiki/public/js/wiki.bundle.js: -------------------------------------------------------------------------------- 1 | import "./wiki"; 2 | import "./render_wiki"; 3 | import "./editor"; 4 | -------------------------------------------------------------------------------- /wiki/public/scss/all-contributions.scss: -------------------------------------------------------------------------------- 1 | .all-contributions { 2 | .table { 3 | td { 4 | &.message-col { 5 | width: auto; 6 | } 7 | &.status-col { 8 | width: 15%; 9 | } 10 | &.raised-by-col { 11 | width: auto; 12 | } 13 | &.date-col { 14 | width: 15%; 15 | } 16 | } 17 | 18 | .patch-row { 19 | cursor: pointer; 20 | transition: background-color 0.2s ease; 21 | 22 | &:hover { 23 | background-color: var(--btn-secondary-hover-bg-color); 24 | } 25 | } 26 | } 27 | 28 | .message-cell { 29 | font-weight: 550; 30 | } 31 | 32 | .message-cell, 33 | .raised-by-cell, 34 | .space-cell { 35 | white-space: nowrap; 36 | overflow: hidden; 37 | text-overflow: ellipsis; 38 | max-width: 0; 39 | } 40 | 41 | .action-buttons { 42 | .btn { 43 | margin-right: 4px; 44 | border-radius: 8px; 45 | padding: 3px 8px; 46 | font-size: 13px; 47 | border: none; 48 | 49 | &.btn-default { 50 | background-color: #e9e9e9; 51 | color: #000; 52 | &:hover { 53 | background-color: #dcdcdc; 54 | } 55 | } 56 | 57 | &.btn-success { 58 | background-color: #000; 59 | color: #fff; 60 | &:hover { 61 | background-color: #333; 62 | } 63 | } 64 | 65 | &.btn-danger { 66 | background-color: #dc3545; 67 | color: #fff; 68 | &:hover { 69 | background-color: #bb2d3b; 70 | } 71 | } 72 | 73 | &:last-child { 74 | margin-right: 0; 75 | } 76 | } 77 | } 78 | 79 | .get_patches { 80 | margin-top: 15px; 81 | } 82 | } 83 | 84 | .diff-content { 85 | text-wrap: auto; 86 | color: var(--text-color); 87 | 88 | ins { 89 | background-color: var(--diff-ins-bg-color); 90 | color: var(--diff-ins-text-color); 91 | text-decoration: none; 92 | } 93 | del { 94 | background-color: var(--diff-del-bg-color); 95 | color: var(--diff-del-text-color); 96 | text-decoration: none; 97 | } 98 | } 99 | 100 | .review-patch-modal { 101 | min-height: calc(100vh - 100px); 102 | 103 | .modal-header { 104 | border-bottom: 1px solid #e9e9e9 !important; 105 | padding: 1rem 1rem !important; 106 | display: flex; 107 | flex-direction: row; 108 | 109 | @media (max-width: 768px) { 110 | flex-direction: column; 111 | } 112 | 113 | .close { 114 | position: absolute; 115 | top: 5px; 116 | right: 8px; 117 | padding: 0; 118 | margin: 0; 119 | } 120 | } 121 | 122 | .view-toggle-buttons-wrapper { 123 | display: flex; 124 | align-items: end; 125 | } 126 | 127 | .view-toggle-buttons { 128 | display: flex; 129 | gap: 6px; 130 | background-color: var(--view-bg-color); 131 | padding: 4px; 132 | width: max-content; 133 | border-radius: 10px; 134 | margin-top: 22px; 135 | margin-right: 14px; 136 | 137 | .btn { 138 | padding: 3px 6px; 139 | border-radius: 8px; 140 | font-size: 12px; 141 | font-weight: 500; 142 | color: var(--view-btn-text); 143 | background-color: var(--view-bg-color); 144 | transition: all 0.2s ease; 145 | 146 | &.active { 147 | background-color: var(--view-btn-bg-active); 148 | color: var(--view-btn-text-active); 149 | } 150 | 151 | &:active { 152 | background-color: var(--view-btn-bg-active) !important; 153 | color: var(--view-btn-text-active) !important; 154 | outline: none !important; 155 | } 156 | } 157 | } 158 | .modal-title { 159 | max-width: 100% !important; 160 | } 161 | 162 | .modal-body { 163 | padding-top: 18px !important; 164 | 165 | .diff-content, 166 | .preview-content { 167 | display: none; 168 | max-width: 100%; 169 | &.active { 170 | display: block; 171 | } 172 | } 173 | } 174 | 175 | .review-modal-footer { 176 | display: flex; 177 | flex-wrap: nowrap; 178 | gap: 8px; 179 | border-top: 1px solid #e9e9e9; 180 | 181 | .btn { 182 | width: auto !important; 183 | } 184 | 185 | .approve-patch { 186 | background-color: #000; 187 | color: #fff; 188 | } 189 | 190 | .reject-patch { 191 | background-color: #fff; 192 | color: #000; 193 | background-color: #f8f8f8; 194 | } 195 | } 196 | } 197 | 198 | .filters-wrapper { 199 | display: flex; 200 | align-items: center; 201 | gap: 24px; 202 | 203 | .space-filter { 204 | display: flex; 205 | flex-direction: column; 206 | width: fit-content; 207 | 208 | label { 209 | font-size: 14px; 210 | font-weight: 550; 211 | margin-bottom: 4px; 212 | } 213 | 214 | select { 215 | position: relative; 216 | border: 1.5px solid #c7c7c7; 217 | border-radius: 8px; 218 | padding: 8px; 219 | padding-top: 2px; 220 | padding-bottom: 2px; 221 | font-size: 14px; 222 | 223 | -moz-appearance: none; 224 | -webkit-appearance: none; 225 | appearance: none; 226 | 227 | svg { 228 | position: absolute; 229 | right: 12px; 230 | top: 50%; 231 | transform: translateY(-50%); 232 | } 233 | } 234 | } 235 | } 236 | 237 | .frappe-card { 238 | margin-top: 12px; 239 | } 240 | 241 | .contributions-view { 242 | width: 100%; 243 | padding: 32px; 244 | padding-top: 18px; 245 | 246 | .btn-warning { 247 | background-color: var(--admin-banner-btn-bg); 248 | color: var(--admin-banner-btn-text) !important; 249 | 250 | &:hover { 251 | background-color: var(--admin-banner-btn-hover-bg); 252 | } 253 | 254 | &:active { 255 | background-color: var(--admin-banner-btn-active-bg) !important; 256 | } 257 | } 258 | 259 | .back-to-content { 260 | font-size: 12px; 261 | cursor: pointer; 262 | 263 | color: var(--text-color); 264 | } 265 | 266 | .page-title { 267 | font-weight: 550; 268 | } 269 | 270 | td { 271 | border-top: 1px solid var(--table-border-color); 272 | } 273 | } 274 | 275 | .pagination-container { 276 | margin-top: 12px; 277 | } 278 | -------------------------------------------------------------------------------- /wiki/public/scss/contributions.bundle.scss: -------------------------------------------------------------------------------- 1 | @import "./contributions.scss"; 2 | @import "./all-contributions.scss"; 3 | -------------------------------------------------------------------------------- /wiki/public/scss/contributions.scss: -------------------------------------------------------------------------------- 1 | .frappe-card { 2 | padding: 0; 3 | background-color: var(--background-color); 4 | } 5 | 6 | .navbar-brand-container { 7 | background-color: var(--background-color); 8 | position: relative; 9 | border: 0; 10 | } 11 | 12 | .navbar .doc-container .navbar-collapse { 13 | margin-left: 0; 14 | } 15 | 16 | .list-jobs { 17 | font-size: 14px; 18 | } 19 | 20 | .table { 21 | margin-bottom: 0px; 22 | margin-top: 0px; 23 | color: var(--text-color); 24 | 25 | td { 26 | padding: 14px 20px; 27 | } 28 | 29 | a { 30 | color: var(--btn-text-color) !important; 31 | } 32 | } 33 | thead td { 34 | border-top: 0 !important; 35 | } 36 | 37 | .table th, 38 | .table td { 39 | margin-top: -0.5px; 40 | } 41 | 42 | .worker-name { 43 | display: flex; 44 | align-items: center; 45 | white-space: nowrap; 46 | } 47 | 48 | .job-name { 49 | font-size: 13px; 50 | font-family: "Courier New", Courier, monospace; 51 | } 52 | 53 | .background-job-row:hover { 54 | background-color: #f9fafa; 55 | } 56 | 57 | .no-background-jobs { 58 | min-height: 320px; 59 | display: flex; 60 | align-items: center; 61 | justify-content: center; 62 | flex-direction: column; 63 | } 64 | 65 | .no-background-jobs > img { 66 | margin-bottom: var(15px); 67 | max-height: 100px; 68 | } 69 | 70 | .footer { 71 | align-items: flex-end; 72 | margin-top: 15px; 73 | font-size: 14px; 74 | } 75 | 76 | .page-content-wrapper { 77 | padding: 0 !important; 78 | margin: 0 0 1rem 0 !important; 79 | } 80 | -------------------------------------------------------------------------------- /wiki/public/scss/dropdowns.scss: -------------------------------------------------------------------------------- 1 | #navbar-dropdown { 2 | flex-grow: 1; 3 | display: flex; 4 | justify-content: end; 5 | height: 80%; 6 | cursor: pointer; 7 | align-items: center; 8 | 9 | @media (max-width: 768px) { 10 | justify-content: unset; 11 | } 12 | } 13 | 14 | #navbar-dropdown-content { 15 | position: absolute; 16 | left: 10px; 17 | top: 100%; 18 | background-color: var(--search-modal-bg-color); 19 | color: var(--search-modal-color); 20 | width: 96%; 21 | border-radius: 8px; 22 | box-shadow: 0 10px 24px -3px #0000001a; 23 | 24 | a { 25 | color: inherit !important; 26 | text-decoration: none !important; 27 | } 28 | 29 | a:hover { 30 | color: inherit !important; 31 | text-decoration: none !important; 32 | cursor: pointer !important; 33 | } 34 | 35 | .app-switcher { 36 | display: flex; 37 | flex-direction: column; 38 | gap: 4px; 39 | font-size: 14px; 40 | padding: 10px; 41 | height: 100%; 42 | max-height: 350px; 43 | overflow: auto; 44 | font-weight: 500; 45 | } 46 | 47 | .pending-reviews-count { 48 | color: var(--badge-color); 49 | background-color: var(--badge-bg-color); 50 | padding: 2px 6px; 51 | border-radius: 10px; 52 | font-size: 12px; 53 | margin-left: auto; 54 | width: 21px; 55 | height: 21px; 56 | text-align: center; 57 | font-weight: 750; 58 | } 59 | 60 | .space-link { 61 | padding: 4px; 62 | padding-left: 8px; 63 | display: flex; 64 | align-items: center; 65 | gap: 8px; 66 | width: 100%; 67 | 68 | span { 69 | display: flex; 70 | align-items: center; 71 | width: 100%; 72 | } 73 | 74 | img { 75 | width: 22px; 76 | } 77 | } 78 | 79 | .space-link:hover { 80 | background-color: var(--search-modal-hover-color); 81 | border-radius: 4px; 82 | } 83 | } 84 | 85 | .dropdown-toggle.wiki-options::after { 86 | display: none; 87 | } 88 | 89 | .wiki-options { 90 | display: flex; 91 | flex-direction: column; 92 | justify-content: center; 93 | width: 2rem; 94 | height: 2rem; 95 | margin-left: 0.5rem; 96 | 97 | svg { 98 | width: 2rem; 99 | } 100 | 101 | .dropdown-menu { 102 | min-width: 7.5rem; 103 | } 104 | } 105 | 106 | .wiki-options:hover { 107 | background-color: var(--gray-200); 108 | border-radius: 5px; 109 | } 110 | 111 | .dark .wiki-options:hover { 112 | background-color: var(--gray-700); 113 | } 114 | -------------------------------------------------------------------------------- /wiki/public/scss/footer.scss: -------------------------------------------------------------------------------- 1 | .wiki-footer { 2 | border-top: 1px solid var(--border-color); 3 | margin-top: 1rem; 4 | width: 100%; 5 | max-width: 650px; 6 | margin: auto; 7 | 8 | .btn { 9 | margin-top: 1rem; 10 | color: var(--text-color); 11 | border: 1px solid var(--border-color); 12 | border-radius: 8px; 13 | padding: 11px 16px 13px !important; 14 | width: 48%; 15 | height: 100%; 16 | transition: border-color 0.25s; 17 | box-shadow: unset; 18 | margin-bottom: 3.5rem; 19 | 20 | p { 21 | line-height: 20px; 22 | margin: 0; 23 | 24 | &:first-child { 25 | font-size: 12px; 26 | font-weight: 500; 27 | color: var(--sidebar-text-color); 28 | } 29 | 30 | &:last-child { 31 | font-size: 15px; 32 | font-weight: 500; 33 | color: var(--primary); 34 | transition: color 0.25s; 35 | } 36 | } 37 | 38 | &:hover { 39 | background-color: transparent; 40 | border: 1px solid var(--primary); 41 | } 42 | } 43 | 44 | .btn.left { 45 | margin-right: auto; 46 | text-align: left; 47 | } 48 | 49 | .btn.right { 50 | margin-left: auto; 51 | text-align: right; 52 | } 53 | } 54 | 55 | @include media-breakpoint-down(md) { 56 | .wiki-footer { 57 | .btn.left, 58 | .btn.right { 59 | width: 100%; 60 | margin-bottom: 10px; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /wiki/public/scss/modal.scss: -------------------------------------------------------------------------------- 1 | .modal { 2 | .modal-content { 3 | background-color: var(--background-color); 4 | } 5 | 6 | .modal-header { 7 | border-bottom: unset; 8 | 9 | .close { 10 | font-weight: 400; 11 | } 12 | } 13 | 14 | .modal-body { 15 | padding-top: 0; 16 | 17 | label { 18 | color: var(--text-color); 19 | } 20 | 21 | input { 22 | width: 100%; 23 | background: $gray-200; 24 | border-radius: 0.375rem; 25 | border: none; 26 | outline: none; 27 | padding: 0.25rem 0.5rem; 28 | font-size: 13px; 29 | line-height: 1.25rem; 30 | 31 | &[type="checkbox"] { 32 | color: #000; 33 | padding: 0%; 34 | border: 1px solid var(--gray-500); 35 | border-radius: 4px; 36 | accent-color: black; 37 | 38 | &:checked { 39 | background-color: var(--primary); 40 | background-image: url("data:image/svg+xml, "), 41 | var(--checkbox-gradient); 42 | background-size: 57%, 100%; 43 | box-shadow: none; 44 | border: none; 45 | background-repeat: no-repeat; 46 | background-position: center; 47 | } 48 | } 49 | } 50 | 51 | input, 52 | textarea { 53 | background-color: var(--searchbar-color); 54 | color: var(--text-color); 55 | } 56 | } 57 | 58 | .modal-footer { 59 | border-top: unset; 60 | justify-content: end; 61 | 62 | .btn { 63 | width: 100%; 64 | } 65 | } 66 | } 67 | 68 | .feedback-modal { 69 | width: 25rem; 70 | 71 | .form-control:focus { 72 | border: 1px solid var(--background-color); 73 | } 74 | 75 | .rating-options-buttons { 76 | display: grid; 77 | border-radius: 6px; 78 | overflow: hidden; 79 | border: 1.5px solid #000; 80 | } 81 | 82 | .rating-options-buttons > .ratings-number { 83 | border-right: 1px solid #000; 84 | 85 | &:last-child { 86 | border-right: none; 87 | } 88 | } 89 | 90 | .ratings-number { 91 | font-size: 15px; 92 | padding: 8px 0px; 93 | border: none; 94 | color: #000; 95 | background-color: #fff; 96 | 97 | &.rating-active { 98 | background-color: #000; 99 | color: #fff; 100 | } 101 | } 102 | 103 | .submit-feedback-btn.disabled { 104 | pointer-events: none; 105 | } 106 | } 107 | 108 | .search-dialog { 109 | .modal-content { 110 | background-color: var(--search-modal-bg-color) !important; 111 | } 112 | } 113 | 114 | .search-modal { 115 | padding: 10px; 116 | border-radius: 1rem; 117 | margin-top: 100px; 118 | 119 | .modal-header { 120 | padding: 0px; 121 | padding-left: 4px; 122 | } 123 | 124 | .search-icon { 125 | width: 16px; 126 | } 127 | 128 | input { 129 | color: var(--search-modal-color); 130 | margin-left: 4px; 131 | background: transparent; 132 | border: transparent; 133 | } 134 | 135 | input:focus { 136 | background: transparent; 137 | border: transparent; 138 | } 139 | 140 | .modal-body { 141 | padding: 0px; 142 | } 143 | 144 | .dropdown-border { 145 | height: 1px; 146 | background-color: var(--border-color); 147 | margin: 4px 0px; 148 | } 149 | 150 | .dropdown-border { 151 | &:last-child { 152 | display: none; 153 | } 154 | } 155 | 156 | .search-dropdown-menu { 157 | max-height: 500px; 158 | overflow-x: hidden !important; 159 | overflow-y: auto !important; 160 | 161 | &:not(:empty) { 162 | margin-top: 18px; 163 | } 164 | 165 | .result-title { 166 | font-size: 14px; 167 | font-weight: 550; 168 | } 169 | 170 | .result-text { 171 | font-size: 12px; 172 | overflow: hidden; 173 | text-overflow: ellipsis; 174 | } 175 | 176 | .match { 177 | font-weight: 750; 178 | } 179 | } 180 | 181 | .dropdown-item { 182 | padding: 8px !important; 183 | color: var(--search-modal-color); 184 | 185 | &:hover { 186 | background-color: var(--search-modal-hover-color); 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /wiki/public/scss/navbar.scss: -------------------------------------------------------------------------------- 1 | .navbar-brand { 2 | padding: 0; 3 | color: var(--text-color) !important; 4 | 5 | img { 6 | height: 20px; 7 | max-width: fit-content; 8 | } 9 | 10 | @include media-breakpoint-down(md) { 11 | border-bottom: unset; 12 | } 13 | } 14 | 15 | .navbar-brand-container { 16 | width: 17rem; 17 | display: flex; 18 | align-items: center; 19 | padding: 10px 18px; 20 | background-color: var(--sidebar-bg-color); 21 | position: sticky; 22 | top: 0; 23 | z-index: 5; 24 | height: 60px; 25 | position: relative; 26 | 27 | @media (max-width: 768px) { 28 | width: 12rem; 29 | } 30 | 31 | @include media-breakpoint-down(md) { 32 | max-width: 14rem; 33 | background-color: var(--background-color); 34 | } 35 | } 36 | 37 | .navbar-nav { 38 | width: 100%; 39 | height: 100%; 40 | display: flex; 41 | align-items: center; 42 | justify-content: flex-end; 43 | background-color: var(--background-color); 44 | padding: 0 14px; 45 | border-left: 1px solid var(--border-color); 46 | 47 | @include media-breakpoint-down(sm) { 48 | max-width: 100vw; 49 | height: auto; 50 | align-items: flex-start; 51 | } 52 | 53 | .search-item { 54 | margin-right: auto; 55 | height: auto !important; 56 | } 57 | 58 | .dropdown-menu { 59 | position: sticky; 60 | border: 1px solid var(--border-color); 61 | 62 | .dropdown-item { 63 | color: var(--text-color); 64 | 65 | &:focus-visible { 66 | outline: none; 67 | } 68 | } 69 | 70 | .dropdown-item:hover { 71 | background-color: var(--sidebar-hover-color); 72 | 73 | .h6 { 74 | color: var(--background-color) !important; 75 | } 76 | } 77 | } 78 | } 79 | 80 | .nav-item { 81 | margin-left: 1rem; 82 | display: flex; 83 | align-items: center; 84 | 85 | @include media-breakpoint-down(md) { 86 | // display:; 87 | } 88 | 89 | #search-container { 90 | padding-right: 0px; 91 | padding-left: 0px; 92 | 93 | .dropdown { 94 | height: 32px; 95 | width: 240px; 96 | background-color: var(--searchbar-color); 97 | 98 | &:hover { 99 | border-color: var(--primary); 100 | } 101 | 102 | kbd { 103 | position: absolute; 104 | top: 7px; 105 | right: 5px; 106 | padding: 0.1rem 0.4rem; 107 | color: var(--sidebar-text-color); 108 | background-color: transparent; 109 | } 110 | 111 | span { 112 | margin-left: 2rem; 113 | margin-right: 3rem; 114 | } 115 | } 116 | } 117 | 118 | select { 119 | height: 100%; 120 | } 121 | } 122 | 123 | .wiki-navbar { 124 | background-color: transparent; 125 | padding: 0px !important; 126 | border-bottom: 1px solid var(--border-color); 127 | 128 | @include media-breakpoint-down(md) { 129 | width: auto; 130 | padding-left: 2rem; 131 | } 132 | 133 | .wiki-navbar-container { 134 | padding-right: 1rem; 135 | height: 60px; 136 | align-items: center; 137 | background-color: var(--background-color); 138 | 139 | @include media-breakpoint-down(md) { 140 | box-shadow: unset; 141 | margin-left: 0; 142 | 143 | .navbar-nav { 144 | padding-left: 10px !important; 145 | max-width: 100vw; 146 | } 147 | } 148 | } 149 | 150 | .doc-container .navbar-collapse { 151 | padding-top: 2rem; 152 | background-color: var(--background-color); 153 | margin-left: 2rem; 154 | padding-bottom: 1rem; 155 | 156 | @include media-breakpoint-down(md) { 157 | padding-top: 0; 158 | margin: 0; 159 | } 160 | } 161 | 162 | .container { 163 | height: 36px; 164 | } 165 | 166 | .sun-moon-container { 167 | cursor: pointer; 168 | margin-left: 24px; 169 | display: flex; 170 | align-items: center; 171 | 172 | svg { 173 | width: 16px !important; 174 | } 175 | 176 | @include media-breakpoint-down(md) { 177 | margin-left: 0px; 178 | } 179 | } 180 | 181 | .mobile-search-icon { 182 | margin: 0 1rem 0 auto; 183 | cursor: pointer; 184 | display: flex; 185 | align-items: center; 186 | } 187 | } 188 | 189 | .navbar { 190 | .navbar-expand-lg { 191 | width: 100%; 192 | position: fixed; 193 | top: 0; 194 | /*ensure navbar stays affixes to the top*/ 195 | left: 0; 196 | right: 0; 197 | } 198 | 199 | .navbar-link { 200 | color: var(--text-color); 201 | font-size: 0.875rem; 202 | font-weight: 500; 203 | padding: 0.5rem 0; 204 | display: block; 205 | 206 | &:hover { 207 | color: var(--primary); 208 | text-decoration: none; 209 | } 210 | } 211 | 212 | .navbar-toggler { 213 | border-color: transparent; 214 | padding: 8px; 215 | 216 | &:focus { 217 | outline: unset; 218 | } 219 | } 220 | 221 | .logged-in { 222 | display: flex; 223 | align-items: center; 224 | } 225 | 226 | .logged-in .nav-avatar { 227 | padding: 0; 228 | } 229 | } 230 | 231 | @include media-breakpoint-down(md) { 232 | .navbar { 233 | position: inherit; 234 | } 235 | 236 | .nav-item { 237 | margin-left: 0.5rem; 238 | 239 | #search-container { 240 | margin: 1rem 0; 241 | width: 140%; 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /wiki/public/scss/page-toc.scss: -------------------------------------------------------------------------------- 1 | .page-toc { 2 | background-color: var(--background-color); 3 | font-size: $font-size-xs; 4 | position: sticky; 5 | top: 90px; 6 | overflow-x: hidden; 7 | overflow-y: auto; 8 | scrollbar-width: none; 9 | height: 90vh; 10 | min-width: 220px; 11 | max-width: 220px; 12 | margin-right: 16px; 13 | padding-bottom: 10rem; 14 | margin-left: auto; 15 | 16 | &::-webkit-scrollbar { 17 | display: none; 18 | } 19 | 20 | .page-toc-title { 21 | text-transform: uppercase; 22 | font-size: 11px; 23 | font-weight: 600; 24 | line-height: 13px; 25 | letter-spacing: 0.09em; 26 | text-align: left; 27 | margin-bottom: 0.75rem; 28 | } 29 | 30 | h5 { 31 | font-size: $font-size-xs; 32 | padding-left: 1rem; 33 | letter-spacing: 0.4px; 34 | line-height: 28px; 35 | font-size: 13px; 36 | font-weight: 600; 37 | margin-bottom: 0; 38 | } 39 | 40 | div { 41 | width: 100%; 42 | padding: 0; 43 | top: 0; 44 | border-left: 1px solid var(--border-color); 45 | 46 | ul { 47 | padding-bottom: 0; 48 | margin-bottom: 0; 49 | 50 | li { 51 | a { 52 | text-overflow: ellipsis; 53 | overflow: hidden; 54 | white-space: nowrap; 55 | } 56 | } 57 | } 58 | } 59 | 60 | .active { 61 | color: var(--text-color); 62 | box-shadow: 1px 0 0 var(--primary) inset; 63 | transition: color 0.2s, box-shadow 0.2s linear, transform 0.2s linear; 64 | } 65 | 66 | a { 67 | font-size: 13px; 68 | padding: 0.25rem; 69 | color: var(--sidebar-text-color); 70 | transform: translateX(-1px); 71 | } 72 | 73 | a:hover { 74 | color: var(--toc-hover-text-color); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /wiki/public/scss/revisions.scss: -------------------------------------------------------------------------------- 1 | .wiki-revision-list { 2 | padding-bottom: 1rem; 3 | margin: auto; 4 | 5 | .wiki-revision-item { 6 | list-style-type: none; 7 | margin-bottom: 1rem; 8 | } 9 | } 10 | 11 | .revisions-modal { 12 | .feather-link { 13 | display: none; 14 | } 15 | 16 | .revision-content { 17 | height: 500px; 18 | overflow-y: auto; 19 | } 20 | 21 | .modal-header { 22 | position: inherit; 23 | 24 | .modal-title { 25 | max-width: 100%; 26 | } 27 | } 28 | 29 | .modal-body { 30 | padding: 1rem; 31 | 32 | .wiki-content { 33 | min-height: unset; 34 | } 35 | } 36 | 37 | .modal-footer { 38 | position: inherit; 39 | 40 | .previous-revision { 41 | width: inherit; 42 | margin-right: auto; 43 | } 44 | 45 | .next-revision { 46 | width: inherit; 47 | margin-left: auto; 48 | } 49 | } 50 | } 51 | 52 | del { 53 | &.diffmod, 54 | &.diffdel { 55 | * { 56 | border: 4px var(--htmldiff-del-color) solid !important; 57 | border-radius: 5px; 58 | } 59 | 60 | all: unset; 61 | } 62 | } 63 | 64 | ins { 65 | &.diffmod, 66 | &.diffins { 67 | * { 68 | border: 4px var(--htmldiff-ins-color) solid !important; 69 | border-radius: 5px; 70 | } 71 | 72 | all: unset; 73 | } 74 | } -------------------------------------------------------------------------------- /wiki/public/scss/sidebar.scss: -------------------------------------------------------------------------------- 1 | .sm-sidebar { 2 | width: 100%; 3 | } 4 | .sm-sidebar .web-sidebar { 5 | margin-top: 4px; 6 | padding-bottom: 2rem; 7 | 8 | @media (max-width: 768px) { 9 | padding-bottom: 0; 10 | } 11 | } 12 | 13 | .web-sidebar { 14 | position: relative; 15 | } 16 | 17 | .doc-sidebar { 18 | margin-bottom: 0; 19 | height: 100vh; 20 | padding-top: 60px; 21 | display: flex; 22 | flex-direction: column; 23 | 24 | .web-sidebar { 25 | flex: 1; 26 | display: flex; 27 | flex-direction: column; 28 | padding: 0px 8px; 29 | overflow-x: hidden; 30 | overflow-y: auto; 31 | height: 100%; 32 | width: 17rem; 33 | 34 | .sidebar-items { 35 | display: flex; 36 | flex-direction: column; 37 | justify-content: space-between; 38 | width: 100%; 39 | padding-top: 8px; 40 | } 41 | } 42 | } 43 | 44 | .sidebar-column { 45 | margin-top: -60px; 46 | border-right: 1px solid var(--border-color); 47 | } 48 | 49 | .sidebar-group-list { 50 | display: flex; 51 | flex-direction: column; 52 | list-style-type: none; 53 | padding: 0px; 54 | } 55 | .sidebar-group-container { 56 | height: 32px; 57 | padding: 5px 10px; 58 | border-radius: 8px; 59 | gap: 8px; 60 | 61 | .icon { 62 | width: 8px; 63 | height: 8px; 64 | margin: 0; 65 | color: #999999; 66 | 67 | &.rotate { 68 | transform: rotate(90deg); 69 | } 70 | } 71 | } 72 | .sidebar-group-title { 73 | font-size: 13px; 74 | font-weight: 420 !important; 75 | line-height: 16px; 76 | letter-spacing: 0.015em; 77 | text-align: left; 78 | } 79 | .sidebar-group-item-list { 80 | display: flex; 81 | flex-direction: column; 82 | list-style-type: none; 83 | margin-left: 10px; 84 | margin-bottom: 4px !important; 85 | gap: 2px; 86 | } 87 | .sidebar-group-item { 88 | display: flex; 89 | align-items: center; 90 | height: 26px; 91 | border-radius: 8px; 92 | } 93 | .sidebar-item-active { 94 | a { 95 | background-color: transparent !important; 96 | color: var(--text-color) !important; 97 | } 98 | } 99 | .sidebar-group-item-title { 100 | color: var(--text-light); 101 | text-overflow: ellipsis; 102 | overflow: hidden; 103 | white-space: nowrap; 104 | font-weight: 420 !important; 105 | } 106 | 107 | .sidebar-group { 108 | margin: 0; 109 | font-style: normal; 110 | font-weight: 500; 111 | font-size: $font-size-base; 112 | line-height: 1.5; 113 | /* identical to box height, or 28px */ 114 | 115 | letter-spacing: -0.011em; 116 | 117 | ul { 118 | padding-left: 14px; 119 | } 120 | 121 | .list-unstyled:empty::after { 122 | font-size: 12px; 123 | font-weight: 400; 124 | font-style: italic; 125 | color: var(--sidebar-text-color); 126 | content: "This Wiki Group will be deleted automatically"; 127 | } 128 | 129 | .collapsible { 130 | padding: 6px 8px; 131 | display: flex; 132 | align-items: center; 133 | width: 100%; 134 | } 135 | 136 | div { 137 | .h6 { 138 | font-size: $font-size-sm; 139 | margin-bottom: 0; 140 | line-height: 1.5rem; 141 | color: var(--text-color); 142 | font-weight: 700; 143 | } 144 | } 145 | 146 | .drop-icon, 147 | .add-sidebar-page { 148 | cursor: pointer; 149 | display: inline-flex; 150 | margin: 0 5px 0 auto; 151 | transition: transform 0.2s ease-in-out; 152 | transform: rotate(0deg); 153 | color: var(--sidebar-text-color); 154 | 155 | &.rotate { 156 | transform: rotate(-90deg); 157 | } 158 | } 159 | } 160 | 161 | .sidebar-group .collapsible, 162 | .sidebar-item { 163 | &:hover { 164 | cursor: pointer; 165 | 166 | &:not(.active) { 167 | background-color: var(--sidebar-hover-color); 168 | border-radius: 0.625rem; 169 | } 170 | } 171 | } 172 | 173 | .sidebar-item { 174 | display: flex; 175 | align-items: center; 176 | min-height: 1.75rem; 177 | 178 | &.active { 179 | background-color: var(--sidebar-active-item-color); 180 | box-shadow: 0 0 #0000, 0 0 #0000, 0px 1px 2px rgba(0, 0, 0, 0.1); 181 | } 182 | 183 | div { 184 | display: flex; 185 | align-items: center; 186 | } 187 | 188 | a { 189 | margin: 0; 190 | width: 100%; 191 | padding: 5px 12px; 192 | 193 | &:hover { 194 | color: unset; 195 | } 196 | } 197 | 198 | :first-child { 199 | width: 100%; 200 | } 201 | } 202 | 203 | .doc-sidebar { 204 | // padding-right: 0.5rem; 205 | background-color: var(--sidebar-bg-color); 206 | color: var(--sidebar-font-color); 207 | } 208 | 209 | .my-contributions, 210 | .new-wiki-page, 211 | .sidebar-edit-mode-btn, 212 | .sidebar-view-mode-btn, 213 | .add-sidebar-group { 214 | cursor: pointer; 215 | margin: auto; 216 | font-weight: 500; 217 | 218 | svg { 219 | margin-bottom: 0.1rem; 220 | } 221 | 222 | span { 223 | font-size: 0.75rem; 224 | } 225 | } 226 | 227 | .sidebar-options { 228 | bottom: 0; 229 | position: sticky; 230 | padding: 0.5rem; 231 | background-color: var(--sidebar-bg-color); 232 | } 233 | 234 | .remove-sidebar-item { 235 | cursor: pointer; 236 | margin: 0 1rem 0 auto; 237 | } 238 | 239 | .collapsible .remove-sidebar-item { 240 | margin-right: calc(15px - 0.5rem); 241 | margin-bottom: 3px; 242 | } 243 | 244 | .trash-icon { 245 | visibility: hidden; 246 | } 247 | 248 | .sidebar-item:hover .trash-icon, 249 | .sidebar-group .collapsible:hover .trash-icon { 250 | visibility: visible; 251 | } 252 | 253 | @media (min-width: 992px) { 254 | .doc-sidebar { 255 | top: 0; 256 | padding-bottom: 0; 257 | } 258 | } 259 | 260 | @include media-breakpoint-down(md) { 261 | .web-sidebar { 262 | padding-top: 0; 263 | } 264 | 265 | .web-sidebar > a { 266 | display: none; 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /wiki/public/scss/wiki.bundle.scss: -------------------------------------------------------------------------------- 1 | @import "./variables.scss"; 2 | @import "./general.scss"; 3 | @import "./markdown.scss"; 4 | @import "./navbar.scss"; 5 | @import "./sidebar.scss"; 6 | @import "./page-toc.scss"; 7 | @import "./footer.scss"; 8 | @import "./modal.scss"; 9 | @import "./dropdowns.scss"; 10 | -------------------------------------------------------------------------------- /wiki/search.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | import json 6 | 7 | import frappe 8 | from frappe.utils import cstr 9 | from redis.commands.search.field import TagField, TextField 10 | from redis.commands.search.indexDefinition import IndexDefinition 11 | from redis.commands.search.query import Query 12 | from redis.exceptions import ResponseError 13 | 14 | 15 | class Search: 16 | def __init__(self, index_name, prefix, schema) -> None: 17 | self.redis = frappe.cache() 18 | self.index_name = index_name 19 | self.prefix = prefix 20 | self.schema = [] 21 | for field in schema: 22 | self.schema.append(frappe._dict(field)) 23 | 24 | def create_index(self): 25 | index_def = IndexDefinition( 26 | prefix=[f"{self.redis.make_key(self.prefix).decode()}:"], 27 | ) 28 | schema = [] 29 | for field in self.schema: 30 | kwargs = {k: v for k, v in field.items() if k in ["weight", "sortable", "no_index", "no_stem"]} 31 | if field.type == "tag": 32 | schema.append(TagField(field.name, **kwargs)) 33 | else: 34 | schema.append(TextField(field.name, **kwargs)) 35 | self.redis.ft(self.index_name).create_index(schema, definition=index_def) 36 | self._index_exists = True 37 | 38 | def add_document(self, id, doc, payload=None): 39 | doc = frappe._dict(doc) 40 | doc_id = self.redis.make_key(f"{self.prefix}:{id}").decode() 41 | mapping = {} 42 | for field in self.schema: 43 | if field.name in doc: 44 | mapping[field.name] = cstr(doc[field.name]) 45 | if self.index_exists(): 46 | self.redis.ft(self.index_name).add_document( 47 | doc_id, payload=json.dumps(payload), replace=True, **mapping 48 | ) 49 | 50 | def remove_document(self, id): 51 | key = self.redis.make_key(f"{self.prefix}:{id}").decode() 52 | if self.index_exists(): 53 | self.redis.ft(self.index_name).delete_document(key) 54 | 55 | def search(self, query, start=0, page_length=50, sort_by=None, highlight=False, with_payloads=False): 56 | query = Query(query).paging(start, page_length) 57 | if highlight: 58 | query = query.highlight(tags=['', ""]) 59 | if sort_by: 60 | parts = sort_by.split(" ") 61 | sort_field = parts[0] 62 | direction = parts[1] if len(parts) > 1 else "asc" 63 | query = query.sort_by(sort_field, asc=direction == "asc") 64 | if with_payloads: 65 | query = query.with_payloads() 66 | 67 | try: 68 | result = self.redis.ft(self.index_name).search(query) 69 | except ResponseError as e: 70 | print(e) 71 | return frappe._dict({"total": 0, "docs": [], "duration": 0}) 72 | 73 | out = frappe._dict(docs=[], total=result.total, duration=result.duration) 74 | for doc in result.docs: 75 | id = doc.id.split(":", 1)[1] 76 | _doc = frappe._dict(doc.__dict__) 77 | _doc.id = id 78 | _doc.payload = json.loads(doc.payload) if doc.payload else None 79 | out.docs.append(_doc) 80 | return out 81 | 82 | def spellcheck(self, query, **kwargs): 83 | return self.redis.ft(self.index_name).spellcheck(query, **kwargs) 84 | 85 | def drop_index(self): 86 | if self.index_exists(): 87 | print(f"Dropping index {self.index_name}") 88 | self.redis.ft(self.index_name).dropindex(delete_documents=True) 89 | 90 | def index_exists(self): 91 | self._index_exists = getattr(self, "_index_exists", None) 92 | if self._index_exists is None: 93 | try: 94 | self.redis.ft(self.index_name).info() 95 | self._index_exists = True 96 | except ResponseError: 97 | self._index_exists = False 98 | return self._index_exists 99 | -------------------------------------------------------------------------------- /wiki/templates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/wiki/af7de232b6a7e25e6e8984abcd5c4f985a17587e/wiki/templates/__init__.py -------------------------------------------------------------------------------- /wiki/templates/pages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/wiki/af7de232b6a7e25e6e8984abcd5c4f985a17587e/wiki/templates/pages/__init__.py -------------------------------------------------------------------------------- /wiki/utils.py: -------------------------------------------------------------------------------- 1 | import difflib 2 | 3 | import frappe 4 | 5 | 6 | def check_app_permission(): 7 | """Check if user has permission to access the app (for showing the app on app screen)""" 8 | 9 | if frappe.session.user == "Administrator": 10 | return True 11 | 12 | roles = frappe.get_roles() 13 | if "Wiki Approver" in roles: 14 | return True 15 | 16 | return False 17 | 18 | 19 | def apply_markdown_diff(original_md, modified_md): 20 | """ 21 | Compares two markdown texts, finds the differences, and applies them to the original text. 22 | 23 | Args: 24 | original_md (str): The original markdown text. 25 | modified_md (str): The modified markdown text. 26 | 27 | Returns: 28 | tuple: A tuple containing the updated markdown text and a list of changes with their positions. 29 | """ 30 | original_lines = original_md.split("\n") 31 | modified_lines = modified_md.split("\n") 32 | 33 | # Initialize the SequenceMatcher to compare the two lists of lines 34 | matcher = difflib.SequenceMatcher(None, original_lines, modified_lines) 35 | opcodes = matcher.get_opcodes() 36 | 37 | # Sort opcodes by the reverse order of the original start index to handle index shifting 38 | sorted_opcodes = sorted(opcodes, key=lambda x: (-x[1], x[0])) 39 | 40 | # Create a copy of the original lines to apply changes 41 | result = list(original_lines) 42 | changes = [] 43 | 44 | for tag, i1, i2, j1, j2 in sorted_opcodes: 45 | # Convert original line indices to 1-based for reporting 46 | original_range = None 47 | if i1 < i2: 48 | original_start = i1 + 1 49 | original_end = i2 50 | original_range = (original_start, original_end) 51 | 52 | if tag == "delete": 53 | # Record the deletion change 54 | changes.append({"type": "delete", "original_lines": original_range, "content": None}) 55 | # Apply deletion to the result 56 | del result[i1:i2] 57 | elif tag == "insert": 58 | # Record the insertion change with position (1-based) 59 | content = modified_lines[j1:j2] 60 | position = i1 + 1 # Convert to 1-based position 61 | changes.append( 62 | {"type": "insert", "original_lines": None, "content": content, "position": position} 63 | ) 64 | # Apply insertion to the result 65 | result[i1:i1] = content 66 | elif tag == "replace": 67 | # Record the replacement change 68 | content = modified_lines[j1:j2] 69 | changes.append({"type": "replace", "original_lines": original_range, "content": content}) 70 | # Apply replacement: delete original lines and insert new content 71 | del result[i1:i2] 72 | result[i1:i1] = content 73 | # 'equal' changes are ignored 74 | 75 | # Join the modified lines back into a single string 76 | updated_md = "\n".join(result) 77 | return updated_md, changes 78 | 79 | 80 | def apply_changes(original_md, changes): 81 | """ 82 | Applies a list of changes to the original markdown text. 83 | 84 | Args: 85 | original_md (str): The original markdown text. 86 | changes (list): A list of changes as returned by apply_markdown_diff. 87 | 88 | Returns: 89 | str: The modified markdown text after applying all changes. 90 | """ 91 | lines = original_md.split("\n") 92 | 93 | # Sort changes in reverse order of their original line numbers to handle index shifting 94 | sorted_changes = sorted( 95 | changes, key=lambda x: (-x["original_lines"][0] if x["original_lines"] else -x["position"]) 96 | ) 97 | 98 | for change in sorted_changes: 99 | if change["type"] == "delete": 100 | start = change["original_lines"][0] - 1 # Convert to 0-based index 101 | end = change["original_lines"][1] 102 | del lines[start:end] 103 | elif change["type"] == "insert": 104 | position = change["position"] - 1 # Convert to 0-based index 105 | lines[position:position] = change["content"] 106 | elif change["type"] == "replace": 107 | start = change["original_lines"][0] - 1 108 | end = change["original_lines"][1] 109 | del lines[start:end] 110 | lines[start:start] = change["content"] 111 | 112 | return "\n".join(lines) 113 | 114 | 115 | def highlight_changes(original_md, changes): 116 | """ 117 | Highlights changes in the original markdown text using and tags. 118 | 119 | Args: 120 | original_md (str): The original markdown text. 121 | changes (list): A list of changes as returned by apply_markdown_diff. 122 | 123 | Returns: 124 | str: The modified markdown text with changes highlighted. 125 | """ 126 | lines = original_md.split("\n") 127 | 128 | # Sort changes in reverse order of their original line numbers to handle index shifting 129 | sorted_changes = sorted( 130 | changes, key=lambda x: (-x["original_lines"][0] if x["original_lines"] else -x["position"]) 131 | ) 132 | 133 | for change in sorted_changes: 134 | if change["type"] == "delete": 135 | start = change["original_lines"][0] - 1 # Convert to 0-based index 136 | end = change["original_lines"][1] 137 | # Wrap deleted lines with tags 138 | for i in range(start, end): 139 | lines[i] = f"{lines[i]}" 140 | elif change["type"] == "insert": 141 | position = change["position"] - 1 # Convert to 0-based index 142 | # Insert new lines with tags 143 | for line in change["content"]: 144 | lines.insert(position, f"{line}") 145 | position += 1 146 | elif change["type"] == "replace": 147 | start = change["original_lines"][0] - 1 148 | end = change["original_lines"][1] 149 | # Wrap deleted lines with tags 150 | for i in range(start, end): 151 | lines[i] = f"{lines[i]}" 152 | # Insert new lines with tags after the deleted lines 153 | insert_pos = end 154 | for line in change["content"]: 155 | lines.insert(insert_pos, f"{line}") 156 | insert_pos += 1 157 | 158 | return "\n".join(lines) 159 | -------------------------------------------------------------------------------- /wiki/wiki/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/wiki/af7de232b6a7e25e6e8984abcd5c4f985a17587e/wiki/wiki/__init__.py -------------------------------------------------------------------------------- /wiki/wiki/doctype/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/wiki/af7de232b6a7e25e6e8984abcd5c4f985a17587e/wiki/wiki/doctype/__init__.py -------------------------------------------------------------------------------- /wiki/wiki/doctype/migrate_to_wiki/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/wiki/af7de232b6a7e25e6e8984abcd5c4f985a17587e/wiki/wiki/doctype/migrate_to_wiki/__init__.py -------------------------------------------------------------------------------- /wiki/wiki/doctype/migrate_to_wiki/migrate_to_wiki.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, Frappe and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("Migrate To Wiki", { 5 | // refresh: function(frm) { 6 | // } 7 | }); 8 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/migrate_to_wiki/migrate_to_wiki.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "creation": "2021-05-07 03:52:41.720362", 4 | "doctype": "DocType", 5 | "editable_grid": 1, 6 | "engine": "InnoDB", 7 | "field_order": [ 8 | "app_name", 9 | "assets_directory", 10 | "docs_directory", 11 | "create_new_assets", 12 | "column_break_5", 13 | "documentation_route", 14 | "docs_base_url", 15 | "assets_prepend" 16 | ], 17 | "fields": [ 18 | { 19 | "fieldname": "app_name", 20 | "fieldtype": "Data", 21 | "label": "App Name" 22 | }, 23 | { 24 | "fieldname": "docs_directory", 25 | "fieldtype": "Data", 26 | "label": "Docs Directory" 27 | }, 28 | { 29 | "fieldname": "assets_directory", 30 | "fieldtype": "Data", 31 | "label": "Assets Directory" 32 | }, 33 | { 34 | "fieldname": "documentation_route", 35 | "fieldtype": "Data", 36 | "label": "New Documentation Route" 37 | }, 38 | { 39 | "description": "with docs base url", 40 | "fieldname": "assets_prepend", 41 | "fieldtype": "Data", 42 | "label": "Assets Prepend" 43 | }, 44 | { 45 | "fieldname": "docs_base_url", 46 | "fieldtype": "Data", 47 | "label": "Docs Base Utl" 48 | }, 49 | { 50 | "default": "0", 51 | "description": "Create New Images if they exist on Disk", 52 | "fieldname": "create_new_assets", 53 | "fieldtype": "Check", 54 | "label": "Create New Assets " 55 | }, 56 | { 57 | "fieldname": "column_break_5", 58 | "fieldtype": "Column Break" 59 | } 60 | ], 61 | "index_web_pages_for_search": 1, 62 | "issingle": 1, 63 | "links": [], 64 | "modified": "2021-06-01 14:22:25.142960", 65 | "modified_by": "Administrator", 66 | "module": "Wiki", 67 | "name": "Migrate To Wiki", 68 | "owner": "Administrator", 69 | "permissions": [ 70 | { 71 | "create": 1, 72 | "delete": 1, 73 | "email": 1, 74 | "print": 1, 75 | "read": 1, 76 | "role": "System Manager", 77 | "share": 1, 78 | "write": 1 79 | } 80 | ], 81 | "sort_field": "modified", 82 | "sort_order": "DESC", 83 | "track_changes": 1 84 | } -------------------------------------------------------------------------------- /wiki/wiki/doctype/migrate_to_wiki/test_migrate_to_wiki.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, Frappe and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | import unittest 6 | 7 | 8 | class TestMigrateToWiki(unittest.TestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_app_switcher_list_table/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/wiki/af7de232b6a7e25e6e8984abcd5c4f985a17587e/wiki/wiki/doctype/wiki_app_switcher_list_table/__init__.py -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_app_switcher_list_table/wiki_app_switcher_list_table.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "creation": "2025-02-19 15:39:10.706506", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "app_title", 10 | "wiki_space" 11 | ], 12 | "fields": [ 13 | { 14 | "fetch_from": "wiki_space.space_name", 15 | "fieldname": "app_title", 16 | "fieldtype": "Data", 17 | "in_list_view": 1, 18 | "label": "App Title", 19 | "placeholder": "Select Wiki Space to see title", 20 | "read_only": 1 21 | }, 22 | { 23 | "fieldname": "wiki_space", 24 | "fieldtype": "Link", 25 | "in_list_view": 1, 26 | "label": "Wiki Space", 27 | "options": "Wiki Space" 28 | } 29 | ], 30 | "index_web_pages_for_search": 1, 31 | "istable": 1, 32 | "links": [], 33 | "modified": "2025-02-19 15:42:44.734010", 34 | "modified_by": "Administrator", 35 | "module": "Wiki", 36 | "name": "Wiki App Switcher List Table", 37 | "owner": "Administrator", 38 | "permissions": [], 39 | "sort_field": "creation", 40 | "sort_order": "DESC", 41 | "states": [] 42 | } -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_app_switcher_list_table/wiki_app_switcher_list_table.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025, Frappe and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class WikiAppSwitcherListTable(Document): 9 | pass 10 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_feedback/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/wiki/af7de232b6a7e25e6e8984abcd5c4f985a17587e/wiki/wiki/doctype/wiki_feedback/__init__.py -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_feedback/patches/delete_wiki_feedback_item.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | 3 | 4 | def execute(): 5 | if not frappe.db.table_exists("Wiki Feedback Item"): 6 | return 7 | 8 | for d in frappe.db.sql("select * from `tabWiki Feedback Item`", as_dict=True): 9 | if not d.parent: 10 | continue 11 | 12 | wiki_page = frappe.db.get_value("Wiki Feedback", d.parent, "wiki_page") 13 | 14 | if not wiki_page: 15 | continue 16 | 17 | doc = frappe.get_doc( 18 | dict( 19 | doctype="Wiki Feedback", 20 | status="Open", 21 | wiki_page=wiki_page, 22 | rating=d.rating, 23 | feedback=d.feedback, 24 | email_id=d.email_id, 25 | ) 26 | ).insert() 27 | 28 | frappe.db.set_value("Wiki Feedback", doc.name, "creation", d.creation) 29 | frappe.db.set_value("Wiki Feedback", doc.name, "modified", d.modified, update_modified=False) 30 | 31 | # delete old 32 | frappe.delete_doc("Wiki Feedback", d.parent) 33 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_feedback/test_wiki_feedback.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Frappe and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestWikiFeedback(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_feedback/wiki_feedback.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023, Frappe and contributors 2 | // For license information, please see license.txt 3 | 4 | // frappe.ui.form.on("Wiki Feedback", { 5 | // refresh(frm) { 6 | 7 | // }, 8 | // }); 9 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_feedback/wiki_feedback.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "creation": "2023-12-05 14:12:12.194036", 5 | "doctype": "DocType", 6 | "engine": "InnoDB", 7 | "field_order": [ 8 | "wiki_page", 9 | "status", 10 | "rating", 11 | "feedback", 12 | "email_id" 13 | ], 14 | "fields": [ 15 | { 16 | "fieldname": "wiki_page", 17 | "fieldtype": "Link", 18 | "in_list_view": 1, 19 | "label": "Wiki Page", 20 | "options": "Wiki Page", 21 | "reqd": 1 22 | }, 23 | { 24 | "fieldname": "rating", 25 | "fieldtype": "Rating", 26 | "in_list_view": 1, 27 | "label": "Rating" 28 | }, 29 | { 30 | "fieldname": "feedback", 31 | "fieldtype": "Small Text", 32 | "in_list_view": 1, 33 | "label": "Feedback" 34 | }, 35 | { 36 | "fieldname": "email_id", 37 | "fieldtype": "Data", 38 | "label": "Email Id" 39 | }, 40 | { 41 | "default": "Open", 42 | "fieldname": "status", 43 | "fieldtype": "Select", 44 | "label": "Status", 45 | "options": "Open\nClosed" 46 | } 47 | ], 48 | "index_web_pages_for_search": 1, 49 | "links": [], 50 | "modified": "2024-06-03 16:29:53.387846", 51 | "modified_by": "Administrator", 52 | "module": "Wiki", 53 | "name": "Wiki Feedback", 54 | "owner": "Administrator", 55 | "permissions": [ 56 | { 57 | "create": 1, 58 | "delete": 1, 59 | "email": 1, 60 | "export": 1, 61 | "print": 1, 62 | "read": 1, 63 | "report": 1, 64 | "role": "System Manager", 65 | "share": 1, 66 | "write": 1 67 | }, 68 | { 69 | "create": 1, 70 | "delete": 1, 71 | "email": 1, 72 | "export": 1, 73 | "print": 1, 74 | "read": 1, 75 | "report": 1, 76 | "role": "Wiki Approver", 77 | "share": 1, 78 | "write": 1 79 | }, 80 | { 81 | "create": 1, 82 | "email": 1, 83 | "export": 1, 84 | "print": 1, 85 | "report": 1, 86 | "role": "All", 87 | "share": 1, 88 | "write": 1 89 | }, 90 | { 91 | "create": 1, 92 | "email": 1, 93 | "export": 1, 94 | "print": 1, 95 | "report": 1, 96 | "role": "Guest", 97 | "share": 1, 98 | "write": 1 99 | } 100 | ], 101 | "sort_field": "modified", 102 | "sort_order": "DESC", 103 | "states": [], 104 | "title_field": "wiki_page" 105 | } -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_feedback/wiki_feedback.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Frappe and contributors 2 | # For license information, please see license.txt 3 | 4 | import frappe 5 | from frappe.model.document import Document 6 | from frappe.rate_limiter import rate_limit 7 | from frappe.utils import validate_email_address 8 | 9 | 10 | class WikiFeedback(Document): 11 | pass 12 | 13 | 14 | def get_feedback_limit(): 15 | wiki_settings = frappe.get_single("Wiki Settings") 16 | return wiki_settings.feedback_submission_limit or 3 17 | 18 | 19 | @frappe.whitelist(allow_guest=True) 20 | @rate_limit(limit=get_feedback_limit, seconds=60 * 60) 21 | def submit_feedback(name, feedback, rating, email=None, feedback_index=None): 22 | email = validate_email_address(email) 23 | doc = frappe.get_doc( 24 | { 25 | "doctype": "Wiki Feedback", 26 | "wiki_page": name, 27 | "rating": rating, 28 | "feedback": feedback, 29 | "email_id": email, 30 | } 31 | ) 32 | doc.insert() 33 | return 1 34 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_group_item/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/wiki/af7de232b6a7e25e6e8984abcd5c4f985a17587e/wiki/wiki/doctype/wiki_group_item/__init__.py -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_group_item/wiki_group_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "autoname": "hash", 5 | "creation": "2023-04-04 18:17:37.507102", 6 | "doctype": "DocType", 7 | "editable_grid": 1, 8 | "engine": "InnoDB", 9 | "field_order": [ 10 | "parent_label", 11 | "wiki_page", 12 | "hide_on_sidebar" 13 | ], 14 | "fields": [ 15 | { 16 | "fieldname": "parent_label", 17 | "fieldtype": "Data", 18 | "in_list_view": 1, 19 | "label": "Parent Label", 20 | "reqd": 1 21 | }, 22 | { 23 | "fieldname": "wiki_page", 24 | "fieldtype": "Link", 25 | "in_list_view": 1, 26 | "label": "Wiki Page", 27 | "options": "Wiki Page", 28 | "reqd": 1 29 | }, 30 | { 31 | "default": "0", 32 | "fieldname": "hide_on_sidebar", 33 | "fieldtype": "Check", 34 | "label": "Hide on sidebar" 35 | } 36 | ], 37 | "index_web_pages_for_search": 1, 38 | "istable": 1, 39 | "links": [], 40 | "modified": "2024-04-16 12:06:18.398870", 41 | "modified_by": "Administrator", 42 | "module": "Wiki", 43 | "name": "Wiki Group Item", 44 | "naming_rule": "Random", 45 | "owner": "Administrator", 46 | "permissions": [], 47 | "sort_field": "modified", 48 | "sort_order": "DESC", 49 | "states": [] 50 | } -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_group_item/wiki_group_item.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Frappe and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class WikiGroupItem(Document): 9 | pass 10 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/wiki/af7de232b6a7e25e6e8984abcd5c4f985a17587e/wiki/wiki/doctype/wiki_page/__init__.py -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page/patches/convert_wiki_content_to_markdown.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import frappe 4 | import six 5 | from bs4 import Comment, Doctype, NavigableString 6 | from markdownify import MarkdownConverter 7 | 8 | html_heading_re = re.compile(r"h[1-6]") 9 | 10 | 11 | class CustomMarkdownConverter(MarkdownConverter): 12 | # override markdownify's process_tag function to escape certain html tags instead of removing them 13 | def process_tag(self, node, convert_as_inline, children_only=False): 14 | text = "" 15 | 16 | # markdown headings or cells can't include 17 | # block elements (elements w/newlines) 18 | isHeading = html_heading_re.match(node.name) is not None 19 | isCell = node.name in ["td", "th"] 20 | convert_children_as_inline = convert_as_inline 21 | 22 | if not children_only and (isHeading or isCell): 23 | convert_children_as_inline = True 24 | 25 | # Remove whitespace-only textnodes in purely nested nodes 26 | def is_nested_node(el): 27 | return el and el.name in ["ol", "ul", "li", "table", "thead", "tbody", "tfoot", "tr", "td", "th"] 28 | 29 | if is_nested_node(node): 30 | for el in node.children: 31 | # Only extract (remove) whitespace-only text node if any of the 32 | # conditions is true: 33 | # - el is the first element in its parent 34 | # - el is the last element in its parent 35 | # - el is adjacent to an nested node 36 | can_extract = ( 37 | not el.previous_sibling 38 | or not el.next_sibling 39 | or is_nested_node(el.previous_sibling) 40 | or is_nested_node(el.next_sibling) 41 | ) 42 | if isinstance(el, NavigableString) and six.text_type(el).strip() == "" and can_extract: 43 | el.extract() 44 | 45 | # Convert the children first 46 | for el in node.children: 47 | if isinstance(el, Comment) or isinstance(el, Doctype): 48 | continue 49 | elif isinstance(el, NavigableString): 50 | text += self.process_text(el) 51 | else: 52 | if el.name in ["video", "iframe", "audio", "embed", "object", "source", "picture", "math"]: 53 | text += self.process_text(el) 54 | 55 | processed_tag = self.process_tag(el, convert_children_as_inline) 56 | if processed_tag is not None: # Ensure it doesn't return None 57 | text += processed_tag 58 | 59 | if not children_only: 60 | convert_fn = getattr(self, f"convert_{node.name}", None) 61 | if convert_fn and self.should_convert_tag(node.name): 62 | text = convert_fn(node, text, convert_as_inline) 63 | 64 | return text 65 | 66 | def convert_img(self, el, text, convert_as_inline): 67 | alt = el.attrs.get("alt", None) or "" 68 | src = el.attrs.get("src", None) or "" 69 | title = el.attrs.get("title", None) or "" 70 | title_part = ' "{}"'.format(title.replace('"', r"\"")) if title else "" 71 | 72 | # Only include image tags with valid src attribute, this is to remove empty image tags added by Prosemirror 73 | if not src or src.strip() == "": 74 | return "" 75 | 76 | if convert_as_inline and el.parent.name not in self.options["keep_inline_images_in"]: 77 | return alt 78 | 79 | return f"![{alt}]({src}{title_part})" 80 | 81 | 82 | def custom_markdownify(html, **options): 83 | return CustomMarkdownConverter(**options).convert(html) 84 | 85 | 86 | def execute(): 87 | wiki_pages = frappe.db.get_all("Wiki Page", fields=["name", "content"]) 88 | for page in wiki_pages: 89 | markdown_content = custom_markdownify(page["content"]).replace("`", "`").replace("${", "${") 90 | frappe.db.set_value("Wiki Page", page["name"], "content", markdown_content) 91 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page/patches/delete_is_new.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | import frappe 6 | 7 | 8 | def execute(): 9 | try: 10 | frappe.db.sql("alter table `tabWiki Page Patch` drop column is_new;") 11 | frappe.db.commit() 12 | except Exception: 13 | pass 14 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page/patches/set_allow_guest.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | import frappe 6 | 7 | 8 | def execute(): 9 | frappe.reload_doctype("Wiki Page") 10 | # set allow_guest to 1 for all records 11 | frappe.db.set_value("Wiki Page", {"name": ("!=", ".")}, "allow_guest", 1) 12 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page/patches/update_escaped_chars.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import frappe 4 | 5 | 6 | def execute(): 7 | wiki_pages = frappe.db.get_all("Wiki Page", fields=["name", "content"]) 8 | for page in wiki_pages: 9 | frappe.db.set_value("Wiki Page", page["name"], "content", edit_content(page["content"])) 10 | 11 | 12 | def edit_content(content): 13 | def replacer(match): 14 | code_content = match.group(0) 15 | # replace inside the code block 16 | code_content = code_content.replace(r"\"", '"') 17 | code_content = code_content.replace(r"\_", "_") 18 | code_content = code_content.replace(r"\t", "") 19 | code_content = code_content.replace(r"\G", "") 20 | code_content = code_content.replace(r"\n", "\n") 21 | return code_content 22 | 23 | content = re.sub(r"(```[\s\S]*?```|`[^`]*`)", replacer, content) 24 | 25 | content = content.replace(r"\*", "*") 26 | 27 | return content 28 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page/patches/update_escaped_code_content.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | 3 | 4 | def execute(): 5 | wiki_pages = frappe.db.get_all("Wiki Page", fields=["name", "content"]) 6 | for page in wiki_pages: 7 | markdown_content = ( 8 | page["content"] 9 | .replace("`", "`") 10 | .replace("${", "${") 11 | .replace(">", ">") 12 | .replace("<", "<") 13 | ) 14 | frappe.db.set_value("Wiki Page", page["name"], "content", markdown_content) 15 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page/review_contributions.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | from frappe import _ 3 | from frappe.utils import cint 4 | 5 | from wiki.utils import apply_changes, apply_markdown_diff, highlight_changes 6 | 7 | 8 | def fetch_patches(start=0, limit=10): 9 | patches = [] 10 | filters = [["status", "=", "Under Review"]] 11 | 12 | # Add space filter if provided in URL 13 | space = frappe.form_dict.get("space") 14 | if space: 15 | wiki_pages = frappe.get_all("Wiki Group Item", filters={"parent": space}, pluck="wiki_page") 16 | if wiki_pages: 17 | filters.append(["wiki_page", "in", wiki_pages]) 18 | 19 | wiki_page_patches = frappe.get_list( 20 | "Wiki Page Patch", 21 | ["name", "message", "status", "raised_by", "modified", "wiki_page"], 22 | start=cint(start), 23 | limit=cint(limit), 24 | filters=filters, 25 | ) 26 | 27 | for patch in wiki_page_patches: 28 | route = frappe.db.get_value("Wiki Page", patch.wiki_page, "route") 29 | wiki_space_name = frappe.get_value("Wiki Group Item", {"wiki_page": patch.wiki_page}, "parent") 30 | patch.space_name = ( 31 | frappe.get_value("Wiki Space", wiki_space_name, "space_name") if wiki_space_name else "" 32 | ) 33 | patch.edit_link = f"/{route}?editWiki=1&wikiPagePatch={patch.name}" 34 | patch.color = "orange" 35 | patch.modified = frappe.utils.pretty_date(patch.modified) 36 | patches.extend([patch]) 37 | 38 | return patches 39 | 40 | 41 | @frappe.whitelist() 42 | def get_patches_api(start=0, limit=10): 43 | patches = fetch_patches(start, limit) 44 | return {"patches": patches} 45 | 46 | 47 | @frappe.whitelist() 48 | def update_patch_status(patch, status): 49 | if not frappe.has_permission("Wiki Page Patch", "write"): 50 | frappe.throw(_("You don't have permission to update patch status")) 51 | 52 | patch_doc = frappe.get_doc("Wiki Page Patch", patch) 53 | if status == "Approved": 54 | patch_doc.status = "Approved" 55 | patch_doc.approved_by = frappe.session.user 56 | patch_doc.submit() 57 | elif status == "Rejected": 58 | patch_doc.status = "Rejected" 59 | patch_doc.submit() 60 | 61 | return True 62 | 63 | 64 | @frappe.whitelist() 65 | def get_patch_diff(patch): 66 | if not frappe.has_permission("Wiki Page Patch", "write"): 67 | frappe.throw(_("You don't have permission to view this patch")) 68 | 69 | patch_doc = frappe.get_doc("Wiki Page Patch", patch) 70 | original_doc = frappe.get_doc("Wiki Page", patch_doc.wiki_page) 71 | 72 | patch_md = patch_doc.orignal_code or "" 73 | original_md = "" if patch_doc.new else original_doc.content or "" 74 | modified_md = patch_doc.new_code or "" 75 | 76 | merge_old_content = apply_markdown_diff(patch_md, modified_md)[1] 77 | 78 | merge_new_content = apply_changes(original_md, merge_old_content) 79 | 80 | new_modified_md = apply_markdown_diff(original_md, merge_new_content)[1] 81 | 82 | return { 83 | "diff": highlight_changes(original_md, new_modified_md), 84 | "raised_by": patch_doc.raised_by, 85 | "raised_on": frappe.utils.pretty_date(patch_doc.modified), 86 | "merged_html": frappe.utils.md_to_html(merge_new_content), 87 | } 88 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page/search.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | import frappe 6 | from frappe.utils import strip_html_tags, update_progress_bar 7 | from frappe.utils.redis_wrapper import RedisWrapper 8 | 9 | from wiki.wiki_search import WikiSearch 10 | 11 | PREFIX = "wiki_page_search_doc" 12 | INDEX_BUILD_FLAG = "wiki_page_index_in_progress" 13 | 14 | 15 | _redisearch_available = False 16 | try: 17 | from redis.commands.search.query import Query 18 | 19 | _redisearch_available = True 20 | except ImportError: 21 | pass 22 | 23 | 24 | @frappe.whitelist(allow_guest=True) 25 | def get_spaces(): 26 | return frappe.db.get_all("Wiki Space", pluck="route") 27 | 28 | 29 | @frappe.whitelist(allow_guest=True) 30 | def search( 31 | query: str, 32 | path: str | None = None, 33 | space: str | None = None, 34 | ): 35 | if not space and path: 36 | space = get_space_route(path) 37 | 38 | if frappe.db.get_single_value("Wiki Settings", "use_sqlite_for_search"): 39 | return sqlite_search(query, space) 40 | 41 | if use_redis_search(): 42 | return redis_search(query, space) 43 | 44 | return web_search(query, space) 45 | 46 | 47 | def use_redis_search(): 48 | return frappe.db.get_single_value("Wiki Settings", "use_redisearch_for_search") and _redisearch_available 49 | 50 | 51 | def sqlite_search(query, space): 52 | from wiki.wiki.doctype.wiki_page.sqlite_search import search 53 | 54 | return { 55 | "docs": search(query, space), 56 | "search_engine": "sqlite_fts", 57 | } 58 | 59 | 60 | def web_search(query, space): 61 | from frappe.search import web_search 62 | 63 | result = web_search(query, space) 64 | 65 | for d in result: 66 | d.title = d.title_highlights or d.title 67 | d.route = d.path 68 | d.content = d.content_highlights 69 | 70 | del d.title_highlights 71 | del d.content_highlights 72 | del d.path 73 | 74 | return { 75 | "docs": result, 76 | "search_engine": "frappe_web_search", 77 | } 78 | 79 | 80 | def redis_search(query, space): 81 | from wiki.wiki_search import WikiSearch 82 | 83 | search = WikiSearch() 84 | search_query = search.clean_query(query) 85 | query_parts = search_query.split(" ") 86 | 87 | if len(query_parts) == 1 and not query_parts[0].endswith("*"): 88 | search_query = f"{query_parts[0]}*" 89 | if len(query_parts) > 1: 90 | search_query = " ".join([f"%%{q}%%" for q in query_parts]) 91 | 92 | result = search.search( 93 | f"@title|content:({search_query})", 94 | space=space, 95 | start=0, 96 | sort_by="modified desc", 97 | highlight=True, 98 | with_payloads=True, 99 | ) 100 | 101 | docs = [] 102 | for doc in result.docs: 103 | docs.append( 104 | { 105 | "content": doc.content, 106 | "name": doc.id.split(":", 1)[1], 107 | "route": doc.route, 108 | "title": doc.title, 109 | } 110 | ) 111 | 112 | return {"docs": docs, "search_engine": "redisearch"} 113 | 114 | 115 | def get_space_route(path): 116 | for space in frappe.db.get_all("Wiki Space", pluck="route"): 117 | if space in path: 118 | return space 119 | 120 | 121 | def create_index_for_records(records, space): 122 | r = frappe.cache() 123 | for i, d in enumerate(records): 124 | if not hasattr(frappe.local, "request") and len(records) > 10: 125 | update_progress_bar(f"Indexing Wiki Pages - {space}", i, len(records), absolute=True) 126 | 127 | key = r.make_key(f"{PREFIX}{space}:{d.name}").decode() 128 | mapping = { 129 | "title": d.title, 130 | "content": strip_html_tags(d.content), 131 | "route": d.route, 132 | } 133 | super(RedisWrapper, r).hset(key, mapping=mapping) 134 | 135 | 136 | def remove_index_for_records(records, space): 137 | from redis.exceptions import ResponseError 138 | 139 | r = frappe.cache() 140 | for d in records: 141 | try: 142 | key = r.make_key(f"{PREFIX}{space}:{d.name}").decode() 143 | r.ft(space).delete_document(key) 144 | except ResponseError: 145 | pass 146 | 147 | 148 | def update_index(doc): 149 | record = frappe._dict({"name": doc.name, "title": doc.title, "content": doc.content, "route": doc.route}) 150 | space = get_space_route(doc.route) 151 | 152 | create_index_for_records([record], space) 153 | 154 | 155 | def remove_index(doc): 156 | record = frappe._dict( 157 | { 158 | "name": doc.name, 159 | "route": doc.route, 160 | } 161 | ) 162 | space = get_space_route(doc.route) 163 | 164 | remove_index_for_records([record], space) 165 | 166 | 167 | def drop_index(space: str | None = None): 168 | if frappe.db.get_single_value("Wiki Settings", "use_sqlite_for_search"): 169 | from wiki.wiki.doctype.wiki_page.sqlite_search import delete_db 170 | 171 | return delete_db() 172 | 173 | if use_redis_search(): 174 | return WikiSearch().drop_index() 175 | 176 | if not space: 177 | return 178 | 179 | from redis.exceptions import ResponseError 180 | 181 | try: 182 | frappe.cache().ft(space).dropindex(delete_documents=True) 183 | except ResponseError: 184 | pass 185 | 186 | 187 | def build_index_in_background(): 188 | if frappe.cache().get_value(INDEX_BUILD_FLAG): 189 | return 190 | 191 | print(f"Queued rebuilding of search index for {frappe.local.site}") 192 | frappe.enqueue(build_index, queue="long") 193 | 194 | 195 | def build_index(): 196 | frappe.cache().set_value(INDEX_BUILD_FLAG, True) 197 | 198 | if frappe.db.get_single_value("Wiki Settings", "use_sqlite_for_search"): 199 | from wiki.wiki.doctype.wiki_page.sqlite_search import build_index 200 | 201 | return build_index() 202 | 203 | if use_redis_search(): 204 | return WikiSearch().build_index() 205 | 206 | frappe.cache().set_value(INDEX_BUILD_FLAG, False) 207 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% block meta_block %} 11 | {% include "templates/includes/meta_block.html" %} 12 | {% endblock %} 13 | 14 | {% block title %}{{ title | striptags }}{% endblock %} 15 | 16 | {% block favicon %} 17 | 19 | {% endblock %} 20 | 21 | 22 | 23 | {%- block head -%} 24 | {% include "templates/includes/head.html" %} 25 | {%- endblock -%} 26 | 27 | {%- block head_include %} 28 | {{ head_include or "" }} 29 | {% endblock -%} 30 | 31 | {%- block style %} 32 | {% if colocated_css -%} 33 | 40 | {%- endif %} 41 | {%- endblock -%} 42 | 43 | 44 | 54 | 55 | 56 | 59 | 71 | 72 | {% include "public/icons/timeless/icons.svg" %} 73 | {%- block banner -%} 74 | {% include "templates/includes/banner_extension.html" ignore missing %} 75 | 76 | {% if banner_html -%} 77 | {{ banner_html or "" }} 78 | {%- endif %} 79 | {%- endblock -%} 80 | 81 | {% block content %} 82 | {{ content }} 83 | {% endblock %} 84 | 85 | {% block base_scripts %} 86 | 87 | 92 | {{ include_script('frappe-web.bundle.js') }} 93 | {% endblock %} 94 | 95 | {%- for link in web_include_js %} 96 | {{ include_script(link) }} 97 | {%- endfor -%} 98 | 99 | {%- block script %} 100 | {% if colocated_js -%} 101 | 102 | {%- endif %} 103 | {%- endblock %} 104 | 105 | {%- block body_include %}{{ body_include or "" }}{% endblock -%} 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page/templates/comment.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 |
7 |
8 |
9 |

Activity

10 |
11 |
12 |
13 | 14 | {% for comment in comments %} 15 |
16 | 17 |
18 | 19 | 20 | 21 |
22 |
23 |
24 | 25 | 26 | 27 | 28 | {% if comment.owner != 'Administrator' %} 29 | {{frappe.db.get_value("User", comment.owner, ["first_name"], as_dict=True).get("first_name")}} - 30 | {% endif %} 31 | {{ comment.owner }} 32 | commented 33 | 34 | {{ frappe.utils.pretty_date(comment.creation) }} 37 | 38 | 39 | 40 | 41 |
42 | {{comment.content}} 43 |
44 | 45 | 46 |
47 |
48 |
49 | 50 | {% endfor %} 51 | 52 |
53 |
54 |
-------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page/templates/feedback.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page/templates/navbar_items.html: -------------------------------------------------------------------------------- 1 | {% macro render_item(item, submenu=False, parent=False) %} 2 | {% if item.child_items %} 3 | 4 | {% if parent %} 5 | 6 | {%- set dropdown_id = 'id-' + frappe.utils.generate_hash('Dropdown', 12) -%} 7 | 18 | {% else %} 19 | {%- set dropdown_id = 'id-' + frappe.utils.generate_hash('Dropdown', 12) -%} 20 | 31 | {% endif %} 32 | 33 | {% else %} 34 | 35 | {% if parent %} 36 | 42 | {% else %} 43 | 45 | {{ _(item.label) }} 46 | 47 | {% endif %} 48 | 49 | {% endif %} 50 | {% endmacro %} 51 | 52 | 91 | {%- if call_to_action -%} 92 | 93 | {{ call_to_action }} 94 | 95 | {%- endif -%} -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page/templates/navbar_search.html: -------------------------------------------------------------------------------- 1 | {% if navbar_search %} 2 | 18 | {% endif %} -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page/templates/page_settings.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page/templates/revisions.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page/templates/show.html: -------------------------------------------------------------------------------- 1 |
2 | 12 |
13 |

{{ title }}

14 |
15 |
16 | {{frappe.utils.md_to_html(content)}} 17 |
18 | 19 | {% include "wiki/doctype/wiki_page/templates/revisions.html" %} 20 | {% include "wiki/doctype/wiki_page/templates/page_settings.html" %} 21 |
22 |
23 | {% include "wiki/doctype/wiki_page/templates/editor.html" %} 24 |
25 | 42 | 43 |
44 | {%- if show_feedback -%} 45 |
46 | Was this article helpful? 47 | 48 |
49 | {% include "wiki/doctype/wiki_page/templates/feedback.html" %} 50 | {%- endif -%} 51 | 52 | {%- if last_revision -%} 53 |
54 |
55 | {%- endif -%} 56 | 78 |
79 | 92 | 93 | 94 | 100 | 101 | {{ include_script('wiki.bundle.js') }} 102 | 103 | 135 | 136 | {%- if script -%} 137 | 138 | {%- endif -%} -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page/templates/web_sidebar.html: -------------------------------------------------------------------------------- 1 | {% macro render_sidebar_item(item) %} 2 | 11 | {% endmacro %} {% macro render_sidebar_items(sidebar_items) %} {%- if 12 | sidebar_items | len > 0 -%} 13 | 41 | 68 | {%- endif -%} {% endmacro %} {% macro my_account() %} 69 | 74 | {% endmacro %} 75 | 76 |
77 | 78 |
-------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page/templates/wiki_navbar.html: -------------------------------------------------------------------------------- 1 | 59 | 60 | 61 | {% if navbar_search %} 62 | 63 | 83 | 84 | {% endif %} -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page/templates/wiki_page.html: -------------------------------------------------------------------------------- 1 | {% extends "wiki/doctype/wiki_page/templates/wiki_doc.html" %} 2 | {%- block page_sidebar -%} 3 | 4 | {%- endblock -%} 5 | 6 | {%- block head_include %} 7 | {{ super() }} 8 | {{ include_style('wiki.bundle.css') }} 9 | 10 | {% endblock -%} 11 | 12 | {% block page_content %} 13 | {%- if frappe.form_dict.edit or frappe.form_dict.new -%} 14 | {% include "wiki/doctype/wiki_page/templates/edit.html" %} 15 | {%- else -%} 16 | {% include "wiki/doctype/wiki_page/templates/show.html" %} 17 | {%- endif -%} 18 | {% endblock %} -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page/templates/wiki_page_row.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page/test_wiki_page.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Frappe and Contributors 2 | # See license.txt 3 | 4 | import unittest 5 | 6 | import frappe 7 | 8 | from wiki.wiki.doctype.wiki_page.wiki_page import delete_wiki_page, update 9 | 10 | 11 | class TestWikiPage(unittest.TestCase): 12 | def setUp(self): 13 | wiki_page_id = frappe.db.get_value("Wiki Page", {"route": "wiki/page"}, "name") 14 | if wiki_page_id: 15 | frappe.delete_doc("Wiki Page", wiki_page_id) 16 | for name in frappe.db.get_all("Wiki Page Revision", {"wiki_page": "wiki/page"}, pluck="name"): 17 | frappe.delete_doc("Wiki Page Revision", name) 18 | 19 | self.wiki_page = frappe.new_doc("Wiki Page") 20 | self.wiki_page.route = "wiki/page" 21 | self.wiki_page.content = "Hello World" 22 | self.wiki_page.title = "Hello World Title" 23 | 24 | self.wiki_page.save() 25 | 26 | def tearDown(self): 27 | self.wiki_page.delete() 28 | 29 | def test_wiki_page_lifecycle(self): 30 | self.assertEqual( 31 | frappe.db.get_value("Wiki Page", {"route": "wiki/page"}, "name"), self.wiki_page.name 32 | ) 33 | 34 | update( 35 | name=self.wiki_page.name, 36 | content="New Content", 37 | title="New Title", 38 | message="test", 39 | ) 40 | 41 | patches = frappe.get_all( 42 | "Wiki Page Patch", 43 | {"wiki_page": self.wiki_page.name}, 44 | ["message", "new_title", "new_code", "name"], 45 | ) 46 | 47 | self.assertEqual(patches[0].message, "test") 48 | self.assertEqual(patches[0].new_title, "New Title") 49 | self.assertEqual(patches[0].new_code, "New Content") 50 | 51 | patch = frappe.get_doc("Wiki Page Patch", patches[0].name) 52 | patch.status = "Approved" 53 | patch.approved_by = "Administrator" 54 | patch.save() 55 | patch.submit() 56 | 57 | wiki_page = frappe.get_doc("Wiki Page", self.wiki_page.name) 58 | 59 | self.assertEqual(wiki_page.title, "New Title") 60 | self.assertEqual(wiki_page.content, "New Content") 61 | 62 | self.assertEqual( 63 | len( 64 | frappe.db.get_all( 65 | "Wiki Page Revision", 66 | filters={"wiki_page": wiki_page.name}, 67 | ) 68 | ), 69 | 2, 70 | ) 71 | 72 | def test_wiki_page_deletion(self): 73 | delete_wiki_page(f"{self.wiki_page.route}") 74 | self.assertEqual(frappe.db.exists("Wiki Page", self.wiki_page.name), None) 75 | 76 | patches = frappe.get_all("Wiki Page Patch", {"wiki_page": self.wiki_page.name}, pluck="name") 77 | self.assertEqual(patches, []) 78 | 79 | sidebar_items = frappe.get_all("Wiki Group Item", {"wiki_page": self.wiki_page.name}, pluck="name") 80 | self.assertEqual(sidebar_items, []) 81 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page/wiki_page.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, Frappe and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("Wiki Page", { 5 | refresh: function (frm) {}, 6 | }); 7 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page/wiki_page.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_guest_to_view": 1, 4 | "allow_import": 1, 5 | "allow_rename": 1, 6 | "autoname": "hash", 7 | "creation": "2020-09-26 15:53:44.849581", 8 | "doctype": "DocType", 9 | "editable_grid": 1, 10 | "engine": "InnoDB", 11 | "field_order": [ 12 | "content_section", 13 | "title", 14 | "column_break_2", 15 | "route", 16 | "published", 17 | "allow_guest", 18 | "section_break_4", 19 | "content", 20 | "meta_tags_section", 21 | "meta_description", 22 | "meta_image", 23 | "meta_keywords" 24 | ], 25 | "fields": [ 26 | { 27 | "allow_in_quick_entry": 1, 28 | "default": "No Content", 29 | "description": "It is recommended that you start your first heading with h2, as the title will be the h1.", 30 | "fieldname": "content", 31 | "fieldtype": "Markdown Editor", 32 | "ignore_xss_filter": 1, 33 | "in_preview": 1, 34 | "label": "Content", 35 | "reqd": 1 36 | }, 37 | { 38 | "default": "0", 39 | "fieldname": "published", 40 | "fieldtype": "Check", 41 | "label": "Published" 42 | }, 43 | { 44 | "fieldname": "route", 45 | "fieldtype": "Data", 46 | "in_list_view": 1, 47 | "in_preview": 1, 48 | "in_standard_filter": 1, 49 | "label": "Route", 50 | "reqd": 1, 51 | "unique": 1 52 | }, 53 | { 54 | "fieldname": "column_break_2", 55 | "fieldtype": "Column Break" 56 | }, 57 | { 58 | "fieldname": "section_break_4", 59 | "fieldtype": "Section Break" 60 | }, 61 | { 62 | "fieldname": "title", 63 | "fieldtype": "Data", 64 | "label": "Title", 65 | "reqd": 1 66 | }, 67 | { 68 | "fieldname": "content_section", 69 | "fieldtype": "Section Break", 70 | "label": "Title & Content" 71 | }, 72 | { 73 | "default": "1", 74 | "fieldname": "allow_guest", 75 | "fieldtype": "Check", 76 | "label": "Allow Guest" 77 | }, 78 | { 79 | "fieldname": "meta_tags_section", 80 | "fieldtype": "Section Break", 81 | "label": "Meta Tags" 82 | }, 83 | { 84 | "fieldname": "meta_description", 85 | "fieldtype": "Small Text", 86 | "label": "Description" 87 | }, 88 | { 89 | "description": "Ideally should be 1200 x 630 pixels.", 90 | "fieldname": "meta_image", 91 | "fieldtype": "Attach Image", 92 | "label": "Image" 93 | }, 94 | { 95 | "description": "Should be a comma separated list", 96 | "fieldname": "meta_keywords", 97 | "fieldtype": "Small Text", 98 | "label": "Keywords" 99 | } 100 | ], 101 | "has_web_view": 1, 102 | "index_web_pages_for_search": 1, 103 | "is_published_field": "published", 104 | "links": [ 105 | { 106 | "group": "Linked Documents", 107 | "link_doctype": "Wiki Page Revision", 108 | "link_fieldname": "wiki_page" 109 | }, 110 | { 111 | "group": "Linked Documents", 112 | "link_doctype": "Wiki Page Patch", 113 | "link_fieldname": "wiki_page" 114 | } 115 | ], 116 | "modified": "2024-09-04 17:06:27.584176", 117 | "modified_by": "Administrator", 118 | "module": "Wiki", 119 | "name": "Wiki Page", 120 | "naming_rule": "Random", 121 | "owner": "Administrator", 122 | "permissions": [ 123 | { 124 | "create": 1, 125 | "delete": 1, 126 | "email": 1, 127 | "export": 1, 128 | "print": 1, 129 | "read": 1, 130 | "report": 1, 131 | "role": "System Manager", 132 | "share": 1, 133 | "write": 1 134 | }, 135 | { 136 | "create": 1, 137 | "delete": 1, 138 | "email": 1, 139 | "export": 1, 140 | "print": 1, 141 | "read": 1, 142 | "report": 1, 143 | "role": "Wiki Approver", 144 | "share": 1, 145 | "write": 1 146 | } 147 | ], 148 | "show_title_field_in_link": 1, 149 | "sort_field": "modified", 150 | "sort_order": "DESC", 151 | "states": [], 152 | "title_field": "title", 153 | "track_changes": 1, 154 | "website_search_field": "content" 155 | } -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page/wiki_renderer.py: -------------------------------------------------------------------------------- 1 | import re 2 | from urllib.parse import quote 3 | 4 | import frappe 5 | from frappe.website.page_renderers.document_page import DocumentPage 6 | from frappe.website.utils import build_response 7 | 8 | from wiki.wiki.doctype.wiki_page.wiki_page import get_sidebar_for_page 9 | 10 | reg = re.compile("") 11 | 12 | 13 | class WikiPageRenderer(DocumentPage): 14 | def can_render(self): 15 | doctype = "Wiki Page" 16 | try: 17 | self.docname = frappe.db.get_value(doctype, {"route": self.path, "published": 1}, "name") 18 | if self.docname: 19 | self.doctype = doctype 20 | return True 21 | except Exception as e: 22 | if not frappe.db.is_missing_column(e): 23 | raise e 24 | 25 | if wiki_space_name := frappe.db.get_value("Wiki Space", {"route": self.path}): 26 | wiki_space = frappe.get_cached_doc("Wiki Space", wiki_space_name) 27 | topmost_wiki_route = frappe.db.get_value( 28 | "Wiki Page", wiki_space.wiki_sidebars[0].wiki_page, "route" 29 | ) 30 | frappe.redirect(f"/{quote(topmost_wiki_route)}") 31 | 32 | def render(self): 33 | html = self.get_html() 34 | html = self.add_csrf_token(html) 35 | html = self.add_sidebar(html) 36 | return build_response(self.path, html, self.http_status_code or 200, self.headers) 37 | 38 | def add_sidebar(self, html): 39 | return reg.sub(get_sidebar_for_page(self.docname), html) 40 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page_patch/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/wiki/af7de232b6a7e25e6e8984abcd5c4f985a17587e/wiki/wiki/doctype/wiki_page_patch/__init__.py -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page_patch/test_wiki_page_patch.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, Frappe and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | import unittest 6 | 7 | 8 | class TestWikiPagePatch(unittest.TestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page_patch/wiki_page_patch.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, Frappe and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("Wiki Page Patch", { 5 | refresh: function (frm) { 6 | $('[data-fieldname="orignal_code"] pre') 7 | .parent(".like-disabled-input") 8 | .html(frm.doc.orignal_code); 9 | $('[data-fieldname="new_code"] pre') 10 | .parent(".like-disabled-input") 11 | .html(frm.doc.new_code); 12 | 13 | if (!frm.doc.new && !frm.doc.__unsaved) 14 | frappe.call({ 15 | method: "wiki.wiki.doctype.wiki_page.wiki_page.preview", 16 | args: { 17 | original_code: frm.doc.orignal_code, 18 | new_code: frm.doc.new_code, 19 | name: frm.doc.wiki_page, 20 | }, 21 | callback: (r) => { 22 | if (r.message) { 23 | $(".wiki-diff").append(r.message); 24 | $(".wiki-diff").append( 25 | ``, 35 | ); 36 | } 37 | }, 38 | }); 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page_patch/wiki_page_patch.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "creation": "2021-05-13 10:08:45.648142", 4 | "doctype": "DocType", 5 | "editable_grid": 1, 6 | "engine": "InnoDB", 7 | "field_order": [ 8 | "wiki_page", 9 | "new_title", 10 | "new_sidebar_group", 11 | "message", 12 | "column_break_3", 13 | "raised_by", 14 | "status", 15 | "approved_by", 16 | "new", 17 | "compare_changes_section", 18 | "compare", 19 | "code", 20 | "orignal_code", 21 | "new_code", 22 | "new_preview_section_section", 23 | "amended_from" 24 | ], 25 | "fields": [ 26 | { 27 | "fieldname": "wiki_page", 28 | "fieldtype": "Link", 29 | "label": "Wiki Page", 30 | "options": "Wiki Page", 31 | "read_only": 1 32 | }, 33 | { 34 | "fieldname": "raised_by", 35 | "fieldtype": "Link", 36 | "label": "Raised By", 37 | "options": "User", 38 | "read_only": 1 39 | }, 40 | { 41 | "depends_on": "eval:doc.status=='Approved';", 42 | "fieldname": "approved_by", 43 | "fieldtype": "Link", 44 | "label": "Approved By", 45 | "mandatory_depends_on": "eval:doc.status=='Approved';", 46 | "options": "User" 47 | }, 48 | { 49 | "default": "Under Review", 50 | "fieldname": "status", 51 | "fieldtype": "Select", 52 | "label": "Status", 53 | "options": "Under Review\nChanges Requested\nRejected\nApproved\nDraft" 54 | }, 55 | { 56 | "depends_on": "eval: !doc.new; ", 57 | "fieldname": "compare_changes_section", 58 | "fieldtype": "Section Break", 59 | "label": "Compare Changes" 60 | }, 61 | { 62 | "fieldname": "compare", 63 | "fieldtype": "HTML", 64 | "label": "Compare", 65 | "options": "
\n\t
\n\t
\n
" 66 | }, 67 | { 68 | "fieldname": "code", 69 | "fieldtype": "Section Break", 70 | "label": "Code" 71 | }, 72 | { 73 | "fieldname": "new_code", 74 | "fieldtype": "Code", 75 | "label": "New HTML" 76 | }, 77 | { 78 | "fieldname": "new_preview_section_section", 79 | "fieldtype": "Section Break" 80 | }, 81 | { 82 | "fieldname": "amended_from", 83 | "fieldtype": "Link", 84 | "label": "Amended From", 85 | "no_copy": 1, 86 | "options": "Wiki Page Patch", 87 | "print_hide": 1, 88 | "read_only": 1 89 | }, 90 | { 91 | "fieldname": "message", 92 | "fieldtype": "Text", 93 | "label": "Message" 94 | }, 95 | { 96 | "fieldname": "column_break_3", 97 | "fieldtype": "Column Break" 98 | }, 99 | { 100 | "default": "0", 101 | "fieldname": "new", 102 | "fieldtype": "Check", 103 | "in_list_view": 1, 104 | "label": "New", 105 | "read_only": 1 106 | }, 107 | { 108 | "fieldname": "new_title", 109 | "fieldtype": "Data", 110 | "in_list_view": 1, 111 | "label": "New Title", 112 | "read_only": 1 113 | }, 114 | { 115 | "depends_on": "eval: !doc.new;", 116 | "fieldname": "orignal_code", 117 | "fieldtype": "Code", 118 | "label": "Original HTML" 119 | }, 120 | { 121 | "fieldname": "new_sidebar_group", 122 | "fieldtype": "Data", 123 | "label": "New Sidebar Group", 124 | "read_only": 1 125 | } 126 | ], 127 | "index_web_pages_for_search": 1, 128 | "is_submittable": 1, 129 | "links": [], 130 | "modified": "2023-11-07 10:27:24.234747", 131 | "modified_by": "Administrator", 132 | "module": "Wiki", 133 | "name": "Wiki Page Patch", 134 | "owner": "Administrator", 135 | "permissions": [ 136 | { 137 | "amend": 1, 138 | "cancel": 1, 139 | "create": 1, 140 | "delete": 1, 141 | "email": 1, 142 | "export": 1, 143 | "print": 1, 144 | "read": 1, 145 | "report": 1, 146 | "role": "System Manager", 147 | "select": 1, 148 | "share": 1, 149 | "submit": 1, 150 | "write": 1 151 | }, 152 | { 153 | "create": 1, 154 | "delete": 1, 155 | "email": 1, 156 | "export": 1, 157 | "if_owner": 1, 158 | "print": 1, 159 | "read": 1, 160 | "report": 1, 161 | "role": "All", 162 | "share": 1, 163 | "write": 1 164 | }, 165 | { 166 | "amend": 1, 167 | "cancel": 1, 168 | "create": 1, 169 | "delete": 1, 170 | "email": 1, 171 | "export": 1, 172 | "print": 1, 173 | "read": 1, 174 | "report": 1, 175 | "role": "Wiki Approver", 176 | "select": 1, 177 | "share": 1, 178 | "submit": 1, 179 | "write": 1 180 | }, 181 | { 182 | "read": 1, 183 | "role": "Guest" 184 | } 185 | ], 186 | "sort_field": "modified", 187 | "sort_order": "DESC", 188 | "states": [], 189 | "title_field": "message", 190 | "track_changes": 1, 191 | "track_seen": 1, 192 | "track_views": 1 193 | } -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page_patch/wiki_page_patch.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, Frappe and contributors 2 | # For license information, please see license.txt 3 | 4 | 5 | import json 6 | import re 7 | 8 | import frappe 9 | from frappe import _ 10 | from frappe.desk.form.utils import add_comment 11 | from frappe.model.document import Document 12 | from frappe.website.utils import cleanup_page_name 13 | 14 | from wiki.utils import apply_changes, apply_markdown_diff, highlight_changes 15 | 16 | 17 | class WikiPagePatch(Document): 18 | def before_save(self): 19 | if not self.new: 20 | self.orignal_code = frappe.db.get_value("Wiki Page", self.wiki_page, "content") 21 | 22 | def after_insert(self): 23 | add_comment_to_patch(self.name, self.message) 24 | frappe.db.commit() 25 | 26 | def on_submit(self): 27 | if self.status == "Rejected": 28 | return 29 | 30 | if self.status != "Approved": 31 | frappe.throw(_("Please approve/ reject the request before submitting")) 32 | 33 | self.wiki_page_doc = frappe.get_doc("Wiki Page", self.wiki_page) 34 | 35 | self.clear_sidebar_cache() 36 | 37 | if self.new: 38 | self.create_new_wiki_page() 39 | self.update_sidebars() 40 | else: 41 | self.update_old_page() 42 | 43 | def clear_sidebar_cache(self): 44 | if self.new or self.new_title != self.wiki_page_doc.title: 45 | for key in frappe.cache().hgetall("wiki_sidebar").keys(): 46 | frappe.cache().hdel("wiki_sidebar", key) 47 | 48 | def create_new_wiki_page(self): 49 | self.new_wiki_page = frappe.new_doc("Wiki Page") 50 | 51 | wiki_page_dict = { 52 | "title": self.new_title, 53 | "content": self.new_code or "content", 54 | "route": f"{self.wiki_page_doc.get_space_route()}/{cleanup_page_name(self.new_title)}", 55 | "published": 1, 56 | "allow_guest": self.wiki_page_doc.allow_guest, 57 | } 58 | 59 | self.new_wiki_page.update(wiki_page_dict) 60 | self.new_wiki_page.save() 61 | 62 | def update_old_page(self): 63 | original_md = self.wiki_page_doc.content or "" 64 | modified_md = self.new_code or "" 65 | 66 | merge_old_content = apply_markdown_diff(self.orignal_code, modified_md)[1] 67 | merge_new_content = apply_changes(original_md, merge_old_content) 68 | new_modified_md = apply_markdown_diff(original_md, merge_new_content)[0] 69 | 70 | self.wiki_page_doc.update_page(self.new_title, new_modified_md, self.message, self.raised_by) 71 | 72 | def update_sidebars(self): 73 | if not hasattr(self, "new_sidebar_items") or not self.new_sidebar_items: 74 | self.insert_on_sidebar(self.new_sidebar_group, self.new_wiki_page.name) 75 | return 76 | 77 | sidebars = json.loads(self.new_sidebar_items) 78 | 79 | sidebar_items = sidebars.items() 80 | if sidebar_items: 81 | idx = 0 82 | for sidebar, items in sidebar_items: 83 | for item in items: 84 | idx += 1 85 | if item["name"] == "new-wiki-page": 86 | item["name"] = self.new_wiki_page.name 87 | self.insert_on_sidebar(list(sidebars)[-1], self.new_wiki_page.name) 88 | 89 | frappe.db.set_value( 90 | "Wiki Group Item", 91 | {"wiki_page": str(item["name"])}, 92 | {"parent_label": sidebar, "idx": idx}, 93 | ) 94 | 95 | def insert_on_sidebar(self, parent_label: str, wiki_page: str): 96 | wiki_space_name = frappe.get_value("Wiki Space", {"route": self.wiki_page_doc.get_space_route()}) 97 | 98 | wiki_space = frappe.get_doc("Wiki Space", wiki_space_name) 99 | wiki_space.append( 100 | "wiki_sidebars", 101 | { 102 | "wiki_page": wiki_page, 103 | "parent_label": parent_label, 104 | }, 105 | ) 106 | wiki_space.save() 107 | 108 | 109 | @frappe.whitelist() 110 | def add_comment_to_patch(reference_name, content): 111 | email = frappe.session.user 112 | name = frappe.db.get_value("User", frappe.session.user, ["first_name"], as_dict=True).get("first_name") 113 | comment = add_comment("Wiki Page Patch", reference_name, content, email, name) 114 | comment.timepassed = frappe.utils.pretty_date(comment.creation) 115 | return comment 116 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page_revision/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/wiki/af7de232b6a7e25e6e8984abcd5c4f985a17587e/wiki/wiki/doctype/wiki_page_revision/__init__.py -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page_revision/patches/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/wiki/af7de232b6a7e25e6e8984abcd5c4f985a17587e/wiki/wiki/doctype/wiki_page_revision/patches/__init__.py -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page_revision/patches/add_usernames.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | 3 | 4 | def execute(): 5 | frappe.reload_doctype("Wiki Page Revision") 6 | 7 | revision = frappe.qb.DocType("Wiki Page Revision") 8 | user = frappe.qb.DocType("User") 9 | 10 | ( 11 | frappe.qb.update(revision) 12 | .join(user) 13 | .on(user.name == revision.raised_by) 14 | .set(revision.raised_by_username, user.username) 15 | ).run() 16 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page_revision/test_wiki_page_revision.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Frappe and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | import unittest 6 | 7 | 8 | class TestWikiPageRevision(unittest.TestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page_revision/wiki_page_revision.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, Frappe and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("Wiki Page Revision", { 5 | refresh: function (frm) { 6 | $('[data-fieldname="content"] pre') 7 | .parent(".like-disabled-input") 8 | .html(frm.doc.content); 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page_revision/wiki_page_revision.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "creation": "2020-09-26 18:04:11.581644", 4 | "doctype": "DocType", 5 | "editable_grid": 1, 6 | "engine": "InnoDB", 7 | "field_order": [ 8 | "content", 9 | "section_break_6", 10 | "raised_by", 11 | "raised_by_username", 12 | "wiki_pages", 13 | "column_break_vovw", 14 | "message" 15 | ], 16 | "fields": [ 17 | { 18 | "fieldname": "message", 19 | "fieldtype": "Text", 20 | "label": "Message" 21 | }, 22 | { 23 | "fieldname": "content", 24 | "fieldtype": "Code", 25 | "label": "Content", 26 | "read_only": 1 27 | }, 28 | { 29 | "fieldname": "raised_by", 30 | "fieldtype": "Link", 31 | "label": "Raised By", 32 | "options": "User", 33 | "read_only": 1 34 | }, 35 | { 36 | "fetch_from": "raised_by.username", 37 | "fieldname": "raised_by_username", 38 | "fieldtype": "Data", 39 | "label": "Raised By Username", 40 | "read_only": 1 41 | }, 42 | { 43 | "fieldname": "section_break_6", 44 | "fieldtype": "Section Break" 45 | }, 46 | { 47 | "fieldname": "wiki_pages", 48 | "fieldtype": "Table", 49 | "label": "Wiki Pages", 50 | "options": "Wiki Page Revision Item" 51 | }, 52 | { 53 | "fieldname": "column_break_vovw", 54 | "fieldtype": "Column Break" 55 | } 56 | ], 57 | "index_web_pages_for_search": 1, 58 | "links": [], 59 | "modified": "2023-07-23 00:22:44.919982", 60 | "modified_by": "Administrator", 61 | "module": "Wiki", 62 | "name": "Wiki Page Revision", 63 | "owner": "Administrator", 64 | "permissions": [ 65 | { 66 | "create": 1, 67 | "delete": 1, 68 | "email": 1, 69 | "export": 1, 70 | "print": 1, 71 | "read": 1, 72 | "report": 1, 73 | "role": "System Manager", 74 | "share": 1, 75 | "write": 1 76 | }, 77 | { 78 | "create": 1, 79 | "delete": 1, 80 | "email": 1, 81 | "export": 1, 82 | "print": 1, 83 | "read": 1, 84 | "report": 1, 85 | "role": "Wiki Approver", 86 | "share": 1, 87 | "write": 1 88 | } 89 | ], 90 | "quick_entry": 1, 91 | "sort_field": "modified", 92 | "sort_order": "DESC", 93 | "states": [], 94 | "title_field": "message", 95 | "track_changes": 1 96 | } -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page_revision/wiki_page_revision.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Frappe and contributors 2 | # For license information, please see license.txt 3 | 4 | 5 | import frappe 6 | from frappe.model.document import Document 7 | from frappe.utils import md_to_html, pretty_date 8 | 9 | 10 | class WikiPageRevision(Document): 11 | pass 12 | 13 | 14 | @frappe.whitelist(allow_guest=True) 15 | def get_revisions(wiki_page_name): 16 | revisions = frappe.db.get_all( 17 | "Wiki Page Revision", 18 | {"wiki_page": wiki_page_name}, 19 | ["content", "creation", "owner", "raised_by", "raised_by_username"], 20 | ) 21 | 22 | for revision in revisions: 23 | revision.revision_time = pretty_date(revision.creation) 24 | revision.author = revision.raised_by_username or revision.raised_by or revision.owner 25 | revision.content = md_to_html(revision.content) 26 | del revision.raised_by_username 27 | del revision.raised_by 28 | del revision.creation 29 | del revision.owner 30 | 31 | return revisions 32 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page_revision_item/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/wiki/af7de232b6a7e25e6e8984abcd5c4f985a17587e/wiki/wiki/doctype/wiki_page_revision_item/__init__.py -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page_revision_item/wiki_page_revision_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "autoname": "hash", 5 | "creation": "2022-02-07 19:42:12.146504", 6 | "doctype": "DocType", 7 | "editable_grid": 1, 8 | "engine": "InnoDB", 9 | "field_order": [ 10 | "wiki_page" 11 | ], 12 | "fields": [ 13 | { 14 | "fieldname": "wiki_page", 15 | "fieldtype": "Link", 16 | "label": "Wiki Page", 17 | "options": "Wiki Page" 18 | } 19 | ], 20 | "index_web_pages_for_search": 1, 21 | "istable": 1, 22 | "links": [], 23 | "modified": "2022-02-07 19:42:12.146504", 24 | "modified_by": "Administrator", 25 | "module": "Wiki", 26 | "name": "Wiki Page Revision Item", 27 | "naming_rule": "Random", 28 | "owner": "Administrator", 29 | "permissions": [], 30 | "sort_field": "modified", 31 | "sort_order": "DESC", 32 | "states": [] 33 | } -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_page_revision_item/wiki_page_revision_item.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class WikiPageRevisionItem(Document): 9 | pass 10 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/wiki/af7de232b6a7e25e6e8984abcd5c4f985a17587e/wiki/wiki/doctype/wiki_settings/__init__.py -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_settings/patches/wiki_navbar_item_migration.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | import frappe 6 | 7 | 8 | def execute(): 9 | navbar_items = frappe.get_single("Website Settings").top_bar_items 10 | 11 | for navbar_item in navbar_items: 12 | wiki_nav_item = frappe.new_doc("Top Bar Item") 13 | wiki_nav_item_dict = { 14 | "label": navbar_item.label, 15 | "parent_label": navbar_item.parent_label, 16 | "url": navbar_item.url, 17 | "parent": "Wiki Settings", 18 | "parenttype": "Wiki Settings", 19 | "parentfield": "navbar", 20 | "idx": navbar_item.idx, 21 | } 22 | wiki_nav_item.update(wiki_nav_item_dict) 23 | wiki_nav_item.save() 24 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_settings/test_wiki_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Frappe and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | import unittest 6 | 7 | 8 | class TestWikiSettings(unittest.TestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_settings/wiki_settings.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, Frappe and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("Wiki Settings", { 5 | refresh: function (frm) { 6 | frm.add_web_link("/wiki", __("See on website")); 7 | 8 | frm.add_custom_button("Clear Wiki Page Cache", () => { 9 | frm.call({ 10 | method: "clear_wiki_page_cache", 11 | callback: (r) => { 12 | if (r.message) { 13 | frappe.show_alert({ 14 | message: "Wiki Page Cache Cleared", 15 | indicator: "blue", 16 | }); 17 | } 18 | }, 19 | }); 20 | }); 21 | }, 22 | 23 | onload: function (frm) { 24 | frm.set_query("default_wiki_space", function () { 25 | return { 26 | query: "wiki.wiki.doctype.wiki_settings.wiki_settings.get_all_spaces", 27 | }; 28 | }); 29 | }, 30 | 31 | onload_post_render: function (frm) { 32 | frm.trigger("set_parent_label_options"); 33 | }, 34 | 35 | set_parent_label_options: function (frm) { 36 | frm.fields_dict.navbar.grid.update_docfield_property( 37 | "parent_label", 38 | "options", 39 | frm.events.get_parent_options(frm, "navbar"), 40 | ); 41 | }, 42 | 43 | get_parent_options: function (frm, table_field) { 44 | var items = frm.doc[table_field] || []; 45 | var main_items = [""]; 46 | for (var i in items) { 47 | var d = items[i]; 48 | if (!d.url && d.label) { 49 | main_items.push(d.label); 50 | } 51 | } 52 | return main_items.join("\n"); 53 | }, 54 | }); 55 | 56 | frappe.ui.form.on("Top Bar Item", { 57 | navbar_delete(frm) { 58 | frm.events.set_parent_label_options(frm); 59 | }, 60 | 61 | navbar_add(frm, cdt, cdn) { 62 | frm.events.set_parent_label_options(frm); 63 | }, 64 | 65 | parent_label: function (frm, doctype, name) { 66 | frm.events.set_parent_label_options(frm); 67 | }, 68 | 69 | url: function (frm, doctype, name) { 70 | frm.events.set_parent_label_options(frm); 71 | }, 72 | 73 | label: function (frm, doctype, name) { 74 | frm.events.set_parent_label_options(frm); 75 | }, 76 | }); 77 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_settings/wiki_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "creation": "2020-09-26 16:02:57.409719", 4 | "doctype": "DocType", 5 | "editable_grid": 1, 6 | "engine": "InnoDB", 7 | "field_order": [ 8 | "general_tab", 9 | "theme_section", 10 | "logo", 11 | "dark_mode_logo", 12 | "section_break_vefv", 13 | "default_wiki_space", 14 | "table_of_contents_section", 15 | "collapse_sidebar_groups", 16 | "enable_table_of_contents", 17 | "disable_guest_access", 18 | "navbar_tab", 19 | "navbar_column", 20 | "navbar", 21 | "app_switcher_list", 22 | "section_break_skhp", 23 | "search_column", 24 | "use_sqlite_for_search", 25 | "add_search_bar", 26 | "column_break_yaoi", 27 | "use_redisearch_for_search", 28 | "feedback_tab", 29 | "feedback_section", 30 | "enable_feedback", 31 | "feedback_submission_limit", 32 | "ask_for_contact_details", 33 | "section_break_mmtu", 34 | "javascript" 35 | ], 36 | "fields": [ 37 | { 38 | "fieldname": "logo", 39 | "fieldtype": "Attach Image", 40 | "label": "Light Mode Logo" 41 | }, 42 | { 43 | "fieldname": "javascript", 44 | "fieldtype": "Code", 45 | "label": "Javascript", 46 | "options": "Javascript" 47 | }, 48 | { 49 | "default": "1", 50 | "fieldname": "add_search_bar", 51 | "fieldtype": "Check", 52 | "label": "Add Search Bar" 53 | }, 54 | { 55 | "fieldname": "dark_mode_logo", 56 | "fieldtype": "Attach Image", 57 | "label": "Dark Mode Logo" 58 | }, 59 | { 60 | "fieldname": "navbar_tab", 61 | "fieldtype": "Tab Break", 62 | "label": "Navbar" 63 | }, 64 | { 65 | "fieldname": "navbar_column", 66 | "fieldtype": "Column Break", 67 | "label": "Navbar" 68 | }, 69 | { 70 | "fieldname": "navbar", 71 | "fieldtype": "Table", 72 | "label": "Navbar Items", 73 | "options": "Top Bar Item" 74 | }, 75 | { 76 | "fieldname": "section_break_mmtu", 77 | "fieldtype": "Section Break" 78 | }, 79 | { 80 | "fieldname": "general_tab", 81 | "fieldtype": "Tab Break", 82 | "label": "General" 83 | }, 84 | { 85 | "default": "Wiki", 86 | "fieldname": "default_wiki_space", 87 | "fieldtype": "Autocomplete", 88 | "label": "Default Wiki Space" 89 | }, 90 | { 91 | "fieldname": "theme_section", 92 | "fieldtype": "Section Break", 93 | "label": "Theme" 94 | }, 95 | { 96 | "fieldname": "section_break_vefv", 97 | "fieldtype": "Section Break", 98 | "label": "Wiki Space" 99 | }, 100 | { 101 | "fieldname": "table_of_contents_section", 102 | "fieldtype": "Section Break", 103 | "label": "Wiki Page Configurations" 104 | }, 105 | { 106 | "default": "1", 107 | "fieldname": "enable_table_of_contents", 108 | "fieldtype": "Check", 109 | "label": "Enable Table of Contents" 110 | }, 111 | { 112 | "fieldname": "section_break_skhp", 113 | "fieldtype": "Section Break", 114 | "label": "Search" 115 | }, 116 | { 117 | "fieldname": "search_column", 118 | "fieldtype": "Column Break" 119 | }, 120 | { 121 | "default": "0", 122 | "depends_on": "eval:doc.add_search_bar;", 123 | "description": "Use Redisearch instead of Frappe Web Search for faster, accurate results", 124 | "fieldname": "use_redisearch_for_search", 125 | "fieldtype": "Check", 126 | "label": "Use Redisearch for Search" 127 | }, 128 | { 129 | "default": "0", 130 | "fieldname": "collapse_sidebar_groups", 131 | "fieldtype": "Check", 132 | "label": "Collapse Sidebar Groups" 133 | }, 134 | { 135 | "fieldname": "feedback_section", 136 | "fieldtype": "Section Break", 137 | "label": "Feedback" 138 | }, 139 | { 140 | "default": "0", 141 | "description": "It will show a feedback widget on every Wiki Page when enabled. It will collect the rating, email and related info about the Wiki Page.", 142 | "fieldname": "enable_feedback", 143 | "fieldtype": "Check", 144 | "label": "Enable Feedback" 145 | }, 146 | { 147 | "default": "0", 148 | "fieldname": "ask_for_contact_details", 149 | "fieldtype": "Check", 150 | "label": "Ask for contact details" 151 | }, 152 | { 153 | "fieldname": "feedback_tab", 154 | "fieldtype": "Tab Break", 155 | "label": "Feedback" 156 | }, 157 | { 158 | "default": "3", 159 | "depends_on": "enable_feedback", 160 | "description": "Hourly rate limit on submitting feedbacks", 161 | "fieldname": "feedback_submission_limit", 162 | "fieldtype": "Int", 163 | "label": "Feedback Submission Limit", 164 | "non_negative": 1 165 | }, 166 | { 167 | "fieldname": "app_switcher_list", 168 | "fieldtype": "Table", 169 | "label": "App Switcher List", 170 | "options": "Wiki App Switcher List Table" 171 | }, 172 | { 173 | "default": "0", 174 | "fieldname": "disable_guest_access", 175 | "fieldtype": "Check", 176 | "label": "Disable guest access" 177 | }, 178 | { 179 | "default": "0", 180 | "description": "Uses SQLite FTS for searching. This takes precedence over Redis search if enabled.\n
\nSQLite based search is experimental.", 181 | "fieldname": "use_sqlite_for_search", 182 | "fieldtype": "Check", 183 | "label": "Use SQLite for Search" 184 | }, 185 | { 186 | "fieldname": "column_break_yaoi", 187 | "fieldtype": "Column Break" 188 | } 189 | ], 190 | "grid_page_length": 50, 191 | "index_web_pages_for_search": 1, 192 | "issingle": 1, 193 | "links": [], 194 | "modified": "2025-05-21 09:32:30.142174", 195 | "modified_by": "Administrator", 196 | "module": "Wiki", 197 | "name": "Wiki Settings", 198 | "owner": "Administrator", 199 | "permissions": [ 200 | { 201 | "create": 1, 202 | "delete": 1, 203 | "email": 1, 204 | "print": 1, 205 | "read": 1, 206 | "role": "System Manager", 207 | "share": 1, 208 | "write": 1 209 | }, 210 | { 211 | "create": 1, 212 | "delete": 1, 213 | "email": 1, 214 | "print": 1, 215 | "read": 1, 216 | "role": "Wiki Approver", 217 | "share": 1, 218 | "write": 1 219 | } 220 | ], 221 | "quick_entry": 1, 222 | "row_format": "Dynamic", 223 | "sort_field": "modified", 224 | "sort_order": "DESC", 225 | "states": [], 226 | "track_changes": 1 227 | } 228 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_settings/wiki_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Frappe and contributors 2 | # For license information, please see license.txt 3 | 4 | import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class WikiSettings(Document): 9 | def on_update(self): 10 | for key in frappe.cache().hgetall("wiki_sidebar").keys(): 11 | frappe.cache().hdel("wiki_sidebar", key) 12 | 13 | clear_wiki_page_cache() 14 | 15 | 16 | @frappe.whitelist() 17 | def get_all_spaces(): 18 | return frappe.get_all("Wiki Space", pluck="route") 19 | 20 | 21 | @frappe.whitelist() 22 | def clear_wiki_page_cache(): 23 | for route in frappe.get_all("Wiki Page", pluck="route"): 24 | frappe.cache().hdel("website_page", route) 25 | 26 | return True 27 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_sidebar/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/wiki/af7de232b6a7e25e6e8984abcd5c4f985a17587e/wiki/wiki/doctype/wiki_sidebar/__init__.py -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_sidebar/test_wiki_sidebar.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, Frappe and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | import unittest 6 | 7 | 8 | class TestWikiSidebar(unittest.TestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_sidebar/wiki_sidebar.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, Frappe and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("Wiki Sidebar", { 5 | refresh: function (frm) { 6 | frm.set_query("type", "sidebar_items", function () { 7 | return { 8 | filters: { 9 | name: ["in", ["Wiki Page", "Wiki Sidebar"]], 10 | }, 11 | }; 12 | }); 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_sidebar/wiki_sidebar.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "autoname": "hash", 5 | "creation": "2021-05-07 01:38:50.629713", 6 | "doctype": "DocType", 7 | "document_type": "Document", 8 | "editable_grid": 1, 9 | "engine": "InnoDB", 10 | "field_order": [ 11 | "wiki_page", 12 | "parent_label" 13 | ], 14 | "fields": [ 15 | { 16 | "fieldname": "parent_label", 17 | "fieldtype": "Data", 18 | "in_list_view": 1, 19 | "label": "Parent Label", 20 | "reqd": 1 21 | }, 22 | { 23 | "fieldname": "wiki_page", 24 | "fieldtype": "Link", 25 | "in_list_view": 1, 26 | "label": "Wiki Page", 27 | "options": "Wiki Page", 28 | "reqd": 1, 29 | "unique": 1 30 | } 31 | ], 32 | "istable": 1, 33 | "links": [], 34 | "modified": "2023-03-11 09:35:52.824480", 35 | "modified_by": "Administrator", 36 | "module": "Wiki", 37 | "name": "Wiki Sidebar", 38 | "naming_rule": "Random", 39 | "nsm_parent_field": "parent_wiki_sidebar", 40 | "owner": "Administrator", 41 | "permissions": [], 42 | "sort_field": "modified", 43 | "sort_order": "DESC", 44 | "states": [], 45 | "track_changes": 1 46 | } -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_sidebar/wiki_sidebar.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, Frappe and contributors 2 | # For license information, please see license.txt 3 | 4 | 5 | # import frappe 6 | from frappe.model.document import Document 7 | 8 | 9 | class WikiSidebar(Document): 10 | pass 11 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_space/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/wiki/af7de232b6a7e25e6e8984abcd5c4f985a17587e/wiki/wiki/doctype/wiki_space/__init__.py -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_space/patches/wiki_navbar_app_switcher_migration.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | 3 | 4 | def execute(): 5 | wiki_spaces = frappe.db.get_all("Wiki Space", fields=["*"]) 6 | for space in wiki_spaces: 7 | if space["space_name"] is None: 8 | frappe.db.set_value( 9 | "Wiki Space", space["name"], "space_name", space["route"].replace("/", " ").capitalize() 10 | ) 11 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_space/patches/wiki_sidebar_migration.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | from collections import OrderedDict 6 | 7 | import frappe 8 | 9 | 10 | def execute(): 11 | wiki_settings = frappe.get_single("Wiki Settings") 12 | wiki_search_scope_tuple = frappe.db.sql( 13 | "SELECT `value` FROM `tabSingles` where `doctype` = 'Wiki Settings' AND `field` = 'wiki_search_scope'" 14 | ) 15 | 16 | if wiki_search_scope_tuple: 17 | # get sidebar from when wiki stored sidebars in `Wiki Settings` and move to a Wiki Space 18 | wiki_search_scope = wiki_search_scope_tuple[0][0] 19 | 20 | frappe.reload_doctype("Wiki Space") 21 | space = frappe.new_doc("Wiki Space") 22 | space.route = wiki_search_scope 23 | 24 | for sidebar_item in frappe.get_all( 25 | "Wiki Group Item", fields=["name", "wiki_page", "parent_label"], order_by="idx asc" 26 | ): 27 | space.append( 28 | "wiki_sidebars", 29 | { 30 | "wiki_page": sidebar_item.wiki_page, 31 | "parent_label": sidebar_item.parent_label, 32 | }, 33 | ) 34 | frappe.db.delete("Wiki Group Item", sidebar_item.name) 35 | space.insert() 36 | 37 | frappe.reload_doctype("Wiki Settings") 38 | wiki_settings.default_wiki_space = wiki_search_scope 39 | wiki_settings.save() 40 | 41 | elif hasattr(wiki_settings, "sidebar"): 42 | # get sidebar from legacy version of wiki 43 | if not (all_sidebars := frappe.db.get_all("Wiki Sidebar", pluck="name", order_by="creation asc")): 44 | return 45 | 46 | # find all root sidebars 47 | sidebars_with_parents = frappe.db.get_all( 48 | "Wiki Sidebar Item", 49 | filters=[["type", "=", "Wiki Sidebar"]], 50 | pluck="item", 51 | order_by="creation asc", 52 | ) 53 | 54 | topmosts = set(all_sidebars) - set(sidebars_with_parents) 55 | 56 | frappe.reload_doctype("Wiki Sidebar") 57 | 58 | for topmost in topmosts: 59 | frappe.reload_doctype("Wiki Space") 60 | space = frappe.new_doc("Wiki Space") 61 | space.route = topmost 62 | 63 | sidebar_items = get_children(frappe.get_doc("Wiki Sidebar", topmost)) 64 | sidebars = get_sidebar_for_patch(sidebar_items, topmost) 65 | 66 | # store sidebars in wiki settings 67 | sidebar_items = sidebars.items() 68 | for sidebar, items in sidebar_items: 69 | for item in items: 70 | if item.type == "Wiki Page" and frappe.db.exists("Wiki Page", item.item): 71 | wiki_sidebar_dict = { 72 | "wiki_page": item.item, 73 | "parent_label": item.group_name, 74 | } 75 | space.append("wiki_sidebars", wiki_sidebar_dict) 76 | 77 | # delete old sidebar groups 78 | frappe.db.delete("Wiki Sidebar", sidebar) 79 | 80 | if space.wiki_sidebars: 81 | space.insert() 82 | wiki_settings.default_wiki_space = topmost 83 | 84 | wiki_settings.save() 85 | 86 | 87 | def find_topmost(me): 88 | parent = frappe.db.get_value("Wiki Sidebar Item", {"item": me, "type": "Wiki Sidebar"}, "parent") 89 | if not parent: 90 | return me 91 | return find_topmost(parent) 92 | 93 | 94 | def get_sidebar_for_patch(sidebar_items, group_name): 95 | sidebar_item = OrderedDict({group_name: []}) 96 | 97 | for item in sidebar_items: 98 | if not item.get("group_title"): 99 | sidebar_item[group_name].append(item) 100 | else: 101 | for group, children in get_sidebar_for_patch( 102 | item.get("group_items"), item.get("group_name") 103 | ).items(): 104 | sidebar_item[group] = children 105 | 106 | return sidebar_item 107 | 108 | 109 | def get_children(doc): 110 | out = get_sidebar_items(doc) 111 | 112 | for idx, sidebar_item in enumerate(out): 113 | if sidebar_item.type == "Wiki Sidebar" and frappe.db.exists("Wiki Sidebar", sidebar_item.item): 114 | sidebar = frappe.get_doc("Wiki Sidebar", sidebar_item.item) 115 | children = get_children(sidebar) 116 | out[idx] = { 117 | "group_title": sidebar_item.title, 118 | "group_items": children, 119 | "name": sidebar_item.item, 120 | "group_name": sidebar.name, 121 | "type": "Wiki Sidebar", 122 | "item": f"/{sidebar.route}", 123 | } 124 | 125 | return out 126 | 127 | 128 | def get_sidebar_items(doc): 129 | items_without_group = [] 130 | items = frappe.get_all( 131 | "Wiki Sidebar Item", 132 | filters={"parent": doc.name}, 133 | fields=["title", "item", "name", "type", "route", "parent"], 134 | order_by="idx asc", 135 | ) 136 | for item in items: 137 | item.group_name = frappe.get_doc("Wiki Sidebar", item.parent).title 138 | items_without_group.append(item) 139 | 140 | return items_without_group 141 | 142 | 143 | def get_root_parent_title(name, last_parent=""): 144 | if parent := frappe.db.get_value("Wiki Sidebar Item", {"item": name, "type": "Wiki Sidebar"}, "parent"): 145 | return get_root_parent_title(parent, name) 146 | else: 147 | if last_parent: 148 | return last_parent 149 | return name 150 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_space/test_wiki_space.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Frappe and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestWikiSpace(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_space/wiki_space.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023, Frappe and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("Wiki Space", { 5 | refresh(frm) { 6 | frm.add_web_link(`/${frm.doc.route}`, __("See on website")); 7 | 8 | frm.add_custom_button("Clone Wiki Space", () => { 9 | frappe.prompt("Enter new Wiki Space's route", ({ value }) => { 10 | frm.call("clone_wiki_space_in_background", { new_space_route: value }); 11 | }); 12 | }); 13 | }, 14 | 15 | onload_post_render: function (frm) { 16 | frm.trigger("set_parent_label_options"); 17 | }, 18 | 19 | set_parent_label_options: function (frm) { 20 | frm.fields_dict.navbar_items.grid.update_docfield_property( 21 | "parent_label", 22 | "options", 23 | frm.events.get_parent_options(frm, "navbar_items"), 24 | ); 25 | }, 26 | 27 | get_parent_options: function (frm, table_field) { 28 | var items = frm.doc[table_field] || []; 29 | var main_items = [""]; 30 | for (var i in items) { 31 | var d = items[i]; 32 | if (!d.url && d.label) { 33 | main_items.push(d.label); 34 | } 35 | } 36 | return main_items.join("\n"); 37 | }, 38 | }); 39 | 40 | frappe.ui.form.on("Top Bar Item", { 41 | navbar_delete(frm) { 42 | frm.events.set_parent_label_options(frm); 43 | }, 44 | 45 | navbar_add(frm, cdt, cdn) { 46 | frm.events.set_parent_label_options(frm); 47 | }, 48 | 49 | parent_label: function (frm, doctype, name) { 50 | frm.events.set_parent_label_options(frm); 51 | }, 52 | 53 | url: function (frm, doctype, name) { 54 | frm.events.set_parent_label_options(frm); 55 | }, 56 | 57 | label: function (frm, doctype, name) { 58 | frm.events.set_parent_label_options(frm); 59 | }, 60 | }); 61 | -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_space/wiki_space.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "autoname": "hash", 5 | "creation": "2023-04-11 12:26:16.593440", 6 | "default_view": "List", 7 | "doctype": "DocType", 8 | "editable_grid": 1, 9 | "engine": "InnoDB", 10 | "field_order": [ 11 | "general_tab", 12 | "space_name", 13 | "route", 14 | "app_switcher_logo", 15 | "wiki_sidebars", 16 | "navbar_tab", 17 | "logo_section", 18 | "light_mode_logo", 19 | "dark_mode_logo", 20 | "section_break_fhui", 21 | "navbar_items" 22 | ], 23 | "fields": [ 24 | { 25 | "fieldname": "route", 26 | "fieldtype": "Data", 27 | "in_list_view": 1, 28 | "label": "Route", 29 | "reqd": 1, 30 | "unique": 1 31 | }, 32 | { 33 | "fieldname": "wiki_sidebars", 34 | "fieldtype": "Table", 35 | "label": "Wiki Sidebars", 36 | "options": "Wiki Group Item" 37 | }, 38 | { 39 | "fieldname": "general_tab", 40 | "fieldtype": "Tab Break", 41 | "label": "General" 42 | }, 43 | { 44 | "fieldname": "navbar_tab", 45 | "fieldtype": "Tab Break", 46 | "label": "Navbar" 47 | }, 48 | { 49 | "fieldname": "navbar_items", 50 | "fieldtype": "Table", 51 | "label": "Navbar Items", 52 | "options": "Top Bar Item" 53 | }, 54 | { 55 | "fieldname": "logo_section", 56 | "fieldtype": "Section Break", 57 | "label": "Logo" 58 | }, 59 | { 60 | "fieldname": "light_mode_logo", 61 | "fieldtype": "Attach Image", 62 | "label": "Light Mode Logo" 63 | }, 64 | { 65 | "fieldname": "dark_mode_logo", 66 | "fieldtype": "Attach Image", 67 | "label": "Dark Mode Logo" 68 | }, 69 | { 70 | "fieldname": "section_break_fhui", 71 | "fieldtype": "Section Break" 72 | }, 73 | { 74 | "fieldname": "space_name", 75 | "fieldtype": "Data", 76 | "label": "Space Name" 77 | }, 78 | { 79 | "fieldname": "app_switcher_logo", 80 | "fieldtype": "Attach Image", 81 | "label": "App Switcher Logo", 82 | "make_attachment_public": 1 83 | } 84 | ], 85 | "index_web_pages_for_search": 1, 86 | "links": [], 87 | "modified": "2025-02-20 12:08:19.790688", 88 | "modified_by": "Administrator", 89 | "module": "Wiki", 90 | "name": "Wiki Space", 91 | "naming_rule": "Random", 92 | "owner": "Administrator", 93 | "permissions": [ 94 | { 95 | "create": 1, 96 | "delete": 1, 97 | "email": 1, 98 | "export": 1, 99 | "print": 1, 100 | "read": 1, 101 | "report": 1, 102 | "role": "System Manager", 103 | "share": 1, 104 | "write": 1 105 | }, 106 | { 107 | "create": 1, 108 | "delete": 1, 109 | "email": 1, 110 | "export": 1, 111 | "print": 1, 112 | "read": 1, 113 | "report": 1, 114 | "role": "Wiki Approver", 115 | "share": 1, 116 | "write": 1 117 | } 118 | ], 119 | "sort_field": "modified", 120 | "sort_order": "DESC", 121 | "states": [], 122 | "title_field": "route" 123 | } -------------------------------------------------------------------------------- /wiki/wiki/doctype/wiki_space/wiki_space.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Frappe and contributors 2 | # For license information, please see license.txt 3 | import json 4 | 5 | import frappe 6 | import pymysql 7 | from frappe.model.document import Document 8 | 9 | from wiki.wiki.doctype.wiki_page.search import build_index_in_background, drop_index 10 | 11 | 12 | class WikiSpace(Document): 13 | def before_insert(self): 14 | # insert a new wiki page when sidebar is empty 15 | if not self.wiki_sidebars: 16 | wiki_page = frappe.get_doc( 17 | { 18 | "doctype": "Wiki Page", 19 | "title": "New Wiki Page", 20 | "route": f"{self.route}/new-wiki-page", 21 | "published": 1, 22 | "content": f"Welcome to Wiki Space {self.route}", 23 | } 24 | ) 25 | wiki_page.insert() 26 | 27 | self.append( 28 | "wiki_sidebars", 29 | { 30 | "wiki_page": wiki_page.name, 31 | "parent_label": "New Group", 32 | }, 33 | ) 34 | 35 | def before_save(self): 36 | self.update_wiki_page_routes() 37 | 38 | def update_wiki_page_routes(self): 39 | # prepend space route to the route of wiki page 40 | old_route = frappe.db.get_value("Wiki Space", self.name, "route") 41 | if not old_route or self.route == old_route: 42 | return 43 | 44 | for i, wiki_sidebar in enumerate(self.wiki_sidebars): 45 | wiki_page = frappe.get_value("Wiki Page", wiki_sidebar.wiki_page, ["name", "route"], as_dict=1) 46 | wiki_page_route = wiki_page.route.replace(old_route, self.route, 1) 47 | 48 | frappe.publish_progress( 49 | percent=i * 100 / len(self.wiki_sidebars), 50 | title=f"Updating Wiki Page routes - {self.route}", 51 | description=f"{i}/{len(self.wiki_sidebars)}", 52 | ) 53 | 54 | try: 55 | if wiki_page_route: 56 | frappe.db.set_value( 57 | "Wiki Page", 58 | wiki_sidebar.wiki_page, 59 | "route", 60 | wiki_page_route, 61 | ) 62 | except Exception as e: 63 | if isinstance(e, pymysql.err.IntegrityError): 64 | frappe.throw(f"Wiki Page with route {wiki_page.route} already exists.") 65 | else: 66 | raise e 67 | 68 | def on_update(self): 69 | build_index_in_background() 70 | 71 | # clear sidebar cache 72 | frappe.cache().hdel("wiki_sidebar", self.name) 73 | 74 | def on_trash(self): 75 | drop_index() 76 | 77 | # clear sidebar cache 78 | frappe.cache().hdel("wiki_sidebar", self.name) 79 | build_index_in_background() 80 | 81 | @frappe.whitelist() 82 | def clone_wiki_space_in_background(self, new_space_route): 83 | frappe.enqueue( 84 | clone_wiki_space, 85 | name=self.name, 86 | route=self.route, 87 | new_space_route=new_space_route, 88 | queue="long", 89 | ) 90 | 91 | 92 | def clone_wiki_space(name, route, new_space_route): 93 | if frappe.db.exists("Wiki Space", new_space_route): 94 | frappe.throw(f"Wiki Space {new_space_route} already exists.") 95 | 96 | items = frappe.get_all( 97 | "Wiki Group Item", 98 | filters={"parent": name}, 99 | fields=["wiki_page", "parent_label"], 100 | order_by="idx asc", 101 | ) 102 | 103 | cloned_wiki_space = frappe.new_doc("Wiki Space") 104 | cloned_wiki_space.route = new_space_route 105 | 106 | for idx, item in enumerate(items, 1): 107 | frappe.publish_progress( 108 | idx * 100 / len(items), 109 | title=f"Cloning into new Wiki Space {new_space_route}", 110 | description=f"{idx}/{len(items)}", 111 | ) 112 | cloned_doc = frappe.get_doc("Wiki Page", item.wiki_page).clone(route, new_space_route) 113 | cloned_wiki_space.append( 114 | "wiki_sidebars", 115 | { 116 | "wiki_page": cloned_doc.name, 117 | "parent_label": item.parent_label, 118 | }, 119 | ) 120 | 121 | cloned_wiki_space.insert() 122 | 123 | return cloned_wiki_space 124 | 125 | 126 | @frappe.whitelist() 127 | def update_sidebar(sidebar_items): 128 | sidebars = json.loads(sidebar_items) 129 | 130 | sidebar_items = sidebars.items() 131 | if sidebar_items: 132 | idx = 0 133 | for sidebar, items in sidebar_items: 134 | for item in items: 135 | idx += 1 136 | frappe.db.set_value( 137 | "Wiki Group Item", {"wiki_page": str(item["name"])}, {"parent_label": sidebar, "idx": idx} 138 | ) 139 | 140 | for key in frappe.cache().hgetall("wiki_sidebar").keys(): 141 | frappe.cache().hdel("wiki_sidebar", key) 142 | -------------------------------------------------------------------------------- /wiki/wiki/report/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/wiki/af7de232b6a7e25e6e8984abcd5c4f985a17587e/wiki/wiki/report/__init__.py -------------------------------------------------------------------------------- /wiki/wiki/report/wiki_broken_links/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/wiki/af7de232b6a7e25e6e8984abcd5c4f985a17587e/wiki/wiki/report/wiki_broken_links/__init__.py -------------------------------------------------------------------------------- /wiki/wiki/report/wiki_broken_links/test_broken_link_checker.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Frappe and Contributors 2 | # See license.txt 3 | 4 | from unittest.mock import patch 5 | 6 | import frappe 7 | from frappe.tests.utils import FrappeTestCase 8 | 9 | from wiki.wiki.report.wiki_broken_links.wiki_broken_links import execute, get_broken_links 10 | 11 | WORKING_EXTERNAL_URL = "https://frappe.io" 12 | BROKEN_EXTERNAL_URL = "https://frappewiki.notavalidtld" 13 | BROKEN_IMG_URL = "https://img.notavalidtld/failed.jpeg" 14 | WORKING_INTERNAL_URL = "/api/method/ping" 15 | BROKEN_INTERNAL_URL = "/api/method/ring" 16 | 17 | 18 | def internal_to_external_urls(internal_url: str) -> str: 19 | if internal_url == WORKING_INTERNAL_URL: 20 | return WORKING_EXTERNAL_URL 21 | else: 22 | return BROKEN_EXTERNAL_URL 23 | 24 | 25 | TEST_MD_WITH_BROKEN_LINK = f""" 26 | ## Hello 27 | 28 | This is a test for a [broken link]({BROKEN_EXTERNAL_URL}). 29 | 30 | This is a [valid link]({WORKING_EXTERNAL_URL}). 31 | And [this is a correct relative link]({WORKING_INTERNAL_URL}). 32 | And [this is an incorrect relative link]({BROKEN_INTERNAL_URL}). 33 | 34 | This [hash link](#hash-link) should be ignored. 35 | 36 | ![Broken Image]({BROKEN_IMG_URL}) 37 | """ 38 | 39 | 40 | class TestWikiBrokenLinkChecker(FrappeTestCase): 41 | def setUp(self): 42 | frappe.db.delete("Wiki Page") 43 | self.test_wiki_page = frappe.get_doc( 44 | { 45 | "doctype": "Wiki Page", 46 | "content": TEST_MD_WITH_BROKEN_LINK, 47 | "title": "My Wiki Page", 48 | "route": "test-wiki-page-route", 49 | } 50 | ).insert() 51 | 52 | self.test_wiki_space = frappe.get_doc({"doctype": "Wiki Space", "route": "test-ws-route"}).insert() 53 | 54 | def test_returns_correct_broken_links(self): 55 | broken_links = get_broken_links(TEST_MD_WITH_BROKEN_LINK) 56 | self.assertEqual(len(broken_links), 2) 57 | 58 | def test_wiki_broken_link_report(self): 59 | _, data = execute() 60 | self.assertEqual(len(data), 1) 61 | self.assertEqual(data[0]["broken_link"], BROKEN_EXTERNAL_URL) 62 | 63 | def test_wiki_broken_link_report_with_wiki_space_filter(self): 64 | _, data = execute({"wiki_space": self.test_wiki_space.name}) 65 | self.assertEqual(len(data), 0) 66 | 67 | self.test_wiki_space.append( 68 | "wiki_sidebars", {"wiki_page": self.test_wiki_page, "parent_label": "Test Parent Label"} 69 | ) 70 | self.test_wiki_space.save() 71 | 72 | _, data = execute({"wiki_space": self.test_wiki_space.name}) 73 | self.assertEqual(len(data), 1) 74 | self.assertEqual(data[0]["wiki_page"], self.test_wiki_page.name) 75 | self.assertEqual(data[0]["broken_link"], BROKEN_EXTERNAL_URL) 76 | 77 | def test_wiki_broken_link_report_with_image_filter(self): 78 | _, data = execute({"check_images": 1}) 79 | self.assertEqual(len(data), 2) 80 | self.assertEqual(data[0]["wiki_page"], self.test_wiki_page.name) 81 | self.assertEqual(data[0]["broken_link"], BROKEN_EXTERNAL_URL) 82 | 83 | self.assertEqual(data[1]["wiki_page"], self.test_wiki_page.name) 84 | self.assertEqual(data[1]["broken_link"], BROKEN_IMG_URL) 85 | 86 | @patch.object(frappe.utils.data, "get_url", side_effect=internal_to_external_urls) 87 | def test_wiki_broken_link_report_with_internal_links(self, _get_url): 88 | # patch the get_url to return valid/invalid external links instead 89 | # of internal links in test 90 | _, data = execute({"check_internal_links": 1}) 91 | 92 | self.assertEqual(len(data), 2) 93 | self.assertEqual(data[0]["wiki_page"], self.test_wiki_page.name) 94 | self.assertEqual(data[0]["broken_link"], BROKEN_EXTERNAL_URL) 95 | 96 | self.assertEqual(data[1]["wiki_page"], self.test_wiki_page.name) 97 | self.assertEqual(data[1]["broken_link"], BROKEN_INTERNAL_URL) 98 | 99 | def tearDown(self): 100 | frappe.db.rollback() 101 | -------------------------------------------------------------------------------- /wiki/wiki/report/wiki_broken_links/wiki_broken_links.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024, Frappe and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.query_reports["Wiki Broken Links"] = { 5 | filters: [ 6 | { 7 | fieldname: "wiki_space", 8 | label: __("Wiki Space"), 9 | fieldtype: "Link", 10 | options: "Wiki Space", 11 | }, 12 | { 13 | fieldname: "check_images", 14 | label: __("Include images?"), 15 | fieldtype: "Check", 16 | default: 1, 17 | }, 18 | { 19 | fieldname: "check_internal_links", 20 | label: __("Include internal links?"), 21 | fieldtype: "Check", 22 | default: 0, 23 | }, 24 | ], 25 | }; 26 | -------------------------------------------------------------------------------- /wiki/wiki/report/wiki_broken_links/wiki_broken_links.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_total_row": 0, 3 | "columns": [], 4 | "creation": "2024-12-11 14:43:18.799835", 5 | "disabled": 0, 6 | "docstatus": 0, 7 | "doctype": "Report", 8 | "filters": [], 9 | "idx": 0, 10 | "is_standard": "Yes", 11 | "letterhead": null, 12 | "modified": "2024-12-11 18:58:14.479423", 13 | "modified_by": "Administrator", 14 | "module": "Wiki", 15 | "name": "Wiki Broken Links", 16 | "owner": "Administrator", 17 | "prepared_report": 1, 18 | "ref_doctype": "Wiki Page", 19 | "report_name": "Wiki Broken Links", 20 | "report_type": "Script Report", 21 | "roles": [ 22 | { 23 | "role": "System Manager" 24 | }, 25 | { 26 | "role": "Wiki Approver" 27 | } 28 | ], 29 | "timeout": 0 30 | } -------------------------------------------------------------------------------- /wiki/wiki/report/wiki_broken_links/wiki_broken_links.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Frappe and contributors 2 | # For license information, please see license.txt 3 | 4 | import frappe 5 | import requests 6 | from bs4 import BeautifulSoup 7 | from frappe import _ 8 | 9 | 10 | def execute(filters: dict | None = None): 11 | """Return columns and data for the report. 12 | 13 | This is the main entry point for the report. It accepts the filters as a 14 | dictionary and should return columns and data. It is called by the framework 15 | every time the report is refreshed or a filter is updated. 16 | """ 17 | columns = get_columns() 18 | data = get_data(filters) 19 | 20 | return columns, data 21 | 22 | 23 | def get_columns() -> list[dict]: 24 | """Return columns for the report. 25 | 26 | One field definition per column, just like a DocType field definition. 27 | """ 28 | return [ 29 | { 30 | "label": _("Wiki Page"), 31 | "fieldname": "wiki_page", 32 | "fieldtype": "Link", 33 | "options": "Wiki Page", 34 | "width": 200, 35 | }, 36 | { 37 | "label": _("Broken Link"), 38 | "fieldname": "broken_link", 39 | "fieldtype": "Data", 40 | "options": "URL", 41 | "width": 400, 42 | }, 43 | ] 44 | 45 | 46 | def get_data(filters: dict | None = None) -> list[list]: 47 | """Return data for the report. 48 | 49 | The report data is a list of rows, with each row being a list of cell values. 50 | """ 51 | data = [] 52 | 53 | wiki_pages = frappe.db.get_all("Wiki Page", fields=["name", "content"]) 54 | 55 | if filters and filters.get("wiki_space"): 56 | wiki_space = filters.get("wiki_space") 57 | wiki_pages = frappe.db.get_all( 58 | "Wiki Group Item", 59 | fields=["wiki_page as name", "wiki_page.content as content"], 60 | filters={"parent": wiki_space, "parenttype": "Wiki Space"}, 61 | ) 62 | 63 | include_images = filters and bool(filters.get("check_images")) 64 | check_internal_links = filters and bool(filters.get("check_internal_links")) 65 | 66 | for page in wiki_pages: 67 | broken_links_for_page = get_broken_links(page.content, include_images, check_internal_links) 68 | rows = [{"broken_link": link, "wiki_page": page["name"]} for link in broken_links_for_page] 69 | data.extend(rows) 70 | 71 | return data 72 | 73 | 74 | def get_broken_links( 75 | md_content: str, include_images: bool = True, include_relative_urls: bool = False 76 | ) -> list[str]: 77 | html = frappe.utils.md_to_html(md_content) 78 | soup = BeautifulSoup(html, "html.parser") 79 | 80 | links = soup.find_all("a") 81 | if include_images: 82 | links += soup.find_all("img") 83 | 84 | broken_links = [] 85 | for el in links: 86 | url = el.attrs.get("href") or el.attrs.get("src") 87 | 88 | if is_hash_link(url): 89 | continue 90 | 91 | is_relative = is_relative_url(url) 92 | relative_url = None 93 | 94 | if is_relative and not include_relative_urls: 95 | continue 96 | 97 | if is_relative: 98 | relative_url = url 99 | url = frappe.utils.data.get_url(url) # absolute URL 100 | 101 | is_broken = is_broken_link(url) 102 | if is_broken: 103 | if is_relative: 104 | broken_links.append(relative_url) # original URL 105 | else: 106 | broken_links.append(url) 107 | 108 | return broken_links 109 | 110 | 111 | def is_relative_url(url: str) -> bool: 112 | return url.startswith("/") 113 | 114 | 115 | def is_hash_link(url: str) -> bool: 116 | return url.startswith("#") 117 | 118 | 119 | def is_broken_link(url: str) -> bool: 120 | try: 121 | status_code = get_request_status_code(url) 122 | if status_code >= 400: 123 | return True 124 | except Exception: 125 | return True 126 | 127 | return False 128 | 129 | 130 | def get_request_status_code(url: str) -> int: 131 | response = requests.head(url, verify=False, timeout=5) 132 | return response.status_code 133 | -------------------------------------------------------------------------------- /wiki/wiki/workspace/wiki/wiki.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "wiki", 3 | "charts": [], 4 | "content": "[{\"id\":\"f6laZQUa0x\",\"type\":\"header\",\"data\":{\"text\":\"Wiki\",\"col\":12}},{\"id\":\"ir8Llemis5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Wiki Pages\",\"col\":4}},{\"id\":\"tZQ_AtqABm\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Wiki Space\",\"col\":4}},{\"id\":\"z4qT3yMggL\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Wiki Settings\",\"col\":4}},{\"id\":\"cTIBC0weUT\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Wiki Page Patches\",\"col\":4}},{\"id\":\"IfrRKY62Tc\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Wiki Feedback\",\"col\":4}},{\"id\":\"BsC6YwujPn\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Wiki Page Revisions\",\"col\":4}},{\"id\":\"3gsyKHzfOC\",\"type\":\"header\",\"data\":{\"text\":\"Reports\",\"col\":12}},{\"id\":\"SI4uvLzSVb\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Broken Links Report\",\"col\":4}}]", 5 | "creation": "2022-09-25 16:45:20.547072", 6 | "custom_blocks": [], 7 | "docstatus": 0, 8 | "doctype": "Workspace", 9 | "for_user": "", 10 | "hide_custom": 0, 11 | "icon": "education", 12 | "idx": 0, 13 | "is_hidden": 0, 14 | "label": "Wiki", 15 | "links": [], 16 | "modified": "2024-12-17 16:49:43.372512", 17 | "modified_by": "Administrator", 18 | "module": "Wiki", 19 | "name": "Wiki", 20 | "number_cards": [], 21 | "owner": "Administrator", 22 | "parent_page": "", 23 | "public": 1, 24 | "quick_lists": [], 25 | "roles": [], 26 | "sequence_id": 31.0, 27 | "shortcuts": [ 28 | { 29 | "color": "Blue", 30 | "doc_view": "List", 31 | "format": "{} Published", 32 | "label": "Wiki Pages", 33 | "link_to": "Wiki Page", 34 | "stats_filter": "{\"published\":[\"=\",1]}", 35 | "type": "DocType" 36 | }, 37 | { 38 | "color": "Grey", 39 | "doc_view": "List", 40 | "label": "Broken Links Report", 41 | "link_to": "Wiki Broken Links", 42 | "type": "Report" 43 | }, 44 | { 45 | "color": "Grey", 46 | "doc_view": "List", 47 | "label": "Wiki Feedback", 48 | "link_to": "Wiki Feedback", 49 | "stats_filter": "[]", 50 | "type": "DocType" 51 | }, 52 | { 53 | "color": "Grey", 54 | "doc_view": "List", 55 | "label": "Wiki Space", 56 | "link_to": "Wiki Space", 57 | "type": "DocType" 58 | }, 59 | { 60 | "color": "Yellow", 61 | "doc_view": "List", 62 | "format": "{} Under Review", 63 | "label": "Wiki Page Patches", 64 | "link_to": "Wiki Page Patch", 65 | "stats_filter": "{\"status\":[\"=\",\"Under Review\"]}", 66 | "type": "DocType" 67 | }, 68 | { 69 | "color": "Grey", 70 | "doc_view": "List", 71 | "label": "Wiki Settings", 72 | "link_to": "Wiki Settings", 73 | "type": "DocType" 74 | }, 75 | { 76 | "color": "Grey", 77 | "doc_view": "List", 78 | "label": "Wiki Page Revisions", 79 | "link_to": "Wiki Page Revision", 80 | "type": "DocType" 81 | } 82 | ], 83 | "title": "Wiki", 84 | "type": "Workspace" 85 | } -------------------------------------------------------------------------------- /wiki/wiki_search.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | import re 5 | 6 | import frappe 7 | from frappe.utils import cstr, strip_html_tags, update_progress_bar 8 | from frappe.utils.redis_wrapper import RedisWrapper 9 | 10 | from wiki.search import Search 11 | 12 | UNSAFE_CHARS = re.compile(r"[\[\]{}<>+]") 13 | 14 | INDEX_BUILD_FLAG = "wiki_page_index_in_progress" 15 | 16 | 17 | class WikiSearch(Search): 18 | def __init__(self) -> None: 19 | schema = [ 20 | {"name": "title", "weight": 5}, 21 | {"name": "content", "weight": 2}, 22 | {"name": "route", "type": "tag"}, 23 | {"name": "meta_description", "weight": 1}, 24 | {"name": "meta_keywords", "weight": 3}, 25 | {"name": "modified", "sortable": True}, 26 | ] 27 | super().__init__("wiki_idx", "wiki_search_doc", schema) 28 | 29 | def search(self, query, space=None, **kwargs): 30 | if query and space: 31 | query = rf"{query} @route:{{{space}\/*}}" 32 | return super().search(query, **kwargs) 33 | 34 | def build_index(self): 35 | self.drop_index() 36 | self.create_index() 37 | records = self.get_records() 38 | total = len(records) 39 | for i, doc in enumerate(records): 40 | self.index_doc(doc) 41 | if not hasattr(frappe.local, "request"): 42 | update_progress_bar("Indexing Wiki Pages", i, total) 43 | if not hasattr(frappe.local, "request"): 44 | print() 45 | 46 | def index_doc(self, doc): 47 | id = f"Wiki Page:{doc.name}" 48 | fields = { 49 | "title": doc.title, 50 | "content": strip_html_tags(doc.content), 51 | "route": doc.route, 52 | "meta_description": doc.meta_description or "", 53 | "meta_keywords": doc.meta_keywords or "", 54 | "modified": doc.modified, 55 | } 56 | payload = { 57 | "route": doc.route, 58 | "published": doc.published, 59 | "allow_guest": doc.allow_guest, 60 | } 61 | self.add_document(id, fields, payload=payload) 62 | 63 | def remove_doc(self, doc): 64 | if doc.doctype == "Wiki Page": 65 | id = f"Wiki Page:{doc.name}" 66 | self.remove_document(id) 67 | 68 | def clean_query(self, query): 69 | query = query.strip().replace("-*", "*") 70 | query = UNSAFE_CHARS.sub(" ", query) 71 | query = query.strip() 72 | return query 73 | 74 | def get_records(self): 75 | return frappe.get_all( 76 | "Wiki Page", 77 | fields=[ 78 | "name", 79 | "title", 80 | "content", 81 | "route", 82 | "meta_description", 83 | "meta_keywords", 84 | "modified", 85 | "published", 86 | "allow_guest", 87 | ], 88 | filters={"published": 1}, 89 | ) 90 | -------------------------------------------------------------------------------- /wiki/www/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/wiki/af7de232b6a7e25e6e8984abcd5c4f985a17587e/wiki/www/__init__.py -------------------------------------------------------------------------------- /wiki/www/__pycache__/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/wiki/af7de232b6a7e25e6e8984abcd5c4f985a17587e/wiki/www/__pycache__/__init__.py -------------------------------------------------------------------------------- /wiki/www/app-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/wiki/af7de232b6a7e25e6e8984abcd5c4f985a17587e/wiki/www/app-icon.png -------------------------------------------------------------------------------- /wiki/www/contributions.html: -------------------------------------------------------------------------------- 1 | {% extends "wiki/doctype/wiki_page/templates/wiki_page.html" %} 2 | 3 | {%- block head_include %} 4 | {{ super() }} 5 | {{ include_style('wiki.bundle.css') }} 6 | 7 | {{ include_style('contributions.bundle.css') }} 8 | 9 | {% endblock -%} 10 | 11 | 12 | {% block page_content %} 13 | 14 | 15 |
{{pilled_title}}
16 | 17 |
18 |
19 |
20 | {% if contributions %} 21 | 22 | 23 | 24 | 29 | 30 | 31 | {% for j in contributions %} 32 | 33 | 35 | 36 | 37 | 39 | 40 | {% endfor %} 41 | 42 |
{{ _("Status") }} 25 | {{ _("Message") }} 26 | {{ _("Last update on") }} 27 | {{ _("Link") }} 28 |
  {{ 34 | j.status }}{{ j.message }}{{ j.modified }} Open Contribution 38 |
43 | {% else %} 44 |
45 | Empty State 46 |

{{ _("No Contributions Made") }}

47 |
48 | {% endif %} 49 | 50 |
51 |
52 |
53 | {% if contributions %} 54 |
55 |
56 |
57 |
58 | 59 | {% endif %} 60 | 61 | {% endblock %} 62 | 63 | {% block base_scripts %} 64 | 65 | 66 | {{ include_script("frappe-web.bundle.js") }} 67 | 68 | 99 | 100 | 101 | {% endblock %} 102 | 103 | {% block page_sidebar %} 104 | {% endblock %} -------------------------------------------------------------------------------- /wiki/www/contributions.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | from frappe import _ 3 | from frappe.utils.data import cint 4 | 5 | from wiki.wiki.doctype.wiki_page.wiki_page import get_open_drafts 6 | 7 | color_map = { 8 | "Changes Requested": "blue", 9 | "Under Review": "orange", 10 | "Rejected": "red", 11 | "Approved": "green", 12 | } 13 | 14 | 15 | def get_context(context): 16 | context.pilled_title = "My Contributions" 17 | context.no_cache = 1 18 | context.no_sidebar = 1 19 | context.contributions = get_user_contributions(0, 10) 20 | context = context.update( 21 | { 22 | "post_login": [ 23 | {"label": _("My Account"), "url": "/me"}, 24 | {"label": _("Logout"), "url": "/?cmd=web_logout"}, 25 | { 26 | "label": _("My Drafts ") + get_open_drafts(), 27 | "url": "/drafts", 28 | }, 29 | ] 30 | } 31 | ) 32 | 33 | return context 34 | 35 | 36 | @frappe.whitelist() 37 | def get_contributions(start, limit): 38 | return {"contributions": get_user_contributions(start, limit)} 39 | 40 | 41 | def get_user_contributions(start, limit): 42 | contributions = [] 43 | wiki_page_patches = frappe.get_list( 44 | "Wiki Page Patch", 45 | ["message", "status", "name", "wiki_page", "modified", "new"], 46 | order_by="modified desc", 47 | start=cint(start), 48 | limit=cint(limit), 49 | filters=[["status", "!=", "Draft"], ["owner", "=", frappe.session.user]], 50 | ) 51 | for wiki_page_patch in wiki_page_patches: 52 | route = frappe.db.get_value("Wiki Page", wiki_page_patch.wiki_page, "route") 53 | wiki_page_patch.edit_link = f"/{route}?editWiki=1&wikiPagePatch={wiki_page_patch.name}" 54 | wiki_page_patch.color = color_map[wiki_page_patch.status] 55 | wiki_page_patch.modified = frappe.utils.pretty_date(wiki_page_patch.modified) 56 | contributions.extend([wiki_page_patch]) 57 | 58 | return contributions 59 | -------------------------------------------------------------------------------- /wiki/www/drafts.html: -------------------------------------------------------------------------------- 1 | {% extends "wiki/doctype/wiki_page/templates/wiki_page.html" %} 2 | 3 | {%- block head_include %} 4 | {{ super() }} 5 | {{ include_style('wiki.bundle.css') }} 6 | 7 | {{ include_style('contributions.bundle.css') }} 8 | 9 | {% endblock -%} 10 | 11 | 12 | {% block page_content %} 13 |
14 | 15 | 16 |
{{pilled_title}}
17 | 18 |
19 |
20 |
21 | {% if contributions %} 22 | 23 | 24 | 25 | 30 | 31 | 32 | {% for j in contributions %} 33 | 34 | 36 | 37 | 38 | 40 | 41 | {% endfor %} 42 | 43 |
{{ _("Status") }} 26 | {{ _("Message") }} 27 | {{ _("Last update on") }} 28 | {{ _("Link") }} 29 |
  {{ 35 | j.status }}{{ j.message }}{{ j.modified }} Open Draft 39 |
44 | {% else %} 45 |
46 | Empty State 47 |

{{ _("No Drafts Made") }}

48 |
49 | {% endif %} 50 | 51 |
52 |
53 |
54 | {% if contributions %} 55 |
56 |
57 |
58 |
59 |
60 | 61 | {% endif %} 62 | 63 | {% endblock %} 64 | 65 | {% block base_scripts %} 66 | 67 | 68 | {{ include_script("frappe-web.bundle.js") }} 69 | 70 | 103 | 104 | 105 | {% endblock %} 106 | 107 | {% block page_sidebar %} 108 | {% endblock %} -------------------------------------------------------------------------------- /wiki/www/drafts.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | from frappe import _ 3 | from frappe.utils.data import cint 4 | 5 | from wiki.wiki.doctype.wiki_page.wiki_page import get_open_contributions 6 | 7 | 8 | def get_context(context): 9 | context.pilled_title = "My Drafts" 10 | context.no_cache = 1 11 | context.no_sidebar = 1 12 | context.contributions = get_user_drafts(0, 10) 13 | context = context.update( 14 | { 15 | "post_login": [ 16 | {"label": _("My Account"), "url": "/me"}, 17 | {"label": _("Logout"), "url": "/?cmd=web_logout"}, 18 | { 19 | "label": _("My Contributions ") + get_open_contributions(), 20 | "url": "/contributions", 21 | }, 22 | ] 23 | } 24 | ) 25 | 26 | return context 27 | 28 | 29 | @frappe.whitelist() 30 | def get_drafts(start, limit): 31 | return {"contributions": get_user_drafts(start, limit)} 32 | 33 | 34 | def get_user_drafts(start, limit): 35 | drafts = [] 36 | wiki_page_patches = frappe.get_list( 37 | "Wiki Page Patch", 38 | ["message", "status", "name", "wiki_page", "modified", "new", "new_sidebar_group"], 39 | order_by="modified desc", 40 | start=cint(start), 41 | limit=cint(limit), 42 | filters=[["status", "=", "Draft"], ["owner", "=", frappe.session.user]], 43 | ) 44 | for wiki_page_patch in wiki_page_patches: 45 | route = frappe.db.get_value("Wiki Page", wiki_page_patch.wiki_page, "route") 46 | if wiki_page_patch.new: 47 | wiki_page_patch.edit_link = ( 48 | f"/{route}?newWiki={wiki_page_patch.new_sidebar_group}&wikiPagePatch={wiki_page_patch.name}" 49 | ) 50 | else: 51 | wiki_page_patch.edit_link = f"/{route}?editWiki=1&wikiPagePatch={wiki_page_patch.name}" 52 | wiki_page_patch.color = "orange" 53 | wiki_page_patch.modified = frappe.utils.pretty_date(wiki_page_patch.modified) 54 | drafts.extend([wiki_page_patch]) 55 | 56 | return drafts 57 | -------------------------------------------------------------------------------- /wiki/www/wiki.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/wiki/af7de232b6a7e25e6e8984abcd5c4f985a17587e/wiki/www/wiki.html -------------------------------------------------------------------------------- /wiki/www/wiki.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | import frappe 6 | 7 | 8 | def get_context(context): 9 | """Find and route to the default wiki space's route, which will further route to it's first wiki page""" 10 | 11 | default_space_route = frappe.get_single("Wiki Settings").default_wiki_space 12 | 13 | if default_space_route: 14 | frappe.response.location = f"/{default_space_route}" 15 | frappe.response.type = "redirect" 16 | raise frappe.Redirect 17 | --------------------------------------------------------------------------------