├── .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 |
4 |
Frappe Wiki
5 |
6 | **Open Source Documentation Tool**
7 |
8 | [](https://cloud.cypress.io/projects/w2jgcb/runs)
9 | [](https://github.com/frappe/wiki/actions/workflows/ci.yml)
10 |
11 |
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 |
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 |
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 |
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""
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 |
--------------------------------------------------------------------------------
/wiki/wiki/doctype/wiki_page/templates/feedback.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
How would you rate this page?
12 |
13 |
14 |
15 | 1
16 |
17 |
18 | 2
19 |
20 |
21 | 3
22 |
23 |
24 | 4
25 |
26 |
27 | 5
28 |
29 |
30 |
31 |
How can we make it better?
32 |
33 | {%- if ask_for_contact_details -%}
34 |
Enter your email if you would like to contribute to the
35 | docs
36 |
37 | {%- endif -%}
38 |
39 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/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 |
37 |
39 | {{ _(item.label) }}
40 |
41 |
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 |
3 |
4 |
16 |
17 |
18 | {% endif %}
--------------------------------------------------------------------------------
/wiki/wiki/doctype/wiki_page/templates/page_settings.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/wiki/wiki/doctype/wiki_page/templates/revisions.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
17 |
18 |
19 | {{ frappe.utils.md_to_html(previous_revision.content) }}
20 |
21 |
22 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/wiki/wiki/doctype/wiki_page/templates/show.html:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
This space has
6 | {{ pending_patches_count }} change(s) pending for
7 | review.
8 |
9 |
Review changes
10 |
11 |
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 |
27 |
28 |
29 |
32 |
33 | Enter title for the new Wiki Group
34 |
35 |
36 |
39 |
40 |
41 |
42 |
43 |
44 | {%- if show_feedback -%}
45 |
46 | Was this article helpful?
47 | Give Feedback
48 |
49 | {% include "wiki/doctype/wiki_page/templates/feedback.html" %}
50 | {%- endif -%}
51 |
52 | {%- if last_revision -%}
53 |
54 |
55 | {%- endif -%}
56 |
57 |
67 |
77 |
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 |
--------------------------------------------------------------------------------
/wiki/wiki/doctype/wiki_page/templates/wiki_navbar.html:
--------------------------------------------------------------------------------
1 |
2 |
36 |
37 | {% if navbar_search %}
38 |
39 |
41 |
42 |
43 |
44 |
45 | {%- endif -%}
46 |
47 |
49 |
50 |
51 |
52 |
53 | {% include "wiki/doctype/wiki_page/templates/navbar_items.html" %}
54 |
55 |
56 |
57 |
58 |
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": ""
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 | 
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 |
16 |
17 |
18 |
19 |
20 | {% if contributions %}
21 |
22 |
23 |
24 | {{ _("Status") }}
25 | {{ _("Message") }}
26 | {{ _("Last update on") }}
27 | {{ _("Link") }}
28 |
29 |
30 |
31 | {% for j in contributions %}
32 |
33 | {{
34 | j.status }}
35 | {{ j.message }}
36 | {{ j.modified }}
37 | Open Contribution
38 |
39 |
40 | {% endfor %}
41 |
42 |
43 | {% else %}
44 |
45 |
46 |
{{ _("No Contributions Made") }}
47 |
48 | {% endif %}
49 |
50 |
51 |
52 |
53 | {% if contributions %}
54 |
55 | More
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 |
17 |
18 |
19 |
20 |
21 | {% if contributions %}
22 |
23 |
24 |
25 | {{ _("Status") }}
26 | {{ _("Message") }}
27 | {{ _("Last update on") }}
28 | {{ _("Link") }}
29 |
30 |
31 |
32 | {% for j in contributions %}
33 |
34 | {{
35 | j.status }}
36 | {{ j.message }}
37 | {{ j.modified }}
38 | Open Draft
39 |
40 |
41 | {% endfor %}
42 |
43 |
44 | {% else %}
45 |
46 |
47 |
{{ _("No Drafts Made") }}
48 |
49 | {% endif %}
50 |
51 |
52 |
53 |
54 | {% if contributions %}
55 |
56 |
More
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 |
--------------------------------------------------------------------------------
Activity
10 |