Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
644895f
fix: standardize and make unique page titles and h1s
HaSistrunk Jun 16, 2026
92dd9ac
chore: improve title action btn layout for all screen sizes
HaSistrunk Jun 16, 2026
e13ad3e
chore: linting
HaSistrunk Jun 17, 2026
77af1ae
fix: align accession and transfer details title/h1
HaSistrunk Jun 17, 2026
fd7efec
fix: create unique labels for buttons in tables
HaSistrunk Jun 26, 2026
ca72804
revert: don't change bagit profile table header
HaSistrunk Jun 26, 2026
73538ec
fix: Shorten button label to better support text resizing
HaSistrunk Jun 26, 2026
14748c9
fix: Corretly connect form field to label
HaSistrunk Jun 26, 2026
32101b8
fix: Correctly connect form field to label
HaSistrunk Aug 11, 2026
b10257b
feat: add aria attrs and focus mngmt for forms
HaSistrunk Aug 11, 2026
ab6e897
feat: apply aria attrs util, cleanup, and ensure consistent validation
HaSistrunk Aug 11, 2026
dd0b146
fix: only save bagit profile fields after validation
HaSistrunk Aug 11, 2026
af4ec61
fix: Preserve rights form data with errors, allow full validation, + …
HaSistrunk Aug 11, 2026
9710c4c
fix: Accession form discarding data on failed submission
HaSistrunk Aug 11, 2026
a13a5b1
fix: Improve accession form validation
HaSistrunk Aug 11, 2026
5e967dd
fix: create org forms to implement aria attrs on validation
HaSistrunk Aug 11, 2026
76e2d4d
merge changes
HaSistrunk Aug 11, 2026
f1bc56a
chore: Linting
HaSistrunk Aug 11, 2026
f30f6d2
chore: adjust line length
HaSistrunk Aug 11, 2026
5cdecc1
fix: improve accession form resource find/dismiss
HaSistrunk Aug 12, 2026
e14983c
feat: add required field legend
HaSistrunk Aug 12, 2026
b816ad3
Merge branch 'development' into a11y-moderate
HaSistrunk Aug 12, 2026
347a749
fix: Prevent loss of focus for bagit form controls
HaSistrunk Aug 13, 2026
1aeb5c8
feat: Improve bagit form add/delete function for screen readers
HaSistrunk Aug 13, 2026
8dbd148
fix: Improve focus management and labels for screen reader users
HaSistrunk Aug 13, 2026
24abca0
chore: Update backstopjs ref imgs
HaSistrunk Aug 13, 2026
a490f87
fix: Revert extra space added to mailer
HaSistrunk Aug 14, 2026
a871fcd
feat: align bagit edit flow with rights edit flow
HaSistrunk Aug 25, 2026
2ba22eb
feat: include no statement exists note in detail page
HaSistrunk Aug 26, 2026
8f7f025
fix: debug rights granted formset add/delete behavior
HaSistrunk Aug 26, 2026
3f8be9c
fix: use correct logic for no granted statements
HaSistrunk Aug 26, 2026
3f3a7c3
chore: update backstop ref imgs
HaSistrunk Aug 26, 2026
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions bag_transfer/accession/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ class Meta:
"creators": forms.widgets.MultipleHiddenInput(),
}

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["resource"].error_messages["required"] = (
"No ArchivesSpace resource target specified."
)


CreatorsFormSet = forms.modelformset_factory(
RecordCreators,
Expand Down
48 changes: 26 additions & 22 deletions bag_transfer/accession/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class AccessionDetailView(PageTitleMixin, OrgReadViewMixin, DetailView):
model = Accession

def get_page_title(self, context):
return context["object"].title
return "Accession: {}".format(context["object"].title)


class AccessionCreateView(PageTitleMixin, AccessioningArchivistMixin, JSONResponseMixin, CreateView):
Expand Down Expand Up @@ -136,6 +136,7 @@ def form_valid(self, form):
messages.error(
self.request,
"There was a problem with your submission. Please correct the error(s) below and try again.")
self._invalid_creators_formset = creators_formset
return super().form_invalid(form)

def get_context_data(self, **kwargs):
Expand All @@ -155,26 +156,29 @@ def get_context_data(self, **kwargs):
organization,
record_type,
", ".join([creator.name for creator in creators_list]))
context["form"] = AccessionForm(
initial={
"title": title,
"start_date": sorted(dates.get("start", []))[0],
"end_date": sorted(dates.get("end", []))[-1],
"description": " ".join(set(descriptions_list)),
"extent_files": extent_files,
"extent_size": extent_size,
"access_restrictions": " ".join(
set(notes.get("other", []) + notes.get("license", []) + notes.get("statute", []))
),
"use_restrictions": " ".join(set(notes.get("copyright", []))),
"acquisition_type": organization.acquisition_type,
"appraisal_note": " ".join(set(notes.get("appraisal", []))),
"organization": organization,
"language": language,
"creators": creators_list,
}
)
context["creators_formset"] = CreatorsFormSet(queryset=RecordCreators.objects.filter(name__in=creators_list))
form = context.get("form")
if form is None or not form.is_bound:
form = AccessionForm(
initial={
"title": title,
"start_date": sorted(dates.get("start", []))[0],
"end_date": sorted(dates.get("end", []))[-1],
"description": " ".join(set(descriptions_list)),
"extent_files": extent_files,
"extent_size": extent_size,
"access_restrictions": " ".join(
set(notes.get("other", []) + notes.get("license", []) + notes.get("statute", []))
),
"use_restrictions": " ".join(set(notes.get("copyright", []))),
"acquisition_type": organization.acquisition_type,
"appraisal_note": " ".join(set(notes.get("appraisal", []))),
"organization": organization,
"language": language,
"creators": creators_list,
}
)
context["form"] = form
context["creators_formset"] = getattr(self, "_invalid_creators_formset", None) or CreatorsFormSet(queryset=RecordCreators.objects.filter(name__in=creators_list))
context["transfers"] = transfers_list
context["rights_statements"] = rights_statements
return context
Expand Down Expand Up @@ -330,7 +334,7 @@ def button(self, accession):
button = "Accession not delivered"
if self.request.user.can_accession():
button = (
'<button class="btn btn--sm btn--blue deliver">Deliver Accession</button>'
'<button class="btn btn--sm btn--blue deliver" aria-label="Deliver accession {0}">Deliver Accession</button>'.format(accession.title)
if (accession.process_status < Accession.DELIVERED)
else '<p>' + accession.get_process_status_display() + "</p>"
)
Expand Down
10 changes: 6 additions & 4 deletions bag_transfer/appraise/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,16 @@ def appraise_buttons(self, transfer):
btn_class = "btn btn--xs btn--light-blue"
note_class = "edit-note"
note_text = "Edit"
note_label = "Edit note for {}".format(transfer.bag_or_failed_name)
else:
btn_class = "btn-info"
note_class = ""
note_text = "Add"
buttons = '<button class="btn btn--xs btn--blue appraisal-accept">Accept</button>\
<button class="btn btn--xs btn--orange appraisal-reject">Reject</button>\
<button class="btn btn--xs btn--light-blue appraisal-note {} {}" data-target="#modal-appraisal-note">{} Note</button>'.format(
btn_class, note_class, note_text
note_label = "Add note for {}".format(transfer.bag_or_failed_name)
buttons = '<button class="btn btn--xs btn--blue appraisal-accept" aria-label="Accept transfer {0}">Accept</button>\
<button class="btn btn--xs btn--orange appraisal-reject" aria-label="Reject transfer {0}">Reject</button>\
<button class="btn btn--xs btn--light-blue appraisal-note {1} {2}" data-target="#modal-appraisal-note" aria-label="{3}">{4} Note</button>'.format(
transfer.bag_or_failed_name, btn_class, note_class, note_label, note_text
)
return buttons

Expand Down
16 changes: 8 additions & 8 deletions bag_transfer/bagit_profiles/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@ class Meta:
"version": forms.widgets.HiddenInput(),
"bagit_profile_identifier": forms.widgets.HiddenInput(),
"external_description": forms.widgets.Textarea(
attrs={
"rows": 3,
"aria-describedby": "id_external_description-help"
}
attrs={"rows": 3}
),
"allow_fetch": forms.widgets.CheckboxInput(attrs={"class": "checkbox checkbox--blue"}),
"serialization": forms.widgets.RadioSelect(attrs={"class": "radiobutton radiobutton--blue"}),
Expand Down Expand Up @@ -82,10 +79,13 @@ class BagItProfileBagInfoValuesForm(forms.ModelForm):
class Meta:
model = BagItProfileBagInfoValues
fields = ("name",)
widgets = {
"name": forms.widgets.TextInput(
attrs={"aria-labelledby": "values-label", })
}

# Read the row's position to set unique aria-label for controlled value input
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
index = self.prefix.rsplit("-", 1)[-1] if self.prefix else ""
if index.isdigit():
self.fields["name"].widget.attrs["aria-label"] = "Controlled value %s" % (int(index) + 1)


BagItProfileBagInfoValuesFormset = forms.inlineformset_factory(
Expand Down
75 changes: 50 additions & 25 deletions bag_transfer/bagit_profiles/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from decimal import Decimal

from django.contrib import messages
from django.db import transaction
from django.http import Http404
from django.shortcuts import get_object_or_404
from django.urls import reverse
Expand All @@ -21,39 +22,54 @@ class BagItProfileManageView(PageTitleMixin):
model = BagItProfile
form_class = BagItProfileForm

def get_organization(self):
if self.object:
return self.object.organization
else:
return get_object_or_404(Organization, pk=self.request.GET.get("org"))

def get_page_title(self, context):
return "Edit BagIt Profile" if self.object else "Create BagIt Profile"
org = self.get_organization()
if self.object:
return "Edit BagIt Profile: {}".format(org)
else:
return "Create BagIt Profile: {}".format(org)

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.object:
form = BagItProfileForm(instance=self.object)
organization = self.object.organization
else:
source_organization = self.request.user.organization
organization = get_object_or_404(Organization, pk=self.request.GET.get("org"))
form = BagItProfileForm(
initial={
"source_organization": source_organization,
"contact_email": "archive@rockarch.org",
"organization": organization})
organization = self.get_organization()

# Reuse the bound form that was built for this submission so errors/values survive
# a failed nested formset save
form = context.get("form")
if form is None or not form.is_bound:
if self.object:
form = BagItProfileForm(instance=self.object)
else:
source_organization = self.request.user.organization
form = BagItProfileForm(
initial={
"source_organization": source_organization,
"contact_email": "archive@rockarch.org",
"organization": organization})
context["form"] = form
context["bag_info_formset"] = BagItProfileBagInfoFormset(instance=self.object, prefix="bag_info")
context["bag_info_formset"] = getattr(self, "_invalid_bag_info_formset", None) or BagItProfileBagInfoFormset(instance=self.object, prefix="bag_info")
context["organization"] = organization
return context

def get_success_url(self):
return reverse("bagit-profiles:detail", kwargs={"pk": self.object.pk})
return reverse("orgs:detail", kwargs={"pk": self.object.organization.pk})

def form_valid(self, form):
"""Saves associated formsets."""
bagit_profile = form.save()
# Only save once all are confirmed valid
bagit_profile = form.save(commit=False)
bag_info_formset = BagItProfileBagInfoFormset(
self.request.POST, instance=bagit_profile, prefix="bag_info")
if not bag_info_formset.is_valid():
error_messages = []
for form in bag_info_formset:
for field, errors in form.errors.items():
for bag_info_form in bag_info_formset:
for field, errors in bag_info_form.errors.items():
for error in errors:
error_messages.append(f"{field}: {error}")

Expand All @@ -67,16 +83,23 @@ def form_valid(self, form):
f"There was a problem with your submission: {detailed_errors} "
"Please correct the error(s) and try again."
)
self._invalid_bag_info_formset = bag_info_formset
return super().form_invalid(form)
else:
with transaction.atomic():
bagit_profile.save()
form.save_m2m()
bag_info_formset.save()
bagit_profile.version = bagit_profile.version + Decimal(1)
bagit_profile.bagit_profile_identifier = self.request.build_absolute_uri(
reverse(
"bagitprofile-detail",
kwargs={"pk": bagit_profile.id, "format": "json"},

self.object = bagit_profile
bagit_profile.version = bagit_profile.version + Decimal(1)
bagit_profile.bagit_profile_identifier = self.request.build_absolute_uri(
reverse(
"bagitprofile-detail",
kwargs={"pk": bagit_profile.id, "format": "json"},
)
)
)
bagit_profile.save()

messages.success(self.request, "BagIt Profile saved")
return super().form_valid(form)

Expand All @@ -102,9 +125,11 @@ class BagItProfileUpdateView(BagItProfileManageView, UpdateView):

class BagItProfileDetailView(PageTitleMixin, OrgReadViewMixin, DetailView):
template_name = "bagit_profiles/detail.html"
page_title = "BagIt Profile"
model = BagItProfile

def get_page_title(self, context):
return "BagIt Profile: {}".format(context["object"].organization)


class BagItProfileAPIAdminView(ManagingArchivistMixin, JSONResponseMixin, TemplateView):

Expand Down
38 changes: 38 additions & 0 deletions bag_transfer/orgs/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from django import forms

from bag_transfer.models import Organization

EMPTY_LABEL = "--- Select an acquisition type ---"


class OrganizationForm(forms.ModelForm):
class Meta:
model = Organization
fields = ["name", "acquisition_type"]
labels = {
"name": "Organization Name",
}

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["acquisition_type"].choices = [
("", EMPTY_LABEL)
] + list(Organization.ACQUISITION_TYPE_CHOICES)


class OrganizationUpdateForm(forms.ModelForm):
class Meta:
model = Organization
fields = ["is_active", "name", "acquisition_type"]
labels = {
"name": "Organization Name",
}
widgets = {
"is_active": forms.widgets.CheckboxInput(attrs={"class": "checkbox checkbox--blue"}),
}

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["acquisition_type"].choices = [
("", EMPTY_LABEL)
] + list(Organization.ACQUISITION_TYPE_CHOICES)
5 changes: 3 additions & 2 deletions bag_transfer/orgs/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
OrgReadViewMixin)
from bag_transfer.mixins.viewmixins import PageTitleMixin
from bag_transfer.models import Organization, Transfer
from bag_transfer.orgs.forms import OrganizationForm, OrganizationUpdateForm


class OrganizationCreateView(PageTitleMixin, ManagingArchivistMixin, SuccessMessageMixin, CreateView):
template_name = "orgs/create.html"
page_title = "Add Organization"
model = Organization
fields = ["name", "acquisition_type"]
form_class = OrganizationForm
success_message = "New Organization Saved!"

def get_success_url(self):
Expand Down Expand Up @@ -47,7 +48,7 @@ def get_page_title(self, context):
class OrganizationEditView(PageTitleMixin, ManagingArchivistMixin, SuccessMessageMixin, UpdateView):
template_name = "orgs/update.html"
model = Organization
fields = ["is_active", "name", "acquisition_type"]
form_class = OrganizationUpdateForm
success_message = "Organization Saved!"

def get_page_title(self, context):
Expand Down
Loading