-
Notifications
You must be signed in to change notification settings - Fork 3
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
PI invoices are now exported as PDFs #129
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think using selenium
is overkill here. You can drive chrome's print-to-pdf functionality directly from the command line:
google-chrome --headless --print-to-pdf=output.pdf --no-pdf-header-footer document.html
This takes a single subprocess.run()
call. You can control the page size via CSS in your source document. Note that in the US, "Letter" size (8.5x11 inches) is much more common than A4.
@larsks I have removed the @larsks @knikolla The unit test failed because it did not find the chromium binary. Should I change the github action file to install Chrome, or should I just mock the |
Mock |
Unit tests shouldn't have external dependencies, which means you should mock out It might be nice to have an integration test that calls out to chrome; this would ensure that our chrome command line is valid. If @knikolla agrees that's a good idea, that could be a future pull request. |
Considering the rapid pace of chrome development and inability to rely on semantic versioning as they bump the major version number too often, I think it's worth having a test in the future that verifies that the command line arguments that we're passing are still accepted by the current version of chrome. For the purposes of this PR, a unit test is enough. |
The PI-specific dataframes will first be converted to HTML tables using Jinja templates, and then converted to PDFs using Chromium. Now, users of the script must provide a path to the Chromium/Chrome binary throught the env var `CHROME_BIN_PATH` A html template folder has been added, and the test cases for the PI-specific invoice will now both check whether the dataframe is formatted correctly and if the PDFs are correctly generated. The dockerfile has been to install chromium
I have created a new issue to create the integration test at #145. I have also implemented all feedback so far. |
else: | ||
return "$" + str(data) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't release need the else
clause here, and prefer f-strings over string concatenation:
else: | |
return "$" + str(data) | |
return f"${data}" |
But see my comment later on about whether or not we even need this function.
column_sums = list() | ||
sum_columns_list = list() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general, prefer []
over list()
to initialize list variables:
column_sums = list() | |
sum_columns_list = list() | |
column_sums = [] | |
sum_columns_list = [] |
|
||
def _create_pdf_invoice(temp_fd_name): | ||
chrome_binary_location = os.environ.get( | ||
"CHROME_BIN_PATH", "usr/bin/chromium" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this actually be /usr/bin/chromium
?
"CHROME_BIN_PATH", "usr/bin/chromium" | |
"CHROME_BIN_PATH", "/usr/bin/chromium" |
"--no-sandbox", | ||
f"--print-to-pdf={invoice_pdf_path}", | ||
"--no-pdf-header-footer", | ||
"file://" + temp_fd_name, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer an f-string (especially since you're using this syntax just a few lines earlier):
"file://" + temp_fd_name, | |
f"file://{temp_fd_name}", |
if not os.path.exists( | ||
self.name | ||
): # self.name is name of folder storing invoices | ||
os.mkdir(self.name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can simplify this using os.makedirs
:
if not os.path.exists( | |
self.name | |
): # self.name is name of folder storing invoices | |
os.mkdir(self.name) | |
os.makedirs(self.name, exist_ok=True) |
def add_dollar_sign(data): | ||
if pandas.isna(data): | ||
return data | ||
else: | ||
return "$" + str(data) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(See my earlier comment about this function.)
for answer_arg in answer_arglist: | ||
self.assertTrue(answer_arg in chrome_arglist[0]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would be more constrained in your check:
for answer_arg in answer_arglist: | |
self.assertTrue(answer_arg in chrome_arglist[0]) | |
self.assertTrue(answer_arglist == chrome_arglist[0][:-1]) |
self.assertIn("ProjectC", pi_df["Project - Allocation"].tolist()) | ||
mock_filter_cols.return_value = test_invoice | ||
mock_path_exists.return_value = True | ||
output_dir = tempfile.TemporaryDirectory() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need to create a temporary directory -- because we're mocking out subprocess.run
, we never create any output.
You can do this instead:
pi_inv = test_utils.new_pi_specific_invoice(
"/fakedir", invoice_month, data=test_invoice
)
pi_inv.process()
pi_inv.export()
pi_pdf_1 = f"/fakedir/BU_PI1_{invoice_month}.pdf"
pi_pdf_2 = f"/fakedir/HU_PI2_{invoice_month}.pdf"
pi_inv = test_utils.new_pi_specific_invoice( | ||
output_dir.name, invoice_month=self.invoice_month, data=self.dataframe | ||
if not group_name: | ||
group_name = [None for _ in range(len(pi))] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can write instead:
group_name = [None for _ in range(len(pi))] | |
group_name = [None] * len(pi) |
Compare:
>>> pi=[1,2,3,4,5]
>>> [None for _ in range(len(pi))]
[None, None, None, None, None]
>>> [None] * len(pi)
[None, None, None, None, None]
) | ||
if not os.path.exists(chrome_binary_location): | ||
sys.exit( | ||
f"Chrome binary does not exist at {chrome_binary_location}. Make sure the env var CHROME_BIN_PATH is set correctly or that Google Chrome is installed" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The message directs the user to ensure that Google Chrome is installed, but the code defaults to Chromium (which could lead to the situation in which Google Chrome is installed but the user still receives this error message). It makes more sense to replace or
with and
:
f"Chrome binary does not exist at {chrome_binary_location}. Make sure the env var CHROME_BIN_PATH is set correctly or that Google Chrome is installed" | |
f"Chrome binary does not exist at {chrome_binary_location}. Make sure that Google Chrome is installed and the environment variable CHROME_BIN_PATH is set correctly." |
Closes #84. This PR consists of the last commit.
This PR mostly involved changes to the pi-specific invoice class. More details in the commit message. Two Python packages were crucial for this task, Jinja and Selenium
@knikolla @larsks I am aware that running the invoice script with this change will make pandas print a bunch of
FutureWarnings
for each PDF printed. Should I address them after the current state of the PR is approved?