Render column selection options

- Guess which header is which
This commit is contained in:
Oliver Walters 2019-06-28 19:40:27 +10:00
parent 60050e9f32
commit fb96651c15
4 changed files with 69 additions and 52 deletions

View File

@ -46,13 +46,9 @@ class BomUploadManager:
""" Class for managing an uploaded BOM file """ """ Class for managing an uploaded BOM file """
# Fields which are absolutely necessary for valid upload # Fields which are absolutely necessary for valid upload
REQUIRED_HEADERS = [ HEADERS = [
'Part', 'Part',
'Quantity', 'Quantity',
]
# Fields which are not necessary but can be populated
USEFUL_HEADERS = [
'Reference', 'Reference',
'Overage', 'Overage',
'Notes' 'Notes'
@ -83,69 +79,49 @@ class BomUploadManager:
except tablib.UnsupportedFormat: except tablib.UnsupportedFormat:
raise ValidationError({'bom_file': _('Error reading BOM file (invalid data)')}) raise ValidationError({'bom_file': _('Error reading BOM file (invalid data)')})
# Now we have BOM data in memory! def guess_headers(self, header, threshold=80):
""" Try to match a header (from the file) to a list of known headers
self.header_map = {}
Args:
for header in self.REQUIRED_HEADERS: header - Header name to look for
match = self.extract_header(header) threshold - Match threshold for fuzzy search
if match is None:
raise ValidationError({'bom_file': _("Missing required field '{f}'".format(f=header))})
else:
self.header_map[header] = match
for header in self.USEFUL_HEADERS:
match = self.extract_header(header)
self.header_map[header] = match
def get_header(self, header_name):
""" Returns the matching header name for the internal name """
if header_name in self.header_map.keys():
return self.header_map[header_name]
else:
return None
def extract_header(self, header_name, threshold=80):
""" Retrieve a matching column header from the uploaded file.
If there is not an exact match, try to match one that is close.
""" """
headers = self.data.headers # Try for an exact match
for h in self.HEADERS:
if h == header:
return h
# First, try for an exact match # Try for a case-insensitive match
for header in headers: for h in self.HEADERS:
if header == header_name: if h.lower() == header.lower():
return header return h
# Next, try for a case-insensitive match
for header in headers:
if header.lower() == header_name.lower():
return header
# Finally, look for a close match using fuzzy matching # Finally, look for a close match using fuzzy matching
matches = [] matches = []
for header in headers: for h in self.HEADERS:
ratio = fuzz.partial_ratio(header, h)
ratio = fuzz.partial_ratio(header, header_name)
if ratio > threshold: if ratio > threshold:
matches.append({'header': header, 'match': ratio}) matches.append({'header': h, 'match': ratio})
if len(matches) > 0: if len(matches) > 0:
matches = sorted(matches, key=lambda item: item['match'], reverse=True) matches = sorted(matches, key=lambda item: item['match'], reverse=True)
# Return the field with the best match
return matches[0]['header'] return matches[0]['header']
return None return None
def get_headers(self): def get_headers(self):
""" Return a list of headers for the thingy """ """ Return a list of headers for the thingy """
headers = [] headers = []
for header in self.data.headers:
headers.append({
'name': header,
'guess': self.guess_header(header)
})
return headers return headers
def col_count(self): def col_count(self):

View File

@ -16,11 +16,24 @@
<table class='table table-striped'> <table class='table table-striped'>
<thead> <thead>
<tr> <tr>
<th>Row</th>
{% for col in bom_cols %}
<th>
<select class='select' id='id_col_{{ forloop.counter0 }}' name='col_{{ forloop.counter0 }}'>
<option value=''>---------</option>
{% for req in req_cols %}
<option value='{{ req }}'{% if req == col.guess %}selected='selected'{% endif %}>{{ req }}</option>
{% endfor %}
</select>
{{ col.name }}
</th>
{% endfor %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for row in bom.rows %} {% for row in bom_rows %}
<tr> <tr>
<td>{% add forloop.counter 1 %}</td>
{% for item in row %} {% for item in row %}
<td> <td>
{{ item }} {{ item }}

View File

@ -20,6 +20,12 @@ def multiply(x, y, *args, **kwargs):
return x * y return x * y
@register.simple_tag()
def add(x, y, *args, **kwargs):
""" Add two numbers together """
return x + y
@register.simple_tag() @register.simple_tag()
def part_allocation_count(build, part, *args, **kwargs): def part_allocation_count(build, part, *args, **kwargs):
""" Return the total number of <part> allocated to <build> """ """ Return the total number of <part> allocated to <build> """

View File

@ -683,6 +683,13 @@ class BomUpload(AjaxView, FormMixin):
return self.renderJsonResponse(request, self.form) return self.renderJsonResponse(request, self.form)
def handleBomFileUpload(self): def handleBomFileUpload(self):
""" Process a BOM file upload form.
This function validates that the uploaded file was valid,
and contains tabulated data that can be extracted.
If the file does not satisfy these requirements,
the "upload file" form is again shown to the user.
"""
bom_file = self.request.FILES.get('bom_file', None) bom_file = self.request.FILES.get('bom_file', None)
@ -713,13 +720,25 @@ class BomUpload(AjaxView, FormMixin):
# BOM file is valid? Proceed to the next step! # BOM file is valid? Proceed to the next step!
form = part_forms.BomUploadSelectFields form = part_forms.BomUploadSelectFields
self.ajax_template_name = 'part/bom_upload/select_fields.html' self.ajax_template_name = 'part/bom_upload/select_fields.html'
ctx['bom'] = manager
# Try to guess at the
# Provide context to the next form
ctx = {
'req_cols': BomUploadManager.HEADERS,
'bom_cols': manager.get_headers(),
'bom_rows': manager.rows(),
}
else: else:
form = self.form form = self.form
return self.renderJsonResponse(self.request, form, data=data, context=ctx) return self.renderJsonResponse(self.request, form, data=data, context=ctx)
def handleFieldSelection(self): def handleFieldSelection(self):
""" Handle the output of the field selection form.
Here the user is presented with the raw data and must select the
column names and which rows to process.
"""
data = { data = {
'form_valid': False, 'form_valid': False,
@ -727,7 +746,10 @@ class BomUpload(AjaxView, FormMixin):
self.ajax_template_name = 'part/bom_upload/select_fields.html' self.ajax_template_name = 'part/bom_upload/select_fields.html'
ctx = {} ctx = {
# The headers that we know about
'known_headers': BomUploadManager.HEADERS,
}
return self.renderJsonResponse(self.request, form=self.get_form(), data=data, context=ctx) return self.renderJsonResponse(self.request, form=self.get_form(), data=data, context=ctx)