Skip to content

feat: Implement backend for to-do list web application #20

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
30 changes: 30 additions & 0 deletions todo_app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Python
__pycache__/
*.py[cod]
*$py.class

# Databases
*.db
*.sqlite3
instance/

# Environment variables
.env

# Flask session files
.webassets-cache

# Virtual environment
venv/
env/
.venv/

# IDE / Editor specific
.vscode/
.idea/
*.sublime-workspace
*.sublime-project

# OS specific
.DS_Store
Thumbs.db
45 changes: 45 additions & 0 deletions todo_app/app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_bcrypt import Bcrypt
from .models import User # Import User model

# Initialize extensions
db = SQLAlchemy()
bcrypt = Bcrypt()
login_manager = LoginManager()
# Assuming 'auth' will be a blueprint for authentication routes and 'login' is the login route.
login_manager.login_view = 'auth.login'
login_manager.login_message_category = 'info' # For flash messages
migrate = Migrate()

def create_app(config_class='config.Config'):
"""
Factory function to create the Flask application.
"""
app = Flask(__name__)
app.config.from_object(config_class)

# Initialize extensions with the app
db.init_app(app)
bcrypt.init_app(app)
login_manager.init_app(app)
migrate.init_app(app, db)

# User loader function for Flask-Login
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))

# Placeholder for blueprint registration
from .main import main_bp # Import main blueprint
app.register_blueprint(main_bp)

from .auth import auth_bp # Import auth blueprint
app.register_blueprint(auth_bp, url_prefix='/auth')

# from .task_routes import task_bp # Placeholder for task routes
# app.register_blueprint(task_bp, url_prefix='/task')

return app
5 changes: 5 additions & 0 deletions todo_app/app/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from flask import Blueprint

auth_bp = Blueprint('auth', __name__, template_folder='templates')

from . import routes
43 changes: 43 additions & 0 deletions todo_app/app/auth/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from flask import render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, login_required, current_user
from .. import db, bcrypt # Corrected import path
from ..models import User # Corrected import path
from ..forms import RegistrationForm, LoginForm # Corrected import path
from . import auth_bp

@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('main.index')) # Assuming main.index will exist
form = RegistrationForm()
if form.validate_on_submit():
user = User(username=form.username.data, role=form.role.data)
user.set_password(form.password.data) # Use the method from User model
db.session.add(user)
db.session.commit()
flash('Your account has been created! You are now able to log in.', 'success')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', title='Register', form=form)

@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('main.index')) # Assuming main.index will exist
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user and user.check_password(form.password.data): # Use the method from User model
login_user(user) # Removed remember=form.remember.data, as 'remember' is not in form
next_page = request.args.get('next')
flash('Login successful.', 'success')
return redirect(next_page) if next_page else redirect(url_for('main.index'))
else:
flash('Login Unsuccessful. Please check username and password.', 'danger')
return render_template('auth/login.html', title='Login', form=form)

@auth_bp.route('/logout')
@login_required
def logout():
logout_user()
flash('You have been logged out.', 'info')
return redirect(url_for('auth.login'))
Empty file.
Empty file.
19 changes: 19 additions & 0 deletions todo_app/app/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from functools import wraps
from flask_login import current_user
from flask import abort

def manager_required(f):
"""
Decorator to ensure a user is logged in and has the 'manager' role.
Aborts with a 403 error if conditions are not met.
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated:
# This case should ideally be handled by @login_required,
# but it's good practice for a role decorator to check.
abort(401) # Unauthorized
if current_user.role != 'manager':
abort(403) # Forbidden
return f(*args, **kwargs)
return decorated_function
64 changes: 64 additions & 0 deletions todo_app/app/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, SelectField, TextAreaField, DateField
from wtforms.validators import DataRequired, Length, EqualTo, ValidationError, Optional
from .models import User # User is already imported, good.

class RegistrationForm(FlaskForm):
username = StringField('Username',
validators=[DataRequired(), Length(min=2, max=80)]) # Max length matches User model
password = PasswordField('Password',
validators=[DataRequired(), Length(min=6)])
confirm_password = PasswordField('Confirm Password',
validators=[DataRequired(), EqualTo('password')])
role = SelectField('Role',
choices=[('employee', 'Employee'), ('manager', 'Manager')],
validators=[DataRequired()])
submit = SubmitField('Sign Up')

def validate_username(self, username):
user = User.query.filter_by(username=username.data).first()
if user:
raise ValidationError('That username is already taken. Please choose a different one.')

class LoginForm(FlaskForm):
username = StringField('Username',
validators=[DataRequired(), Length(min=2, max=80)]) # Max length matches User model
password = PasswordField('Password', validators=[DataRequired()])
submit = SubmitField('Login')

class TaskForm(FlaskForm):
title = StringField('Title', validators=[DataRequired()])
description = TextAreaField('Description')
due_date = DateField('Due Date', format='%Y-%m-%d', validators=[Optional()])
status = SelectField('Status',
choices=[('pending', 'Pending'),
('in progress', 'In Progress'),
('completed', 'Completed')],
validators=[DataRequired()]) # Default is typically the first choice
assignee_id = SelectField('Assign To', coerce=int, validators=[Optional()]) # Added Optional for now, will adjust in __init__
submit = SubmitField('Save Task')

def __init__(self, *args, **kwargs):
current_user_role = kwargs.pop('current_user_role', None)
super(TaskForm, self).__init__(*args, **kwargs)

if current_user_role == 'manager':
# Populate choices for assignee_id
employees = User.query.filter_by(role='employee').all()
self.assignee_id.choices = [(user.id, user.username) for user in employees]
# Add an option for 'Unassigned' or 'Assign to Self (Manager)' if desired
# For now, let's make it required to select an employee if manager is creating/editing
self.assignee_id.choices.insert(0, (0, 'Unassigned / Assign to Self')) # Representing unassigned or manager self-assignment
self.assignee_id.validators = [DataRequired()] # Make it required for manager
else:
# For employees, this field might be hidden or disabled in the template.
# Or, we can remove it from the form items if it's not relevant.
# For now, let's give it a default choice that makes sense if it were to be submitted.
self.assignee_id.choices = [] # No choices for employee to change assignee
# If we want to ensure it's not submitted by employees, we can clear validators or set a default.
# However, the route logic will handle setting assignee_id to current_user.id for employees.
# So, an empty choices list and Optional validator is fine.
# If current_user is available here, we could set a default choice:
# self.assignee_id.choices = [(current_user.id, current_user.username)]
# self.assignee_id.data = current_user.id
pass # Keep validators as Optional for non-managers
5 changes: 5 additions & 0 deletions todo_app/app/main/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from flask import Blueprint

main_bp = Blueprint('main', __name__, template_folder='templates')

from . import routes
174 changes: 174 additions & 0 deletions todo_app/app/main/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
from flask import render_template, redirect, url_for, flash, abort, request
from flask_login import login_required, current_user
from .. import db # Assuming db is initialized in app's __init__.py
from ..models import Task # Assuming Task model is in app's models.py
from ..forms import TaskForm # Assuming TaskForm is in app's forms.py
from ..decorators import manager_required # Import manager_required
from datetime import datetime
from . import main_bp

@main_bp.route('/')
@login_required
def index():
# Redirect to the list of tasks for the logged-in user
return redirect(url_for('main.list_tasks'))

# Placeholder for list_tasks, to be implemented next
@main_bp.route('/tasks')
@login_required
def list_tasks():
# Query tasks where assignee_id == current_user.id
tasks = Task.query.filter_by(assignee_id=current_user.id).order_by(Task.due_date.asc(), Task.created_at.desc()).all()
# Assuming template 'main/list_tasks.html' exists or will be created
return render_template('main/list_tasks.html', tasks=tasks, title="My Tasks")


@main_bp.route('/task/new', methods=['GET', 'POST'])
@login_required
def create_task():
form = TaskForm(current_user_role=current_user.role) # Pass current_user_role
if form.validate_on_submit():
due_date_val = form.due_date.data
task = Task(
title=form.title.data,
description=form.description.data,
due_date=due_date_val,
status=form.status.data,
creator_id=current_user.id
# assignee_id will be set below
)

if current_user.role == 'manager' and form.assignee_id.data and form.assignee_id.data != 0:
task.assignee_id = form.assignee_id.data
elif current_user.role == 'manager' and form.assignee_id.data == 0: # Manager chose 'Unassigned / Assign to Self'
task.assignee_id = current_user.id # Assign to self (manager)
else: # Employee is creating the task
task.assignee_id = current_user.id # Employee assigns to self

db.session.add(task)
db.session.commit()
flash('Your task has been created!', 'success')
return redirect(url_for('main.list_tasks'))
# Assuming template 'main/create_edit_task.html' exists or will be created
return render_template('main/create_edit_task.html', title='New Task', form=form, is_edit=False)


@main_bp.route('/task/<int:task_id>')
@login_required
def view_task(task_id):
task = Task.query.get_or_404(task_id)
if task.assignee_id != current_user.id:
# For now, only assignee can view. Manager logic can be added later.
abort(403)
# Assuming template 'main/view_task.html' exists or will be created
return render_template('main/view_task.html', task=task, title=task.title)


@main_bp.route('/task/<int:task_id>/edit', methods=['GET', 'POST'])
@login_required
def edit_task(task_id):
task = Task.query.get_or_404(task_id)
# Permission check: Manager can edit any task. Employee can only edit their own assigned tasks.
if current_user.role != 'manager' and task.assignee_id != current_user.id:
abort(403)

form = TaskForm(current_user_role=current_user.role) # Pass current_user_role
if form.validate_on_submit():
task.title = form.title.data
task.description = form.description.data
task.due_date = form.due_date.data
task.status = form.status.data
task.updated_at = datetime.utcnow()

if current_user.role == 'manager':
if form.assignee_id.data and form.assignee_id.data != 0:
task.assignee_id = form.assignee_id.data
elif form.assignee_id.data == 0: # Manager chose 'Unassigned / Assign to Self'
# If the task was assigned to someone else, and manager chooses 'Unassigned',
# it means it becomes assigned to the manager themselves.
task.assignee_id = current_user.id
# If no assignee_id is provided by a manager (e.g. if field was optional for some reason),
# do not change the assignee, or assign to self - current logic implies it must be selected.
# The form validator DataRequired for manager role ensures a selection is made.
# Employees cannot change assignee, so no 'else' needed here for task.assignee_id

db.session.commit()
flash('Your task has been updated!', 'success')
return redirect(url_for('main.view_task', task_id=task.id))
elif request.method == 'GET':
form.title.data = task.title
form.description.data = task.description
form.due_date.data = task.due_date
form.status.data = task.status
if current_user.role == 'manager':
# If task.assignee_id is None or not a valid choice, WTForms might have issues.
# The (0, 'Unassigned / Assign to Self') choice handles if task.assignee_id is current_user.id (the manager)
# or if it's genuinely unassigned (though our model implies assignee is often set).
# If task.assignee_id points to an employee, it will select them.
# If task.assignee_id is current_user.id (manager), it should select the 'Unassigned / Assign to Self' (0) option.
if task.assignee_id == current_user.id:
form.assignee_id.data = 0 # Select the 'Unassigned / Assign to Self' option
else:
form.assignee_id.data = task.assignee_id

# Assuming template 'main/create_edit_task.html' exists or will be created
return render_template('main/create_edit_task.html', title='Edit Task', form=form, task=task, is_edit=True)


@main_bp.route('/task/<int:task_id>/delete', methods=['POST'])
@login_required
def delete_task(task_id):
task = Task.query.get_or_404(task_id)
if task.assignee_id != current_user.id:
abort(403) # User cannot delete tasks not assigned to them
db.session.delete(task)
db.session.commit()
flash('Your task has been deleted!', 'success')
return redirect(url_for('main.list_tasks'))


@main_bp.route('/task/<int:task_id>/complete', methods=['POST'])
@login_required
def complete_task(task_id):
task = Task.query.get_or_404(task_id)
if task.assignee_id != current_user.id:
abort(403) # User cannot complete tasks not assigned to them

task.status = 'completed'
task.updated_at = datetime.utcnow()
db.session.commit()
flash('Task marked as completed!', 'success')
return redirect(url_for('main.list_tasks'))


# The old view_task_old placeholder can be removed.
# (The old view_task_old has been removed by not including it here)


@main_bp.route('/admin/tasks')
@login_required
@manager_required
def view_all_tasks():
# Query all tasks. Join with User to access creator/assignee usernames if needed in template.
# Ordered by due_date, then by creation_date.
tasks = Task.query.order_by(Task.due_date.asc(), Task.created_at.desc()).all()
# Assuming template 'main/all_tasks.html' exists or will be created
return render_template('main/all_tasks.html', tasks=tasks, title="All Tasks")

@main_bp.route('/task/<int:task_id>/approve', methods=['POST'])
@login_required
@manager_required
def approve_task(task_id):
task = Task.query.get_or_404(task_id)

if task.status != 'completed':
flash('Task must be marked as "completed" before it can be approved.', 'warning')
return redirect(url_for('main.view_task', task_id=task.id)) # Or main.all_tasks

task.status = 'approved'
task.approved_by_id = current_user.id
task.updated_at = datetime.utcnow()
db.session.commit()

flash('Task has been approved successfully!', 'success')
return redirect(url_for('main.view_task', task_id=task.id)) # Or main.all_tasks
Loading