User Profiles and Account Settings
User Model Fields
Profile fields on the User model:
first_name,last_name,emailgender– enum (female, male)location,timezone,locale,birthdatelinkedin_profile_urlcommunity_id– community affiliationhas_one_attached :profile_image– via ActiveStorage with fallback to default
Account Settings (app/controllers/users/account_settings_controller.rb)
AuthorizedController with:
index– renders edit formupdate– updatesfirst_name,last_name,gender
Pre-Signup Language Selection (app/controllers/users/user_informations_controller.rb)
PublicController for collecting language preferences before registration:
new– shows language pickercreate– storesfluent_languageandtarget_languagein session
These preferences are carried through to the user record after registration.
Profile Image Management
Users can upload a profile image (stored via ActiveStorage). A remove action is available at users/:id/remove_profile_image. The shared partial app/views/shared/_profile_image.html.erb handles display with a fallback for users without an image.
GDPR Data Export (app/controllers/users/gdpr_exports_controller.rb)
AuthenticatedController with:
index– triggers a GDPR data export email
The Users::GdprExportMailer builds a CSV from fields defined in config/initializers/gdpr.rb (user ID, email, name, languages, location, timestamps) and emails it as an attachment.
Routes
resources :account_settings, only: :index do
collection { patch :update }
end
resources :user_informations, only: %i[new create]
resources :gdpr_exports, only: :index
resources :users, except: %i[new create index] do
member { get :remove_profile_image }
end
Profile Command Center (#298, #460)
/profile is the learner command center (canvas:
docs/design/returns/298-profile/design-final.html). It reopens the
last-used sub-tab (users.settings.profile_tab, a Sticky Setting). Tabs:
Progress, Activity, Evaluate, Vocabulary, Teachers, Account. Chats is
designed (canvas 1t-1v) but blocked on #300 — chats need a user
association and a title first.
Controllers live in app/controllers/profile/, all inheriting
Profile::BaseController (AuthenticatedController — every query is
scoped to current_user); routes in config/routes/profile.rb. Shared
stats live in app/services/profile/ (ProgressStats, ActivityStats,
Heatmap) so the owner’s tabs and a teacher’s read-only view (below)
compute identical numbers.
Evaluate (#460, canvas 1k-1o)
Peer evaluation — “a classroom courtesy, not a leaderboard”. A grade is
one overall impression on a three-word scale (excellent / good /
getting_there) plus an optional comment; deliberately not a rubric.
peer_grades:grader_id/gradee_iduser refs,impression,comment. Self-grading is rejected at the model.evaluation_tokens: short-lived (10 minutes), single-use codes. Chosen as a table rather than a signed/expiring message because single-use semantics need server-side state (aconsumed_atflag) anyway — the table is the simpler design. Codes use a lookalike-free alphabet (ShortCode) and display with a middot (MK4·T7Q); lookup normalizes case and separators.- Flow: “Show my code” issues/reuses an active token
(
Profile::EvaluationTokensController); the classmate types it in (“Enter a code” — the designed camera-unavailable path; there is no camera scanning and no QR image, the typed code is the whole mechanism), grades viaProfile::PeerGradesController, which consumes the token under a row lock. Expired/used codes render the request-a-fresh-code state. - Every grade received logs a
StudyEvent(source: "peer_evaluation",modality: "speak", resource: the grade) for the gradee — this is the speaking evidence on the Progress production axis and appears in the Activity study log.
Teachers (#460, canvas 1w-1y)
Scoped, revocable, code-based access grants — no email exchange.
user_access_grants: owneruser_id,teacher_id(null until claimed),scopes(jsonb array, subset ofprogress/activity/vocabulary),code(unique,TCHR·XXXX), optionalexpires_at,claimed_at,revoked_at. Unclaimed codes lapse after 7 days (CLAIM_WINDOW).- The owner creates a code from the Teachers tab dialog (scopes +
expiry: end of term / one month / one week / none) and can cancel or
revoke at any time (
Profile::GrantsController). - The teacher claims at
/teach/claim(Teach::ClaimsController), then sees/teach/:grant_id/(progress|activity|vocabulary)— read-only, granted tabs only, under a persistent banner with the student’s name and the expiry. No edit affordances, no Up next, no bulk actions. UserAccessGrantPolicyenforces everything:view_*?requires claimed-by-this-user + unrevoked + unexpired + scope granted; a revoked/expired/unclaimed grant 404s exactly like a stranger’s guess.