feat: add profile edit
Profile page at /profile with first name, last name, display name, telephone and mobile read from AD and shown with the username and e-mail. Only changed fields are written through the directory adapter in one transaction with the audit entry (field names only); numbers are accepted in any common notation and stored as +country code and digits; names are required. Plain-language errors per field, German text and tests; review findings logged in the plan. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FocPJ75XPma7TcuYEMy8zm
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
ae4b30ec22
commit
3fd8f27127
+53
-1
@@ -10,12 +10,13 @@ from django.views.decorators.debug import sensitive_post_parameters
|
||||
from django.views.decorators.http import require_http_methods
|
||||
|
||||
from portal.accounts.decorators import login_required
|
||||
from portal.accounts.identity import remember
|
||||
from portal.accounts.throttle import Throttled
|
||||
from portal.adapters.directory import DirectoryUnavailable
|
||||
from portal.adapters.registry import get_directory
|
||||
|
||||
from . import services
|
||||
from .forms import PasswordChangeForm
|
||||
from .forms import DETAIL_FIELDS, DetailsForm, PasswordChangeForm
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -107,3 +108,54 @@ def password_done(request):
|
||||
if flag is None: # opened directly: nothing to report
|
||||
return redirect("password")
|
||||
return render(request, "profile/password_done.html", {"mail": flag["mail"]})
|
||||
|
||||
|
||||
# -- profile details (FR-9) -------------------------------------------------------------------
|
||||
|
||||
NOTICE_FLAG = "profile_notice" # "saved" or "unchanged", shown once after the redirect
|
||||
|
||||
|
||||
def _details_page(request, form, details, *, status=200, notice=None):
|
||||
context = {
|
||||
"form": form,
|
||||
"details": details,
|
||||
"notice": notice,
|
||||
"fields": [
|
||||
{"field": form[name], "label": label, "max_length": max_length}
|
||||
for name, (label, max_length, _required) in DETAIL_FIELDS.items()
|
||||
],
|
||||
}
|
||||
return render(request, "profile/details.html", context, status=status)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def profile(request):
|
||||
user = request.portal_user.user # the session user only: nothing is read from the request
|
||||
if request.method == "GET":
|
||||
try:
|
||||
details = services.load_details(user)
|
||||
except DirectoryUnavailable:
|
||||
return _unavailable(request)
|
||||
notice = request.session.pop(NOTICE_FLAG, None)
|
||||
return _details_page(request, DetailsForm(initial=details.values), details, notice=notice)
|
||||
|
||||
form = DetailsForm(request.POST)
|
||||
form.is_valid() # trims; the checks need the directory value and run in the service
|
||||
try:
|
||||
result = services.update_details(user, form.cleaned_data, ip=request.client_ip)
|
||||
except services.DetailsInvalid as exc:
|
||||
form.apply_errors(exc.errors)
|
||||
return _details_page(request, form, exc.details, status=400)
|
||||
except services.DetailsNotAccepted as exc:
|
||||
form.add_error(None, _("A value was not accepted. Please check your entries."))
|
||||
return _details_page(request, form, exc.details, status=400)
|
||||
except DirectoryUnavailable:
|
||||
return _unavailable(request)
|
||||
except services.OutcomeUnknown:
|
||||
return render(request, "profile/details_unknown.html", status=500)
|
||||
|
||||
if result.changed:
|
||||
remember(request, result.account) # this session shows the new name at once
|
||||
request.session[NOTICE_FLAG] = "saved" if result.changed else "unchanged"
|
||||
return redirect("profile")
|
||||
|
||||
Reference in New Issue
Block a user