Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ def create_app(config_name='default'):
from app.routes.progress import progress_bp
app.register_blueprint(progress_bp)

from app.routes.days import days_bp
app.register_blueprint(days_bp)

from app.routes.user_addiction import user_addiction_bp
app.register_blueprint(user_addiction_bp)

Expand Down
29 changes: 29 additions & 0 deletions backend/app/dto/days_dto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from dataclasses import dataclass


@dataclass
class UpdateDayStatusRequest:
"""DTO for update day status request."""
status: str

def to_dict(self):
"""Convert DTO to dictionary for JSON serialization."""
return {
'status': self.status
}


@dataclass
class DayEntryResponse:
"""DTO for a single day entry response."""
date: str
day_of_week: str
status: str

def to_dict(self):
"""Convert DTO to dictionary for JSON serialization."""
return {
'date': self.date,
'dayOfWeek': self.day_of_week,
'status': self.status
}
3 changes: 0 additions & 3 deletions backend/app/routes/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ def get_dashboard():
status of all days (from start date to today), and current success streak.
Dates calculated according to Europe/Warsaw timezone (can be overridden with X-Timezone header).

Args:
current_user (str): Username from JWT token

Returns:
JSON: Dashboard data
"""
Expand Down
79 changes: 79 additions & 0 deletions backend/app/routes/days.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from flask import Blueprint, jsonify, request, g
from app.services.days_service import DaysService
from app.routes.authorization import token_required

days_bp = Blueprint('days', __name__)

@days_bp.route('/api/days/<string:date>', methods=['PATCH'])
@token_required
def update_day_status(date):
"""
Endpoint to update status for a specific day.

Updates the status of the day indicated by date (YYYY-MM-DD).
Only today's and past dates are allowed. Future dates return 422 Unprocessable Entity.
Operation is idempotent - setting the same status returns 200 with current entry.

Args:
date (str): Date in YYYY-MM-DD format

Returns:
JSON: Updated day entry
"""
try:
# Get request data
data = request.get_json()
if not data or 'status' not in data:
return jsonify({
'code': 'VALIDATION_ERROR',
'message': 'Status is required',
'details': [{'field': 'status', 'issue': 'missing_field'}]
}), 400

status = data['status']

# Validate status value
if status not in ['success', 'failure', 'none']:
return jsonify({
'code': 'VALIDATION_ERROR',
'message': 'Invalid status value',
'details': [{'field': 'status', 'issue': 'invalid_value', 'value': status}]
}), 400

# Get timezone from header (default: Europe/Warsaw)
timezone = request.headers.get('X-Timezone', 'Europe/Warsaw')

# Update day status
day_entry = DaysService.update_day_status(g.current_user, date, status, timezone)
serialized_data = DaysService.serialize_day_entry_response(day_entry)

return jsonify(serialized_data), 200

except Exception as e:
error_message = str(e)

# Handle specific error cases
if 'future' in error_message.lower():
return jsonify({
'code': 'VALIDATION_ERROR',
'message': error_message,
'details': [{'field': 'date', 'issue': 'future_not_allowed', 'value': date}]
}), 422

if 'not found' in error_message.lower():
return jsonify({
'code': 'NOT_FOUND',
'message': error_message
}), 404

if 'invalid date' in error_message.lower():
return jsonify({
'code': 'VALIDATION_ERROR',
'message': error_message,
'details': [{'field': 'date', 'issue': 'invalid_format', 'value': date}]
}), 422

return jsonify({
'code': 'INTERNAL_ERROR',
'message': error_message
}), 500
3 changes: 0 additions & 3 deletions backend/app/routes/progress.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ def get_progress():
total savings, and all day entries from start date to today.
Dates calculated according to Europe/Warsaw timezone (can be overridden with X-Timezone header).

Args:
current_user (str): Username from JWT token

Returns:
JSON: Progress data
"""
Expand Down
21 changes: 21 additions & 0 deletions backend/app/schemas/days_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from marshmallow import Schema, fields, validate


class UpdateDayStatusRequestSchema(Schema):
"""Schema for update day status request."""
status = fields.String(
required=True,
validate=validate.OneOf(['success', 'failure', 'none']),
description="Status of the day: success, failure, or none"
)


class DayEntryResponseSchema(Schema):
"""Schema for a single day entry response."""
date = fields.String(required=True, description="Date in YYYY-MM-DD format")
dayOfWeek = fields.String(required=True, description="Day of week abbreviation")
status = fields.String(
required=True,
validate=validate.OneOf(['success', 'failure', 'none']),
description="Status of the day: success, failure, or none"
)
136 changes: 136 additions & 0 deletions backend/app/services/days_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
from datetime import datetime
import pytz

from app import db
from app.models.user import User
from app.models.user_addiction import UserAddiction
from app.models.daily_log import DailyLog
from app.dto.days_dto import DayEntryResponse
from app.schemas.days_schema import DayEntryResponseSchema


class DaysService:

# Polish day abbreviations mapping
DAY_ABBREVIATIONS = {
0: 'pon', # Monday
1: 'wt', # Tuesday
2: 'śr', # Wednesday
3: 'czw', # Thursday
4: 'pt', # Friday
5: 'sob', # Saturday
6: 'nd' # Sunday
}

DEFAULT_MOOD = 5 # Default neutral mood value

@staticmethod
def update_day_status(username: str, date_str: str, status: str, timezone: str = 'Europe/Warsaw') -> DayEntryResponse:
"""
Update status for a specific day.

Args:
username (str): The username
date_str (str): Date in YYYY-MM-DD format
status (str): Status to set (success, failure, or none)
timezone (str): Timezone for date calculations (default: Europe/Warsaw)

Returns:
DayEntryResponse: Updated day entry

Raises:
Exception: If validation fails or user/addiction not found
"""
try:
# Get timezone
tz = pytz.timezone(timezone)
today = datetime.now(tz).date()

# Parse and validate date
try:
target_date = datetime.strptime(date_str, '%Y-%m-%d').date()
except ValueError:
raise Exception("Invalid date format. Expected YYYY-MM-DD")

# Check if date is not in the future
if target_date > today:
raise Exception("Cannot set status for future dates")

# Get user
user = User.query.filter_by(name=username).first()
if not user:
raise Exception(f"User '{username}' not found")

# Get user's addiction
user_addiction = UserAddiction.query.filter_by(user_id=user.id).first()
if not user_addiction:
raise Exception(f"No addiction found for user '{username}'")

# Find existing daily log for this date
daily_log = DailyLog.query.filter(
DailyLog.users_addiction == user_addiction.id,
DailyLog.date == date_str
).first()

# Handle status update based on value
if status == 'none':
# Delete the entry if it exists
if daily_log:
db.session.delete(daily_log)
db.session.commit()
elif status == 'success':
# Create or update with relapse=0
if daily_log:
daily_log.relapse = 0
daily_log.mood = DaysService.DEFAULT_MOOD
else:
daily_log = DailyLog(
date=date_str,
relapse=0,
mood=DaysService.DEFAULT_MOOD,
users_addiction=user_addiction.id
)
db.session.add(daily_log)
db.session.commit()
elif status == 'failure':
# Create or update with relapse=1
if daily_log:
daily_log.relapse = 1
daily_log.mood = DaysService.DEFAULT_MOOD
else:
daily_log = DailyLog(
date=date_str,
relapse=1,
mood=DaysService.DEFAULT_MOOD,
users_addiction=user_addiction.id
)
db.session.add(daily_log)
db.session.commit()

# Get day of week abbreviation
day_of_week = DaysService.DAY_ABBREVIATIONS[target_date.weekday()]

# Return the updated entry
return DayEntryResponse(
date=date_str,
day_of_week=day_of_week,
status=status
)

except Exception as e:
db.session.rollback()
raise Exception(f"Days service error: {str(e)}")

@staticmethod
def serialize_day_entry_response(day_entry: DayEntryResponse) -> dict:
"""
Serialize day entry DTO using Marshmallow schema.

Args:
day_entry: DayEntryResponse to serialize

Returns:
dict: Serialized day entry data ready for JSON response
"""
schema = DayEntryResponseSchema()
return schema.dump(day_entry.to_dict())
Loading