Skip to content
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
108 changes: 108 additions & 0 deletions 003-team-sprint-board/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from flask import Flask, request, jsonify, abort
import uuid

app = Flask(__name__)

# In-memory storage
# board = { "Backlog": [], "In Progress": [], "Review": [], "Done": [] }
# cards = { card_id: { ...card_data... } }
board_columns = ["Backlog", "In Progress", "Review", "Done"]
cards = {}

VALID_STORY_POINTS = {None, 1, 2, 3, 5, 8, 13}

def get_user():
user = request.headers.get('X-User-Name')
if not user:
abort(400, description="Display name is required in X-User-Name header")
return user

@app.route('/board', methods=['GET'])
def get_board():
get_user()
result = {col: [] for col in board_columns}
for card_id, card in cards.items():
col = card['status']
if col in result:
card_copy = card.copy()
card_copy['id'] = card_id
result[col].append(card_copy)
return jsonify(result)

@app.route('/cards', methods=['POST'])
def create_card():
get_user()
data = request.json or {}
column = data.get('column')
title = data.get('title')
description = data.get('description', '')
story_points = data.get('story_points')

if column not in board_columns:
abort(400, description="Invalid column")
if not title:
abort(400, description="Title is required")

# Validate story points
if story_points is not None and story_points not in VALID_STORY_POINTS:
abort(400, description="Invalid story points. Allowed: 1, 2, 3, 5, 8, 13 or blank")

card_id = str(uuid.uuid4())
cards[card_id] = {
'title': title,
'description': description,
'story_points': story_points,
'status': column
}
return jsonify({'id': card_id}), 201

@app.route('/cards/<card_id>', methods=['PUT'])
def update_card(card_id):
get_user()
if card_id not in cards:
abort(404, description="Card not found")

data = request.json or {}
card = cards[card_id]

title = data.get('title', card['title'])
if not title:
abort(400, description="Title is required")

description = data.get('description', card['description'])
story_points = data.get('story_points', card['story_points'])
status = data.get('status', card['status'])

if status not in board_columns:
abort(400, description="Invalid status")

if story_points is not None and story_points not in VALID_STORY_POINTS:
abort(400, description="Invalid story points. Allowed: 1, 2, 3, 5, 8, 13 or blank")

card.update({
'title': title,
'description': description,
'story_points': story_points,
'status': status
})

return jsonify(card)

@app.route('/cards/<card_id>', methods=['DELETE'])
def delete_card(card_id):
get_user()
if card_id not in cards:
abort(404, description="Card not found")
del cards[card_id]
return '', 204

@app.route('/board/done', methods=['DELETE'])
def clear_done():
get_user()
to_delete = [card_id for card_id, card in cards.items() if card['status'] == 'Done']
for card_id in to_delete:
del cards[card_id]
return '', 204

if __name__ == '__main__':
app.run(debug=True, port=5000)
1 change: 1 addition & 0 deletions 003-team-sprint-board/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Flask==3.0.3
8 changes: 8 additions & 0 deletions 003-team-sprint-board/run_tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
pip install -r requirements.txt
python3 app.py > app.log 2>&1 &
APP_PID=$!
sleep 2
python3 test_api.py
RESULT=$?
kill $APP_PID
exit $RESULT
113 changes: 113 additions & 0 deletions 003-team-sprint-board/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import requests
import json

BASE_URL = "http://127.0.0.1:5000"
HEADERS = {"X-User-Name": "TestUser", "Content-Type": "application/json"}

def test_user_identity():
print("Testing User Identity...")
# Request without header should fail
resp = requests.get(f"{BASE_URL}/board")
assert resp.status_code == 400
print("OK: Request without identity rejected")

def test_create_cards():
print("Testing Create Cards...")
# Valid card
payload = {"column": "Backlog", "title": "Task 1", "description": "Desc 1", "story_points": 3}
resp = requests.post(f"{BASE_URL}/cards", json=payload, headers=HEADERS)
assert resp.status_code == 201
card_id = resp.json()['id']

# Card with only title
payload = {"column": "In Progress", "title": "Task 2"}
resp = requests.post(f"{BASE_URL}/cards", json=payload, headers=HEADERS)
assert resp.status_code == 201
card_id_2 = resp.json()['id']

# Invalid: No title
payload = {"column": "Backlog", "description": "No title"}
resp = requests.post(f"{BASE_URL}/cards", json=payload, headers=HEADERS)
assert resp.status_code == 400

# Invalid: Wrong story points
payload = {"column": "Backlog", "title": "Task 3", "story_points": 4}
resp = requests.post(f"{BASE_URL}/cards", json=payload, headers=HEADERS)
assert resp.status_code == 400
print("OK: Card creation and validation working")
return card_id, card_id_2

def test_get_board(card_id, card_id_2):
print("Testing Get Board...")
resp = requests.get(f"{BASE_URL}/board", headers=HEADERS)
assert resp.status_code == 200
board = resp.json()
assert "Backlog" in board and "In Progress" in board
# Verify card 1 is in Backlog
assert any(c['id'] == card_id for c in board['Backlog'])
# Verify card 2 is in In Progress
assert any(c['id'] == card_id_2 for c in board['In Progress'])
print("OK: Board retrieval working")

def test_edit_and_move_card(card_id):
print("Testing Edit and Move Card...")
# Update title and move to Review
payload = {"title": "Updated Task 1", "status": "Review", "story_points": 5}
resp = requests.put(f"{BASE_URL}/cards/{card_id}", json=payload, headers=HEADERS)
assert resp.status_code == 200
assert resp.json()['title'] == "Updated Task 1"
assert resp.json()['status'] == "Review"

# Verify board reflects move
resp = requests.get(f"{BASE_URL}/board", headers=HEADERS)
board = resp.json()
assert any(c['id'] == card_id for c in board['Review'])
assert not any(c['id'] == card_id for c in board['Backlog'])
print("OK: Card editing and moving working")

def test_delete_card(card_id):
print("Testing Delete Card...")
resp = requests.delete(f"{BASE_URL}/cards/{card_id}", headers=HEADERS)
assert resp.status_code == 204

resp = requests.get(f"{BASE_URL}/board", headers=HEADERS)
board = resp.json()
for col in board:
assert not any(c['id'] == card_id for c in board[col])
print("OK: Card deletion working")

def test_clear_done():
print("Testing Clear Done...")
# Create a card in Done
payload = {"column": "Done", "title": "Done Task"}
resp = requests.post(f"{BASE_URL}/cards", json=payload, headers=HEADERS)
card_id = resp.json()['id']

# Create a card in Backlog
payload = {"column": "Backlog", "title": "Backlog Task"}
resp = requests.post(f"{BASE_URL}/cards", json=payload, headers=HEADERS)
backlog_id = resp.json()['id']

# Clear Done
resp = requests.delete(f"{BASE_URL}/board/done", headers=HEADERS)
assert resp.status_code == 204

# Verify
resp = requests.get(f"{BASE_URL}/board", headers=HEADERS)
board = resp.json()
assert len(board['Done']) == 0
assert any(c['id'] == backlog_id for c in board['Backlog'])
print("OK: Clear Done working")

if __name__ == "__main__":
try:
test_user_identity()
c1, c2 = test_create_cards()
test_get_board(c1, c2)
test_edit_and_move_card(c1)
test_delete_card(c2) # deleting c2
test_clear_done()
print("\nALL TESTS PASSED!")
except Exception as e:
print(f"\nTEST FAILED: {e}")
exit(1)