From 43b3a5c0ee1749f95768c28ff1e193b2f3c6ef9f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 20:32:38 +0000 Subject: [PATCH 1/4] Initial plan From 70a355bdd15a348a6232a989873892e8ee6f1834 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 20:41:19 +0000 Subject: [PATCH 2/4] Add responsive HTML UI with full CRUD functionality Co-authored-by: liamchampton <42477259+liamchampton@users.noreply.github.com> --- src/app.py | 6 +- src/templates/index.html | 456 +++++++++++++++++++++++++++++++++++++++ src/test_app.py | 26 ++- 3 files changed, 486 insertions(+), 2 deletions(-) create mode 100644 src/templates/index.html diff --git a/src/app.py b/src/app.py index c1224fe..3c83af6 100644 --- a/src/app.py +++ b/src/app.py @@ -1,4 +1,4 @@ -from flask import Flask, jsonify, request, abort +from flask import Flask, jsonify, request, abort, render_template from uuid import uuid4 app = Flask(__name__) @@ -6,6 +6,10 @@ # In-memory product store data = {} +@app.route('/') +def index(): + return render_template('index.html') + @app.route('/products', methods=['GET']) def get_products(): return jsonify(list(data.values())), 200 diff --git a/src/templates/index.html b/src/templates/index.html new file mode 100644 index 0000000..cf8fbdc --- /dev/null +++ b/src/templates/index.html @@ -0,0 +1,456 @@ + + + + + + Product Store Demo + + + +
+
+

Product Store Demo

+
+ + + +
+

Add New Product

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+

Products

+
Loading products...
+ +
+
+ + + + \ No newline at end of file diff --git a/src/test_app.py b/src/test_app.py index 7f7ed38..12ed55a 100644 --- a/src/test_app.py +++ b/src/test_app.py @@ -1 +1,25 @@ -# Unit test file \ No newline at end of file +import pytest +from app import app + +@pytest.fixture +def client(): + app.config['TESTING'] = True + with app.test_client() as client: + yield client + +def test_create_and_get_product(client): + # Create + rv = client.post('/products', json={'name': 'Test', 'description': 'Desc'}) + assert rv.status_code == 201 + product = rv.get_json() + # Get + rv = client.get(f"/products/{product['id']}") + assert rv.status_code == 200 + assert rv.get_json()['name'] == 'Test' + +def test_index_route(client): + """Test that the main route serves the HTML UI""" + rv = client.get('/') + assert rv.status_code == 200 + assert b'Product Store Demo' in rv.data + assert b'Add New Product' in rv.data \ No newline at end of file From e298fc0e7657b8b4519b9efdb10b181d091c46cb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 20:45:17 +0000 Subject: [PATCH 3/4] Fix XSS vulnerabilities by using event delegation and data attributes Co-authored-by: liamchampton <42477259+liamchampton@users.noreply.github.com> --- src/templates/index.html | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/templates/index.html b/src/templates/index.html index cf8fbdc..f9e1822 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -269,6 +269,19 @@

Products

this.cancelBtn.addEventListener('click', () => this.cancelEdit()); } + bindProductEvents() { + // Use event delegation for dynamically created buttons + this.productGrid.addEventListener('click', (e) => { + if (e.target.classList.contains('edit-btn')) { + const productId = e.target.getAttribute('data-id'); + this.editProduct(productId); + } else if (e.target.classList.contains('delete-btn')) { + const productId = e.target.getAttribute('data-id'); + this.deleteProduct(productId); + } + }); + } + async loadProducts() { try { this.showLoading(); @@ -289,17 +302,18 @@

Products

this.productGrid.innerHTML = '
No products found. Add your first product above!
'; } else { this.productGrid.innerHTML = this.products.map(product => ` -
+
${this.escapeHtml(product.name)}
${this.escapeHtml(product.description || 'No description')}
- - + +
`).join(''); } this.productGrid.classList.remove('hidden'); + this.bindProductEvents(); } async handleSubmit(e) { From c96a4c2cab80ee1c9e3f81932a0dc915d45df980 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:05:01 +0000 Subject: [PATCH 4/4] Add category dropdown field to products with 9 predefined categories Co-authored-by: liamchampton <42477259+liamchampton@users.noreply.github.com> --- src/app.py | 4 ++-- src/templates/index.html | 35 ++++++++++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/app.py b/src/app.py index 3c83af6..b686a93 100644 --- a/src/app.py +++ b/src/app.py @@ -27,7 +27,7 @@ def create_product(): if not body or 'name' not in body: abort(400) product_id = str(uuid4()) - product = {'id': product_id, 'name': body['name'], 'description': body.get('description', '')} + product = {'id': product_id, 'name': body['name'], 'description': body.get('description', ''), 'category': body.get('category', '')} data[product_id] = product return jsonify(product), 201 @@ -38,7 +38,7 @@ def update_product(product_id): body = request.get_json() if not body or 'name' not in body: abort(400) - data[product_id].update({'name': body['name'], 'description': body.get('description', '')}) + data[product_id].update({'name': body['name'], 'description': body.get('description', ''), 'category': body.get('category', '')}) return jsonify(data[product_id]), 200 @app.route('/products/', methods=['DELETE']) diff --git a/src/templates/index.html b/src/templates/index.html index f9e1822..3c7ee87 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -55,7 +55,7 @@ font-weight: 500; } - input, textarea { + input, textarea, select { width: 100%; padding: 0.75rem; border: 1px solid #ddd; @@ -139,6 +139,16 @@ min-height: 40px; } + .product-category { + display: inline-block; + background: #3498db; + color: white; + padding: 0.25rem 0.75rem; + border-radius: 4px; + font-size: 0.875rem; + margin-bottom: 0.75rem; + } + .product-actions { display: flex; gap: 0.5rem; @@ -227,6 +237,21 @@

Add New Product

+
+ + +
@@ -249,6 +274,7 @@

Products

this.form = document.getElementById('product-form'); this.nameInput = document.getElementById('product-name'); this.descriptionInput = document.getElementById('product-description'); + this.categoryInput = document.getElementById('product-category'); this.submitBtn = document.getElementById('submit-btn'); this.cancelBtn = document.getElementById('cancel-btn'); this.formTitle = document.getElementById('form-title'); @@ -304,6 +330,7 @@

Products

this.productGrid.innerHTML = this.products.map(product => `
${this.escapeHtml(product.name)}
+ ${product.category ? `
${this.escapeHtml(product.category)}
` : ''}
${this.escapeHtml(product.description || 'No description')}
@@ -321,6 +348,7 @@

Products

const name = this.nameInput.value.trim(); const description = this.descriptionInput.value.trim(); + const category = this.categoryInput.value; if (!name) { this.showError('Product name is required'); @@ -329,10 +357,10 @@

Products

try { if (this.editingId) { - await this.updateProduct(this.editingId, { name, description }); + await this.updateProduct(this.editingId, { name, description, category }); this.showSuccess('Product updated successfully'); } else { - await this.createProduct({ name, description }); + await this.createProduct({ name, description, category }); this.showSuccess('Product added successfully'); } @@ -405,6 +433,7 @@

Products

this.editingId = id; this.nameInput.value = product.name; this.descriptionInput.value = product.description || ''; + this.categoryInput.value = product.category || ''; this.formTitle.textContent = 'Edit Product'; this.submitBtn.textContent = 'Update Product';