').attr('id', 'qr_info').html(prompt || '{{ _("Verification code email not sent. Please contact Administrator.") }}');
+ var direction = $('
').attr('id', 'qr_info').html(prompt || {{ _("Verification code email not sent. Please contact Administrator.") | tojson }});
email_div.append(direction);
$('#otp_div').prepend(email_div);
}
diff --git a/frappe/test_runner.py b/frappe/test_runner.py
index 35911269cb..62e5dd599a 100644
--- a/frappe/test_runner.py
+++ b/frappe/test_runner.py
@@ -38,6 +38,7 @@ def xmlrunner_wrapper(output):
def main(
+ site=None,
app=None,
module=None,
doctype=None,
@@ -50,9 +51,18 @@ def main(
doctype_list_path=None,
failfast=False,
case=None,
+ skip_test_records=False,
+ skip_before_tests=False,
):
global unittest_runner
+ frappe.init(site=site)
+ if not frappe.db:
+ frappe.connect()
+
+ frappe.flags.skip_before_tests = skip_before_tests
+ frappe.flags.skip_test_records = skip_test_records
+
if doctype_list_path:
app, doctype_list_path = doctype_list_path.split(os.path.sep, 1)
with open(frappe.get_app_path(app, doctype_list_path)) as f:
@@ -69,9 +79,6 @@ def main(
frappe.flags.print_messages = verbose
frappe.flags.in_test = True
- if not frappe.db:
- frappe.connect()
-
# workaround! since there is no separate test db
frappe.clear_cache()
scheduler_disabled_by_user = frappe.utils.scheduler.is_scheduler_disabled(verbose=False)
@@ -89,9 +96,22 @@ def main(
doctype, verbose, tests, force, profile, failfast=failfast, junit_xml_output=junit_xml_output
)
elif module_def:
- doctypes = frappe.db.get_list(
- "DocType", filters={"module": module_def, "istable": 0}, pluck="name"
+ doctypes = []
+ doctypes_ = frappe.get_list(
+ "DocType",
+ filters={"module": module_def, "istable": 0},
+ fields=["name", "module"],
+ as_list=True,
)
+ for doctype, module in doctypes_:
+ test_module = get_module_name(doctype, module, "test_", app=app)
+ try:
+ importlib.import_module(test_module)
+ except Exception:
+ pass
+ else:
+ doctypes.append(doctype)
+
ret = run_tests_for_doctype(
doctypes, verbose, tests, force, profile, failfast=failfast, junit_xml_output=junit_xml_output
)
@@ -329,9 +349,6 @@ def _add_test(app, path, filename, verbose, test_suite=None):
def make_test_records(doctype, verbose=0, force=False, commit=False):
- if not frappe.db:
- frappe.connect()
-
if frappe.flags.skip_test_records:
return
diff --git a/frappe/tests/test_api.py b/frappe/tests/test_api.py
index 8d3065982b..8dd68dd644 100644
--- a/frappe/tests/test_api.py
+++ b/frappe/tests/test_api.py
@@ -15,7 +15,7 @@ from werkzeug.test import TestResponse
import frappe
from frappe.installer import update_site_config
from frappe.tests.utils import FrappeTestCase, patch_hooks
-from frappe.utils import cint, get_test_client, get_url
+from frappe.utils import cint, get_site_url, get_test_client, get_url
try:
_site = frappe.local.site
@@ -432,6 +432,21 @@ class TestResponse(FrappeAPITestCase):
self.assertGreater(cint(response.headers["content-length"]), 0)
self.assertEqual(response.headers["content-disposition"], f'filename="{encoded_filename}"')
+ def test_download_private_file_with_unique_url(self):
+ test_content = frappe.generate_hash()
+ file = frappe.get_doc(
+ {
+ "doctype": "File",
+ "file_name": test_content,
+ "content": test_content,
+ "is_private": 1,
+ }
+ )
+ file.insert()
+
+ self.assertEqual(self.get(file.unique_url, {"sid": self.sid}).text, test_content)
+ self.assertEqual(self.get(file.file_url, {"sid": self.sid}).text, test_content)
+
def generate_admin_keys():
from frappe.core.doctype.user.user import generate_keys
diff --git a/frappe/tests/test_commands.py b/frappe/tests/test_commands.py
index 7eff954054..fc39adc9a3 100644
--- a/frappe/tests/test_commands.py
+++ b/frappe/tests/test_commands.py
@@ -173,14 +173,18 @@ class BaseTestCommands(FrappeTestCase):
cmd_config = {
"test_site": TEST_SITE,
"admin_password": frappe.conf.admin_password,
- "root_login": frappe.conf.root_login,
- "root_password": frappe.conf.root_password,
"db_type": frappe.conf.db_type,
+ "db_root_username": frappe.conf.root_login,
+ "db_root_password": frappe.conf.root_password,
}
if not os.path.exists(os.path.join(TEST_SITE, "site_config.json")):
cls.execute(
- "bench new-site {test_site} --admin-password {admin_password} --db-type" " {db_type}",
+ "bench new-site {test_site} "
+ "--admin-password {admin_password} "
+ "--db-root-username {db_root_username} "
+ "--db-root-password {db_root_password} "
+ "--db-type {db_type}",
cmd_config,
)
diff --git a/frappe/tests/test_fmt_money.py b/frappe/tests/test_fmt_money.py
index fff5011189..0fbd38cbcc 100644
--- a/frappe/tests/test_fmt_money.py
+++ b/frappe/tests/test_fmt_money.py
@@ -100,10 +100,3 @@ class TestFmtMoney(FrappeTestCase):
frappe.db.set_value("Currency", "JPY", "symbol_on_right", 1)
self.assertEqual(fmt_money(100.0, format="#,###.##", currency="JPY"), "100.00 ¥")
self.assertEqual(fmt_money(100.0, format="#,###.##", currency="USD"), "$ 100.00")
-
-
-if __name__ == "__main__":
- import unittest
-
- frappe.connect()
- unittest.main()
diff --git a/frappe/translate.py b/frappe/translate.py
index fc4461e102..a0ee8001eb 100644
--- a/frappe/translate.py
+++ b/frappe/translate.py
@@ -20,35 +20,11 @@ from csv import reader, writer
import frappe
from frappe.gettext.extractors.javascript import extract_javascript
+from frappe.gettext.extractors.utils import extract_messages_from_code, is_translatable
from frappe.gettext.translate import get_translations_from_mo
-from frappe.model.utils import InvalidIncludePath, render_include
from frappe.query_builder import DocType, Field
from frappe.utils import cstr, get_bench_path, is_html, strip, strip_html_tags, unique
-TRANSLATE_PATTERN = re.compile(
- r"_\(\s*" # starts with literal `_(`, ignore following whitespace/newlines
- # BEGIN: message search
- r"([\"']{,3})" # start of message string identifier - allows: ', ", """, '''; 1st capture group
- r"(?P
((?!\1).)*)" # Keep matching until string closing identifier is met which is same as 1st capture group
- r"\1" # match exact string closing identifier
- # END: message search
- # BEGIN: python context search
- r"(\s*,\s*context\s*=\s*" # capture `context=` with ignoring whitespace
- r"([\"'])" # start of context string identifier; 5th capture group
- r"(?P((?!\5).)*)" # capture context string till closing id is found
- r"\5" # match context string closure
- r")?" # match 0 or 1 context strings
- # END: python context search
- # BEGIN: JS context search
- r"(\s*,\s*(.)*?\s*(,\s*" # skip message format replacements: ["format", ...] | null | []
- r"([\"'])" # start of context string; 11th capture group
- r"(?P((?!\11).)*)" # capture context string till closing id is found
- r"\11" # match context string closure
- r")*"
- r")*" # match one or more context string
- # END: JS context search
- r"\s*\)" # Closing function call ignore leading whitespace/newlines
-)
REPORT_TRANSLATE_PATTERN = re.compile('"([^:,^"]*):')
CSV_STRIP_WHITESPACE_PATTERN = re.compile(r"{\s?([0-9]+)\s?}")
@@ -239,9 +215,6 @@ def get_translation_dict_from_file(path, lang, app, throw=False) -> dict[str, st
def get_user_translations(lang):
- if not frappe.db:
- frappe.connect()
-
def _read_from_db():
user_translations = {}
translations = frappe.get_all(
@@ -676,59 +649,6 @@ def extract_messages_from_javascript_code(code: str) -> list[tuple[int, str, str
return messages
-def extract_messages_from_code(code):
- """
- Extracts translatable strings from a code file
- :param code: code from which translatable files are to be extracted
- """
- from jinja2 import TemplateError
-
- try:
- code = frappe.as_unicode(render_include(code))
-
- # Exception will occur when it encounters John Resig's microtemplating code
- except (TemplateError, ImportError, InvalidIncludePath, OSError) as e:
- if isinstance(e, InvalidIncludePath):
- frappe.clear_last_message()
-
- messages = []
-
- for m in TRANSLATE_PATTERN.finditer(code):
- message = m.group("message")
- context = m.group("py_context") or m.group("js_context")
- pos = m.start()
-
- if is_translatable(message):
- messages.append([pos, message, context])
-
- return add_line_number(messages, code)
-
-
-def is_translatable(m):
- if (
- re.search("[a-zA-Z]", m)
- and not m.startswith("fa fa-")
- and not m.endswith("px")
- and not m.startswith("eval:")
- ):
- return True
- return False
-
-
-def add_line_number(messages, code):
- ret = []
- messages = sorted(messages, key=lambda x: x[0])
- newlines = [m.start() for m in re.compile(r"\n").finditer(code)]
- line = 1
- newline_i = 0
- for pos, message, context in messages:
- while newline_i < len(newlines) and pos > newlines[newline_i]:
- line += 1
- newline_i += 1
- ret.append([line, message, context])
- return ret
-
-
def read_csv_file(path):
"""Read CSV file and return as list of list
@@ -1054,9 +974,6 @@ def get_all_languages(with_language_name: bool = False) -> list:
def get_all_language_with_name():
return frappe.get_all("Language", ["language_code", "language_name"], {"enabled": 1})
- if not frappe.db:
- frappe.connect()
-
if with_language_name:
return frappe.cache.get_value("languages_with_name", get_all_language_with_name)
else:
@@ -1106,44 +1023,6 @@ def print_language(language: str):
frappe.local.jenv = _jenv
-@functools.total_ordering
-class LazyTranslate:
- __slots__ = ("msg", "lang", "context")
-
- def __init__(self, msg: str, lang: str | None = None, context: str | None = None) -> None:
- self.msg = msg
- self.lang = lang
- self.context = context
-
- @property
- def value(self) -> str:
- return frappe._(str(self.msg), self.lang, self.context)
-
- def __str__(self):
- return self.value
-
- def __add__(self, other):
- if isinstance(other, (str, LazyTranslate)):
- return self.value + str(other)
- raise NotImplementedError
-
- def __radd__(self, other):
- if isinstance(other, (str, LazyTranslate)):
- return str(other) + self.value
- return NotImplementedError
-
- def __repr__(self) -> str:
- return f"'{self.value}'"
-
- # NOTE: it's required to override these methods and raise error as default behaviour will
- # return `False` in all cases.
- def __eq__(self, other):
- raise NotImplementedError
-
- def __lt__(self, other):
- raise NotImplementedError
-
-
# Backward compatibility
get_full_dict = get_all_translations
load_lang = get_translations_from_apps
diff --git a/frappe/twofactor.py b/frappe/twofactor.py
index 6b23e3c0e4..a53ec27d06 100644
--- a/frappe/twofactor.py
+++ b/frappe/twofactor.py
@@ -43,11 +43,9 @@ def toggle_two_factor_auth(state, roles=None):
def two_factor_is_enabled(user=None):
"""Return True if 2FA is enabled."""
- enabled = cint(frappe.db.get_single_value("System Settings", "enable_two_factor_auth"))
+ enabled = cint(frappe.get_system_settings("enable_two_factor_auth"))
if enabled:
- bypass_two_factor_auth = cint(
- frappe.db.get_single_value("System Settings", "bypass_2fa_for_retricted_ip_users")
- )
+ bypass_two_factor_auth = cint(frappe.get_system_settings("bypass_2fa_for_retricted_ip_users"))
if bypass_two_factor_auth and user:
user_doc = frappe.get_doc("User", user)
restrict_ip_list = (
@@ -144,7 +142,7 @@ def get_otpsecret_for_(user):
def get_verification_method():
- return frappe.db.get_single_value("System Settings", "two_factor_method")
+ return frappe.get_system_settings("two_factor_method")
def confirm_otp_token(login_manager, otp=None, tmp_id=None):
@@ -190,7 +188,7 @@ def confirm_otp_token(login_manager, otp=None, tmp_id=None):
def get_verification_obj(user, token, otp_secret):
- otp_issuer = frappe.db.get_single_value("System Settings", "otp_issuer_name")
+ otp_issuer = frappe.get_system_settings("otp_issuer_name")
verification_method = get_verification_method()
verification_obj = None
if verification_method == "SMS":
@@ -263,7 +261,7 @@ def process_2fa_for_email(user, token, otp_secret, otp_issuer, method="Email"):
def get_email_subject_for_2fa(kwargs_dict):
"""Get email subject for 2fa."""
subject_template = _("Login Verification Code from {}").format(
- frappe.db.get_single_value("System Settings", "otp_issuer_name")
+ frappe.get_system_settings("otp_issuer_name")
)
return frappe.render_template(subject_template, kwargs_dict)
@@ -281,7 +279,7 @@ def get_email_body_for_2fa(kwargs_dict):
def get_email_subject_for_qr_code(kwargs_dict):
"""Get QRCode email subject."""
subject_template = _("One Time Password (OTP) Registration Code from {}").format(
- frappe.db.get_single_value("System Settings", "otp_issuer_name")
+ frappe.get_system_settings("otp_issuer_name")
)
return frappe.render_template(subject_template, kwargs_dict)
@@ -299,7 +297,7 @@ def get_link_for_qrcode(user, totp_uri):
key = frappe.generate_hash(length=20)
key_user = f"{key}_user"
key_uri = f"{key}_uri"
- lifespan = int(frappe.db.get_single_value("System Settings", "lifespan_qrcode_image")) or 240
+ lifespan = int(frappe.get_system_settings("lifespan_qrcode_image")) or 240
frappe.cache.set_value(key_uri, totp_uri, expires_in_sec=lifespan)
frappe.cache.set_value(key_user, user, expires_in_sec=lifespan)
return get_url(f"/qrcode?k={key}")
@@ -433,7 +431,7 @@ def should_remove_barcode_image(barcode):
"""Check if it's time to delete barcode image from server."""
if isinstance(barcode, str):
barcode = frappe.get_doc("File", barcode)
- lifespan = frappe.db.get_single_value("System Settings", "lifespan_qrcode_image") or 240
+ lifespan = frappe.get_system_settings("lifespan_qrcode_image") or 240
if time_diff_in_seconds(get_datetime(), barcode.creation) > int(lifespan):
return True
return False
diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py
index 295aff2b4d..40aba696f5 100644
--- a/frappe/utils/backups.py
+++ b/frappe/utils/backups.py
@@ -370,6 +370,8 @@ class BackupGenerator:
n.write(c.read())
def take_dump(self):
+ import shlex
+
import frappe.utils
from frappe.utils.change_log import get_app_branch
@@ -419,15 +421,15 @@ class BackupGenerator:
extra = []
if self.db_type == "mariadb":
if self.backup_includes:
- extra.extend([f"'{x}'" for x in self.backup_includes])
+ extra.extend(self.backup_includes)
elif self.backup_excludes:
- extra.extend([f"--ignore-table='{self.db_name}.{table}'" for table in self.backup_excludes])
+ extra.extend([f"--ignore-table={self.db_name}.{table}" for table in self.backup_excludes])
elif self.db_type == "postgres":
if self.backup_includes:
- extra.extend([f"--table='public.\"{table}\"'" for table in self.backup_includes])
+ extra.extend([f'--table=public."{table}"' for table in self.backup_includes])
elif self.backup_excludes:
- extra.extend([f"--exclude-table-data='public.\"{table}\"'" for table in self.backup_excludes])
+ extra.extend([f'--exclude-table-data=public."{table}"' for table in self.backup_excludes])
from frappe.database import get_command
@@ -446,11 +448,11 @@ class BackupGenerator:
exc=frappe.ExecutableNotFound,
)
cmd.append(bin)
- cmd.extend(args)
+ cmd.append(shlex.join(args))
command = " ".join(["set -o pipefail;"] + cmd + ["|", gzip_exc, ">>", self.backup_path_db])
if self.verbose:
- print(command.replace(frappe.utils.esc(self.password, "$ "), "*" * 10) + "\n")
+ print(command.replace(shlex.quote(self.password), "*" * 10) + "\n")
frappe.utils.execute_in_shell(command, low_priority=True, check_exit_code=True)
diff --git a/frappe/utils/data.py b/frappe/utils/data.py
index c1026b7807..612b9f191a 100644
--- a/frappe/utils/data.py
+++ b/frappe/utils/data.py
@@ -2486,18 +2486,27 @@ def is_site_link(link: str) -> bool:
return urlparse(link).netloc == urlparse(frappe.utils.get_url()).netloc
-def add_trackers_to_url(url: str, source: str, campaign: str, medium: str = "email") -> str:
+def add_trackers_to_url(
+ url: str,
+ source: str,
+ campaign: str | None = None,
+ medium: str | None = None,
+ content: str | None = None,
+) -> str:
url_parts = list(urlparse(url))
if url_parts[0] == "mailto":
return url
- trackers = {
- "source": source,
- "medium": medium,
- }
+ trackers = {"utm_source": source}
+
+ if medium:
+ trackers["utm_medium"] = medium
if campaign:
- trackers["campaign"] = campaign
+ trackers["utm_campaign"] = campaign
+
+ if content:
+ trackers["utm_content"] = content
query = dict(parse_qsl(url_parts[4])) | trackers
diff --git a/frappe/utils/password.py b/frappe/utils/password.py
index 3ee92eabda..f5f83cef1e 100644
--- a/frappe/utils/password.py
+++ b/frappe/utils/password.py
@@ -215,4 +215,4 @@ def get_encryption_key():
def get_password_reset_limit():
- return frappe.db.get_single_value("System Settings", "password_reset_limit") or 0
+ return frappe.get_system_settings("password_reset_limit") or 3
diff --git a/frappe/utils/password_strength.py b/frappe/utils/password_strength.py
index 70ca375938..5b77eca4dd 100644
--- a/frappe/utils/password_strength.py
+++ b/frappe/utils/password_strength.py
@@ -50,9 +50,7 @@ default_feedback: "PasswordStrengthFeedback" = {
def get_feedback(score: int, sequence: list) -> "PasswordStrengthFeedback":
"""Return the feedback dictionary consisting of ("warning","suggestions") for the given sequences."""
global default_feedback
- minimum_password_score = int(
- frappe.db.get_single_value("System Settings", "minimum_password_score") or 2
- )
+ minimum_password_score = int(frappe.get_system_settings("minimum_password_score") or 2)
# Starting feedback
if len(sequence) == 0:
diff --git a/frappe/utils/pdf.py b/frappe/utils/pdf.py
index 05bcccfb88..8b521dec05 100644
--- a/frappe/utils/pdf.py
+++ b/frappe/utils/pdf.py
@@ -227,7 +227,7 @@ def read_options_from_html(html):
return str(soup), options
-def prepare_header_footer(soup):
+def prepare_header_footer(soup: BeautifulSoup):
options = {}
head = soup.find("head").contents
@@ -238,9 +238,11 @@ def prepare_header_footer(soup):
# extract header and footer
for html_id in ("header-html", "footer-html"):
- content = soup.find(id=html_id)
- if content:
- # there could be multiple instances of header-html/footer-html
+ if content := soup.find(id=html_id):
+ content = content.extract()
+ # `header/footer-html` are extracted, rendered as html
+ # and passed in wkhtmltopdf options (as '--header/footer-html')
+ # Remove instances of them from main content for render_template
for tag in soup.find_all(id=html_id):
tag.extract()
diff --git a/frappe/utils/response.py b/frappe/utils/response.py
index 9fe10545d1..116f3cd611 100644
--- a/frappe/utils/response.py
+++ b/frappe/utils/response.py
@@ -265,7 +265,15 @@ def download_backup(path):
def download_private_file(path: str) -> Response:
"""Checks permissions and sends back private file"""
- files = frappe.get_all("File", filters={"file_url": path}, fields="*")
+ if frappe.session.user == "Guest":
+ raise Forbidden(_("You don't have permission to access this file"))
+
+ filters = {"file_url": path}
+ if frappe.form_dict.fid:
+ filters["name"] = str(frappe.form_dict.fid)
+
+ files = frappe.get_all("File", filters=filters, fields="*")
+
# this file might be attached to multiple documents
# if the file is accessible from any one of those documents
# then it should be downloadable
diff --git a/frappe/website/doctype/blog_settings/blog_settings.json b/frappe/website/doctype/blog_settings/blog_settings.json
index fe5417f812..95236c8102 100644
--- a/frappe/website/doctype/blog_settings/blog_settings.json
+++ b/frappe/website/doctype/blog_settings/blog_settings.json
@@ -1,7 +1,7 @@
{
"actions": [],
"creation": "2013-03-11 17:48:16",
- "description": "Blog Settings",
+ "description": "Settings to control blog categories and interactions like comments and likes",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
@@ -131,7 +131,7 @@
"idx": 1,
"issingle": 1,
"links": [],
- "modified": "2022-10-18 15:01:36.202010",
+ "modified": "2024-01-30 14:13:12.477755",
"modified_by": "Administrator",
"module": "Website",
"name": "Blog Settings",
diff --git a/frappe/website/doctype/web_page_view/web_page_view.json b/frappe/website/doctype/web_page_view/web_page_view.json
index 2e514ffaec..57718c4cef 100644
--- a/frappe/website/doctype/web_page_view/web_page_view.json
+++ b/frappe/website/doctype/web_page_view/web_page_view.json
@@ -82,6 +82,12 @@
"fieldtype": "Data",
"label": "Medium",
"read_only": 1
+ },
+ {
+ "fieldname": "content",
+ "fieldtype": "Data",
+ "label": "Content",
+ "read_only": 1
}
],
"in_create": 1,
@@ -111,4 +117,4 @@
"sort_order": "DESC",
"states": [],
"title_field": "path"
-}
\ No newline at end of file
+}
diff --git a/frappe/website/doctype/web_page_view/web_page_view.py b/frappe/website/doctype/web_page_view/web_page_view.py
index 7c18d1ff66..05d482e0fd 100644
--- a/frappe/website/doctype/web_page_view/web_page_view.py
+++ b/frappe/website/doctype/web_page_view/web_page_view.py
@@ -47,6 +47,7 @@ def make_view_log(
source=None,
campaign=None,
medium=None,
+ content=None,
visitor_id=None,
):
if not is_tracking_enabled():
@@ -85,6 +86,7 @@ def make_view_log(
view.source = source
view.campaign = campaign
view.medium = (medium or "").lower()
+ view.content = content
view.visitor_id = visitor_id
try:
diff --git a/frappe/website/report/website_analytics/website_analytics.js b/frappe/website/report/website_analytics/website_analytics.js
index ac1445eaed..f163f5da81 100644
--- a/frappe/website/report/website_analytics/website_analytics.js
+++ b/frappe/website/report/website_analytics/website_analytics.js
@@ -38,6 +38,7 @@ frappe.query_reports["Website Analytics"] = {
{ value: "source", label: __("Source") },
{ value: "campaign", label: __("Campaign") },
{ value: "medium", label: __("Medium") },
+ { value: "content", label: __("Content") },
],
default: "path",
},
diff --git a/frappe/website/router.py b/frappe/website/router.py
index 3bffa78fee..332be38cda 100644
--- a/frappe/website/router.py
+++ b/frappe/website/router.py
@@ -322,7 +322,9 @@ def clear_routing_cache():
from frappe.website.doctype.web_form.web_form import get_published_web_forms
from frappe.website.doctype.web_page.web_page import get_dynamic_web_pages
from frappe.website.page_renderers.document_page import _find_matching_document_webview
+ from frappe.www.sitemap import get_public_pages_from_doctypes
_find_matching_document_webview.clear_cache()
get_dynamic_web_pages.clear_cache()
get_published_web_forms.clear_cache()
+ get_public_pages_from_doctypes.clear_cache()
diff --git a/frappe/www/printview.html b/frappe/www/printview.html
index a377fab8ea..9841bd045a 100644
--- a/frappe/www/printview.html
+++ b/frappe/www/printview.html
@@ -31,14 +31,18 @@
document.addEventListener('DOMContentLoaded', () => {
const page_div = document.querySelector('.page-break');
- page_div.style.display = 'flex';
- page_div.style.flexDirection = 'column';
+ if (page_div) {
+ page_div.style.display = 'flex';
+ page_div.style.flexDirection = 'column';
+ }
const footer_html = document.getElementById('footer-html');
- footer_html.classList.add('hidden-pdf');
- footer_html.classList.remove('visible-pdf');
- footer_html.style.order = 1;
- footer_html.style.marginTop = '20px';
+ if (footer_html) {
+ footer_html.classList.add('hidden-pdf');
+ footer_html.classList.remove('visible-pdf');
+ footer_html.style.order = 1;
+ footer_html.style.marginTop = '20px';
+ }
});