Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

75 add option to sum in editing rates #96

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1774325
New form for adding and multiplying rate options implemented
amynickolls Oct 1, 2024
53a0179
form validation updated
amynickolls Oct 2, 2024
14878ca
form logic and JSON encoder fixes
amynickolls Oct 3, 2024
4ff6a22
multinomial combine_rates takes dataframe with add and multiply rates
amynickolls Oct 3, 2024
0103767
errors added to dynamicform tabletags
amynickolls Oct 3, 2024
e578abd
JSONen/decoder fixes for single index
amynickolls Oct 3, 2024
d3ae505
form cleaning for single index update
amynickolls Oct 3, 2024
9f0b825
typing adjustments
amynickolls Oct 3, 2024
0c22bad
dynamicrateform implemented across all rate change views
amynickolls Oct 3, 2024
1c4bcf9
adjusted initialisation of initial values in form
amynickolls Oct 3, 2024
fcd7085
combine_rates logic update
amynickolls Oct 3, 2024
d8eca5b
form initial value fix
amynickolls Oct 3, 2024
0eefd08
unit tests
amynickolls Oct 3, 2024
8daecb2
template update
amynickolls Oct 3, 2024
d567854
DRYing rate saving method
amynickolls Oct 14, 2024
aef3df1
return to left join in combine rates
amynickolls Oct 14, 2024
28b3e8d
form changes
amynickolls Oct 15, 2024
6e0a8d1
form changes
amynickolls Oct 15, 2024
452406a
form changes
amynickolls Oct 15, 2024
c59f3d4
JSON and saving adjustments
amynickolls Oct 15, 2024
6fbdba1
test updates
amynickolls Oct 15, 2024
cecf286
deleted valueerror
amynickolls Oct 16, 2024
87e67c1
delete print statements
amynickolls Oct 17, 2024
ef4efba
Merge branch 'main' into 75-add-option-to-sum-in-editing-rates
amynickolls Dec 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 122 additions & 1 deletion dm_regional_app/forms.py
amynickolls marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ def clean(self):
value = cleaned_data.get(field_name)
if value is not None and value < 0:
negative_numbers.append(field_name)
self.add_error(field_name, "Negative numbers are not allowed!")
self.add_error(field_name, "Negative numbers are not allowed")

if negative_numbers:
raise forms.ValidationError(
Expand Down Expand Up @@ -325,3 +325,124 @@ class DataSourceUploadForm(forms.Form):
)
],
)


class DynamicRateForm(forms.Form):
def __init__(self, *args, **kwargs):
self.dataframe = kwargs.pop("dataframe", None)
initial_data = kwargs.pop("initial_data", pd.DataFrame)

super(DynamicRateForm, self).__init__(*args, **kwargs)
self.initialize_fields(initial_data)

def initialize_fields(self, initial_data):
"""
Dynamically create two fields for each option: one for multiplication and one for addition.
"""
for index in self.dataframe.index:
multiply_field_name = f"multiply_{index}"
add_field_name = f"add_{index}"

initial_multiply = None
initial_add = None

# Set initial values if available, otherwise leave as None
if initial_data is not None:
try:
initial_multiply = initial_data.loc[index, "multiply_value"]
except (KeyError, IndexError, ValueError):
pass

try:
initial_add = initial_data.loc[index, "add_value"]
except (KeyError, IndexError, ValueError):
pass
amynickolls marked this conversation as resolved.
Show resolved Hide resolved

# Create form fields for each option
self.fields[multiply_field_name] = forms.FloatField(
required=False,
initial=initial_multiply,
widget=forms.NumberInput(attrs={"placeholder": "Multiply Rate"}),
)
self.fields[add_field_name] = forms.FloatField(
required=False,
initial=initial_add,
widget=forms.NumberInput(attrs={"placeholder": "Add to Rate"}),
)

def clean(self):
"""
Validate form data and ensure:
Only a multiply or an add value exists for each rate
No negative values are entered in multiply fields
"""
cleaned_data = super().clean()

for index in self.dataframe.index:
multiply_field_name = f"multiply_{index}"
add_field_name = f"add_{index}"

multiply_value = cleaned_data.get(multiply_field_name)
add_value = cleaned_data.get(add_field_name)

# Validation logic: Ensure only one field is filled or none
if multiply_value is not None and add_value is not None:
self.add_error(
multiply_field_name, "You cannot fill both multiply and add fields."
)
self.add_error(
add_field_name, "You cannot fill both multiply and add fields."
)

for index in self.dataframe.index:
multiply_field_name = f"multiply_{index}"
multiply_value = cleaned_data.get(multiply_field_name)
if multiply_value is not None and multiply_value < 0:
self.add_error(
multiply_field_name,
"You cannot multiply a rate by a negative value",
)
amynickolls marked this conversation as resolved.
Show resolved Hide resolved

return cleaned_data

def save(self):
"""
Output a DataFrame with the inputs for multiplication and addition fields.
"""
transitions = []
multiply_values = []
add_values = []

# Loop through the form fields and collect the input values
for index in self.dataframe.index:
multiply_value = self.cleaned_data.get(f"multiply_{index}", None)
add_value = self.cleaned_data.get(f"add_{index}", None)

if multiply_value is not None or add_value is not None:
# multiply_value = np.nan if multiply_value is None else multiply_value
amynickolls marked this conversation as resolved.
Show resolved Hide resolved
# add_value = np.nan if add_value is None else add_value
transitions.append(index)
multiply_values.append(multiply_value)
add_values.append(add_value)

# Create a DataFrame with the collected input values
data = pd.DataFrame(
{
"transition": transitions,
"multiply_value": multiply_values,
"add_value": add_values,
}
)

# Convert the transition column to a MultiIndex if it contains tuples
amynickolls marked this conversation as resolved.
Show resolved Hide resolved
if all(isinstance(idx, tuple) for idx in data["transition"]):
# Convert the "transition" column to a MultiIndex
data.set_index(pd.MultiIndex.from_tuples(data["transition"]), inplace=True)
data.drop(
columns=["transition"], inplace=True
) # Drop the column after conversion
else:
# Otherwise, set transition column as index
data.set_index(["transition"], inplace=True)

return data
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tab1tha can you review this html please?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.

Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ <h1>Adjust entry rates</h1>
<table id="transition-rate" class="table table-hover">
<form method="post">
{% csrf_token %}
{% convert_data_frame_to_html_table_plus_form entry_rate_table form "Rate multiplication" %}
{{ entry_rate_table|convert_df_and_dynamicrateform_to_table:form |safe}}
<tr>
<td colspan="3">
<td colspan="4">
<div class="p-2 bd-highlight"><a class="btn btn-secondary" href="adjusted">Back</a></div>
</td>
<td>
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tab1tha and again please?

Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ <h1>Adjust exit rates</h1>
<table id="transition-rate" class="table table-hover">
<form method="post">
{% csrf_token %}
{% convert_data_frame_to_html_table_plus_form exit_rate_table form "Rate multiplication" %}
{{ exit_rate_table|convert_df_and_dynamicrateform_to_table:form |safe}}
<tr>
<td colspan="3">
<td colspan="4">
<div class="p-2 bd-highlight"><a class="btn btn-secondary" href="adjusted">Back</a></div>
</td>
<td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ <h1>Adjust placement proportions</h1>
<table id="transition-rate" class="table table-hover">
<form method="post">
{% csrf_token %}
{% convert_data_frame_to_html_table_plus_form placement_types form "New forecast proportion" %}
{% convert_df_plus_dynamicform_to_html_table placement_types form "New forecast proportion" %}
<tr>
{% if placement_types.columns|length == 3 %}
<td colspan="3">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ <h1>Adjust transition rates</h1>
<table id="transition-rate" class="table table-hover">
<form method="post">
{% csrf_token %}
{% convert_data_frame_to_html_table_plus_form transition_rate_table form "Rate multiplication" %}
{{ transition_rate_table|convert_df_and_dynamicrateform_to_table:form |safe}}
<tr>
<td colspan="3">
<td colspan="4">
<div class="p-2 bd-highlight"><a class="btn btn-secondary" href="adjusted">Back</a></div>
</td>
<td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ <h1>Adjust weekly costs</h1>
<table id="transition-rate" class="table table-hover">
<form method="post">
{% csrf_token %}
{% convert_data_frame_to_html_table_plus_form placement_types form "Weekly cost" %}
{% convert_df_plus_dynamicform_to_html_table placement_types form "Weekly cost" %}
<tr>
<td colspan="1">
<div class="p-2 bd-highlight"><a class="btn btn-secondary" href="costs">Back</a></div>
Expand Down
102 changes: 97 additions & 5 deletions dm_regional_app/templatetags/table_tags.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import pandas as pd
from django import template
from django.utils.safestring import mark_safe

Expand Down Expand Up @@ -42,28 +41,51 @@ def format_value(value):


@register.simple_tag
def convert_data_frame_to_html_table_plus_form(df, form, header="Rate multiplication"):
def convert_df_plus_dynamicform_to_html_table(df, form, header):
"""
This takes both dataframe and form
This takes both dataframe and DynamicForm
It will create a row for each item that shares an index
The form input field will be at the end of each row
"""

# Start building the HTML table
html = "<thead><tr>"

# Create headers for each column in the dataframe
for value in df.columns:
html += f'<th scope="col">{value.capitalize()}</th>'

# Add header for form input column
html += f'<th scope="col">{header}</th></tr></thead><tbody>'

# Iterate over the dataframe rows and add the form fields
for index, row in df.iterrows():
row_html = "<tr>"
for value in row:
if isinstance(value, str):
row_html += f'<th scope="row" style="font-size: 15px; padding-top: 8px;">{value}</th>'
else:
row_html += f'<td scope="row" style="font-size: 15px; padding-top: 8px;">{value}</td>'

# Create form field and add to row
field_html = str(form[str(index)])
row_html += f'<td scope="row" style="font-size: 15px; padding-top: 8px;">{field_html}</td>'
row_html += "</tr>"
row_html += (
f'<td scope="row" style="font-size: 15px; padding-top: 8px;">{field_html}'
)

# Add error messages for field if they exist
if form[f"{index}"].errors:
row_html += '<div class="text-danger">'
for error in form[f"{index}"].errors:
row_html += f"<p>{error}</p>"
row_html += "</div>"

# Close cell and row
row_html += "</td></tr>"

html += row_html

# Close table body
html += "</tbody>"
return mark_safe(html)

Expand Down Expand Up @@ -102,3 +124,73 @@ def format_value(value):

html += "</tbody>"
return html


@register.filter
def convert_df_and_dynamicrateform_to_table(df, form):
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious about these functions - is the end goal to create tables from the dynamic form?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we end up with the form input integrated into the table - means we can make it look like a multi-index and get the original data in there too

image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok, and why is the html built here rather than using a template and passing data to fill in the template?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed on our call, happy for this to be a refactor in another ticket that we can tackle later 🙂

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think both of us might have made an issue for this in different words

This takes both dataframe and DynamicRateForm.
It will create a row for each item that shares an index.
The form input fields (multiply and add) will be at the end of each row,
and any errors will be displayed under the corresponding input fields.
"""

# Start building the HTML table
html = "<thead><tr>"

# Create headers for each column in the dataframe
for value in df.columns:
html += f'<th scope="col">{value.capitalize()}</th>'

# Add headers for multiplication and addition
html += '<th scope="col">Multiply Rate</th>'
html += '<th scope="col">Add to Rate</th>'
html += "</tr></thead><tbody>"

# Iterate over the dataframe rows and add the form fields
for index, row in df.iterrows():
row_html = "<tr>"

# For each value in the row, create a table cell
for value in row:
if isinstance(value, str):
row_html += f'<th scope="row" style="font-size: 15px; padding-top: 8px;">{value}</th>'
else:
row_html += f'<td scope="row" style="font-size: 15px; padding-top: 8px;">{value}</td>'

# Create form fields for multiply and add rates
multiply_field_html = str(form[f"multiply_{index}"])
add_field_html = str(form[f"add_{index}"])

# Add form fields to the row
row_html += f'<td scope="row" style="font-size: 15px; padding-top: 8px;">{multiply_field_html}'

# Add error messages for multiply field if they exist
if form[f"multiply_{index}"].errors:
row_html += '<div class="text-danger">'
for error in form[f"multiply_{index}"].errors:
row_html += f"<p>{error}</p>"
row_html += "</div>"

row_html += "</td>"

# Add form fields for add rate and include error messages
row_html += f'<td scope="row" style="font-size: 15px; padding-top: 8px;">{add_field_html}'

# Add error messages for add field if they exist
if form[f"add_{index}"].errors:
row_html += '<div class="text-danger">'
for error in form[f"add_{index}"].errors:
row_html += f"<p>{error}</p>"
row_html += "</div>"

row_html += "</td>"

# Close the table row
row_html += "</tr>"
html += row_html

# Close the table body
html += "</tbody>"

return html
Loading
Loading