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 """
# Fields which are absolutely necessary for valid upload
REQUIRED_HEADERS = [
HEADERS = [
'Part',
'Quantity',
]
# Fields which are not necessary but can be populated
USEFUL_HEADERS = [
'Reference',
'Overage',
'Notes'
@ -83,69 +79,49 @@ class BomUploadManager:
except tablib.UnsupportedFormat:
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 = {}
for header in self.REQUIRED_HEADERS:
match = self.extract_header(header)
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.
Args:
header - Header name to look for
threshold - Match threshold for fuzzy search
"""
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
for header in headers:
if header == header_name:
return header
# Next, try for a case-insensitive match
for header in headers:
if header.lower() == header_name.lower():
return header
# Try for a case-insensitive match
for h in self.HEADERS:
if h.lower() == header.lower():
return h
# Finally, look for a close match using fuzzy matching
matches = []
for header in headers:
ratio = fuzz.partial_ratio(header, header_name)
for h in self.HEADERS:
ratio = fuzz.partial_ratio(header, h)
if ratio > threshold:
matches.append({'header': header, 'match': ratio})
matches.append({'header': h, 'match': ratio})
if len(matches) > 0:
matches = sorted(matches, key=lambda item: item['match'], reverse=True)
# Return the field with the best match
return matches[0]['header']
return None
def get_headers(self):
""" Return a list of headers for the thingy """
headers = []
for header in self.data.headers:
headers.append({
'name': header,
'guess': self.guess_header(header)
})
return headers
def col_count(self):

View File

@ -16,11 +16,24 @@
<table class='table table-striped'>
<thead>
<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>
</thead>
<tbody>
{% for row in bom.rows %}
{% for row in bom_rows %}
<tr>
<td>{% add forloop.counter 1 %}</td>
{% for item in row %}
<td>
{{ item }}

View File

@ -20,6 +20,12 @@ def multiply(x, y, *args, **kwargs):
return x * y
@register.simple_tag()
def add(x, y, *args, **kwargs):
""" Add two numbers together """
return x + y
@register.simple_tag()
def part_allocation_count(build, part, *args, **kwargs):
""" 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)
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)
@ -713,13 +720,25 @@ class BomUpload(AjaxView, FormMixin):
# BOM file is valid? Proceed to the next step!
form = part_forms.BomUploadSelectFields
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:
form = self.form
return self.renderJsonResponse(self.request, form, data=data, context=ctx)
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 = {
'form_valid': False,
@ -727,7 +746,10 @@ class BomUpload(AjaxView, FormMixin):
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)