Add files via upload
This commit is contained in:
33
backend/app/__init__.py
Normal file
33
backend/app/__init__.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import os
|
||||
from flask import Flask, send_from_directory
|
||||
from .extensions import db, migrate, cors
|
||||
from config import config
|
||||
|
||||
def create_app(config_name=None):
|
||||
if config_name is None:
|
||||
config_name = os.environ.get('FLASK_ENV', 'production')
|
||||
|
||||
app = Flask(__name__, static_folder='static', static_url_path='')
|
||||
app.config.from_object(config.get(config_name, config['default']))
|
||||
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
cors.init_app(app, resources={r'/api/*': {'origins': '*'}})
|
||||
|
||||
from .routes.projects import projects_bp
|
||||
from .routes.deliverables import deliverables_bp
|
||||
app.register_blueprint(projects_bp, url_prefix='/api')
|
||||
app.register_blueprint(deliverables_bp, url_prefix='/api')
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
@app.route('/', defaults={'path': ''})
|
||||
@app.route('/<path:path>')
|
||||
def serve_react(path):
|
||||
static_folder = app.static_folder
|
||||
if path and os.path.exists(os.path.join(static_folder, path)):
|
||||
return send_from_directory(static_folder, path)
|
||||
return send_from_directory(static_folder, 'index.html')
|
||||
|
||||
return app
|
||||
7
backend/app/extensions.py
Normal file
7
backend/app/extensions.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_migrate import Migrate
|
||||
from flask_cors import CORS
|
||||
|
||||
db = SQLAlchemy()
|
||||
migrate = Migrate()
|
||||
cors = CORS()
|
||||
51
backend/app/models.py
Normal file
51
backend/app/models.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from .extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
class Project(db.Model):
|
||||
__tablename__ = 'projects'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(200), nullable=False)
|
||||
color = db.Column(db.String(7), nullable=False, default='#C9A84C')
|
||||
description = db.Column(db.Text)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
deliverables = db.relationship(
|
||||
'Deliverable', backref='project',
|
||||
cascade='all, delete-orphan', lazy=True
|
||||
)
|
||||
|
||||
def to_dict(self, include_deliverables=True):
|
||||
data = {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'color': self.color,
|
||||
'description': self.description,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
}
|
||||
if include_deliverables:
|
||||
data['deliverables'] = [
|
||||
d.to_dict() for d in sorted(self.deliverables, key=lambda x: x.due_date)
|
||||
]
|
||||
return data
|
||||
|
||||
|
||||
class Deliverable(db.Model):
|
||||
__tablename__ = 'deliverables'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
project_id = db.Column(db.Integer, db.ForeignKey('projects.id', ondelete='CASCADE'), nullable=False)
|
||||
title = db.Column(db.String(300), nullable=False)
|
||||
due_date = db.Column(db.Date, nullable=False)
|
||||
status = db.Column(db.String(20), nullable=False, default='upcoming')
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'project_id': self.project_id,
|
||||
'title': self.title,
|
||||
'due_date': self.due_date.isoformat() if self.due_date else None,
|
||||
'status': self.status,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
}
|
||||
0
backend/app/routes/__init__.py
Normal file
0
backend/app/routes/__init__.py
Normal file
44
backend/app/routes/deliverables.py
Normal file
44
backend/app/routes/deliverables.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from flask import Blueprint, jsonify, request
|
||||
from ..models import Deliverable
|
||||
from ..extensions import db
|
||||
from datetime import date
|
||||
|
||||
deliverables_bp = Blueprint('deliverables', __name__)
|
||||
|
||||
@deliverables_bp.route('/deliverables', methods=['GET'])
|
||||
def get_deliverables():
|
||||
project_id = request.args.get('project_id')
|
||||
q = Deliverable.query
|
||||
if project_id:
|
||||
q = q.filter_by(project_id=int(project_id))
|
||||
return jsonify([d.to_dict() for d in q.order_by(Deliverable.due_date).all()])
|
||||
|
||||
@deliverables_bp.route('/deliverables', methods=['POST'])
|
||||
def create_deliverable():
|
||||
data = request.get_json()
|
||||
d = Deliverable(
|
||||
project_id=data['project_id'],
|
||||
title=data['title'],
|
||||
due_date=date.fromisoformat(data['due_date']),
|
||||
status=data.get('status', 'upcoming'),
|
||||
)
|
||||
db.session.add(d)
|
||||
db.session.commit()
|
||||
return jsonify(d.to_dict()), 201
|
||||
|
||||
@deliverables_bp.route('/deliverables/<int:id>', methods=['PATCH'])
|
||||
def update_deliverable(id):
|
||||
d = Deliverable.query.get_or_404(id)
|
||||
data = request.get_json()
|
||||
if 'title' in data: d.title = data['title']
|
||||
if 'due_date' in data: d.due_date = date.fromisoformat(data['due_date'])
|
||||
if 'status' in data: d.status = data['status']
|
||||
db.session.commit()
|
||||
return jsonify(d.to_dict())
|
||||
|
||||
@deliverables_bp.route('/deliverables/<int:id>', methods=['DELETE'])
|
||||
def delete_deliverable(id):
|
||||
d = Deliverable.query.get_or_404(id)
|
||||
db.session.delete(d)
|
||||
db.session.commit()
|
||||
return jsonify({'message': 'Deliverable deleted'}), 200
|
||||
54
backend/app/routes/projects.py
Normal file
54
backend/app/routes/projects.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from flask import Blueprint, jsonify, request
|
||||
from ..models import Project, Deliverable
|
||||
from ..extensions import db
|
||||
from datetime import date
|
||||
|
||||
projects_bp = Blueprint('projects', __name__)
|
||||
|
||||
@projects_bp.route('/projects', methods=['GET'])
|
||||
def get_projects():
|
||||
projects = Project.query.order_by(Project.created_at.desc()).all()
|
||||
return jsonify([p.to_dict() for p in projects])
|
||||
|
||||
@projects_bp.route('/projects/<int:id>', methods=['GET'])
|
||||
def get_project(id):
|
||||
project = Project.query.get_or_404(id)
|
||||
return jsonify(project.to_dict())
|
||||
|
||||
@projects_bp.route('/projects', methods=['POST'])
|
||||
def create_project():
|
||||
data = request.get_json()
|
||||
project = Project(
|
||||
name=data['name'],
|
||||
color=data.get('color', '#C9A84C'),
|
||||
description=data.get('description', ''),
|
||||
)
|
||||
db.session.add(project)
|
||||
db.session.flush()
|
||||
for d in data.get('deliverables', []):
|
||||
if d.get('title') and d.get('due_date'):
|
||||
db.session.add(Deliverable(
|
||||
project_id=project.id,
|
||||
title=d['title'],
|
||||
due_date=date.fromisoformat(d['due_date']),
|
||||
status=d.get('status', 'upcoming'),
|
||||
))
|
||||
db.session.commit()
|
||||
return jsonify(project.to_dict()), 201
|
||||
|
||||
@projects_bp.route('/projects/<int:id>', methods=['PATCH'])
|
||||
def update_project(id):
|
||||
project = Project.query.get_or_404(id)
|
||||
data = request.get_json()
|
||||
for field in ('name', 'color', 'description'):
|
||||
if field in data:
|
||||
setattr(project, field, data[field])
|
||||
db.session.commit()
|
||||
return jsonify(project.to_dict())
|
||||
|
||||
@projects_bp.route('/projects/<int:id>', methods=['DELETE'])
|
||||
def delete_project(id):
|
||||
project = Project.query.get_or_404(id)
|
||||
db.session.delete(project)
|
||||
db.session.commit()
|
||||
return jsonify({'message': 'Project deleted'}), 200
|
||||
18
backend/config.py
Normal file
18
backend/config.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import os
|
||||
|
||||
class Config:
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-secret-change-in-production')
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', 'sqlite:///fabdash.db')
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
|
||||
class DevelopmentConfig(Config):
|
||||
DEBUG = True
|
||||
|
||||
class ProductionConfig(Config):
|
||||
DEBUG = False
|
||||
|
||||
config = {
|
||||
'development': DevelopmentConfig,
|
||||
'production': ProductionConfig,
|
||||
'default': DevelopmentConfig,
|
||||
}
|
||||
6
backend/requirements.txt
Normal file
6
backend/requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
flask==3.1.0
|
||||
flask-sqlalchemy==3.1.1
|
||||
flask-migrate==4.0.7
|
||||
flask-cors==5.0.0
|
||||
gunicorn==23.0.0
|
||||
python-dotenv==1.0.1
|
||||
6
backend/run.py
Normal file
6
backend/run.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from app import create_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5000, debug=False)
|
||||
Reference in New Issue
Block a user