Skip to content

Commit

Permalink
Add functions to easily download template files, create new labels/re…
Browse files Browse the repository at this point in the history
…ports, and update settings (#212)

* Add functions to easily download template files, create new labels/reports, and update settings

* Handle closing of files after upload

* Clarify if statement

* Add tests for creating and downloading report/label templates

* Extend test to use save method, other methods of specifying template file. Fix template file location

* Correct the order of arguments in isinstance

* Compare contents of file, not names..

* Handle open files better

* Put file comparison in function for easier maintenance

* Make sure to close files also in create method

* Rename variable for clarity

* Change name of argument

* Skip tests for ReportBuild method (due to inventree/InvenTree#6213)

* That didn't work as expected...

* Replace with assertIsInstance method

* Again, not working
  • Loading branch information
miggland authored Jan 12, 2024
1 parent da29478 commit c18cc32
Show file tree
Hide file tree
Showing 7 changed files with 388 additions and 18 deletions.
8 changes: 4 additions & 4 deletions inventree/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -547,13 +547,13 @@ def downloadFile(self, url, destination, overwrite=False, params=None, proxies=d
if url.startswith('/'):
url = url[1:]

url = urljoin(self.base_url, url)
fullurl = urljoin(self.base_url, url)

if os.path.exists(destination) and os.path.isdir(destination):

destination = os.path.join(
destination,
os.path.basename(url)
os.path.basename(fullurl)
)

destination = os.path.abspath(destination)
Expand All @@ -571,7 +571,7 @@ def downloadFile(self, url, destination, overwrite=False, params=None, proxies=d
auth = self.auth

with requests.get(
url,
fullurl,
stream=True,
auth=auth,
headers=headers,
Expand All @@ -595,7 +595,7 @@ def downloadFile(self, url, destination, overwrite=False, params=None, proxies=d

headers = response.headers

if 'Content-Type' in headers and 'text/html' in headers['Content-Type']:
if not url.startswith('media/report') and not url.startswith('media/label') and 'Content-Type' in headers and 'text/html' in headers['Content-Type']:
logger.error(f"Error downloading file '{url}': Server return invalid response (text/html)")
return False

Expand Down
80 changes: 77 additions & 3 deletions inventree/label.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,19 +77,93 @@ def printlabel(self, label, plugin=None, destination=None, *args, **kwargs):
return response


class LabelLocation(inventree.base.MetadataMixin, inventree.base.InventreeObject):
class LabelFunctions(inventree.base.MetadataMixin, inventree.base.InventreeObject):
"""Base class for label functions"""

@classmethod
def create(cls, api, data, label, **kwargs):
"""Create a new label by uploading a label template file. Convenience wrapper around base create() method.
Args:
data: Dict of data including at least name and description for the template
label: Either a string (filename) or a file object
"""

# POST endpoints for creating new reports were added in API version 156
cls.REQUIRED_API_VERSION = 156

try:
# If label is already a readable object, don't convert it
if label.readable() is False:
raise ValueError("Label template file must be readable")
except AttributeError:
label = open(label)
if label.readable() is False:
raise ValueError("Label template file must be readable")

try:
response = super().create(api, data=data, files={'label': label}, **kwargs)
finally:
if label is not None:
label.close()
return response

def save(self, data=None, label=None, **kwargs):
"""Save label to database. Convenience wrapper around save() method.
Args:
data (optional): Dict of data to change for the template.
label (optional): Either a string (filename) or a file object, to upload a new label template
"""

# PUT/PATCH endpoints for updating data were available before POST endpoints
self.REQUIRED_API_VERSION = None

if label is not None:
try:
# If template is already a readable object, don't convert it
if label.readable() is False:
raise ValueError("Label template file must be readable")
except AttributeError:
label = open(label, 'r')
if label.readable() is False:
raise ValueError("Label template file must be readable")

if 'files' in kwargs:
files = kwargs.pop('kwargs')
files['label'] = label
else:
files = {'label': label}
else:
files = None

try:
response = super().save(data=data, files=files)
finally:
if label is not None:
label.close()
return response

def downloadTemplate(self, destination, overwrite=False):
"""Download template file for the label to the given destination"""

# Use downloadFile method to get the file
return self._api.downloadFile(url=self._data['label'], destination=destination, overwrite=overwrite)


class LabelLocation(LabelFunctions):
""" Class representing the Label/Location database model """

URL = 'label/location'


class LabelPart(inventree.base.MetadataMixin, inventree.base.InventreeObject):
class LabelPart(LabelFunctions):
""" Class representing the Label/Part database model """

URL = 'label/part'


class LabelStock(inventree.base.MetadataMixin, inventree.base.InventreeObject):
class LabelStock(LabelFunctions):
""" Class representing the Label/stock database model """

URL = 'label/stock'
91 changes: 84 additions & 7 deletions inventree/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,43 +50,120 @@ def printreport(self, report, destination, *args, **kwargs):
return self._api.downloadFile(url=URL, destination=destination, params=params, *args, **kwargs)


class ReportBoM(inventree.base.MetadataMixin, inventree.base.InventreeObject):
class ReportFunctions(inventree.base.MetadataMixin, inventree.base.InventreeObject):
"""Base class for report functions"""

@classmethod
def create(cls, api, data, template, **kwargs):
"""Create a new report by uploading a template file. Convenience wrapper around base create() method.
Args:
data: Dict of data including at least name and description for the template
template: Either a string (filename) or a file object
"""

# POST endpoints for creating new reports were added in API version 156
cls.REQUIRED_API_VERSION = 156

try:
# If template is already a readable object, don't convert it
if template.readable() is False:
raise ValueError("Template file must be readable")
except AttributeError:
template = open(template)
if template.readable() is False:
raise ValueError("Template file must be readable")

try:
response = super().create(api, data=data, files={'template': template}, **kwargs)
finally:
if template is not None:
template.close()
return response

def save(self, data=None, template=None, **kwargs):
"""Save report data to database. Convenience wrapper around save() method.
Args:
data (optional): Dict of data to change for the template.
template (optional): Either a string (filename) or a file object, to upload a new template
"""

# PUT/PATCH endpoints for updating data were available before POST endpoints
self.REQUIRED_API_VERSION = None

if template is not None:
try:
# If template is already a readable object, don't convert it
if template.readable() is False:
raise ValueError("Template file must be readable")
except AttributeError:
template = open(template, 'r')
if template.readable() is False:
raise ValueError("Template file must be readable")

if 'files' in kwargs:
files = kwargs.pop('kwargs')
files['template'] = template
else:
files = {'template': template}
else:
files = None

try:
response = super().save(data=data, files=files)
finally:
if template is not None:
template.close()
return response

def downloadTemplate(self, destination, overwrite=False):
"""Download template file for the report to the given destination"""

# Use downloadFile method to get the file
return self._api.downloadFile(url=self._data['template'], destination=destination, overwrite=overwrite)


class ReportBoM(ReportFunctions):
"""Class representing ReportBoM"""

URL = 'report/bom'


class ReportBuild(inventree.base.MetadataMixin, inventree.base.InventreeObject):
class ReportBuild(ReportFunctions):
"""Class representing ReportBuild"""

URL = 'report/build'


class ReportPurchaseOrder(inventree.base.MetadataMixin, inventree.base.InventreeObject):
class ReportPurchaseOrder(ReportFunctions):
"""Class representing ReportPurchaseOrder"""

URL = 'report/po'


class ReportSalesOrder(inventree.base.MetadataMixin, inventree.base.InventreeObject):
class ReportSalesOrder(ReportFunctions):
"""Class representing ReportSalesOrder"""

URL = 'report/so'


class ReportReturnOrder(inventree.base.MetadataMixin, inventree.base.InventreeObject):
class ReportReturnOrder(ReportFunctions):
"""Class representing ReportReturnOrder"""

URL = 'report/ro'


class ReportStockLocation(inventree.base.MetadataMixin, inventree.base.InventreeObject):
class ReportStockLocation(ReportFunctions):
"""Class representing ReportStockLocation"""

# The Stock location report was added when API version was 127, but the API version was not incremented at the same time
# The closest API version which has the SLR report is 128
REQUIRED_API_VERSION = 128
URL = 'report/slr'


class ReportTest(inventree.base.MetadataMixin, inventree.base.InventreeObject):
class ReportTest(ReportFunctions):
"""Class representing ReportTest"""

URL = 'report/test'
5 changes: 5 additions & 0 deletions test/dummytemplate.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% extends "label/label_base.html" %}

<pre>TEST LABEL</pre>

{% endblock content %}
5 changes: 5 additions & 0 deletions test/dummytemplate2.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% extends "label/label_base.html" %}

<pre>TEST LABEL TWO</pre>

{% endblock content %}
99 changes: 99 additions & 0 deletions test/test_label.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,105 @@ def test_label_list(self):
self.assertGreater(len(lbl_location_list_filtered), 0)
self.assertEqual(lbl_location_list, lbl_location_list_filtered)

def test_label_create_download(self):
"""
Tests creating a new label from API, by uploading the dummy template file
"""

def comparefiles(file1, file2):
"""Compare content of two files, return True if equal, else False"""

F1 = open(file1, 'r')
contents_1 = F1.read()
F1.close()

F2 = open(file2, 'r')
contents_2 = F2.read()
F2.close()

return contents_1 == contents_2

dummytemplate = os.path.join(os.path.dirname(__file__), 'dummytemplate.html')
dummytemplate2 = os.path.join(os.path.dirname(__file__), 'dummytemplate2.html')

for RepClass in (LabelPart, LabelStock, LabelLocation):
# Test for all Label classes sequentially

#
# Test with file name
#

# Create a new label based on the dummy template
newlabel = RepClass.create(
self.api,
{'name': 'Dummy label', 'description': 'Label created as test'},
dummytemplate
)

# The return value should be a LabelPart object
self.assertIsInstance(newlabel, RepClass)

# Try to download the template file
newlabel.downloadTemplate(destination="dummytemplate_download.html")

self.assertTrue(comparefiles("dummytemplate_download.html", dummytemplate))

# Remove the test file
os.remove("dummytemplate_download.html")

#
# Test with open(...)
#

# Create a new label based on the dummy template
with open(dummytemplate) as template_upload:
newlabel2 = RepClass.create(
self.api,
{'name': 'Dummy label', 'description': 'Label created as test'},
template_upload
)

# The return value should be a LabelPart object
self.assertIsInstance(newlabel2, RepClass)

# Try to download the template file
newlabel2.downloadTemplate(destination="dummytemplate_download.html")

self.assertTrue(comparefiles("dummytemplate_download.html", dummytemplate))

# Remove the test file
os.remove("dummytemplate_download.html")

#
# Test overwriting the label file with save method
# Use file name
#

newlabel2.save(data=None, label=dummytemplate2)

# Try to download the template file
newlabel2.downloadTemplate(destination="dummytemplate2_download.html")

self.assertTrue(comparefiles("dummytemplate2_download.html", dummytemplate2))
# Remove the test file
os.remove("dummytemplate2_download.html")

#
# Test overwriting the template file with save method
# Use open(...)
#

with open(dummytemplate) as template_upload:
newlabel2.save(data=None, label=template_upload)

# Try to download the template file
newlabel2.downloadTemplate(destination="dummytemplate_download.html")

self.assertTrue(comparefiles("dummytemplate_download.html", dummytemplate))

# Remove the test file
os.remove("dummytemplate_download.html")

def test_label_printing(self):
"""
Tests for using label printing function to download PDF files
Expand Down
Loading

0 comments on commit c18cc32

Please sign in to comment.