Merge pull request 'Update logging level to INFO, refine chore update logic, and enhance invite acceptance flow' (#58) from ph4 into prod

Reviewed-on: #58
This commit is contained in:
mo 2025-06-07 22:09:00 +02:00
commit ef2caaee56
10 changed files with 626 additions and 2499 deletions

View File

@ -231,7 +231,7 @@ async def update_group_chore(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Chore's group_id if provided must match path group_id ({group_id}).")
# Ensure chore_in has the correct type for the CRUD operation
chore_payload = chore_in.model_copy(update={"type": ChoreTypeEnum.group, "group_id": group_id} if chore_in.type is None else chore_in)
chore_payload = chore_in.model_copy(update={"type": ChoreTypeEnum.group, "group_id": group_id} if chore_in.type is None else {"group_id": group_id})
try:
updated_chore = await crud_chore.update_chore(db=db, chore_id=chore_id, chore_in=chore_payload, user_id=current_user.id, group_id=group_id)

View File

@ -8,6 +8,7 @@ from app.auth import current_active_user
from app.models import User as UserModel, UserRoleEnum
from app.schemas.invite import InviteAccept
from app.schemas.message import Message
from app.schemas.group import GroupPublic
from app.crud import invite as crud_invite
from app.crud import group as crud_group
from app.core.exceptions import (
@ -16,7 +17,8 @@ from app.core.exceptions import (
InviteAlreadyUsedError,
InviteCreationError,
GroupNotFoundError,
GroupMembershipError
GroupMembershipError,
GroupOperationError
)
logger = logging.getLogger(__name__)
@ -24,7 +26,7 @@ router = APIRouter()
@router.post(
"/accept", # Route relative to prefix "/invites"
response_model=Message,
response_model=GroupPublic,
summary="Accept Group Invite",
tags=["Invites"]
)
@ -34,28 +36,19 @@ async def accept_invite(
current_user: UserModel = Depends(current_active_user),
):
"""Accepts a group invite using the provided invite code."""
logger.info(f"User {current_user.email} attempting to accept invite code: {invite_in.invite_code}")
logger.info(f"User {current_user.email} attempting to accept invite code: {invite_in.code}")
# Get the invite
invite = await crud_invite.get_invite_by_code(db, invite_code=invite_in.invite_code)
# Get the invite - this function should only return valid, active invites
invite = await crud_invite.get_active_invite_by_code(db, code=invite_in.code)
if not invite:
logger.warning(f"Invalid invite code attempted by user {current_user.email}: {invite_in.invite_code}")
raise InviteNotFoundError(invite_in.invite_code)
# Check if invite is expired
if invite.is_expired():
logger.warning(f"Expired invite code attempted by user {current_user.email}: {invite_in.invite_code}")
raise InviteExpiredError(invite_in.invite_code)
# Check if invite has already been used
if invite.used_at:
logger.warning(f"Already used invite code attempted by user {current_user.email}: {invite_in.invite_code}")
raise InviteAlreadyUsedError(invite_in.invite_code)
logger.warning(f"Invalid or inactive invite code attempted by user {current_user.email}: {invite_in.code}")
# We can use a more generic error or a specific one. InviteNotFound is reasonable.
raise InviteNotFoundError(invite_in.code)
# Check if group still exists
group = await crud_group.get_group_by_id(db, group_id=invite.group_id)
if not group:
logger.error(f"Group {invite.group_id} not found for invite {invite_in.invite_code}")
logger.error(f"Group {invite.group_id} not found for invite {invite_in.code}")
raise GroupNotFoundError(invite.group_id)
# Check if user is already a member
@ -64,11 +57,23 @@ async def accept_invite(
logger.warning(f"User {current_user.email} already a member of group {invite.group_id}")
raise GroupMembershipError(invite.group_id, "join (already a member)")
# Add user to group and mark invite as used
success = await crud_invite.accept_invite(db, invite=invite, user_id=current_user.id)
if not success:
logger.error(f"Failed to accept invite {invite_in.invite_code} for user {current_user.email}")
raise InviteCreationError(invite.group_id)
# Add user to the group
added_to_group = await crud_group.add_user_to_group(db, group_id=invite.group_id, user_id=current_user.id)
if not added_to_group:
logger.error(f"Failed to add user {current_user.email} to group {invite.group_id} during invite acceptance.")
# This could be a race condition or other issue, treat as an operational error.
raise GroupOperationError("Failed to add user to group.")
logger.info(f"User {current_user.email} successfully joined group {invite.group_id} via invite {invite_in.invite_code}")
return Message(detail="Successfully joined the group")
# Deactivate the invite so it cannot be used again
await crud_invite.deactivate_invite(db, invite=invite)
logger.info(f"User {current_user.email} successfully joined group {invite.group_id} via invite {invite_in.code}")
# Re-fetch the group to get the updated member list
updated_group = await crud_group.get_group_by_id(db, group_id=invite.group_id)
if not updated_group:
# This should ideally not happen as we found it before
logger.error(f"Could not re-fetch group {invite.group_id} after user {current_user.email} joined.")
raise GroupNotFoundError(invite.group_id)
return updated_group

View File

@ -2,5 +2,5 @@
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 100
}
"printWidth": 150
}

View File

@ -944,7 +944,7 @@ select.form-input {
max-width: 550px;
box-shadow: var(--shadow-lg);
position: relative;
/* overflow: hidden; */
overflow-y: scroll;
/* Can cause tooltip clipping */
transform: scale(0.95) translateY(-20px);
transition: transform var(--transition-speed) var(--transition-ease-out);

View File

@ -2,7 +2,7 @@
export const API_VERSION = 'v1'
// API Base URL
export const API_BASE_URL = (window as any).ENV?.VITE_API_URL || 'https://mitlistbe.mohamad.dev'
export const API_BASE_URL = (window as any).ENV?.VITE_API_URL || 'http://localhost:8000'
// API Endpoints
export const API_ENDPOINTS = {
@ -32,6 +32,8 @@ export const API_ENDPOINTS = {
LISTS: {
BASE: '/lists',
BY_ID: (id: string) => `/lists/${id}`,
STATUS: (id: string) => `/lists/${id}/status`,
STATUSES: '/lists/statuses',
ITEMS: (listId: string) => `/lists/${listId}/items`,
ITEM: (listId: string, itemId: string) => `/lists/${listId}/items/${itemId}`,
EXPENSES: (listId: string) => `/lists/${listId}/expenses`,
@ -66,7 +68,7 @@ export const API_ENDPOINTS = {
INVITES: {
BASE: '/invites',
BY_ID: (id: string) => `/invites/${id}`,
ACCEPT: (id: string) => `/invites/accept/${id}`,
ACCEPT: '/invites/accept',
DECLINE: (id: string) => `/invites/decline/${id}`,
REVOKE: (id: string) => `/invites/revoke/${id}`,
LIST: '/invites',

View File

@ -90,95 +90,17 @@
},
"choresPage": {
"title": "Chores",
"tabs": {
"overdue": "Overdue",
"today": "Today",
"upcoming": "Upcoming",
"allPending": "All Pending",
"completed": "Completed"
},
"viewToggle": {
"calendarLabel": "Calendar View",
"calendarText": "Calendar",
"listLabel": "List View",
"listText": "List"
},
"newChoreButtonLabel": "New Chore",
"newChoreButtonText": "New Chore",
"loadingState": {
"loadingChores": "Loading chores..."
},
"calendar": {
"prevMonthLabel": "Previous month",
"nextMonthLabel": "Next month",
"weekdays": {
"sun": "Sun",
"mon": "Mon",
"tue": "Tue",
"wed": "Wed",
"thu": "Thu",
"fri": "Fri",
"sat": "Sat"
},
"addChoreToDayLabel": "Add chore to this day",
"emptyState": "No chores to display for this period."
},
"listView": {
"choreTypePersonal": "Personal",
"choreTypeGroupFallback": "Group",
"completedDatePrefix": "Completed:",
"actions": {
"doneTitle": "Mark as Done",
"doneText": "Done",
"undoTitle": "Mark as Not Done",
"undoText": "Undo",
"editTitle": "Edit",
"editLabel": "Edit chore",
"editText": "Edit",
"deleteTitle": "Delete",
"deleteLabel": "Delete chore",
"deleteText": "Delete"
},
"emptyState": {
"message": "No chores in this view. Well done!",
"viewAllButton": "View All Pending"
}
},
"choreModal": {
"editTitle": "Edit Chore",
"newTitle": "New Chore",
"closeButtonLabel": "Close modal",
"nameLabel": "Name",
"namePlaceholder": "Enter chore name",
"typeLabel": "Type",
"typePersonal": "Personal",
"typeGroup": "Group",
"groupLabel": "Group",
"groupSelectDefault": "Select a group",
"descriptionLabel": "Description",
"descriptionPlaceholder": "Add a description (optional)",
"frequencyLabel": "Frequency",
"intervalLabel": "Interval (days)",
"intervalPlaceholder": "e.g. 3",
"dueDateLabel": "Due Date",
"quickDueDateToday": "Today",
"quickDueDateTomorrow": "Tomorrow",
"quickDueDateNextWeek": "Next Week",
"cancelButton": "Cancel",
"saveButton": "Save"
},
"deleteDialog": {
"title": "Delete Chore",
"confirmationText": "Are you sure you want to delete this chore? This action cannot be undone.",
"deleteButton": "Delete"
},
"shortcutsModal": {
"title": "Keyboard Shortcuts",
"descNewChore": "New Chore",
"descToggleView": "Toggle View (List/Calendar)",
"descToggleShortcuts": "Show/Hide Shortcuts",
"descCloseModal": "Close any open Modal/Dialog"
"addChore": "+",
"edit": "Edit",
"delete": "Delete",
"empty": {
"title": "No Chores Yet",
"message": "Get started by adding your first chore!",
"addFirstChore": "Add First Chore"
},
"today": "Today",
"completedToday": "Completed today",
"completedOn": "Completed on {date}",
"frequencyOptions": {
"oneTime": "One Time",
"daily": "Daily",
@ -186,34 +108,44 @@
"monthly": "Monthly",
"custom": "Custom"
},
"formatters": {
"noDueDate": "No due date",
"dueToday": "Due Today",
"dueTomorrow": "Due Tomorrow",
"overdueFull": "Overdue: {date}",
"dueFull": "Due {date}",
"invalidDate": "Invalid Date"
"frequency": {
"customInterval": "Every {n} day | Every {n} days"
},
"form": {
"name": "Name",
"description": "Description",
"dueDate": "Due Date",
"frequency": "Frequency",
"interval": "Interval (days)",
"type": "Type",
"personal": "Personal",
"group": "Group",
"assignGroup": "Assign to Group",
"cancel": "Cancel",
"save": "Save Changes",
"create": "Create",
"editChore": "Edit Chore",
"createChore": "Create Chore"
},
"deleteConfirm": {
"title": "Confirm Deletion",
"message": "Really want to delete? This action cannot be undone.",
"cancel": "Cancel",
"delete": "Delete"
},
"notifications": {
"loadFailed": "Failed to load chores",
"updateSuccess": "Chore '{name}' updated successfully",
"createSuccess": "Chore '{name}' created successfully",
"updateFailed": "Failed to update chore",
"createFailed": "Failed to create chore",
"deleteSuccess": "Chore '{name}' deleted successfully",
"deleteFailed": "Failed to delete chore",
"markedDone": "{name} marked as done.",
"markedNotDone": "{name} marked as not done.",
"statusUpdateFailed": "Failed to update chore status."
},
"validation": {
"nameRequired": "Chore name is required.",
"groupRequired": "Please select a group for group chores.",
"intervalRequired": "Custom interval must be at least 1 day.",
"dueDateRequired": "Due date is required.",
"invalidDueDate": "Invalid due date format."
},
"unsavedChangesConfirmation": "You have unsaved changes in the chore form. Are you sure you want to leave?"
"loadFailed": "Failed to load chores.",
"loadGroupsFailed": "Failed to load groups.",
"updateSuccess": "Chore updated successfully!",
"createSuccess": "Chore created successfully!",
"saveFailed": "Failed to save the chore.",
"deleteSuccess": "Chore deleted successfully.",
"deleteFailed": "Failed to delete chore.",
"completed": "Chore marked as complete!",
"uncompleted": "Chore marked as incomplete.",
"updateFailed": "Failed to update chore status.",
"createAssignmentFailed": "Failed to create assignment for chore."
}
},
"errorNotFoundPage": {
"errorCode": "404",

File diff suppressed because it is too large Load Diff

View File

@ -85,7 +85,7 @@
<div class="modal-body">
<div class="form-group">
<label for="newGroupNameInput" class="form-label">{{ t('groupsPage.createDialog.groupNameLabel')
}}</label>
}}</label>
<input type="text" id="newGroupNameInput" v-model="newGroupName" class="form-input" required
ref="newGroupNameInputRef" />
<p v-if="createGroupFormError" class="form-error-text">{{ createGroupFormError }}</p>
@ -106,7 +106,7 @@
<div class="modal-body">
<div class="form-group">
<label for="joinInviteCodeInput" class="form-label">{{ t('groupsPage.joinGroup.inputLabel', 'Invite Code')
}}</label>
}}</label>
<input type="text" id="joinInviteCodeInput" v-model="inviteCodeToJoin" class="form-input"
:placeholder="t('groupsPage.joinGroup.inputPlaceholder')" required ref="joinInviteCodeInputRef" />
<p v-if="joinGroupFormError" class="form-error-text">{{ joinGroupFormError }}</p>
@ -302,7 +302,9 @@ const handleJoinGroup = async () => {
joinGroupFormError.value = null;
joiningGroup.value = true;
try {
const response = await apiClient.post(API_ENDPOINTS.INVITES.ACCEPT(inviteCodeToJoin.value));
const response = await apiClient.post(API_ENDPOINTS.INVITES.ACCEPT, {
code: inviteCodeToJoin.value.trim()
});
const joinedGroup = response.data as Group; // Adjust based on actual API response for joined group
if (joinedGroup && joinedGroup.id && joinedGroup.name) {
// Check if group already in list to prevent duplicates if API returns the group info

View File

@ -34,7 +34,7 @@ export interface ChoreUpdate extends Partial<ChoreCreate> { }
export interface ChoreAssignment {
id: number
chore_id: number
assigned_to_id: number
assigned_to_user_id: number
assigned_by_id: number
due_date: string
is_complete: boolean
@ -47,7 +47,7 @@ export interface ChoreAssignment {
export interface ChoreAssignmentCreate {
chore_id: number
assigned_to_id: number
assigned_to_user_id: number
due_date: string
}

View File

@ -20,7 +20,7 @@ const pwaOptions: Partial<VitePWAOptions> = {
name: 'mitlist',
short_name: 'mitlist',
description: 'mitlist pwa',
theme_color: '#ff7b54',
theme_color: '#fff8f0',
background_color: '#f3f3f3',
display: 'standalone',
orientation: 'portrait',