Skip to content

Commit 729c2e5

Browse files
committed
upgrade Python and JavaScript dependencies; allow multi-threaded code to run in Celery tasks; avoid potential error with undefined gathering mode; prevent translation of field modifiers that are Mako but are not user-visible text; fix issue with gathering dictionaries; change the way Azure mail is sent so that bcc recipients are correctly handled; added _edit_button() and _delete_button() methods to DADict; rename class method of DAGlobal from delete, which is already used, to remove; change the way that the restart process works after changing the configuration or synchronizing the Playground so that Ajax requests are sent from the browser to poll the server to check that it has restarted
1 parent 72ec9ff commit 729c2e5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+2650
-1651
lines changed

CHANGELOG.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,44 @@
11
# Change Log
22

3+
## [1.9.0] - 2025-12-31
4+
5+
### Added
6+
7+
- The `.cancel_add_or_edit()` method of `DADict`.
8+
- The methods `_edit_button()`, `_delete_button()`, and
9+
`_add_action_button()` of the `DADict` class, so that `DADict` can
10+
be subclassed for customizing the appearance of these buttons.
11+
- The `remove()` class method of `DAGlobal` that can be used to delete
12+
any key from the data store, for any given base.
13+
14+
### Changed
15+
16+
- Upgraded Python dependencies. Note that if you are using third-party
17+
Python packages, you may encounter dependency conflicts. It is
18+
possible that your interviews will need to be updated.
19+
- Upgraded Font Awesome, Bootstrap, and CodeMirror.
20+
- Use process-local rather than thread-local variables to store global
21+
information in the context of the Celery background task system.
22+
- The `plain()`, `bold()`, and `italic()` functions will return the
23+
empty string if given blank space.
24+
- Added additional information to Mako error messages.
25+
- The `.complete_elements()` method of `DAList` and `DADict` now
26+
returns an object of the same class, rather than a pure Python data
27+
structure.
28+
- The Google Drive sync and OneDrive sync result page now wait for
29+
the server to finish restarting.
30+
31+
### Fixed
32+
- Avoided `KeyError` error messages related to `gathering_mode` and
33+
`save_status`.
34+
- Avoided translation of non-user-facing Mako template text in field
35+
modifiers.
36+
- Fix to dictionary gathering.
37+
- The restart process from the Configuration page could show a 502
38+
error to the user when the Python app is behind a reverse proxy.
39+
- The Microsoft Azure mail sending feature did not support sending to
40+
bcc recipients.
41+
342
## [1.8.18] - 2025-11-01
443

544
### Fixed

Dockerfile

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -52,39 +52,39 @@ bash -c \
5252
&& cp /usr/local/bin/unoconv /usr/bin/unoconv \
5353
&& python3 -m venv --copies /usr/share/docassemble/local3.12 \
5454
&& source /usr/share/docassemble/local3.12/bin/activate \
55-
&& pip install --upgrade pip==25.2 \
55+
&& pip install --upgrade pip==25.3 \
5656
&& pip install --upgrade wheel==0.45.1 \
5757
&& pip install --upgrade mod_wsgi==5.0.2 \
5858
&& pip install --upgrade \
59-
acme==4.1.1 \
60-
certbot==4.1.1 \
61-
certbot-apache==4.1.1 \
62-
certbot-nginx==4.1.1 \
63-
certifi==2025.8.3 \
64-
cffi==1.17.1 \
65-
charset-normalizer==3.4.2 \
66-
click==8.2.1 \
59+
acme==5.2.2 \
60+
certbot==5.2.2 \
61+
certbot-apache==5.2.2 \
62+
certbot-nginx==5.2.2 \
63+
certifi==2025.11.12 \
64+
cffi==2.0.0 \
65+
charset-normalizer==3.4.4 \
66+
click==8.3.1 \
6767
ConfigArgParse==1.7.1 \
6868
configobj==5.0.9 \
69-
cryptography==45.0.7 \
69+
cryptography==46.0.3 \
7070
distro==1.9.0 \
71-
idna==3.10 \
72-
joblib==1.5.1 \
73-
josepy==2.1.0 \
74-
nltk==3.9.1 \
71+
idna==3.11 \
72+
joblib==1.5.3 \
73+
josepy==2.2.0 \
74+
nltk==3.9.2 \
7575
parsedatetime==2.6 \
76-
pycparser==2.22 \
77-
pyOpenSSL==25.1.0 \
78-
pyparsing==3.2.3 \
79-
pyRFC3339==2.0.1 \
76+
pycparser==2.23 \
77+
pyOpenSSL==25.3.0 \
78+
pyparsing==3.3.1 \
79+
pyRFC3339==2.1.0 \
8080
python-augeas==1.2.0 \
8181
pytz==2025.2 \
82-
regex==2025.7.34 \
83-
requests==2.32.4 \
82+
regex==2025.11.3 \
83+
requests==2.32.5 \
8484
six==1.17.0 \
8585
tqdm==4.67.1 \
86-
typing_extensions==4.14.1 \
87-
urllib3==2.5.0 \
86+
typing_extensions==4.15.0 \
87+
urllib3==2.6.2 \
8888
&& pip install \
8989
/tmp/docassemble/docassemble_base \
9090
/tmp/docassemble/docassemble_demo \

docassemble_base/docassemble/base/data/sources/base-words.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,7 @@
596596
"Montserrat": Null
597597
"Morocco": Null
598598
"Move down": Null
599+
"Move up": Null
599600
"Mozambique": Null
600601
"Multiselect box": Null
601602
"Must be a two-letter code": Null

docassemble_base/docassemble/base/functions.py

Lines changed: 47 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import markdown
2626
import nltk
2727
import ruamel.yaml
28+
from types import SimpleNamespace
2829
from docassemble.base.save_status import SS_NEW, SS_OVERWRITE, SS_IGNORE
2930

3031
try:
@@ -205,7 +206,10 @@ def set_gathering_mode(mode, instanceName):
205206
# logmessage("set_gathering_mode: using " + str(get_current_variable()))
206207
this_thread.gathering_mode[instanceName] = get_current_variable()
207208
else:
208-
del this_thread.gathering_mode[instanceName]
209+
try:
210+
del this_thread.gathering_mode[instanceName]
211+
except KeyError:
212+
pass
209213

210214

211215
def get_gathering_mode(instanceName):
@@ -229,7 +233,10 @@ def reset_gathering_mode(*pargs):
229233
todel.append(instanceName)
230234
# logmessage("reset_gathering_mode: deleting " + repr([y for y in todel]))
231235
for item in todel:
232-
del this_thread.gathering_mode[item]
236+
try:
237+
del this_thread.gathering_mode[item]
238+
except KeyError:
239+
pass
233240

234241

235242
def set_uid(uid):
@@ -2176,31 +2183,41 @@ def __init__(self):
21762183
# self.__dict__.update(kw)
21772184

21782185
this_thread = threading.local()
2179-
this_thread.language = server.default_language
2180-
this_thread.dialect = server.default_dialect
2181-
this_thread.voice = server.default_voice
2182-
this_thread.country = server.default_country
2183-
this_thread.locale = server.default_locale
2184-
this_thread.current_info = {}
2185-
this_thread.internal = {}
2186-
this_thread.initialized = False
2187-
this_thread.session_id = None
2188-
this_thread.current_package = None
2189-
this_thread.interview = None
2190-
this_thread.interview_status = None
2191-
this_thread.evaluation_context = None
2192-
this_thread.gathering_mode = {}
2193-
this_thread.global_vars = GenericObject()
2194-
this_thread.current_variable = []
2195-
this_thread.open_files = set()
2196-
this_thread.markdown = markdown.Markdown(extensions=['smarty', 'markdown.extensions.sane_lists', 'markdown.extensions.tables', 'markdown.extensions.attr_list', 'markdown.extensions.md_in_html', 'footnotes'], output_format='html5')
2197-
this_thread.saved_files = {}
2198-
this_thread.message_log = []
2199-
this_thread.misc = {}
2200-
this_thread.probing = False
2201-
this_thread.prevent_going_back = False
2202-
this_thread.current_question = None
2203-
this_thread.current_section = None
2186+
2187+
def populate_this_thread_defaults():
2188+
this_thread.language = server.default_language
2189+
this_thread.dialect = server.default_dialect
2190+
this_thread.voice = server.default_voice
2191+
this_thread.country = server.default_country
2192+
this_thread.locale = server.default_locale
2193+
this_thread.current_info = {}
2194+
this_thread.internal = {}
2195+
this_thread.initialized = False
2196+
this_thread.session_id = None
2197+
this_thread.current_package = None
2198+
this_thread.interview = None
2199+
this_thread.interview_status = None
2200+
this_thread.evaluation_context = None
2201+
this_thread.gathering_mode = {}
2202+
this_thread.global_vars = GenericObject()
2203+
this_thread.current_variable = []
2204+
this_thread.open_files = set()
2205+
this_thread.markdown = markdown.Markdown(extensions=['smarty', 'markdown.extensions.sane_lists', 'markdown.extensions.tables', 'markdown.extensions.attr_list', 'markdown.extensions.md_in_html', 'footnotes'], output_format='html5')
2206+
this_thread.saved_files = {}
2207+
this_thread.message_log = []
2208+
this_thread.misc = {}
2209+
this_thread.probing = False
2210+
this_thread.prevent_going_back = False
2211+
this_thread.current_question = None
2212+
this_thread.current_section = None
2213+
2214+
populate_this_thread_defaults()
2215+
2216+
2217+
def enable_threading():
2218+
global this_thread
2219+
this_thread = SimpleNamespace()
2220+
populate_this_thread_defaults()
22042221

22052222

22062223
def backup_thread_variables():
@@ -5470,7 +5487,7 @@ def class_name(the_object):
54705487
def plain(text, default=None):
54715488
"""Substitutes empty string or the value of the default parameter if the text is empty."""
54725489
ensure_definition(text, default)
5473-
if text is None or not str(text).strip():
5490+
if text is None or str(text).strip() == '':
54745491
if default is None:
54755492
return ''
54765493
return default
@@ -5480,7 +5497,7 @@ def plain(text, default=None):
54805497
def bold(text, default=None):
54815498
"""Adds Markdown tags to make the text bold if it is not blank."""
54825499
ensure_definition(text, default)
5483-
if text is None or not str(text).strip():
5500+
if text is None or str(text).strip() == '':
54845501
if default is None:
54855502
return ''
54865503
return '**' + str(default) + '**'
@@ -5490,7 +5507,7 @@ def bold(text, default=None):
54905507
def italic(text, default=None):
54915508
"""Adds Markdown tags to make the text italic if it is not blank."""
54925509
ensure_definition(text, default)
5493-
if text is None or not str(text).strip():
5510+
if text is None or str(text).strip() == '':
54945511
if default is None:
54955512
return ''
54965513
return '_' + str(default) + '_'

docassemble_base/docassemble/base/parse.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3212,22 +3212,22 @@ def __init__(self, orig_data, caller, **kwargs):
32123212
raise DASourceError("The css classifier specifier in an action buttons item must refer to plain text." + self.idebug(data))
32133213
if not isinstance(forget_prior, bool):
32143214
raise DASourceError("The forget prior specifier in an action buttons item must refer to true or false." + self.idebug(data))
3215-
button = {'action': TextObject(definitions + action, question=self), 'label': TextObject(definitions + label, question=self), 'color': TextObject(definitions + color, question=self)}
3215+
button = {'action': TextObject(definitions + action, question=self), 'label': TextObject(definitions + label, question=self), 'color': TextObject(definitions + color, question=self, translate=False)}
32163216
button['show if'] = showif
32173217
if target is not None:
3218-
button['target'] = TextObject(definitions + target, question=self)
3218+
button['target'] = TextObject(definitions + target, question=self, translate=False)
32193219
else:
32203220
button['target'] = None
32213221
if icon is not None:
3222-
button['icon'] = TextObject(definitions + icon, question=self)
3222+
button['icon'] = TextObject(definitions + icon, question=self, translate=False)
32233223
else:
32243224
button['icon'] = None
32253225
if placement is not None:
3226-
button['placement'] = TextObject(definitions + placement, question=self)
3226+
button['placement'] = TextObject(definitions + placement, question=self, translate=False)
32273227
else:
32283228
button['placement'] = None
32293229
if css_class is not None:
3230-
button['css_class'] = TextObject(definitions + css_class, question=self)
3230+
button['css_class'] = TextObject(definitions + css_class, question=self, translate=False)
32313231
else:
32323232
button['css_class'] = None
32333233
if forget_prior:

docassemble_base/docassemble/base/util.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3783,7 +3783,8 @@ def gather(self, item_object_type=None, number=None, minimum=None, complete_attr
37833783
elif hasattr(self, 'there_is_another'):
37843784
# logmessage("0gather " + self.instanceName + ": del on there_is_another")
37853785
delattr(self, 'there_is_another')
3786-
while (number is not None and len(self.elements) < int(number)) or (minimum is not None and len(self.elements) < int(minimum)) or (self.ask_number is False and minimum != 0 and (self.there_is_another or (hasattr(self, 'there_is_one_other') and self.there_is_one_other))):
3786+
should_break_out = False
3787+
while not should_break_out and ((number is not None and len(self.elements) < int(number)) or (minimum is not None and len(self.elements) < int(minimum)) or (self.ask_number is False and minimum != 0 and (self.there_is_another or (hasattr(self, 'there_is_one_other') and self.there_is_one_other)))):
37873788
if item_object_type is not None:
37883789
self.initializeObject(self.new_item_name, item_object_type, **new_item_parameters)
37893790
if hasattr(self, 'there_is_one_other'):
@@ -3808,6 +3809,7 @@ def gather(self, item_object_type=None, number=None, minimum=None, complete_attr
38083809
else:
38093810
the_name = self.new_item_name
38103811
self.__getitem__(the_name) # pylint: disable=unnecessary-dunder-call
3812+
should_break_out = True
38113813
if hasattr(self, 'new_item_name'):
38123814
delattr(self, 'new_item_name')
38133815
if hasattr(self, 'there_is_one_other'):
@@ -4105,6 +4107,12 @@ def pronoun_subjective(self, **kwargs):
41054107
return capitalize_func(output)
41064108
return output
41074109

4110+
def _edit_button(self, url, classes):
4111+
return f'<a href="{url}" role="button" class="{classes}"><span class="text-nowrap"><i class="fa-solid fa-pencil-alt"></i> {word("Edit")}</span></a> '
4112+
4113+
def _delete_button(self, url, classes):
4114+
return f'<a href="{url}" role="button" class="{classes}"><span class="text-nowrap"><i class="fa-solid fa-trash"></i> {word("Delete")}</span></a>'
4115+
41084116
def item_actions(self, *pargs, **kwargs):
41094117
"""Returns HTML for editing the items in a dictionary"""
41104118
the_args = list(pargs)
@@ -4139,13 +4147,13 @@ def item_actions(self, *pargs, **kwargs):
41394147
items += [{'action': '_da_define', 'arguments': {'variables': [item.instanceName + '.' + attrib for attrib in self._complete_attributes()]}}]
41404148
if ensure_complete:
41414149
items += [{'action': '_da_dict_ensure_complete', 'arguments': {'group': self.instanceName}}]
4142-
output += '<a href="' + docassemble.base.functions.url_action('_da_dict_edit', items=items) + '" role="button" class="btn btn-sm ' + server.button_class_prefix + server.daconfig['button colors'].get('edit', 'secondary') + ' btn-darevisit"><i class="fa-solid fa-pencil-alt"></i> ' + word('Edit') + '</a> '
4150+
output += self._edit_button(docassemble.base.functions.url_action('_da_dict_edit', items=items), 'btn btn-sm ' + server.button_class_prefix + server.daconfig['button colors'].get('edit', 'secondary') + ' btn-darevisit')
41434151
if use_delete and can_delete:
41444152
if kwargs.get('confirm', False):
41454153
areyousure = ' daremovebutton'
41464154
else:
41474155
areyousure = ''
4148-
output += '<a href="' + docassemble.base.functions.url_action('_da_dict_remove', dict=self.instanceName, item=repr(index)) + '" role="button" class="btn btn-sm ' + server.button_class_prefix + server.daconfig['button colors'].get('delete', 'danger') + ' btn-darevisit' + areyousure + '"><i class="fa-solid fa-trash"></i> ' + word('Delete') + '</a>'
4156+
output += self._delete_button(docassemble.base.functions.url_action('_da_dict_remove', dict=self.instanceName, item=repr(index)), 'btn btn-sm ' + server.button_class_prefix + server.daconfig['button colors'].get('delete', 'danger') + ' btn-darevisit' + areyousure)
41494157
if kwargs.get('edit_url_only', False):
41504158
return docassemble.base.functions.url_action('_da_dict_edit', items=items)
41514159
if kwargs.get('delete_url_only', False):
@@ -7083,8 +7091,8 @@ def defined(cls, base, key):
70837091
return server.server_sql_defined(globalkey)
70847092

70857093
@classmethod
7086-
def delete(cls, base, key):
7087-
"""Deletes the key from the base in the global storage area."""
7094+
def remove(cls, base, key):
7095+
"""Deletes the key from the data store."""
70887096
if base == 'interview':
70897097
globalkey = 'da:daglobal:i:' + str(this_thread.current_info.get('yaml_filename', '')) + ':' + str(key)
70907098
elif base == 'global':
@@ -9968,7 +9976,7 @@ def ocr_file(image_file, language=None, psm=6, f=None, l=None, x=None, y=None, W
99689976
except subprocess.CalledProcessError as err:
99699977
raise DAError("ocr_file: failed to OCR file: " + str(err) + " " + str(err.output.decode()))
99709978
elif TESSERACT_MODE == REMOTE:
9971-
result = run_tesseract.delay(['stdin', 'stdout', '-l', str(lang), '--psm', str(psm)], mode=0, file_path=file_to_read.name).get(disable_sync_subtasks=False)
9979+
result = run_tesseract.delay(['stdin', 'stdout', '-l', str(lang), '--psm', str(psm)], mode=0, file_path=file_to_read.name).get(disable_sync_subtasks=False) # pylint: disable=possibly-used-before-assignment
99729980
if not result.ok:
99739981
raise DAError("ocr_file: failed to OCR file")
99749982
text = result.content
@@ -11304,7 +11312,7 @@ def ocr_pdf(*pargs, target=None, filename=None, lang=None, psm=6, dafilelist=Non
1130411312
result = 1
1130511313
logmessage("ocr_pdf: call to gs took too long")
1130611314
elif TESSERACT_MODE == REMOTE:
11307-
result = run_gs.delay(params[1:]).get(disable_sync_subtasks=False)
11315+
result = run_gs.delay(params[1:]).get(disable_sync_subtasks=False) # pylint: disable=possibly-used-before-assignment
1130811316
if result is None:
1130911317
result = 1
1131011318
logmessage("ocr_pdf: call to gs took too long")
@@ -11372,6 +11380,7 @@ def ocr_pdf(*pargs, target=None, filename=None, lang=None, psm=6, dafilelist=Non
1137211380

1137311381
def ocr_page(indexno, doc=None, lang=None, pdf_to_ppm='pdf_to_ppm', ocr_resolution=300, psm=6, page=None, x=None, y=None, W=None, H=None, user_code=None, user=None, pdf=False, preserve_color=False): # pylint: disable=unused-argument
1137411382
"""Runs optical character recognition on an image or a page of a PDF file and returns the recognized text."""
11383+
text = ''
1137511384
if page is None:
1137611385
page = 1
1137711386
if psm is None:

0 commit comments

Comments
 (0)