User Profiles and Account Settings

User Model Fields

Profile fields on the User model:

  • first_name, last_name, email
  • gender – enum (female, male)
  • location, timezone, locale, birthdate
  • linkedin_profile_url
  • community_id – community affiliation
  • has_one_attached :profile_image – via ActiveStorage with fallback to default

Account Settings (app/controllers/users/account_settings_controller.rb)

AuthorizedController with:

  • index – renders edit form
  • update – updates first_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 picker
  • create – stores fluent_language and target_language in 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_id user 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 (a consumed_at flag) 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 via Profile::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: owner user_id, teacher_id (null until claimed), scopes (jsonb array, subset of progress/activity/vocabulary), code (unique, TCHR·XXXX), optional expires_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.
  • UserAccessGrantPolicy enforces everything: view_*? requires claimed-by-this-user + unrevoked + unexpired + scope granted; a revoked/expired/unclaimed grant 404s exactly like a stranger’s guess.

This site uses Just the Docs, a documentation theme for Jekyll.