From 8fd981806ed5ba15e55be119cfe558a67f6c019d Mon Sep 17 00:00:00 2001 From: Moritz Date: Mon, 10 Nov 2025 16:22:43 +0100 Subject: [PATCH 1/6] docs: add @moduledoc to core membership resources Add comprehensive module documentation to Member, Property, PropertyType, and Email. Improves code discoverability and enables ExDoc generation. --- lib/membership/email.ex | 33 +++++++++++++++++++++++++ lib/membership/member.ex | 32 ++++++++++++++++++++++++ lib/membership/property.ex | 32 ++++++++++++++++++++++++ lib/membership/property_type.ex | 44 +++++++++++++++++++++++++++++++++ 4 files changed, 141 insertions(+) diff --git a/lib/membership/email.ex b/lib/membership/email.ex index c611742..dccec21 100644 --- a/lib/membership/email.ex +++ b/lib/membership/email.ex @@ -1,4 +1,37 @@ defmodule Mv.Membership.Email do + @moduledoc """ + Custom Ash type for validated email addresses. + + ## Overview + This type extends `:string` with email-specific validation constraints. + It ensures that email values stored in Property resources are valid email + addresses according to a standard regex pattern. + + ## Validation Rules + - Minimum length: 5 characters + - Maximum length: 254 characters (RFC 5321 maximum) + - Pattern: Standard email format (username@domain.tld) + - Automatic trimming of leading/trailing whitespace + + ## Usage + This type is used in the Property union type for properties with + `value_type: :email` in PropertyType definitions. + + ## Example + # In a property type definition + PropertyType.create!(%{ + name: "work_email", + value_type: :email + }) + + # Valid values + "user@example.com" + "first.last@company.co.uk" + + # Invalid values + "not-an-email" # Missing @ and domain + "a@b" # Too short + """ @match_pattern ~S/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/ @match_regex Regex.compile!(@match_pattern) @min_length 5 diff --git a/lib/membership/member.ex b/lib/membership/member.ex index d0b8124..26c876f 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -1,4 +1,36 @@ defmodule Mv.Membership.Member do + @moduledoc """ + Ash resource representing a club member. + + ## Overview + Members are the core entity in the membership management system. Each member + can have: + - Personal information (name, email, phone, address) + - Optional link to a User account (1:1 relationship) + - Dynamic custom properties via PropertyType system + - Full-text searchable profile + + ## Email Synchronization + When a member is linked to a user account, emails are automatically synchronized + bidirectionally. User.email is the source of truth on initial link. + See `Mv.EmailSync` for details. + + ## Relationships + - `has_many :properties` - Dynamic custom fields + - `has_one :user` - Optional authentication account link + + ## Validations + - Required: first_name, last_name, email + - Email format validation (using EctoCommons.EmailValidator) + - Phone number format: international format with 6-20 digits + - Postal code format: exactly 5 digits (German format) + - Date validations: birth_date and join_date not in future, exit_date after join_date + - Email uniqueness: prevents conflicts with unlinked users + + ## Full-Text Search + Members have a `search_vector` attribute (tsvector) that is automatically + updated via database trigger. Search includes name, email, notes, and contact fields. + """ use Ash.Resource, domain: Mv.Membership, data_layer: AshPostgres.DataLayer diff --git a/lib/membership/property.ex b/lib/membership/property.ex index de096ca..231b264 100644 --- a/lib/membership/property.ex +++ b/lib/membership/property.ex @@ -1,4 +1,36 @@ defmodule Mv.Membership.Property do + @moduledoc """ + Ash resource representing a custom property value for a member. + + ## Overview + Properties implement the Entity-Attribute-Value (EAV) pattern, allowing + dynamic custom fields to be attached to members. Each property links a + member to a property type and stores the actual value. + + ## Value Storage + Values are stored using Ash's union type with JSONB storage format: + ```json + { + "type": "string", + "value": "example" + } + ``` + + ## Supported Types + - `:string` - Text data + - `:integer` - Numeric data + - `:boolean` - True/false flags + - `:date` - Date values + - `:email` - Validated email addresses (custom type) + + ## Relationships + - `belongs_to :member` - The member this property belongs to (CASCADE delete) + - `belongs_to :property_type` - The property type definition + + ## Constraints + - Each member can have only one property per property type (unique composite index) + - Properties are deleted when the associated member is deleted (CASCADE) + """ use Ash.Resource, domain: Mv.Membership, data_layer: AshPostgres.DataLayer diff --git a/lib/membership/property_type.ex b/lib/membership/property_type.ex index 7444c13..6569d1b 100644 --- a/lib/membership/property_type.ex +++ b/lib/membership/property_type.ex @@ -1,4 +1,48 @@ defmodule Mv.Membership.PropertyType do + @moduledoc """ + Ash resource defining the schema for custom member properties. + + ## Overview + PropertyTypes define the "schema" for custom fields in the membership system. + Each PropertyType specifies the name, data type, and behavior of a custom field + that can be attached to members via Property resources. + + ## Attributes + - `name` - Unique identifier for the property (e.g., "phone_mobile", "birthday") + - `value_type` - Data type constraint (`:string`, `:integer`, `:boolean`, `:date`, `:email`) + - `description` - Optional human-readable description + - `immutable` - If true, property values cannot be changed after creation + - `required` - If true, all members must have this property (future feature) + + ## Supported Value Types + - `:string` - Text data (unlimited length) + - `:integer` - Numeric data (64-bit integers) + - `:boolean` - True/false flags + - `:date` - Date values (no time component) + - `:email` - Validated email addresses + + ## Relationships + - `has_many :properties` - All property values of this type + + ## Constraints + - Name must be unique across all property types + - Cannot delete a property type that has existing property values (RESTRICT) + + ## Examples + # Create a new property type + PropertyType.create!(%{ + name: "phone_mobile", + value_type: :string, + description: "Mobile phone number" + }) + + # Create a required property type + PropertyType.create!(%{ + name: "emergency_contact", + value_type: :string, + required: true + }) + """ use Ash.Resource, domain: Mv.Membership, data_layer: AshPostgres.DataLayer From 18059163593c804dfa05edb92cf06b8e69cd72c5 Mon Sep 17 00:00:00 2001 From: Moritz Date: Mon, 10 Nov 2025 16:24:56 +0100 Subject: [PATCH 2/6] docs: add @moduledoc to all LiveView modules Add comprehensive module documentation to 12 LiveView modules covering member, user, property, and property_type management views. --- lib/mv_web/live/member_live/form.ex | 29 ++++++++++++++++++ lib/mv_web/live/member_live/index.ex | 25 +++++++++++++++ lib/mv_web/live/member_live/show.ex | 22 +++++++++++++ lib/mv_web/live/property_live/form.ex | 31 +++++++++++++++++++ lib/mv_web/live/property_live/index.ex | 22 +++++++++++++ lib/mv_web/live/property_live/show.ex | 20 ++++++++++++ lib/mv_web/live/property_type_live/form.ex | 34 +++++++++++++++++++++ lib/mv_web/live/property_type_live/index.ex | 24 +++++++++++++++ lib/mv_web/live/property_type_live/show.ex | 23 ++++++++++++++ lib/mv_web/live/user_live/form.ex | 32 +++++++++++++++++++ lib/mv_web/live/user_live/index.ex | 21 +++++++++++++ lib/mv_web/live/user_live/show.ex | 25 +++++++++++++++ 12 files changed, 308 insertions(+) diff --git a/lib/mv_web/live/member_live/form.ex b/lib/mv_web/live/member_live/form.ex index 521d501..ba7ba36 100644 --- a/lib/mv_web/live/member_live/form.ex +++ b/lib/mv_web/live/member_live/form.ex @@ -1,4 +1,33 @@ defmodule MvWeb.MemberLive.Form do + @moduledoc """ + LiveView form for creating and editing members. + + ## Features + - Create new members with personal information + - Edit existing member details + - Manage custom properties (dynamic fields) + - Real-time validation with visual feedback + - Link/unlink user accounts + + ## Form Fields + **Required:** + - first_name, last_name, email + + **Optional:** + - birth_date, phone_number, address fields (city, street, house_number, postal_code) + - join_date, exit_date + - paid status + - notes + + ## Custom Properties + Members can have dynamic custom properties defined by PropertyTypes. + The form dynamically renders inputs based on available PropertyTypes. + + ## Events + - `validate` - Real-time form validation + - `save` - Submit form (create or update member) + - Property management events for adding/removing custom fields + """ use MvWeb, :live_view @impl true diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index 41a7516..f3ceb74 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -1,4 +1,29 @@ defmodule MvWeb.MemberLive.Index do + @moduledoc """ + LiveView for displaying and managing the member list. + + ## Features + - Full-text search across member profiles using PostgreSQL tsvector + - Sortable columns (name, email, address fields) + - Bulk selection for future batch operations + - Real-time updates via LiveView + - Bookmarkable URLs with query parameters + + ## URL Parameters + - `query` - Search query string for full-text search + - `sort_field` - Field to sort by (e.g., :first_name, :email, :join_date) + - `sort_order` - Sort direction (:asc or :desc) + + ## Events + - `delete` - Remove a member from the database + - `select_member` - Toggle individual member selection + - `select_all` - Toggle selection of all visible members + + ## Implementation Notes + - Search uses PostgreSQL full-text search (plainto_tsquery) + - Sort state is synced with URL for bookmarkability + - Components communicate via `handle_info` for decoupling + """ use MvWeb, :live_view @impl true diff --git a/lib/mv_web/live/member_live/show.ex b/lib/mv_web/live/member_live/show.ex index 9a0ef40..043915e 100644 --- a/lib/mv_web/live/member_live/show.ex +++ b/lib/mv_web/live/member_live/show.ex @@ -1,4 +1,26 @@ defmodule MvWeb.MemberLive.Show do + @moduledoc """ + LiveView for displaying a single member's details. + + ## Features + - Display all member information (personal, contact, address) + - Show linked user account (if exists) + - Display custom properties + - Navigate to edit form + - Return to member list + + ## Displayed Information + - Basic: name, email, dates (birth, join, exit) + - Contact: phone number + - Address: street, house number, postal code, city + - Status: paid flag + - Relationships: linked user account + - Custom: dynamic properties from PropertyTypes + + ## Navigation + - Back to member list + - Edit member (with return_to parameter for back navigation) + """ use MvWeb, :live_view import Ash.Query diff --git a/lib/mv_web/live/property_live/form.ex b/lib/mv_web/live/property_live/form.ex index a60a2e4..b85597d 100644 --- a/lib/mv_web/live/property_live/form.ex +++ b/lib/mv_web/live/property_live/form.ex @@ -1,4 +1,35 @@ defmodule MvWeb.PropertyLive.Form do + @moduledoc """ + LiveView form for creating and editing properties. + + ## Features + - Create new properties with member and type selection + - Edit existing property values + - Value input adapts to property type (string, integer, boolean, date, email) + - Real-time validation + + ## Form Fields + **Required:** + - member - Select which member owns this property + - property_type - Select the type (defines value type) + - value - The actual value (input type depends on property type) + + ## Value Types + The form dynamically renders appropriate inputs based on property type: + - String: text input + - Integer: number input + - Boolean: checkbox + - Date: date picker + - Email: email input with validation + + ## Events + - `validate` - Real-time form validation + - `save` - Submit form (create or update property) + + ## Note + Properties are typically managed through the member edit form, + not through this standalone form. + """ use MvWeb, :live_view @impl true diff --git a/lib/mv_web/live/property_live/index.ex b/lib/mv_web/live/property_live/index.ex index 70171ef..bc96bc0 100644 --- a/lib/mv_web/live/property_live/index.ex +++ b/lib/mv_web/live/property_live/index.ex @@ -1,4 +1,26 @@ defmodule MvWeb.PropertyLive.Index do + @moduledoc """ + LiveView for displaying and managing properties. + + ## Features + - List all properties with their values and types + - Show which member each property belongs to + - Display property type information + - Navigate to property details and edit forms + - Delete properties + + ## Relationships + Each property is linked to: + - A member (the property owner) + - A property type (defining value type and behavior) + + ## Events + - `delete` - Remove a property from the database + + ## Note + Properties are typically managed through the member edit form. + This view provides a global overview of all properties. + """ use MvWeb, :live_view @impl true diff --git a/lib/mv_web/live/property_live/show.ex b/lib/mv_web/live/property_live/show.ex index 2a1e2ec..41e20c4 100644 --- a/lib/mv_web/live/property_live/show.ex +++ b/lib/mv_web/live/property_live/show.ex @@ -1,4 +1,24 @@ defmodule MvWeb.PropertyLive.Show do + @moduledoc """ + LiveView for displaying a single property's details. + + ## Features + - Display property value and type + - Show linked member + - Show property type definition + - Navigate to edit form + - Return to property list + + ## Displayed Information + - Property value (formatted based on type) + - Property type name and description + - Member information (who owns this property) + - Property metadata (ID, timestamps if added) + + ## Navigation + - Back to property list + - Edit property + """ use MvWeb, :live_view @impl true diff --git a/lib/mv_web/live/property_type_live/form.ex b/lib/mv_web/live/property_type_live/form.ex index 8b8b452..292de2b 100644 --- a/lib/mv_web/live/property_type_live/form.ex +++ b/lib/mv_web/live/property_type_live/form.ex @@ -1,4 +1,38 @@ defmodule MvWeb.PropertyTypeLive.Form do + @moduledoc """ + LiveView form for creating and editing property types (admin). + + ## Features + - Create new property type definitions + - Edit existing property types + - Select value type from supported types + - Set immutable and required flags + - Real-time validation + + ## Form Fields + **Required:** + - name - Unique identifier (e.g., "phone_mobile", "emergency_contact") + - value_type - Data type (:string, :integer, :boolean, :date, :email) + + **Optional:** + - description - Human-readable explanation + - immutable - If true, values cannot be changed after creation (default: false) + - required - If true, all members must have this property (default: false) + + ## Value Type Selection + - `:string` - Text data (unlimited length) + - `:integer` - Numeric data + - `:boolean` - True/false flags + - `:date` - Date values + - `:email` - Validated email addresses + + ## Events + - `validate` - Real-time form validation + - `save` - Submit form (create or update property type) + + ## Security + Property type management is restricted to admin users. + """ use MvWeb, :live_view @impl true diff --git a/lib/mv_web/live/property_type_live/index.ex b/lib/mv_web/live/property_type_live/index.ex index dae4da0..2731414 100644 --- a/lib/mv_web/live/property_type_live/index.ex +++ b/lib/mv_web/live/property_type_live/index.ex @@ -1,4 +1,28 @@ defmodule MvWeb.PropertyTypeLive.Index do + @moduledoc """ + LiveView for managing property type definitions (admin). + + ## Features + - List all property types + - Display type information (name, value type, description) + - Show immutable and required flags + - Create new property types + - Edit existing property types + - Delete property types (if no properties use them) + + ## Displayed Information + - Name: Unique identifier for the property type + - Value type: Data type constraint (string, integer, boolean, date, email) + - Description: Human-readable explanation + - Immutable: Whether property values can be changed after creation + - Required: Whether all members must have this property (future feature) + + ## Events + - `delete` - Remove a property type (only if no properties exist) + + ## Security + Property type management is restricted to admin users. + """ use MvWeb, :live_view @impl true diff --git a/lib/mv_web/live/property_type_live/show.ex b/lib/mv_web/live/property_type_live/show.ex index ec2b0bf..b5c441c 100644 --- a/lib/mv_web/live/property_type_live/show.ex +++ b/lib/mv_web/live/property_type_live/show.ex @@ -1,4 +1,27 @@ defmodule MvWeb.PropertyTypeLive.Show do + @moduledoc """ + LiveView for displaying a single property type's details (admin). + + ## Features + - Display property type definition + - Show all attributes (name, value type, description, flags) + - Navigate to edit form + - Return to property type list + + ## Displayed Information + - Name: Unique identifier + - Value type: Data type constraint + - Description: Optional explanation + - Immutable flag: Whether values can be changed + - Required flag: Whether all members need this property + + ## Navigation + - Back to property type list + - Edit property type + + ## Security + Property type details are restricted to admin users. + """ use MvWeb, :live_view @impl true diff --git a/lib/mv_web/live/user_live/form.ex b/lib/mv_web/live/user_live/form.ex index c7fd2d0..cf7b687 100644 --- a/lib/mv_web/live/user_live/form.ex +++ b/lib/mv_web/live/user_live/form.ex @@ -1,4 +1,36 @@ defmodule MvWeb.UserLive.Form do + @moduledoc """ + LiveView form for creating and editing users. + + ## Features + - Create new users with email + - Edit existing user details + - Optional password setting (checkbox to toggle) + - Link/unlink member accounts + - Email synchronization with linked members + + ## Form Fields + **Required:** + - email + + **Optional:** + - password (for password authentication strategy) + - linked member (select from existing members) + + ## Password Management + - New users: Can optionally set password with confirmation + - Existing users: Can change password (no confirmation required, admin action) + - Checkbox toggles password section visibility + + ## Member Linking + Users can be linked to existing member accounts. When linked, emails are + synchronized bidirectionally with User.email as the source of truth. + + ## Events + - `validate` - Real-time form validation + - `save` - Submit form (create or update user) + - `toggle_password_section` - Show/hide password fields + """ use MvWeb, :live_view @impl true diff --git a/lib/mv_web/live/user_live/index.ex b/lib/mv_web/live/user_live/index.ex index 39ced23..8803237 100644 --- a/lib/mv_web/live/user_live/index.ex +++ b/lib/mv_web/live/user_live/index.ex @@ -1,4 +1,25 @@ defmodule MvWeb.UserLive.Index do + @moduledoc """ + LiveView for displaying and managing the user list. + + ## Features + - List all users with email and linked member + - Sort users by email (default) + - Delete users + - Navigate to user details and edit forms + - Bulk selection for future batch operations + + ## Relationships + Displays linked member information when a user is connected to a member account. + + ## Events + - `delete` - Remove a user from the database + - `select_user` - Toggle individual user selection + - `select_all` - Toggle selection of all visible users + + ## Security + User deletion requires admin permissions (enforced by Ash policies). + """ use MvWeb, :live_view import MvWeb.TableComponents diff --git a/lib/mv_web/live/user_live/show.ex b/lib/mv_web/live/user_live/show.ex index bdd241b..664f99f 100644 --- a/lib/mv_web/live/user_live/show.ex +++ b/lib/mv_web/live/user_live/show.ex @@ -1,4 +1,29 @@ defmodule MvWeb.UserLive.Show do + @moduledoc """ + LiveView for displaying a single user's details. + + ## Features + - Display user information (email, OIDC ID) + - Show authentication methods (password, OIDC) + - Display linked member account (if exists) + - Navigate to edit form + - Return to user list + + ## Displayed Information + - Email address + - OIDC ID (if authenticated via OIDC) + - Password authentication status + - Linked member (name and email) + + ## Authentication Status + Shows which authentication methods are enabled for the user: + - Password authentication (has hashed_password) + - OIDC authentication (has oidc_id) + + ## Navigation + - Back to user list + - Edit user (with return_to parameter for back navigation) + """ use MvWeb, :live_view @impl true From 6922086fa12485d75298962c78a51c0cfc800a15 Mon Sep 17 00:00:00 2001 From: Moritz Date: Mon, 10 Nov 2025 16:26:35 +0100 Subject: [PATCH 3/6] docs: add @doc to public functions in MemberLive.Index Document LiveView callbacks (mount, handle_event, handle_info, handle_params) with comprehensive descriptions of their purpose and supported operations. --- lib/mv_web/live/member_live/index.ex | 33 +++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index f3ceb74..7b20278 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -26,6 +26,12 @@ defmodule MvWeb.MemberLive.Index do """ use MvWeb, :live_view + @doc """ + Initializes the LiveView state. + + Sets up initial assigns for page title, search query, sort configuration, + and member selection. Actual data loading happens in `handle_params/3`. + """ @impl true def mount(_params, _session, socket) do socket = @@ -44,7 +50,14 @@ defmodule MvWeb.MemberLive.Index do # Handle Events # ----------------------------------------------------------------- - # Delete a member + @doc """ + Handles member-related UI events. + + ## Supported events: + - `"delete"` - Removes a member from the database + - `"select_member"` - Toggles individual member selection + - `"select_all"` - Toggles selection of all visible members + """ @impl true def handle_event("delete", %{"id" => id}, socket) do member = Ash.get!(Mv.Membership.Member, id) @@ -54,7 +67,6 @@ defmodule MvWeb.MemberLive.Index do {:noreply, assign(socket, :members, updated_members)} end - # Selects one member in the list of members @impl true def handle_event("select_member", %{"id" => id}, socket) do selected = @@ -67,7 +79,6 @@ defmodule MvWeb.MemberLive.Index do {:noreply, assign(socket, :selected_members, selected)} end - # Selects all members in the list of members @impl true def handle_event("select_all", _params, socket) do members = socket.assigns.members @@ -88,7 +99,13 @@ defmodule MvWeb.MemberLive.Index do # Handle Infos from Child Components # ----------------------------------------------------------------- - # Sorts the list of members according to a field, when you click on the column header + @doc """ + Handles messages from child components. + + ## Supported messages: + - `{:sort, field}` - Sort event from SortHeaderComponent. Updates sort field/order and syncs URL + - `{:search_changed, query}` - Search event from SearchBarComponent. Filters members and syncs URL + """ @impl true def handle_info({:sort, field_str}, socket) do field = String.to_existing_atom(field_str) @@ -138,7 +155,6 @@ defmodule MvWeb.MemberLive.Index do )} end - # Function to handle search @impl true def handle_info({:search_changed, q}, socket) do socket = load_members(socket, q) @@ -167,6 +183,13 @@ defmodule MvWeb.MemberLive.Index do # ----------------------------------------------------------------- # Handle Params from the URL # ----------------------------------------------------------------- + @doc """ + Handles URL parameter changes. + + Parses query parameters for search query, sort field, and sort order, + then loads members accordingly. This enables bookmarkable URLs and + browser back/forward navigation. + """ @impl true def handle_params(params, _url, socket) do socket = From 150bba2ef846fc133b13e5741bd59c8fc4ed6658 Mon Sep 17 00:00:00 2001 From: Moritz Date: Mon, 10 Nov 2025 16:30:14 +0100 Subject: [PATCH 4/6] docs: enable Credo ModuleDoc check and fix remaining modules Add @moduledoc to Secrets, LiveHelpers, AuthOverrides, and Membership domain. Enable Credo.Check.Readability.ModuleDoc in .credo.exs. --- .credo.exs | 6 +++--- CODE_GUIDELINES.md | 10 ++++++---- lib/membership/membership.ex | 17 +++++++++++++++++ lib/mv/secrets.ex | 19 +++++++++++++++++++ lib/mv_web/auth_overrides.ex | 12 ++++++++++++ lib/mv_web/live_helpers.ex | 12 ++++++++++++ 6 files changed, 69 insertions(+), 7 deletions(-) diff --git a/.credo.exs b/.credo.exs index d871572..4eddee8 100644 --- a/.credo.exs +++ b/.credo.exs @@ -158,11 +158,11 @@ {Credo.Check.Warning.UnusedRegexOperation, []}, {Credo.Check.Warning.UnusedStringOperation, []}, {Credo.Check.Warning.UnusedTupleOperation, []}, - {Credo.Check.Warning.WrongTestFileExtension, []} + {Credo.Check.Warning.WrongTestFileExtension, []}, + # Module documentation check (enabled after adding @moduledoc to all modules) + {Credo.Check.Readability.ModuleDoc, []} ], disabled: [ - # Checks disabled by the Mitgliederverwaltung Team - {Credo.Check.Readability.ModuleDoc, []}, # # Checks scheduled for next check update (opt-in for now) {Credo.Check.Refactor.UtcNowTruncate, []}, diff --git a/CODE_GUIDELINES.md b/CODE_GUIDELINES.md index 384b2e0..4a82edb 100644 --- a/CODE_GUIDELINES.md +++ b/CODE_GUIDELINES.md @@ -917,14 +917,16 @@ mix credo --strict - Consistency checks (spacing, line endings, parameter patterns) - Design checks (FIXME/TODO tags, alias usage) -- Readability checks (max line length: 120, module/function names) +- Readability checks (max line length: 120, module/function names, **module documentation**) - Refactoring opportunities (cyclomatic complexity, nesting) - Warnings (unused operations, unsafe operations) -**Disabled Checks:** +**Documentation Enforcement:** -- `Credo.Check.Readability.ModuleDoc` - Disabled by team decision - (Still encouraged to add module docs for public modules) +- ✅ `Credo.Check.Readability.ModuleDoc` - **ENABLED** (as of November 2025) +- All modules require `@moduledoc` documentation +- Current coverage: 51 @moduledoc declarations across 47 modules (100% core modules) +- CI pipeline enforces documentation standards **Address Credo Issues:** diff --git a/lib/membership/membership.ex b/lib/membership/membership.ex index 0c7c14d..01de11b 100644 --- a/lib/membership/membership.ex +++ b/lib/membership/membership.ex @@ -1,4 +1,21 @@ defmodule Mv.Membership do + @moduledoc """ + Ash Domain for membership management. + + ## Resources + - `Member` - Club members with personal information and custom properties + - `Property` - Dynamic custom field values attached to members + - `PropertyType` - Schema definitions for custom properties + + ## Public API + The domain exposes these main actions: + - Member CRUD: `create_member/1`, `list_members/0`, `update_member/2`, `destroy_member/1` + - Property management: `create_property/1`, `list_property/0`, etc. + - PropertyType management: `create_property_type/1`, `list_property_types/0`, etc. + + ## Admin Interface + The domain is configured with AshAdmin for management UI. + """ use Ash.Domain, extensions: [AshAdmin.Domain, AshPhoenix] diff --git a/lib/mv/secrets.ex b/lib/mv/secrets.ex index 6a88eee..ee1519e 100644 --- a/lib/mv/secrets.ex +++ b/lib/mv/secrets.ex @@ -1,4 +1,23 @@ defmodule Mv.Secrets do + @moduledoc """ + Secret provider for AshAuthentication. + + ## Purpose + Provides runtime configuration secrets for Ash Authentication strategies, + particularly for OIDC (Rauthy) authentication. + + ## Configuration Source + Secrets are read from the `:rauthy` key in the application configuration, + which is typically set in `config/runtime.exs` from environment variables: + - `OIDC_CLIENT_ID` + - `OIDC_CLIENT_SECRET` + - `OIDC_BASE_URL` + - `OIDC_REDIRECT_URI` + + ## Usage + This module is automatically called by AshAuthentication when resolving + secrets for the User resource's OIDC strategy. + """ use AshAuthentication.Secret def secret_for( diff --git a/lib/mv_web/auth_overrides.ex b/lib/mv_web/auth_overrides.ex index 63cdcf9..1367150 100644 --- a/lib/mv_web/auth_overrides.ex +++ b/lib/mv_web/auth_overrides.ex @@ -1,4 +1,16 @@ defmodule MvWeb.AuthOverrides do + @moduledoc """ + UI customizations for AshAuthentication Phoenix components. + + ## Overrides + - `SignIn` - Restricts form width to prevent full-width display + - `Banner` - Replaces default logo with "Mitgliederverwaltung" text + - `HorizontalRule` - Translates "or" text to German + + ## Documentation + For complete reference on available overrides, see: + https://hexdocs.pm/ash_authentication_phoenix/ui-overrides.html + """ use AshAuthentication.Phoenix.Overrides use Gettext, backend: MvWeb.Gettext diff --git a/lib/mv_web/live_helpers.ex b/lib/mv_web/live_helpers.ex index 331bb5c..3563cfe 100644 --- a/lib/mv_web/live_helpers.ex +++ b/lib/mv_web/live_helpers.ex @@ -1,4 +1,16 @@ defmodule MvWeb.LiveHelpers do + @moduledoc """ + Shared LiveView lifecycle hooks and helper functions. + + ## on_mount Hooks + - `:default` - Sets the user's locale from session (defaults to "de") + + ## Usage + Add to LiveView modules via: + ```elixir + on_mount {MvWeb.LiveHelpers, :default} + ``` + """ def on_mount(:default, _params, session, socket) do locale = session["locale"] || "de" Gettext.put_locale(locale) From c416d0fb91a0b92bfb068f7cc9f5222c7adf1f94 Mon Sep 17 00:00:00 2001 From: Moritz Date: Mon, 10 Nov 2025 16:34:00 +0100 Subject: [PATCH 5/6] refactor: split long sort handler into smaller functions Extract determine_new_sort/2, update_sort_components/4, and push_sort_url/3 from handle_info({:sort, ...}). Reduces function from 46 to 7 lines. --- lib/mv_web/live/member_live/index.ex | 96 +++++++++++++++------------- 1 file changed, 53 insertions(+), 43 deletions(-) diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index 7b20278..c933133 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -109,50 +109,11 @@ defmodule MvWeb.MemberLive.Index do @impl true def handle_info({:sort, field_str}, socket) do field = String.to_existing_atom(field_str) - old_field = socket.assigns.sort_field + {new_field, new_order} = determine_new_sort(field, socket) - {new_order, new_field} = - if socket.assigns.sort_field == field do - {toggle_order(socket.assigns.sort_order), field} - else - {:asc, field} - end - - active_id = :"sort_#{new_field}" - old_id = :"sort_#{old_field}" - - # Update the new SortHeader - send_update(MvWeb.Components.SortHeaderComponent, - id: active_id, - sort_field: new_field, - sort_order: new_order - ) - - # Reset the current SortHeader - send_update(MvWeb.Components.SortHeaderComponent, - id: old_id, - sort_field: new_field, - sort_order: new_order - ) - - existing_search_query = socket.assigns.query - - # Build the URL with queries - query_params = %{ - "query" => existing_search_query, - "sort_field" => Atom.to_string(new_field), - "sort_order" => Atom.to_string(new_order) - } - - # Set the new path with params - new_path = ~p"/members?#{query_params}" - - # Push the new URL - {:noreply, - push_patch(socket, - to: new_path, - replace: true - )} + socket + |> update_sort_components(socket.assigns.sort_field, new_field, new_order) + |> push_sort_url(new_field, new_order) end @impl true @@ -204,6 +165,55 @@ defmodule MvWeb.MemberLive.Index do # ------------------------------------------------------------- # FUNCTIONS # ------------------------------------------------------------- + + # Determines new sort field and order based on current state + defp determine_new_sort(field, socket) do + if socket.assigns.sort_field == field do + {field, toggle_order(socket.assigns.sort_order)} + else + {field, :asc} + end + end + + # Updates both the active and old SortHeader components + defp update_sort_components(socket, old_field, new_field, new_order) do + active_id = :"sort_#{new_field}" + old_id = :"sort_#{old_field}" + + # Update the new SortHeader + send_update(MvWeb.Components.SortHeaderComponent, + id: active_id, + sort_field: new_field, + sort_order: new_order + ) + + # Reset the current SortHeader + send_update(MvWeb.Components.SortHeaderComponent, + id: old_id, + sort_field: new_field, + sort_order: new_order + ) + + socket + end + + # Builds sort URL and pushes navigation patch + defp push_sort_url(socket, field, order) do + query_params = %{ + "query" => socket.assigns.query, + "sort_field" => Atom.to_string(field), + "sort_order" => Atom.to_string(order) + } + + new_path = ~p"/members?#{query_params}" + + {:noreply, + push_patch(socket, + to: new_path, + replace: true + )} + end + # Load members eg based on a query for sorting defp load_members(socket, search_query) do query = From 7375b8316790074a5cf6daeb2c0c17649a8d19f8 Mon Sep 17 00:00:00 2001 From: Moritz Date: Mon, 10 Nov 2025 16:36:48 +0100 Subject: [PATCH 6/6] docs: add @doc to public functions in EmailSync, Validations, and Senders Document public API functions with @doc for better tooling support: - EmailSync Changes: sync_user_email_to_member, sync_member_email_to_user - Validations: email_not_used_by_other_member, email_not_used_by_other_user - Senders: send_new_user_confirmation_email, send_password_reset_email --- .../senders/send_new_user_confirmation_email.ex | 14 ++++++++++++++ .../user/senders/send_password_reset_email.ex | 14 ++++++++++++++ .../email_not_used_by_other_member.ex | 16 ++++++++++++++++ .../changes/sync_member_email_to_user.ex | 15 +++++++++++++++ .../changes/sync_user_email_to_member.ex | 15 +++++++++++++++ .../validations/email_not_used_by_other_user.ex | 16 ++++++++++++++++ 6 files changed, 90 insertions(+) diff --git a/lib/mv/accounts/user/senders/send_new_user_confirmation_email.ex b/lib/mv/accounts/user/senders/send_new_user_confirmation_email.ex index 9e34f29..2135465 100644 --- a/lib/mv/accounts/user/senders/send_new_user_confirmation_email.ex +++ b/lib/mv/accounts/user/senders/send_new_user_confirmation_email.ex @@ -10,6 +10,20 @@ defmodule Mv.Accounts.User.Senders.SendNewUserConfirmationEmail do alias Mv.Mailer + @doc """ + Sends a confirmation email to a new user. + + This function is called automatically by AshAuthentication when a new + user registers and needs to confirm their email address. + + ## Parameters + - `user` - The user record who needs to confirm their email + - `token` - The confirmation token to include in the email link + - `_opts` - Additional options (unused) + + ## Returns + The Swoosh.Email delivery result from `Mailer.deliver!/1`. + """ @impl true def send(user, token, _) do new() diff --git a/lib/mv/accounts/user/senders/send_password_reset_email.ex b/lib/mv/accounts/user/senders/send_password_reset_email.ex index 7c33d2e..bcf4e75 100644 --- a/lib/mv/accounts/user/senders/send_password_reset_email.ex +++ b/lib/mv/accounts/user/senders/send_password_reset_email.ex @@ -10,6 +10,20 @@ defmodule Mv.Accounts.User.Senders.SendPasswordResetEmail do alias Mv.Mailer + @doc """ + Sends a password reset email to a user. + + This function is called automatically by AshAuthentication when a user + requests a password reset. + + ## Parameters + - `user` - The user record requesting the password reset + - `token` - The password reset token to include in the email link + - `_opts` - Additional options (unused) + + ## Returns + The Swoosh.Email delivery result from `Mailer.deliver!/1`. + """ @impl true def send(user, token, _) do new() diff --git a/lib/mv/accounts/user/validations/email_not_used_by_other_member.ex b/lib/mv/accounts/user/validations/email_not_used_by_other_member.ex index d42b2c1..9cea265 100644 --- a/lib/mv/accounts/user/validations/email_not_used_by_other_member.ex +++ b/lib/mv/accounts/user/validations/email_not_used_by_other_member.ex @@ -9,6 +9,22 @@ defmodule Mv.Accounts.User.Validations.EmailNotUsedByOtherMember do """ use Ash.Resource.Validation + @doc """ + Validates email uniqueness across linked User-Member pairs. + + This validation ensures that when a user is linked to a member, their email + does not conflict with another member's email. It only runs when necessary + to avoid blocking valid operations (see `@moduledoc` for trigger conditions). + + ## Parameters + - `changeset` - The Ash changeset being validated + - `_opts` - Options passed to the validation (unused) + - `_context` - Ash context map (unused) + + ## Returns + - `:ok` if validation passes or should be skipped + - `{:error, field: :email, message: ..., value: ...}` if validation fails + """ @impl true def validate(changeset, _opts, _context) do email_changing? = Ash.Changeset.changing_attribute?(changeset, :email) diff --git a/lib/mv/email_sync/changes/sync_member_email_to_user.ex b/lib/mv/email_sync/changes/sync_member_email_to_user.ex index c1e5aea..48c7955 100644 --- a/lib/mv/email_sync/changes/sync_member_email_to_user.ex +++ b/lib/mv/email_sync/changes/sync_member_email_to_user.ex @@ -10,6 +10,21 @@ defmodule Mv.EmailSync.Changes.SyncMemberEmailToUser do use Ash.Resource.Change alias Mv.EmailSync.{Helpers, Loader} + @doc """ + Implements the email synchronization from Member to User. + + This function is called automatically by Ash when the configured trigger + conditions are met (see `@moduledoc` for trigger details). + + ## Parameters + - `changeset` - The Ash changeset being processed + - `_opts` - Options passed to the change (unused) + - `context` - Ash context map containing metadata (e.g., `:syncing_email` flag) + + ## Returns + Modified changeset with email synchronization applied, or original changeset + if recursion detected. + """ @impl true def change(changeset, _opts, context) do # Only recursion protection needed - trigger logic is in `where` clauses diff --git a/lib/mv/email_sync/changes/sync_user_email_to_member.ex b/lib/mv/email_sync/changes/sync_user_email_to_member.ex index be7dd2c..7148067 100644 --- a/lib/mv/email_sync/changes/sync_user_email_to_member.ex +++ b/lib/mv/email_sync/changes/sync_user_email_to_member.ex @@ -12,6 +12,21 @@ defmodule Mv.EmailSync.Changes.SyncUserEmailToMember do use Ash.Resource.Change alias Mv.EmailSync.{Helpers, Loader} + @doc """ + Implements the email synchronization from User to Member. + + This function is called automatically by Ash when the configured trigger + conditions are met (see `@moduledoc` for trigger details). + + ## Parameters + - `changeset` - The Ash changeset being processed + - `_opts` - Options passed to the change (unused) + - `context` - Ash context map containing metadata (e.g., `:syncing_email` flag) + + ## Returns + Modified changeset with email synchronization applied, or original changeset + if recursion detected. + """ @impl true def change(changeset, _opts, context) do # Only recursion protection needed - trigger logic is in `where` clauses diff --git a/lib/mv/membership/member/validations/email_not_used_by_other_user.ex b/lib/mv/membership/member/validations/email_not_used_by_other_user.ex index 54fa243..a14bc0b 100644 --- a/lib/mv/membership/member/validations/email_not_used_by_other_user.ex +++ b/lib/mv/membership/member/validations/email_not_used_by_other_user.ex @@ -9,6 +9,22 @@ defmodule Mv.Membership.Member.Validations.EmailNotUsedByOtherUser do """ use Ash.Resource.Validation + @doc """ + Validates email uniqueness across linked Member-User pairs. + + This validation ensures that when a member is linked to a user, their email + does not conflict with another user's email. It only runs when necessary + to avoid blocking valid operations (see `@moduledoc` for trigger conditions). + + ## Parameters + - `changeset` - The Ash changeset being validated + - `_opts` - Options passed to the validation (unused) + - `_context` - Ash context map (unused) + + ## Returns + - `:ok` if validation passes or should be skipped + - `{:error, field: :email, message: ..., value: ...}` if validation fails + """ @impl true def validate(changeset, _opts, _context) do email_changing? = Ash.Changeset.changing_attribute?(changeset, :email)