Skip to content

Commit 0b49127

Browse files
committed
Cleanup hidden button handling; enable name sorting in import parties; check for empty method step description; move Missing Value Codes sections up on pages; reduce out-of-memory failures in Check Data Tables (fixes #150, fixes #157, closes #154, closes #153, closes #152)
1 parent 2eb9c7b commit 0b49127

File tree

17 files changed

+478
-293
lines changed

17 files changed

+478
-293
lines changed

webapp/config.py.template

+4-1
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,7 @@ class Config(object):
9898
COLLABORATION_BETA_TESTERS_ONLY = False
9999
COLLABORATION_BETA_TESTERS = []
100100

101-
LOG_FILE_HANDLING_DETAILS = False
101+
LOG_FILE_HANDLING_DETAILS = False
102+
LOG_MEMORY_USAGE = False
103+
LOG_REQUESTS = False
104+
LOG_RESPONSES = False

webapp/home/forms.py

+6
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,12 @@ class ImportEMLItemsForm(FlaskForm):
161161
target = RadioField('Target', choices=[], validators=[])
162162

163163

164+
class ImportPartiesFromTemplateForm(FlaskForm):
165+
to_import = MultiCheckboxField('Import', choices=[], validators=[])
166+
to_import_sorted = MultiCheckboxField('Import', choices=[], validators=[])
167+
target = RadioField('Target', choices=[], validators=[])
168+
169+
164170
class SelectUserForm(FlaskForm):
165171
user = RadioField('User', choices=[], validators=[])
166172

webapp/home/home_utils.py

+107-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
""" Basic utility functions. """
22

3-
import sys, daiquiri
3+
import sys
4+
import daiquiri
45

56
from flask import session
67
from flask_login import current_user
78

9+
import psutil
10+
from pympler import muppy, summary
11+
12+
from webapp.home.utils.hidden_buttons import is_hidden_button, handle_hidden_buttons
13+
814
from metapype.model.node import Node
915

10-
RELEASE_NUMBER = '2023.11.29'
16+
RELEASE_NUMBER = '2024.01.03'
1117

1218

1319
def extract_caller_module_name():
@@ -68,8 +74,106 @@ def log_available_memory():
6874
"""
6975
Log the available system memory.
7076
"""
71-
import psutil
7277
available_memory = psutil.virtual_memory().available / 1024 / 1024
7378
process_usage = psutil.Process().memory_info().rss / 1024 / 1024
7479
log_info(f"Memory usage: available system memory:{available_memory:.1f} MB process usage:{process_usage:.1f} MB")
7580

81+
82+
# def profile_and_save(func, *args, **kwargs):
83+
# # Profile the function and get memory usage
84+
# mem_usage = memory_usage((func, args, kwargs))
85+
#
86+
# # Save the results to a file
87+
# with open('memory_profile.txt', 'a') as file:
88+
# file.write("Memory usage (in MB):\n")
89+
# for point in mem_usage:
90+
# file.write(f"{point}\n")
91+
#
92+
#
93+
# def profile_and_save_with_return(func, *args, **kwargs):
94+
# # Function to wrap the original function and capture its return value
95+
# def wrapper():
96+
# return func(*args, **kwargs)
97+
#
98+
# # Profile the memory usage of the wrapper function
99+
# mem_usage, retval = memory_usage((wrapper,), retval=True, max_usage=True)
100+
#
101+
# # Save the memory usage data to a file
102+
# with open('memory_profile.txt', 'w') as file:
103+
# file.write("Memory usage (in MB):\n")
104+
# file.write(f"{mem_usage}\n")
105+
#
106+
# # Return the original function's return value
107+
# return retval
108+
109+
110+
def log_profile_details(all_objects_before, all_objects_after):
111+
from pympler import asizeof
112+
113+
ids_before = {id(obj) for obj in all_objects_before}
114+
ids_after = {id(obj) for obj in all_objects_after}
115+
116+
# Find new object IDs
117+
new_ids = ids_after - ids_before
118+
119+
# Retrieve new objects
120+
new_objects = [obj for obj in all_objects_after if id(obj) in new_ids]
121+
122+
# Optionally, filter by type (e.g., list)
123+
new_lists = [obj for obj in new_objects if isinstance(obj, list)]
124+
125+
# Sort by size and save the results to a file
126+
# original_stdout = sys.stdout
127+
with open('memory_profile.txt', 'a') as file:
128+
sys.stdout = file
129+
file.write("*********** Details ***********\n")
130+
obj = new_lists[0]
131+
print(f"List size: {asizeof.asizeof(obj)} bytes, Length: {len(obj)}, Example content: {str(obj[:10])}...")
132+
133+
for obj in sorted(new_lists, key=lambda x: asizeof.asizeof(x), reverse=True)[:5]:
134+
print(f"List size: {asizeof.asizeof(obj)} bytes, Length: {len(obj)}, Example content: {str(obj[:10])}...")
135+
file.write("*********** End of details ***********\n")
136+
# sys.stdout = original_stdout
137+
138+
139+
def profile_and_save(func, *args, **kwargs):
140+
# Profile the function and get memory usage
141+
142+
# Start tracking memory
143+
all_objects_before = muppy.get_objects()
144+
summary_1 = summary.summarize(all_objects_before)
145+
146+
# Execute the function
147+
retval = func(*args, **kwargs)
148+
149+
# Check memory after the function execution
150+
all_objects_after = muppy.get_objects()
151+
summary_2 = summary.summarize(all_objects_after)
152+
153+
# Compare before and after snapshots
154+
diff = summary.get_diff(summary_1, summary_2)
155+
156+
original_stdout = sys.stdout
157+
158+
try:
159+
# Save the results to a file
160+
with open('memory_profile.txt', 'a') as file:
161+
sys.stdout = file
162+
file.write(f"*********** Summary of memory usage: {func_name} ***********\n")
163+
summary.print_(diff)
164+
file.write("*********** End of summary ***********\n")
165+
166+
# Log details about the new objects
167+
# log_profile_details(all_objects_before, all_objects_after)
168+
except Exception as e:
169+
log_error(f"Error writing memory profile: {e}")
170+
raise
171+
finally:
172+
sys.stdout = original_stdout
173+
174+
return retval
175+
176+
177+
178+
179+

webapp/home/templates/base.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
{% import '_macros.html' as macros %}
1212

1313
{# The following must agree with RELEASE_NUMBER in home_utils.py #}
14-
{% set release_number = '2023.11.29' %}
14+
{% set release_number = '2024.01.03' %}
1515
{% set optional = 'Black' %}
1616

1717
{% block head %}

webapp/home/templates/import_parties_2.html

+86-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
{% set help_import_responsible_parties_2_btn = help_import_responsible_parties_2_id ~ '_btn' %}
77
{% set help_import_responsible_parties_2_dialog = help_import_responsible_parties_2_id ~ '_dialog' %}
88

9-
<body onload="setTarget('{{ target }}');">
9+
<body onload="setTarget('{{ target }};');hideSorted(true);">
1010
<script>
1111
function setTarget(target) {
1212
let target_list = document.getElementsByName('target');
@@ -16,6 +16,19 @@
1616
}
1717
}
1818
}
19+
function hideSorted(val) {
20+
let unsorted = document.getElementById('to_import');
21+
let sorted = document.getElementById('to_import_sorted');
22+
if (sorted) {
23+
if (val) {
24+
unsorted.style.display = 'block';
25+
sorted.style.display = 'none';
26+
} else {
27+
unsorted.style.display = 'none';
28+
sorted.style.display = 'block';
29+
}
30+
}
31+
}
1932
</script>
2033

2134
<table>
@@ -27,7 +40,78 @@
2740
<form method="POST" action="" class="form" role="form">
2841
{{ form.csrf_token }}
2942
<table>
30-
{{ macros.import_selection(form, source_filename, "responsible parties") }}
43+
44+
<script>
45+
function choose_options(all) {
46+
$('input:checkbox').not(this).prop('checked', all);
47+
}
48+
function reconcile_checkbox_statuses(sorted) {
49+
// sorted is a boolean indicating whether the sorted list is the one displayed, i.e., the one whose
50+
// checkbox statuses should be copied to the other list.
51+
let from_items = null;
52+
let to_items = null;
53+
if (sorted) {
54+
from_items = Array.from(document.getElementById('to_import_sorted').getElementsByTagName('li'));
55+
to_items = Array.from(document.getElementById('to_import').getElementsByTagName('li'));
56+
} else {
57+
from_items = Array.from(document.getElementById('to_import').getElementsByTagName('li'));
58+
to_items = Array.from(document.getElementById('to_import_sorted').getElementsByTagName('li'));
59+
}
60+
from_items.forEach(function(item, index) {
61+
let checkbox = item.querySelector('input[type="checkbox"]');
62+
checked = from_items[index].querySelector('input[type="checkbox"]').checked;
63+
default_value = checkbox.defaultValue;
64+
// Find the item in the other list
65+
let other_item = to_items.find(function(element) {
66+
return element.querySelector('input[type="checkbox"]').defaultValue == default_value;
67+
});
68+
if (other_item) {
69+
other_item.querySelector('input[type="checkbox"]').checked = checked;
70+
}
71+
});
72+
}
73+
function toggle_order() {
74+
let sorted = document.getElementById('sorted');
75+
76+
let to_import = document.getElementById('to_import');
77+
let to_import_sorted = document.getElementById('to_import_sorted');
78+
let items = null;
79+
80+
reconcile_checkbox_statuses(sorted.value == 'true');
81+
82+
if (sorted.value == 'false') {
83+
items = Array.from(to_import.getElementsByTagName('li'));
84+
to_import.style.display = 'none';
85+
to_import_sorted.style.display = 'block';
86+
sorted.value = 'true';
87+
} else {
88+
items = Array.from(to_import_sorted.getElementsByTagName('li'));
89+
to_import.style.display = 'block';
90+
to_import_sorted.style.display = 'none';
91+
sorted.value = 'false';
92+
}
93+
}
94+
function sort_state() {
95+
let sorted = document.getElementById('sorted');
96+
return sorted.value;
97+
}
98+
</script>
99+
<tr><h5>Select responsible parties to import from "{{ source_filename }}":
100+
&nbsp;&nbsp;&nbsp;&nbsp;[&nbsp;<button class="link" type="button" onclick="choose_options(true); return false;">Select All</button>&nbsp;]
101+
&nbsp;&nbsp;&nbsp;&nbsp;[&nbsp;<button class="link" type="button" onclick="choose_options(false); return false;">Clear All</button>&nbsp;]
102+
&nbsp;&nbsp;&nbsp;&nbsp;[&nbsp;<button class="link" type="button" onclick="toggle_order(); return false;">Toggle Sort</button>&nbsp;]
103+
<input type="hidden" id="sorted" value="false" >
104+
</h5>
105+
</tr>
106+
{% if form.to_import.choices %}
107+
<tr>{{ form.to_import(style="list-style:none;") }}</tr>
108+
{% if form.to_import_sorted.choices %}
109+
<tr>{{ form.to_import_sorted(style="list-style:none;") }}</tr>
110+
{% endif %}
111+
{% else %}
112+
<tr><span style="font-style: italic;">&nbsp;&nbsp;"{{ target_filename }}" contains no {{ item_name }}.<p></p></span><br></tr>
113+
{% endif %}
114+
31115
{% if form.to_import.choices %}
32116
<tr><h5>Import as:</h5></tr>
33117
<tr>{{ form.target(style="list-style:none;") }}</tr>

webapp/home/utils/hidden_buttons.py

+21-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
so the Title method can save the title data and then go to the Open page.
1414
"""
1515

16-
from flask import request
16+
from flask import request, redirect, url_for
17+
from flask_login import current_user
18+
19+
from functools import wraps
1720

1821
from webapp.buttons import *
1922
from webapp.pages import *
@@ -106,4 +109,20 @@ def check_val_for_hidden_buttons(val, target_page):
106109
for button in HIDDEN_TARGETS:
107110
if val == button:
108111
return HIDDEN_TARGETS[button]
109-
return target_page
112+
return target_page
113+
114+
115+
def non_saving_hidden_buttons_decorator(func):
116+
"""
117+
Decorator to check for hidden buttons and handle them for routes that do not save changes to the document,
118+
i.e., routes that don't involve the user filling out a form whose contents need to be saved.
119+
For routes that do save changes, we need to go to the route and save the changes before handling the hidden
120+
buttons.
121+
"""
122+
@wraps(func)
123+
def wrapper(*args, **kwargs):
124+
if is_hidden_button():
125+
current_document = current_user.get_filename()
126+
return redirect(url_for(handle_hidden_buttons(), filename=current_document))
127+
return func(*args, **kwargs)
128+
return wrapper

webapp/home/utils/import_nodes.py

+11-6
Original file line numberDiff line numberDiff line change
@@ -300,15 +300,15 @@ def import_related_project_nodes(target_package, node_ids_to_import):
300300
load_and_save.save_both_formats(target_package, target_eml_node)
301301

302302

303-
def compose_rp_label(rp_node:Node=None):
303+
def compose_rp_label(rp_node:Node=None, last_name_first:bool=False):
304304
"""
305305
Compose a label for a responsible party node. The label is a string that can be displayed to the user.
306306
307307
What we display for a responsible party depends on the type of responsible party, i.e. whether it is an individual,
308308
organization, or position. We also display the role, if any.
309309
"""
310310

311-
def compose_individual_name_label(rp_node: Node = None):
311+
def compose_individual_name_label(rp_node: Node = None, last_name_first: bool = False):
312312
label = ''
313313
if rp_node:
314314
salutation_nodes = rp_node.find_all_children(names.SALUTATION)
@@ -317,17 +317,22 @@ def compose_individual_name_label(rp_node: Node = None):
317317
if salutation_node and salutation_node.content:
318318
label = label + " " + salutation_node.content
319319

320+
given_name = ''
320321
given_name_nodes = rp_node.find_all_children(names.GIVENNAME)
321322
if given_name_nodes:
322323
for given_name_node in given_name_nodes:
323324
if given_name_node and given_name_node.content:
324-
label = label + " " + given_name_node.content
325+
given_name = given_name + " " + given_name_node.content
325326

327+
surname = ''
326328
surname_node = rp_node.find_child(names.SURNAME)
327329
if surname_node and surname_node.content:
328-
label = label + " " + surname_node.content
330+
surname = surname_node.content
329331

330-
return label
332+
if last_name_first:
333+
return surname + "," + given_name
334+
else:
335+
return given_name + " " + surname
331336

332337
def compose_simple_label(rp_node: Node = None, child_node_name: str = ''):
333338
label = ''
@@ -341,7 +346,7 @@ def compose_simple_label(rp_node: Node = None, child_node_name: str = ''):
341346
if rp_node:
342347
individual_name_node = rp_node.find_child(names.INDIVIDUALNAME)
343348
individual_name_label = (
344-
compose_individual_name_label(individual_name_node))
349+
compose_individual_name_label(individual_name_node, last_name_first))
345350
role_node = rp_node.find_child(names.ROLE)
346351
if role_node:
347352
role_label = role_node.content

0 commit comments

Comments
 (0)