## Summary
Implements per-member progress tracking on shared books.
### Backend (apps/groups)
- **ReadingGroup** model: named groups linked to EBooks with creator tracking
- **GroupMembership** model: user-group association with member/admin roles
- **MemberProgress** model: per-member progress with section tracking, percentage, time_spent, privacy toggles, and device-position preservation
- **ReadingGroupViewSet**: CRUD, join/leave, members list, progress endpoints
### API Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET/POST | `/api/groups/` | List / create groups |
| GET/PUT/PATCH/DELETE | `/api/groups/{id}/` | Group detail |
| POST | `/api/groups/{id}/join/` | Join a group |
| POST | `/api/groups/{id}/leave/` | Leave a group |
| GET | `/api/groups/{id}/members/` | List group members |
| GET | `/api/groups/{id}/members/progress/` | All members progress (privacy-aware) |
| GET/PATCH | `/api/groups/{id}/progress/` | Own progress |
| GET | `/api/groups/{id}/progress/summary/` | Admin summary |
### Frontend
- Groups list page (`/groups`) with create modal and book selector
- Group detail page (`/groups/:id`) with progress tab and admin summary tab
- Progress bar visualization per member with privacy-aware display
- Admin stat cards (total members, started, finished, avg progress, avg time)
- Navigation link from Library header
### Acceptance Criteria
- ✅ Member sees their own current section/position saved from previous session
- ✅ Progress is saved automatically and reflected in member progress list
- ✅ Members see other members section progress (e.g., "Section 3 of 12") without exact position
- ✅ Own progress shows detailed view including current section, percentage, time spent
- ✅ Progress syncs across devices (via existing ReadingProgress sync + MemberProgress)
- ✅ Admin sees summary of all members with averages and per-member details
- ✅ New members start from beginning while seeing group current progress
Closes #30
Backend (apps/groups):
- ReadingGroup model: named groups linked to EBooks with creator tracking
- GroupMembership model: user-group association with member/admin roles
- MemberProgress model: per-member progress with section tracking,
percentage, time_spent, privacy toggles, and device-position preservation
- ReadingGroupViewSet: CRUD, join/leave, members list, progress endpoints
- GET /api/groups/{id}/members/progress/ — all members' progress
(public: section label only; own: full detail; private: stubbed)
- PATCH /api/groups/{id}/progress/ — update own progress with auto-
percentage calculation from section boundaries
- GET /api/groups/{id}/progress/summary/ — admin dashboard with
averages, started/finished counts, and per-member details
- Permissions: IsGroupMember, IsGroupAdmin
Frontend:
- Groups list page (/groups) with create modal and book selector
- Group detail page (/groups/:id) with progress and admin summary tabs
- Progress bar visualization per member with privacy-aware display
- Admin stat cards (total members, started, finished, avg progress, avg time)
- Navigation link from Library header
Shared:
- ReadingGroupSummary and MemberProgressPublic types
- API endpoint constants for groups routes
Closes#30
These MUST be resolved before this PR can be merged.
[File: backend/apps/groups/views.py | Line ~657]unhandled exception — GroupMembership.objects.get(group=group, user=request.user) in my_progress can raise DoesNotExist without being caught. If a user somehow reaches this endpoint without a valid membership (edge case), it will return 500 instead of a proper error response. Wrap in try/except or use get_object_or_404.
[File: frontend/src/pages/GroupDetail.tsx | Line ~897-899]buggy logic — isAdmin calculation compares m.user_id against (group as ReadingGroupDetail & { _myUserId?: number })._myUserId, but _myUserId is never set on the group object. This logic will always be false, breaking the admin tab visibility.
[File: frontend/src/pages/GroupDetail.tsx | Line ~1034-1038]buggy logic — isMember uses progressEntries.some((e) => "id" in e) which checks if ANY progress entry has an "id" field - this does not determine if the CURRENT user is a member. The Leave button visibility and join/leave logic depends on this, which is fundamentally broken.
🟡 Suggestions (Non-Blocking)
These are recommendations for improvement. Not required for merge.
[File: backend/apps/groups/serializers.py] Potential N+1 query: get_my_progress runs a query per group in the list view. Consider annotating the progress in the queryset instead.
[File: frontend/src/pages/GroupDetail.tsx] Missing newline at EOF.
[File: backend/apps/groups/views.py] Missing newline at EOF.
Progress sync across devices via last_position JSON field
Progress visibility settings (is_public boolean)
GET /groups/{id}/members/progress endpoint
Progress calculation from section boundaries
Reading time tracking (time_spent_seconds)
Progress summary for admin dashboard
## Reid's Review — PR #36
**Verdict:** 🔴 Changes Required
---
### 🔴 Blocking Issues
> These MUST be resolved before this PR can be merged.
- **[File: backend/apps/groups/views.py | Line ~657]** `unhandled exception` — `GroupMembership.objects.get(group=group, user=request.user)` in `my_progress` can raise `DoesNotExist` without being caught. If a user somehow reaches this endpoint without a valid membership (edge case), it will return 500 instead of a proper error response. Wrap in try/except or use `get_object_or_404`.
- **[File: frontend/src/pages/GroupDetail.tsx | Line ~897-899]** `buggy logic` — `isAdmin` calculation compares `m.user_id` against `(group as ReadingGroupDetail & { _myUserId?: number })._myUserId`, but `_myUserId` is never set on the group object. This logic will always be `false`, breaking the admin tab visibility.
- **[File: frontend/src/pages/GroupDetail.tsx | Line ~1034-1038]** `buggy logic` — `isMember` uses `progressEntries.some((e) => "id" in e)` which checks if ANY progress entry has an "id" field - this does not determine if the CURRENT user is a member. The Leave button visibility and join/leave logic depends on this, which is fundamentally broken.
---
### 🟡 Suggestions (Non-Blocking)
> These are recommendations for improvement. Not required for merge.
- **[File: backend/apps/groups/serializers.py]** Potential N+1 query: `get_my_progress` runs a query per group in the list view. Consider annotating the progress in the queryset instead.
- **[File: frontend/src/pages/GroupDetail.tsx]** Missing newline at EOF.
- **[File: backend/apps/groups/views.py]** Missing newline at EOF.
---
### 📋 AC Coverage
> Based on linked issue #30
All acceptance criteria items are implemented:
- [x] MemberProgress model with user_id, group_id, current_section, percentage, time_spent
- [x] Progress tracking endpoint (PATCH /groups/{id}/progress/)
- [x] Progress sync across devices via last_position JSON field
- [x] Progress visibility settings (is_public boolean)
- [x] GET /groups/{id}/members/progress endpoint
- [x] Progress calculation from section boundaries
- [x] Reading time tracking (time_spent_seconds)
- [x] Progress summary for admin dashboard
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Implements per-member progress tracking on shared books.
Backend (apps/groups)
API Endpoints
/api/groups//api/groups/{id}//api/groups/{id}/join//api/groups/{id}/leave//api/groups/{id}/members//api/groups/{id}/members/progress//api/groups/{id}/progress//api/groups/{id}/progress/summary/Frontend
/groups) with create modal and book selector/groups/:id) with progress tab and admin summary tabAcceptance Criteria
Closes #30
Backend (apps/groups): - ReadingGroup model: named groups linked to EBooks with creator tracking - GroupMembership model: user-group association with member/admin roles - MemberProgress model: per-member progress with section tracking, percentage, time_spent, privacy toggles, and device-position preservation - ReadingGroupViewSet: CRUD, join/leave, members list, progress endpoints - GET /api/groups/{id}/members/progress/ — all members' progress (public: section label only; own: full detail; private: stubbed) - PATCH /api/groups/{id}/progress/ — update own progress with auto- percentage calculation from section boundaries - GET /api/groups/{id}/progress/summary/ — admin dashboard with averages, started/finished counts, and per-member details - Permissions: IsGroupMember, IsGroupAdmin Frontend: - Groups list page (/groups) with create modal and book selector - Group detail page (/groups/:id) with progress and admin summary tabs - Progress bar visualization per member with privacy-aware display - Admin stat cards (total members, started, finished, avg progress, avg time) - Navigation link from Library header Shared: - ReadingGroupSummary and MemberProgressPublic types - API endpoint constants for groups routes Closes #30Reid's Review — PR #36
Verdict: 🔴 Changes Required
🔴 Blocking Issues
[File: backend/apps/groups/views.py | Line ~657]
unhandled exception—GroupMembership.objects.get(group=group, user=request.user)inmy_progresscan raiseDoesNotExistwithout being caught. If a user somehow reaches this endpoint without a valid membership (edge case), it will return 500 instead of a proper error response. Wrap in try/except or useget_object_or_404.[File: frontend/src/pages/GroupDetail.tsx | Line ~897-899]
buggy logic—isAdmincalculation comparesm.user_idagainst(group as ReadingGroupDetail & { _myUserId?: number })._myUserId, but_myUserIdis never set on the group object. This logic will always befalse, breaking the admin tab visibility.[File: frontend/src/pages/GroupDetail.tsx | Line ~1034-1038]
buggy logic—isMemberusesprogressEntries.some((e) => "id" in e)which checks if ANY progress entry has an "id" field - this does not determine if the CURRENT user is a member. The Leave button visibility and join/leave logic depends on this, which is fundamentally broken.🟡 Suggestions (Non-Blocking)
get_my_progressruns a query per group in the list view. Consider annotating the progress in the queryset instead.📋 AC Coverage
All acceptance criteria items are implemented: