From 15b5bd63e10267ac2833a4a97e72b327c613f7e9 Mon Sep 17 00:00:00 2001 From: cdez Date: Mon, 24 Mar 2025 00:29:18 -0700 Subject: [PATCH] Start the timeman project, initial commit. --- app.py | 137 +++++++++++++++++++++++++++++++++ init_db.py | 4 + templates/completed_tasks.html | 50 ++++++++++++ templates/index.html | 110 ++++++++++++++++++++++++++ 4 files changed, 301 insertions(+) create mode 100644 app.py create mode 100644 init_db.py create mode 100644 templates/completed_tasks.html create mode 100644 templates/index.html diff --git a/app.py b/app.py new file mode 100644 index 0000000..810a2e5 --- /dev/null +++ b/app.py @@ -0,0 +1,137 @@ +from flask import Flask, render_template, request, redirect, url_for, jsonify +from flask_sqlalchemy import SQLAlchemy +from flask_socketio import SocketIO, emit +from datetime import datetime, timedelta + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///tasks.db' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +db = SQLAlchemy(app) +socketio = SocketIO(app) + +# Database models +class Task(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + priority = db.Column(db.String(1), nullable=False) # A, B, C, or D + duration = db.Column(db.Integer, nullable=False) # Duration in minutes + status = db.Column(db.String(10), default='pending') + due_date = db.Column(db.Date, nullable=True) # Due date for the task + completed_at = db.Column(db.DateTime, nullable=True) # Timestamp for when the task was completed + +def format_duration(minutes): + """Convert duration in minutes to 'H M' format.""" + hours = minutes // 60 + remaining_minutes = minutes % 60 + if hours > 0: + return f"{hours}H {remaining_minutes}M" if remaining_minutes > 0 else f"{hours}H" + return f"{remaining_minutes}M" + +@app.context_processor +def utility_processor(): + return dict(format_duration=format_duration) + +def cleanup_completed_tasks(): + """Remove tasks that have been completed for more than 1 hour.""" + expiration_time = datetime.utcnow() - timedelta(hours=1) + old_tasks = Task.query.filter(Task.status == 'completed', Task.completed_at < expiration_time).all() + for task in old_tasks: + db.session.delete(task) + db.session.commit() + +# Routes +@app.route('/') +def index(): + cleanup_completed_tasks() # Remove old completed tasks + + # Get the sorting parameter from the query string (default is "priority") + sort_by = request.args.get('sort_by', 'priority') + + # Dynamically sort tasks based on the parameter + if sort_by == 'priority': + tasks = Task.query.filter(Task.status == 'pending').order_by(Task.priority).all() + elif sort_by == 'due_date': + tasks = Task.query.filter(Task.status == 'pending').order_by(Task.due_date).all() + elif sort_by == 'duration': + tasks = Task.query.filter(Task.status == 'pending').order_by(Task.duration).all() + else: + tasks = Task.query.filter(Task.status == 'pending').all() # Default: no sorting + + return render_template('index.html', tasks=tasks, sort_by=sort_by) + +@app.route('/add_task', methods=['POST']) +def add_task(): + name = request.form['name'] + priority = request.form['priority'] + duration_input = request.form['duration'] + due_date_input = request.form['due_date'] + + # Parse duration (e.g., "10M" -> 10 minutes, "2H" -> 120 minutes) + if duration_input.endswith('M'): + duration = int(duration_input[:-1]) # Extract the number before 'M' + elif duration_input.endswith('H'): + duration = int(duration_input[:-1]) * 60 # Convert hours to minutes + else: + return "Invalid duration format. Use 'M' for minutes or 'H' for hours.", 400 + + # Parse due date (e.g., "2025-03-30") + try: + due_date = datetime.strptime(due_date_input, '%Y-%m-%d').date() if due_date_input else None + except ValueError: + return "Invalid due date format. Use 'YYYY-MM-DD'.", 400 + + # Create and save the new task + new_task = Task(name=name, priority=priority, duration=duration, due_date=due_date) + db.session.add(new_task) + db.session.commit() + + # Emit the new task to connected clients + socketio.emit('task_update', { + 'action': 'add', + 'task': { + 'id': new_task.id, + 'name': name, + 'priority': priority, + 'duration': duration, + 'due_date': due_date_input, + 'status': 'pending' + } + }) + return redirect(url_for('index')) + +@app.route('/complete_task/') +def complete_task(task_id): + task = Task.query.get(task_id) + if task: + task.status = 'completed' + task.completed_at = datetime.utcnow() # Store the timestamp in UTC + db.session.commit() + socketio.emit('task_update', {'action': 'complete', 'task_id': task_id}) + return redirect(url_for('index')) + +@app.route('/completed_tasks') +def completed_tasks(): + cleanup_completed_tasks() # Remove old completed tasks + + # Get the sorting parameter from the query string (default is "completed_at") + sort_by = request.args.get('sort_by', 'completed_at') + + # Dynamically sort tasks based on the parameter + if sort_by == 'priority': + tasks = Task.query.filter(Task.status == 'completed').order_by(Task.priority).all() + elif sort_by == 'due_date': + tasks = Task.query.filter(Task.status == 'completed').order_by(Task.due_date).all() + elif sort_by == 'duration': + tasks = Task.query.filter(Task.status == 'completed').order_by(Task.duration).all() + elif sort_by == 'completed_at': + tasks = Task.query.filter(Task.status == 'completed').order_by(Task.completed_at.desc()).all() + else: + tasks = Task.query.filter(Task.status == 'completed').all() # Default: no sorting + + return render_template('completed_tasks.html', tasks=tasks, sort_by=sort_by) + +if __name__ == '__main__': + with app.app_context(): + # Create database tables within the app context + db.create_all() + socketio.run(app, debug=True) \ No newline at end of file diff --git a/init_db.py b/init_db.py new file mode 100644 index 0000000..0028636 --- /dev/null +++ b/init_db.py @@ -0,0 +1,4 @@ +from app import db + +db.create_all() +print("Database and tables created successfully.") \ No newline at end of file diff --git a/templates/completed_tasks.html b/templates/completed_tasks.html new file mode 100644 index 0000000..52a9d36 --- /dev/null +++ b/templates/completed_tasks.html @@ -0,0 +1,50 @@ + + + + + + Completed Tasks + + + +
+

Completed Tasks

+ Back to Pending Tasks + + +
+ + +
+ + +
    + {% for task in tasks %} +
  • +
    + {{ task.name }} + (Priority: {{ task.priority }}, Duration: {{ format_duration(task.duration) }}) + {% if task.due_date %} + - Due: {{ task.due_date }} + {% endif %} + - Completed At: {{ task.completed_at }} +
    +
  • + {% endfor %} +
+
+ + + + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..e334108 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,110 @@ + + + + + + Task Manager + + + +
+

Task Manager

+ + +
+ + +
+ + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + View Completed Tasks + + +

Tasks

+
    + {% for task in tasks %} +
  • +
    + {{ task.name }} + (Priority: {{ task.priority }}, Duration: {{ task.duration }} mins / {{ format_duration(task.duration) }}) + {% if task.due_date %} + - Due: {{ task.due_date }} + {% endif %} +
    + {% if task.status == 'pending' %} + Complete + {% else %} + Completed + {% endif %} +
  • + {% endfor %} +
+
+ + + + + \ No newline at end of file