feat(csv-import): Enhance source unit handling and custom conversion options
- Updated the source_unit_choices_for_field function to include a custom option for user-defined conversion factors, improving flexibility in unit conversions. - Modified the AdminCsvTemplateEditorPage to support custom conversion factors, allowing users to input specific scaling factors for their data. - Added tests to ensure the custom option is correctly included in the source unit choices and functions as expected in the template editor.
This commit is contained in:
parent
bb6eefc837
commit
fe7a69fb07
|
|
@ -68,7 +68,7 @@ def source_unit_choices_for_field(module: str, db_field: str) -> list[dict[str,
|
|||
choices = _UNIT_FAMILY.get(cu)
|
||||
if not choices:
|
||||
return []
|
||||
return [
|
||||
out: list[dict[str, Any]] = [
|
||||
{
|
||||
"id": c["id"],
|
||||
"label": c["label"],
|
||||
|
|
@ -77,6 +77,15 @@ def source_unit_choices_for_field(module: str, db_field: str) -> list[dict[str,
|
|||
}
|
||||
for c in choices
|
||||
]
|
||||
out.append(
|
||||
{
|
||||
"id": "custom",
|
||||
"label": "Benutzerdefiniert (Konvertierungsfaktor, z. B. ml→g je nach Dichte)",
|
||||
"canonical_unit": cu,
|
||||
"is_canonical": False,
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def factor_source_to_canonical(module: str, db_field: str, source_unit: str | None) -> float:
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from csv_parser.core import (
|
|||
get_csv_import_limits,
|
||||
iter_csv_dict_rows,
|
||||
)
|
||||
from csv_parser.field_units import source_unit_choices_for_field
|
||||
from csv_parser.mapping_suggest import build_type_conversions_for_mapping
|
||||
from csv_parser.type_converter import convert_value, build_row_after_mapping
|
||||
|
||||
|
|
@ -213,3 +214,9 @@ def test_datetime_flexible():
|
|||
spec = {"type": "datetime", "format": "yyyy-mm-dd HH:MM:SS", "flexible": True}
|
||||
dtv = convert_value("15.01.2024 14:30:00", "t", spec)
|
||||
assert dtv.month == 1 and dtv.day == 15 and dtv.hour == 14
|
||||
|
||||
|
||||
def test_source_unit_choices_include_custom_at_end():
|
||||
opts = source_unit_choices_for_field("nutrition", "protein_g")
|
||||
assert opts[-1]["id"] == "custom"
|
||||
assert any(o["id"] == "mg" for o in opts)
|
||||
|
|
|
|||
|
|
@ -187,6 +187,10 @@ export default function AdminCsvTemplateEditorPage() {
|
|||
const hit = opts.find((o) => o.id === sid)
|
||||
if (hit) return hit.id
|
||||
}
|
||||
if (tc[fieldKey]?.conversion_factor != null && tc[fieldKey]?.conversion_factor !== '') {
|
||||
const hasCustomOpt = opts.some((o) => o.id === 'custom')
|
||||
if (hasCustomOpt) return 'custom'
|
||||
}
|
||||
return canonical || ''
|
||||
}
|
||||
|
||||
|
|
@ -204,13 +208,59 @@ export default function AdminCsvTemplateEditorPage() {
|
|||
tc[fieldKey] && typeof tc[fieldKey] === 'object'
|
||||
? { ...tc[fieldKey] }
|
||||
: { type: 'float', decimal_separator: 'auto', flexible: true }
|
||||
delete base.target_unit
|
||||
if (sourceUnitId === canonical || sourceUnitId === '' || sourceUnitId == null) {
|
||||
delete base.source_unit
|
||||
delete base.conversion_factor
|
||||
} else if (sourceUnitId === 'custom') {
|
||||
base.source_unit = 'custom'
|
||||
// conversion_factor nur per Eingabefeld setzen/löschen
|
||||
} else {
|
||||
base.source_unit = sourceUnitId
|
||||
delete base.conversion_factor
|
||||
}
|
||||
tc[fieldKey] = base
|
||||
setTypeConversionsText(JSON.stringify(tc, null, 2))
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const getCustomConversionFactorInputValue = (fieldKey) => {
|
||||
let tc
|
||||
try {
|
||||
tc = JSON.parse(typeConversionsText || '{}')
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
const v = tc[fieldKey]?.conversion_factor
|
||||
if (v == null || v === '') return ''
|
||||
return String(v)
|
||||
}
|
||||
|
||||
const updateCustomConversionFactor = (fieldKey, raw) => {
|
||||
let tc
|
||||
try {
|
||||
tc = JSON.parse(typeConversionsText || '{}')
|
||||
} catch {
|
||||
setError('type_conversions: ungültiges JSON (Faktor kann nicht gesetzt werden).')
|
||||
return
|
||||
}
|
||||
const base =
|
||||
tc[fieldKey] && typeof tc[fieldKey] === 'object'
|
||||
? { ...tc[fieldKey] }
|
||||
: { type: 'float', decimal_separator: 'auto', flexible: true }
|
||||
const t = String(raw).trim().replace(',', '.')
|
||||
if (t === '') {
|
||||
delete base.conversion_factor
|
||||
} else {
|
||||
const n = Number(t)
|
||||
if (Number.isNaN(n)) {
|
||||
setError('Konvertierungsfaktor: bitte eine gültige Zahl eingeben.')
|
||||
return
|
||||
}
|
||||
base.conversion_factor = n
|
||||
base.source_unit = 'custom'
|
||||
}
|
||||
delete base.target_unit
|
||||
delete base.conversion_factor
|
||||
tc[fieldKey] = base
|
||||
setTypeConversionsText(JSON.stringify(tc, null, 2))
|
||||
setError(null)
|
||||
|
|
@ -591,36 +641,60 @@ export default function AdminCsvTemplateEditorPage() {
|
|||
<div className="card" style={{ padding: 16, marginBottom: 16 }}>
|
||||
<div className="form-label">3b. Quelleinheit (optional)</div>
|
||||
<p style={{ fontSize: 13, color: 'var(--text2)', marginTop: 8, lineHeight: 1.55 }}>
|
||||
Ziel-Einheit kommt aus dem Datenmodell (z. B. kcal, g, kg, km). Hier nur angeben, falls die CSV
|
||||
abweicht (z. B. FDDB kJ → kcal, Makros in kg → g, Gewicht in lb → kg). Beim Ändern hier werden
|
||||
pro Feld <code>conversion_factor</code> und <code>target_unit</code> entfernt (verhindert
|
||||
Doppel-Umrechnung); bei Bedarf wieder in Abschnitt 4 ergänzen.
|
||||
Ziel-Einheit kommt aus dem Datenmodell (z. B. kcal, g, kg, km). Standard-Umrechnungen wählen; für
|
||||
abweichende Skalen (z. B. Volumen→Masse mit variabler Dichte) "Benutzerdefiniert" und den
|
||||
Faktor eintragen (CSV-Wert × Faktor → Speicher-Einheit). Bei Registry-Optionen werden{' '}
|
||||
<code>target_unit</code> und ein alter <code>conversion_factor</code> entfernt; bei
|
||||
Benutzerdefiniert bleibt der Faktor erhalten, bis du ihn leerst.
|
||||
</p>
|
||||
<div style={{ marginTop: 14, display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<div style={{ marginTop: 14, display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{unitTargets.map(({ field: fkey, options }) => (
|
||||
<label key={fkey} style={{ display: 'block' }}>
|
||||
<span className="form-label" style={{ display: 'block', marginBottom: 6 }}>
|
||||
{fkey}
|
||||
</span>
|
||||
<select
|
||||
className="form-input"
|
||||
value={getSourceUnitSelectValue(fkey) || ''}
|
||||
onChange={(e) => updateSourceUnit(fkey, e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
minHeight: 46,
|
||||
textAlign: 'left',
|
||||
padding: '11px 14px',
|
||||
fontSize: 15,
|
||||
}}
|
||||
>
|
||||
{options.map((o) => (
|
||||
<option key={o.id} value={o.id}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<div key={fkey}>
|
||||
<label style={{ display: 'block' }}>
|
||||
<span className="form-label" style={{ display: 'block', marginBottom: 6 }}>
|
||||
{fkey}
|
||||
</span>
|
||||
<select
|
||||
className="form-input"
|
||||
value={getSourceUnitSelectValue(fkey) || ''}
|
||||
onChange={(e) => updateSourceUnit(fkey, e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
minHeight: 46,
|
||||
textAlign: 'left',
|
||||
padding: '11px 14px',
|
||||
fontSize: 15,
|
||||
}}
|
||||
>
|
||||
{options.map((o) => (
|
||||
<option key={o.id} value={o.id}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
{getSourceUnitSelectValue(fkey) === 'custom' && (
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<span className="form-label" style={{ display: 'block', marginBottom: 6 }}>
|
||||
Konvertierungsfaktor (× CSV-Wert → Wert in Speicher-Einheit)
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
className="form-input"
|
||||
placeholder="z. B. 1.03 bei ml→g (Dichte ≈ 1,03 g/ml)"
|
||||
value={getCustomConversionFactorInputValue(fkey)}
|
||||
onChange={(e) => updateCustomConversionFactor(fkey, e.target.value)}
|
||||
style={{ width: '100%', textAlign: 'left' }}
|
||||
/>
|
||||
{getCustomConversionFactorInputValue(fkey) === '' && (
|
||||
<p style={{ fontSize: 12, color: 'var(--text3)', marginTop: 6 }}>
|
||||
Ohne Faktor gilt keine zusätzliche Skalierung (wie 1:1 nach dem Parsen).
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user