Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 4.2.5 on 2026-04-05 18:41

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("instruments", "0013_alter_avresource_format_and_more"),
]

operations = [
migrations.AlterField(
model_name="instrument",
name="source",
field=models.CharField(
blank=True,
help_text="Source/reference for this instrument entry",
max_length=255,
),
),
migrations.AlterField(
model_name="instrumentname",
name="source_name",
field=models.CharField(
help_text="Who or what called the instrument this?", max_length=255
),
),
]
2 changes: 1 addition & 1 deletion web-app/django/VIM/apps/instruments/models/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class Instrument(models.Model):
help_text="User who created this instrument (null for Wikidata imports)",
)
source = models.CharField(
max_length=200,
max_length=255,
blank=True,
help_text="Source/reference for this instrument entry",
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class InstrumentName(models.Model):
language = models.ForeignKey("Language", on_delete=models.PROTECT)
name = models.CharField(max_length=100, blank=False)
source_name = models.CharField(
max_length=50, blank=False, help_text="Who or what called the instrument this?"
max_length=255, blank=False, help_text="Who or what called the instrument this?"
) # Stand-in for source data; format TBD
verification_status = models.CharField(
max_length=50,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,11 @@ def create_instrument(request: HttpRequest) -> JsonResponse:
)

# Validate instrument source length
if len(instrument_source) > 200:
if len(instrument_source) > 255:
raise ValidationException(
ErrorCode.FIELD_TOO_LONG,
"Instrument source must be 200 characters or less",
details={"field": "instrument_source", "max_length": 200},
"Instrument source must be 255 characters or less",
details={"field": "instrument_source", "max_length": 255},
)

if not validate_hbs_classification(hbs_class):
Expand Down
8 changes: 6 additions & 2 deletions web-app/django/VIM/templates/instruments/create.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,16 @@ <h5>Instrument Information</h5>
id="instrumentSource"
name="instrument_source"
placeholder="Enter instrument source"
maxlength="255"
required />
<div class="form-text">
The primary source or reference for this instrument's existence.
</div>
<div class="valid-feedback"></div>
<div class="invalid-feedback"></div>
<div class="d-flex justify-content-between align-items-center">
<div class="valid-feedback"></div>
<div class="invalid-feedback"></div>
<small class="text-muted ms-auto"><span id="instrumentSourceCounter">0</span> / 255</small>
</div>
</div>
<div class="col-md-6">
<label for="hornbostelSachsClass" class="form-label">
Expand Down
4 changes: 2 additions & 2 deletions web-app/django/VIM/templates/instruments/detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ <h2 class="notranslate mb-0">{{ active_instrument_label.name }}</h2>
</td>
<td>
<div class="d-flex flex-row justify-content-between align-items-center gap-2 edit-container">
<span class="view-field flex-grow-1">{{ names.label.source_name }}</span>
<span class="view-field flex-grow-1">{{ names.label.source_name|truncatechars:40 }}</span>
</div>
</td>
{% if user.is_authenticated %}
Expand Down Expand Up @@ -221,7 +221,7 @@ <h2 class="notranslate mb-0">{{ active_instrument_label.name }}</h2>
<!-- Source -->
<div class="mb-1">
<div class="fw-bold text-muted small">Source</div>
<span class="view-field">{{ names.label.source_name }}</span>
<span class="view-field">{{ names.label.source_name|truncatechars:40 }}</span>
</div>
{% if user.is_authenticated %}
<!-- Status column -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ export class CreateInstrumentManager {
}

/**
* Initializes the form with one required row
* Initializes the form with one required row and sets up the source counter
*/
initializeForm(): void {
this.nameRowManager.resetRows(true);
this.setupInstrumentSourceCounter();
}

/**
Expand All @@ -53,6 +54,28 @@ export class CreateInstrumentManager {
}
}

/**
* Sets up the live character counter for the Instrument Source input field.
*/
setupInstrumentSourceCounter(): void {
const sourceInput = document.getElementById(
'instrumentSource',
) as HTMLInputElement;
const counterSpan = document.getElementById('instrumentSourceCounter');
if (!sourceInput || !counterSpan) return;

const updateCounter = () => {
const length = sourceInput.value.length;
counterSpan.textContent = length.toString();
};

// Listen to input events and update counter immediately
sourceInput.addEventListener('input', updateCounter);

// Initialize counter on page load in case of prefilled value
updateCounter();
}

/**
* Toggles image source field visibility based on image selection
*/
Expand Down Expand Up @@ -437,7 +460,7 @@ export class CreateInstrumentManager {
}

/**
* Sets up form submission handling
* Sets up form submission handling and instrument source counter
*/
setupFormSubmission(): void {
const form = document.getElementById('createInstrumentForm');
Expand All @@ -454,5 +477,8 @@ export class CreateInstrumentManager {
this.submitInstrument();
});
}

// Ensure the instrument source counter is set up on form activation as well
this.setupInstrumentSourceCounter();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export class CreateInstrumentValidator {
}

/**
* Validates source field
* Validates source field (must be filled and under 255 characters)
*/
validateSource(source: string): ValidationResult {
if (!source || source.trim() === '') {
Expand All @@ -170,7 +170,13 @@ export class CreateInstrumentValidator {
type: 'error',
};
}

if (source.trim().length > 255) {
return {
isValid: false,
message: 'Source must be 255 characters or less.',
type: 'error',
};
}
return {
isValid: true,
message: '',
Expand Down Expand Up @@ -385,13 +391,21 @@ export class CreateInstrumentValidator {
});
}

// Validate source required and under 255 chars for each entry
if (!entry.source?.trim()) {
isValid = false;
errors.set(`source${rowIndex}`, {
isValid: false,
message: 'Source is required for this entry',
type: 'error',
});
} else if (entry.source.trim().length > 255) {
isValid = false;
errors.set(`source${rowIndex}`, {
isValid: false,
message: 'Source must be 255 characters or less.',
type: 'error',
});
}
}
}
Expand Down
28 changes: 25 additions & 3 deletions web-app/frontend/src/instruments/helpers/NameRowManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ export class NameRowManager {
const requiredAttr = config.isRequired ? 'required' : '';
const deleteButtonStyle = config.isRequired ? 'visibility: hidden;' : '';

// Max length for source input (as enforced by validation logic)
const maxSourceLen = 255;

row.innerHTML = `
<div class="col-md-3 col-12 language-input mb-2 mb-md-0">
<label for="language${index}" class="form-label">Language ${requiredMark}</label>
Expand All @@ -74,9 +77,12 @@ export class NameRowManager {
<div class="col-md-3 col-12 source-input mb-2 mb-md-0">
<label for="source${index}" class="form-label">Source ${requiredMark}</label>
<input type="text" class="form-control" id="source${index}"
name="source[]" placeholder="Enter source" ${requiredAttr} />
<div class="valid-feedback"></div>
<div class="invalid-feedback"></div>
name="source[]" placeholder="Enter source" maxlength="${maxSourceLen}" ${requiredAttr} />
<div class="d-flex justify-content-between align-items-center">
<div class="valid-feedback"></div>
<div class="invalid-feedback"></div>
<small class="text-muted ms-auto"><span id="source${index}Counter">0</span> / ${maxSourceLen}</small>
</div>
</div>
<div class="col-md-2 col-12 mb-2 mb-md-0 align-self-end d-flex justify-content-center">
<button type="button" class="btn btn-outline-danger remove-row-btn w-50" title="Remove this row" style="${deleteButtonStyle}">
Expand Down Expand Up @@ -113,6 +119,22 @@ export class NameRowManager {
}
});

// Add character counter functionality to the source input
const sourceInput = row.querySelector(
`#source${index}`,
) as HTMLInputElement;
const sourceCounter = row.querySelector(
`#source${index}Counter`,
) as HTMLElement;

if (sourceInput && sourceCounter) {
// Set initial count
sourceCounter.textContent = String(sourceInput.value.length);
sourceInput.addEventListener('input', () => {
sourceCounter.textContent = String(sourceInput.value.length);
});
}

return row;
}

Expand Down
12 changes: 10 additions & 2 deletions web-app/frontend/src/instruments/helpers/NameValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,16 +88,24 @@ export class NameValidator {
}

/**
* Validates that the source field is not empty
* Validates that the source field is not empty and under 255 characters
*/
validateSource(sourceInput: string): ValidationResult {
if (sourceInput.trim() === '') {
const trimmed = sourceInput.trim();
if (trimmed === '') {
return {
isValid: false,
message: 'Please enter the source of this name.',
type: 'error',
};
}
if (trimmed.length > 255) {
return {
isValid: false,
message: 'Source must be 255 characters or less.',
type: 'error',
};
}

return {
isValid: true,
Expand Down
Loading