Update logging level to INFO, refine chore update logic, and enhance invite acceptance flow #58
@ -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)
|
||||
|
@ -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
|
@ -2,5 +2,5 @@
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
"printWidth": 150
|
||||
}
|
@ -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);
|
||||
|
@ -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',
|
||||
|
@ -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
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user