Update logging level to INFO, refine chore update logic, and enhance invite acceptance flow
All checks were successful
Deploy to Production, build images and push to Gitea Registry / build_and_push (pull_request) Successful in 1m47s
All checks were successful
Deploy to Production, build images and push to Gitea Registry / build_and_push (pull_request) Successful in 1m47s
- Changed logging level from WARNING to INFO in config.py for better visibility during development. - Adjusted chore update logic in chores.py to ensure correct payload structure. - Improved invite acceptance process in invites.py by refining error handling and updating response models for better clarity. - Updated API endpoint configurations in api-config.ts for consistency and added new endpoints for list statuses. - Enhanced UI components in ChoresPage.vue and GroupsPage.vue for improved user experience and accessibility.
This commit is contained in:
parent
92c919785a
commit
944976b1cc
@ -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}).")
|
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
|
# 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:
|
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)
|
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.models import User as UserModel, UserRoleEnum
|
||||||
from app.schemas.invite import InviteAccept
|
from app.schemas.invite import InviteAccept
|
||||||
from app.schemas.message import Message
|
from app.schemas.message import Message
|
||||||
|
from app.schemas.group import GroupPublic
|
||||||
from app.crud import invite as crud_invite
|
from app.crud import invite as crud_invite
|
||||||
from app.crud import group as crud_group
|
from app.crud import group as crud_group
|
||||||
from app.core.exceptions import (
|
from app.core.exceptions import (
|
||||||
@ -16,7 +17,8 @@ from app.core.exceptions import (
|
|||||||
InviteAlreadyUsedError,
|
InviteAlreadyUsedError,
|
||||||
InviteCreationError,
|
InviteCreationError,
|
||||||
GroupNotFoundError,
|
GroupNotFoundError,
|
||||||
GroupMembershipError
|
GroupMembershipError,
|
||||||
|
GroupOperationError
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -24,7 +26,7 @@ router = APIRouter()
|
|||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/accept", # Route relative to prefix "/invites"
|
"/accept", # Route relative to prefix "/invites"
|
||||||
response_model=Message,
|
response_model=GroupPublic,
|
||||||
summary="Accept Group Invite",
|
summary="Accept Group Invite",
|
||||||
tags=["Invites"]
|
tags=["Invites"]
|
||||||
)
|
)
|
||||||
@ -34,28 +36,19 @@ async def accept_invite(
|
|||||||
current_user: UserModel = Depends(current_active_user),
|
current_user: UserModel = Depends(current_active_user),
|
||||||
):
|
):
|
||||||
"""Accepts a group invite using the provided invite code."""
|
"""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
|
# Get the invite - this function should only return valid, active invites
|
||||||
invite = await crud_invite.get_invite_by_code(db, invite_code=invite_in.invite_code)
|
invite = await crud_invite.get_active_invite_by_code(db, code=invite_in.code)
|
||||||
if not invite:
|
if not invite:
|
||||||
logger.warning(f"Invalid invite code attempted by user {current_user.email}: {invite_in.invite_code}")
|
logger.warning(f"Invalid or inactive invite code attempted by user {current_user.email}: {invite_in.code}")
|
||||||
raise InviteNotFoundError(invite_in.invite_code)
|
# We can use a more generic error or a specific one. InviteNotFound is reasonable.
|
||||||
|
raise InviteNotFoundError(invite_in.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)
|
|
||||||
|
|
||||||
# Check if group still exists
|
# Check if group still exists
|
||||||
group = await crud_group.get_group_by_id(db, group_id=invite.group_id)
|
group = await crud_group.get_group_by_id(db, group_id=invite.group_id)
|
||||||
if not group:
|
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)
|
raise GroupNotFoundError(invite.group_id)
|
||||||
|
|
||||||
# Check if user is already a member
|
# 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}")
|
logger.warning(f"User {current_user.email} already a member of group {invite.group_id}")
|
||||||
raise GroupMembershipError(invite.group_id, "join (already a member)")
|
raise GroupMembershipError(invite.group_id, "join (already a member)")
|
||||||
|
|
||||||
# Add user to group and mark invite as used
|
# Add user to the group
|
||||||
success = await crud_invite.accept_invite(db, invite=invite, user_id=current_user.id)
|
added_to_group = await crud_group.add_user_to_group(db, group_id=invite.group_id, user_id=current_user.id)
|
||||||
if not success:
|
if not added_to_group:
|
||||||
logger.error(f"Failed to accept invite {invite_in.invite_code} for user {current_user.email}")
|
logger.error(f"Failed to add user {current_user.email} to group {invite.group_id} during invite acceptance.")
|
||||||
raise InviteCreationError(invite.group_id)
|
# 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}")
|
# Deactivate the invite so it cannot be used again
|
||||||
return Message(detail="Successfully joined the group")
|
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",
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
"semi": false,
|
"semi": false,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"printWidth": 100
|
"printWidth": 150
|
||||||
}
|
}
|
@ -944,7 +944,7 @@ select.form-input {
|
|||||||
max-width: 550px;
|
max-width: 550px;
|
||||||
box-shadow: var(--shadow-lg);
|
box-shadow: var(--shadow-lg);
|
||||||
position: relative;
|
position: relative;
|
||||||
/* overflow: hidden; */
|
overflow-y: scroll;
|
||||||
/* Can cause tooltip clipping */
|
/* Can cause tooltip clipping */
|
||||||
transform: scale(0.95) translateY(-20px);
|
transform: scale(0.95) translateY(-20px);
|
||||||
transition: transform var(--transition-speed) var(--transition-ease-out);
|
transition: transform var(--transition-speed) var(--transition-ease-out);
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
export const API_VERSION = 'v1'
|
export const API_VERSION = 'v1'
|
||||||
|
|
||||||
// API Base URL
|
// 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
|
// API Endpoints
|
||||||
export const API_ENDPOINTS = {
|
export const API_ENDPOINTS = {
|
||||||
@ -32,6 +32,8 @@ export const API_ENDPOINTS = {
|
|||||||
LISTS: {
|
LISTS: {
|
||||||
BASE: '/lists',
|
BASE: '/lists',
|
||||||
BY_ID: (id: string) => `/lists/${id}`,
|
BY_ID: (id: string) => `/lists/${id}`,
|
||||||
|
STATUS: (id: string) => `/lists/${id}/status`,
|
||||||
|
STATUSES: '/lists/statuses',
|
||||||
ITEMS: (listId: string) => `/lists/${listId}/items`,
|
ITEMS: (listId: string) => `/lists/${listId}/items`,
|
||||||
ITEM: (listId: string, itemId: string) => `/lists/${listId}/items/${itemId}`,
|
ITEM: (listId: string, itemId: string) => `/lists/${listId}/items/${itemId}`,
|
||||||
EXPENSES: (listId: string) => `/lists/${listId}/expenses`,
|
EXPENSES: (listId: string) => `/lists/${listId}/expenses`,
|
||||||
@ -66,7 +68,7 @@ export const API_ENDPOINTS = {
|
|||||||
INVITES: {
|
INVITES: {
|
||||||
BASE: '/invites',
|
BASE: '/invites',
|
||||||
BY_ID: (id: string) => `/invites/${id}`,
|
BY_ID: (id: string) => `/invites/${id}`,
|
||||||
ACCEPT: (id: string) => `/invites/accept/${id}`,
|
ACCEPT: '/invites/accept',
|
||||||
DECLINE: (id: string) => `/invites/decline/${id}`,
|
DECLINE: (id: string) => `/invites/decline/${id}`,
|
||||||
REVOKE: (id: string) => `/invites/revoke/${id}`,
|
REVOKE: (id: string) => `/invites/revoke/${id}`,
|
||||||
LIST: '/invites',
|
LIST: '/invites',
|
||||||
|
@ -90,95 +90,17 @@
|
|||||||
},
|
},
|
||||||
"choresPage": {
|
"choresPage": {
|
||||||
"title": "Chores",
|
"title": "Chores",
|
||||||
"tabs": {
|
"addChore": "+",
|
||||||
"overdue": "Overdue",
|
"edit": "Edit",
|
||||||
|
"delete": "Delete",
|
||||||
|
"empty": {
|
||||||
|
"title": "No Chores Yet",
|
||||||
|
"message": "Get started by adding your first chore!",
|
||||||
|
"addFirstChore": "Add First Chore"
|
||||||
|
},
|
||||||
"today": "Today",
|
"today": "Today",
|
||||||
"upcoming": "Upcoming",
|
"completedToday": "Completed today",
|
||||||
"allPending": "All Pending",
|
"completedOn": "Completed on {date}",
|
||||||
"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"
|
|
||||||
},
|
|
||||||
"frequencyOptions": {
|
"frequencyOptions": {
|
||||||
"oneTime": "One Time",
|
"oneTime": "One Time",
|
||||||
"daily": "Daily",
|
"daily": "Daily",
|
||||||
@ -186,34 +108,44 @@
|
|||||||
"monthly": "Monthly",
|
"monthly": "Monthly",
|
||||||
"custom": "Custom"
|
"custom": "Custom"
|
||||||
},
|
},
|
||||||
"formatters": {
|
"frequency": {
|
||||||
"noDueDate": "No due date",
|
"customInterval": "Every {n} day | Every {n} days"
|
||||||
"dueToday": "Due Today",
|
},
|
||||||
"dueTomorrow": "Due Tomorrow",
|
"form": {
|
||||||
"overdueFull": "Overdue: {date}",
|
"name": "Name",
|
||||||
"dueFull": "Due {date}",
|
"description": "Description",
|
||||||
"invalidDate": "Invalid Date"
|
"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": {
|
"notifications": {
|
||||||
"loadFailed": "Failed to load chores",
|
"loadFailed": "Failed to load chores.",
|
||||||
"updateSuccess": "Chore '{name}' updated successfully",
|
"loadGroupsFailed": "Failed to load groups.",
|
||||||
"createSuccess": "Chore '{name}' created successfully",
|
"updateSuccess": "Chore updated successfully!",
|
||||||
"updateFailed": "Failed to update chore",
|
"createSuccess": "Chore created successfully!",
|
||||||
"createFailed": "Failed to create chore",
|
"saveFailed": "Failed to save the chore.",
|
||||||
"deleteSuccess": "Chore '{name}' deleted successfully",
|
"deleteSuccess": "Chore deleted successfully.",
|
||||||
"deleteFailed": "Failed to delete chore",
|
"deleteFailed": "Failed to delete chore.",
|
||||||
"markedDone": "{name} marked as done.",
|
"completed": "Chore marked as complete!",
|
||||||
"markedNotDone": "{name} marked as not done.",
|
"uncompleted": "Chore marked as incomplete.",
|
||||||
"statusUpdateFailed": "Failed to update chore status."
|
"updateFailed": "Failed to update chore status.",
|
||||||
},
|
"createAssignmentFailed": "Failed to create assignment for chore."
|
||||||
"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?"
|
|
||||||
},
|
},
|
||||||
"errorNotFoundPage": {
|
"errorNotFoundPage": {
|
||||||
"errorCode": "404",
|
"errorCode": "404",
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -302,7 +302,9 @@ const handleJoinGroup = async () => {
|
|||||||
joinGroupFormError.value = null;
|
joinGroupFormError.value = null;
|
||||||
joiningGroup.value = true;
|
joiningGroup.value = true;
|
||||||
try {
|
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
|
const joinedGroup = response.data as Group; // Adjust based on actual API response for joined group
|
||||||
if (joinedGroup && joinedGroup.id && joinedGroup.name) {
|
if (joinedGroup && joinedGroup.id && joinedGroup.name) {
|
||||||
// Check if group already in list to prevent duplicates if API returns the group info
|
// 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 {
|
export interface ChoreAssignment {
|
||||||
id: number
|
id: number
|
||||||
chore_id: number
|
chore_id: number
|
||||||
assigned_to_id: number
|
assigned_to_user_id: number
|
||||||
assigned_by_id: number
|
assigned_by_id: number
|
||||||
due_date: string
|
due_date: string
|
||||||
is_complete: boolean
|
is_complete: boolean
|
||||||
@ -47,7 +47,7 @@ export interface ChoreAssignment {
|
|||||||
|
|
||||||
export interface ChoreAssignmentCreate {
|
export interface ChoreAssignmentCreate {
|
||||||
chore_id: number
|
chore_id: number
|
||||||
assigned_to_id: number
|
assigned_to_user_id: number
|
||||||
due_date: string
|
due_date: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ const pwaOptions: Partial<VitePWAOptions> = {
|
|||||||
name: 'mitlist',
|
name: 'mitlist',
|
||||||
short_name: 'mitlist',
|
short_name: 'mitlist',
|
||||||
description: 'mitlist pwa',
|
description: 'mitlist pwa',
|
||||||
theme_color: '#ff7b54',
|
theme_color: '#fff8f0',
|
||||||
background_color: '#f3f3f3',
|
background_color: '#f3f3f3',
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
orientation: 'portrait',
|
orientation: 'portrait',
|
||||||
|
Loading…
Reference in New Issue
Block a user