Skip to content

Commit f0b1979

Browse files
committed
Merge remote-tracking branch 'upstream/version-16' into add_pdf_button
2 parents 5bd02f8 + 61e2d9a commit f0b1979

6 files changed

Lines changed: 379 additions & 7 deletions

File tree

.github/helper/install.sh

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
cd ~ || exit
6+
7+
sudo apt update && sudo apt install redis-server libcups2-dev
8+
9+
install_whktml() {
10+
wget -O /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.jammy_amd64.deb
11+
sudo apt install /tmp/wkhtmltox.deb
12+
}
13+
install_whktml &
14+
wkpid=$!
15+
16+
pip install frappe-bench
17+
18+
git clone https://github.com/frappe/frappe --branch version-16 --depth 1
19+
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
20+
21+
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"
22+
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
23+
24+
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "FLUSH PRIVILEGES"
25+
26+
cd ~/frappe-bench || exit
27+
28+
sed -i 's/watch:/# watch:/g' Procfile
29+
sed -i 's/schedule:/# schedule:/g' Procfile
30+
sed -i 's/socketio:/# socketio:/g' Procfile
31+
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
32+
33+
bench get-app pdf_on_submit "${GITHUB_WORKSPACE}"
34+
bench setup requirements --dev
35+
36+
bench new-site --db-root-password root --admin-password admin test_site
37+
bench --site test_site set-config host_name "http://test_site:8000"
38+
39+
CI=Yes bench build --production
40+
41+
bench start &> bench_start.log &
42+
43+
wait $wkpid

.github/workflows/linters.yaml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: Linters
2+
3+
on:
4+
pull_request: { }
5+
6+
jobs:
7+
8+
linters:
9+
name: linters
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v6
13+
14+
- name: Set up Python 3.14
15+
uses: actions/setup-python@v6
16+
with:
17+
python-version: '3.14'
18+
cache: pip
19+
20+
- name: Set up Node.js 24
21+
uses: actions/setup-node@v6
22+
with:
23+
node-version: '24'
24+
check-latest: true
25+
26+
- name: Install and Run Pre-commit
27+
uses: pre-commit/action@v3.0.1
28+
29+
- name: Download Semgrep rules
30+
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
31+
32+
- name: Download semgrep
33+
run: pip install semgrep
34+
35+
- name: Run Semgrep rules
36+
run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness
37+
38+
deps-vulnerable-check:
39+
name: 'Vulnerable Dependency Check'
40+
runs-on: ubuntu-latest
41+
42+
steps:
43+
- uses: actions/setup-python@v6
44+
with:
45+
python-version: '3.14'
46+
47+
- uses: actions/checkout@v6
48+
49+
- name: Cache pip
50+
uses: actions/cache@v5
51+
with:
52+
path: ~/.cache/pip
53+
key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }}
54+
restore-keys: |
55+
${{ runner.os }}-pip-
56+
${{ runner.os }}-
57+
58+
- name: Install and run pip-audit
59+
run: |
60+
pip install pip-audit
61+
cd ${GITHUB_WORKSPACE}
62+
pip-audit --desc on .

.github/workflows/server-tests.yml

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
2+
name: Server
3+
4+
on:
5+
pull_request:
6+
paths-ignore:
7+
- "**.css"
8+
- "**.js"
9+
- "**.md"
10+
- "**.html"
11+
- "**.csv"
12+
- "**.pot"
13+
- "**.po"
14+
15+
concurrency:
16+
group: version-16-pdf_on_submit-${{ github.event.number }}
17+
cancel-in-progress: true
18+
19+
jobs:
20+
tests:
21+
name: Unit Tests
22+
runs-on: ubuntu-latest
23+
timeout-minutes: 60
24+
env:
25+
NODE_ENV: "production"
26+
27+
strategy:
28+
fail-fast: false
29+
30+
services:
31+
mariadb:
32+
image: mariadb:11.8
33+
env:
34+
MARIADB_ROOT_PASSWORD: root
35+
ports:
36+
- 3306:3306
37+
options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=5s --health-timeout=2s --health-retries=3
38+
39+
steps:
40+
- name: Clone
41+
uses: actions/checkout@v6
42+
43+
- name: Setup Python
44+
uses: actions/setup-python@v6
45+
with:
46+
python-version: '3.14'
47+
48+
- name: Setup Node
49+
uses: actions/setup-node@v6
50+
with:
51+
node-version: 24
52+
check-latest: true
53+
54+
- name: Add to Hosts
55+
run: |
56+
echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
57+
58+
- name: Cache pip
59+
uses: actions/cache@v5
60+
with:
61+
path: ~/.cache/pip
62+
key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }}
63+
restore-keys: |
64+
${{ runner.os }}-pip-
65+
${{ runner.os }}-
66+
67+
- name: Install
68+
run: |
69+
bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
70+
71+
- name: Install pdf_on_submit
72+
working-directory: /home/runner/frappe-bench
73+
run: |
74+
bench --site test_site install-app pdf_on_submit
75+
env:
76+
TYPE: server
77+
78+
- name: Run Tests
79+
working-directory: /home/runner/frappe-bench
80+
run: |
81+
bench --site test_site set-config allow_tests true
82+
bench --site test_site run-tests --app pdf_on_submit
83+
env:
84+
TYPE: server
85+
86+
- name: Uninstall pdf_on_submit
87+
working-directory: /home/runner/frappe-bench
88+
run: |
89+
bench --site test_site uninstall-app --yes --no-backup pdf_on_submit
90+
env:
91+
TYPE: server
92+
93+
- name: Show bench output
94+
if: ${{ always() }}
95+
run: cat ~/frappe-bench/bench_start.log || true

pdf_on_submit/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "16.1.0"
1+
__version__ = "16.2.0"

pdf_on_submit/quill.py

Lines changed: 110 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
1+
import copy
2+
13
from bs4 import BeautifulSoup
4+
from bs4.element import NavigableString, Tag
5+
6+
7+
LIST_TAGS = {"ol", "ul"}
28

39

410
def split_quill(html: str) -> list[str]:
5-
"""Split a Text Editor HTML string into a list of HTML strings, each representing a direct child of the editor div.
11+
"""Split a Text Editor HTML string into printable HTML fragments.
612
7-
This is useful for breaking text-editor content into separate rows in a print format."""
13+
Top-level Quill blocks are returned separately so print formats can render them
14+
as individual table rows. Lists are split further by item because wkhtmltopdf
15+
handles page breaks between table rows more reliably than page breaks inside
16+
long table cells."""
817
soup = BeautifulSoup(html, "html.parser")
918
nested_divs = soup.find_all("div", recursive=False)
1019

@@ -13,7 +22,102 @@ def split_quill(html: str) -> list[str]:
1322
return [html]
1423

1524
div = nested_divs[0]
16-
if div.has_attr("class") and "ql-editor" in div["class"]:
17-
return [f'<div class="ql-editor">{str(child)}</div>' for child in div.children]
18-
else:
19-
return [str(child) for child in div.children]
25+
fragments = _split_children(div)
26+
27+
if _is_quill_editor(div):
28+
return [_wrap_in_editor(div, fragment) for fragment in fragments]
29+
30+
return fragments
31+
32+
33+
def _split_children(parent: Tag) -> list[str]:
34+
fragments = []
35+
36+
for child in parent.children:
37+
if _is_blank_text(child):
38+
continue
39+
40+
if isinstance(child, Tag) and child.name in LIST_TAGS:
41+
fragments.extend(_split_list(child))
42+
else:
43+
fragments.append(str(child))
44+
45+
return fragments
46+
47+
48+
def _split_list(list_tag: Tag) -> list[str]:
49+
list_items = [child for child in list_tag.children if isinstance(child, Tag) and child.name == "li"]
50+
if not list_items:
51+
return [str(list_tag)]
52+
53+
fragments = []
54+
ordered_counters = [0] * 10
55+
56+
for item in list_items:
57+
fragment = _new_tag_like(list_tag)
58+
59+
if _is_ordered_list_item(list_tag, item):
60+
indent = _get_quill_indent(item)
61+
ordered_counters[indent] += 1
62+
ordered_counters[indent + 1 :] = [0] * (len(ordered_counters) - indent - 1)
63+
_set_ordered_list_start(fragment, indent, ordered_counters[indent])
64+
65+
fragment.append(BeautifulSoup(str(item), "html.parser").find("li"))
66+
fragments.append(str(fragment))
67+
68+
return fragments
69+
70+
71+
def _is_quill_editor(tag: Tag) -> bool:
72+
return "ql-editor" in tag.get("class", [])
73+
74+
75+
def _is_blank_text(element: Tag | NavigableString) -> bool:
76+
return isinstance(element, NavigableString) and not element.strip()
77+
78+
79+
def _new_tag_like(tag: Tag) -> Tag:
80+
soup = BeautifulSoup("", "html.parser")
81+
new_tag = soup.new_tag(tag.name)
82+
new_tag.attrs = copy.deepcopy(tag.attrs)
83+
return new_tag
84+
85+
86+
def _wrap_in_editor(editor_tag: Tag, fragment: str) -> str:
87+
soup = BeautifulSoup("", "html.parser")
88+
editor = soup.new_tag("div")
89+
editor.attrs = copy.deepcopy(editor_tag.attrs)
90+
fragment_soup = BeautifulSoup(fragment, "html.parser")
91+
92+
for child in list(fragment_soup.contents):
93+
editor.append(child)
94+
95+
return str(editor)
96+
97+
98+
def _is_ordered_list_item(list_tag: Tag, item: Tag) -> bool:
99+
data_list = item.get("data-list")
100+
if data_list:
101+
return data_list == "ordered"
102+
103+
return list_tag.name == "ol"
104+
105+
106+
def _get_quill_indent(item: Tag) -> int:
107+
for class_name in item.get("class", []):
108+
if class_name.startswith("ql-indent-"):
109+
try:
110+
return int(class_name.removeprefix("ql-indent-"))
111+
except ValueError:
112+
pass
113+
114+
return 0
115+
116+
117+
def _set_ordered_list_start(list_tag: Tag, indent: int, number: int) -> None:
118+
if indent == 0 and list_tag.name == "ol":
119+
list_tag["start"] = str(number)
120+
121+
style = list_tag.get("style", "").rstrip()
122+
counter_reset = f"counter-reset: list-{indent} {number - 1};"
123+
list_tag["style"] = f"{style.rstrip(';')}; {counter_reset}" if style else counter_reset

0 commit comments

Comments
 (0)