From c94932fd80f258b69e13263bd0c3caaf2a92f431 Mon Sep 17 00:00:00 2001 From: kl Date: Thu, 25 Sep 2025 13:15:19 +0200 Subject: [PATCH 1/5] api.py fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Funktionen korrigiert - Tests erstellt, um Funktionalitäten abzudecken --- api.py | 59 ++++++++++++++++--------- test_api.py | 122 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+), 21 deletions(-) create mode 100644 test_api.py diff --git a/api.py b/api.py index 86bc192..dd02776 100644 --- a/api.py +++ b/api.py @@ -5,23 +5,32 @@ appointments = [] next_id = 1 -@app.route("/appointments", methods=["GET"]) + +def extract_data_fields(json_data): + if not is_valid_appointment(json_data): + raise ValueError("Invalid appointment: wrong or missing fields") + + return json_data.get("title"), json_data.get("start"), json_data.get("end") + + +def is_valid_appointment(json_data): + return {"title", "start", "end"} == set(json_data.keys()) + + +@app.route("/appointments", methods = ["GET"]) def list_appointments(): return jsonify(appointments), 200 -@app.route("/appointments", methods=["POST"]) + +@app.route("/appointments", methods = ["POST"]) def create_appointment(): global next_id data = request.get_json() - - start = data.get("start") - end = data.get("end") - title = data.get("title", "") + title, start, end = extract_data_fields(data) for appointment in appointments: - if (start >= appointment["start"] and start <= appointment["end"]) or \ - (end >= appointment["start"] and end <= appointment["end"]): - return jsonify({"error": "Overlapping appointment"}), 200 + if end >= appointment["start"] and start <= appointment["end"]: + return jsonify({"error": "Overlapping appointment"}), 409 appointment = { "id": next_id, @@ -32,28 +41,36 @@ def create_appointment(): appointments.append(appointment) next_id += 1 - return jsonify(appointment), 200 + return jsonify(appointment), 201 + -@app.route("/appointments/", methods=["PUT"]) +@app.route("/appointments/", methods = ["PUT"]) def update_appointment(appt_id): data = request.get_json() + title, start, end = extract_data_fields(data) for appt in appointments: + if end >= appt["start"] and start <= appt["end"]: + return jsonify({"error": "Overlapping appointment"}), 409 + if appt["id"] == appt_id: - appt["title"] = data.get("title", appt["title"]) - appt["start"] = data.get("start", appt["start"]) - appt["end"] = data.get("end", appt["end"]) + appt["title"] = title + appt["start"] = start + appt["end"] = end return jsonify(appt), 200 - return jsonify({"error": "Appointment not found"}), 200 + return jsonify({"error": "Appointment not found"}), 404 -@app.route("/appointments/", methods=["DELETE"]) +@app.route("/appointments/", methods = ["DELETE"]) def delete_appointment(appt_id): - global appointments - appointments = [a for a in appointments if a["id"] != appt_id] - return jsonify({"status": "deleted"}), 200 + for appt in appointments: + if appt["id"] == appt_id: + appointments.remove(appt) + return jsonify({"status": "deleted"}), 200 + + return jsonify({"error": "Appointment not found"}), 404 -if __name__ == "__main__": - app.run(debug=True) +if __name__ == "__main__": # pragma: no coverage + app.run(debug = True) diff --git a/test_api.py b/test_api.py new file mode 100644 index 0000000..d3297eb --- /dev/null +++ b/test_api.py @@ -0,0 +1,122 @@ +import unittest +from unittest.mock import patch + +from api import app, appointments, is_valid_appointment, extract_data_fields + + +class TestApi(unittest.TestCase): + + def setUp(self): + appointments.clear() + self.client = app.test_client() + + def test_list_appointments_empty(self): + response = self.client.get("/appointments") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json, []) + + def test_list_appointments_with_entries(self): + self.client.post("/appointments", json = {"title": "Meeting", "start": "10:00", "end": "12:00"}) + self.client.post("/appointments", json = {"title": "Meeting2", "start": "13:00", "end": "14:00"}) + + response = self.client.get("/appointments") + self.assertEqual(response.status_code, 200) + + self.assertEqual(len(appointments), 2) + + def test_create_appointment(self): + response = self.client.post("/appointments", json = {"title": "Meeting", "start": "10:00", "end": "12:00"}) + self.assertEqual(response.status_code, 201) + self.assertEqual(appointments[0]["title"], "Meeting") + self.assertEqual(appointments[0]["start"], "10:00") + self.assertEqual(appointments[0]["end"], "12:00") + + def test_create_overlapping_appointment(self): + self.client.post("/appointments", json = {"title": "Meeting", "start": "10:00", "end": "12:00"}) + response = self.client.post("/appointments", json = {"title": "Meeting2", "start": "09:00", "end": "13:00"}) + self.assertEqual(response.status_code, 409) + self.assertIn("error", response.json) + self.assertEqual(response.json["error"], "Overlapping appointment") + + def test_update_appointment(self): + self.client.post("/appointments", json = {"title": "Meeting", "start": "10:00", "end": "12:00"}) + appt_id = appointments[0]["id"] + response = self.client.put(f"/appointments/{appt_id}", + json = {"title": "Sommerfest Meeting", "start": "13:00", "end": "15:00"}) + self.assertEqual(response.status_code, 200) + self.assertEqual(appointments[0]["title"], "Sommerfest Meeting") + self.assertEqual(appointments[0]["start"], "13:00") + self.assertEqual(appointments[0]["end"], "15:00") + + def test_update_overlapping_appointment(self): + self.client.post("/appointments", + json = {"title": "Meeting", "start": "10:00", "end": "12:00"}) + self.client.post("/appointments", json = {"title": "Meeting2", "start": "13:00", "end": "15:00"}) + response = self.client.put("/appointments/2", + json = {"title": "Meeting2", "start": "11:00", "end": "14:00"}) + self.assertEqual(response.status_code, 409) + self.assertEqual(response.json["error"], "Overlapping appointment") + + def test_update_overlapping_appointment_id_error(self): + self.client.post("/appointments", json = {"title": "Meeting", "start": "10:00", "end": "12:00"}) + response = self.client.put("/appointments/2", + json = {"title": "Meeting", "start": "13:00", "end": "14:00"}) + self.assertEqual(response.status_code, 404) + self.assertEqual(response.json["error"], "Appointment not found") + + def test_delete_appointment(self): + self.client.post("/appointments", json = {"title": "Meeting", "start": "10:00", "end": "12:00"}) + appt_id = appointments[0]["id"] + response = self.client.delete(f"/appointments/{appt_id}") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json["status"], "deleted") + self.assertEqual(len(appointments), 0) + + def test_delete_appointment_not_found(self): + self.client.post("/appointments", json = {"title": "Meeting", "start": "10:00", "end": "12:00"}) + response = self.client.delete("/appointments/8") + self.assertEqual(response.status_code, 404) + self.assertEqual(response.json["error"], "Appointment not found") + self.assertEqual(len(appointments), 1) + + def test_invalide_termine(self): + fehlerhafte_termine = [ + {}, + {"title": "1"}, + {"title": "1", "end": "1"}, + {"start": "1", "end": "1"}, + {"title": "1", "start": "1"}, + {"title": "1", "start": "1", "end": "1", "geheim": "1"} + ] + + for json_data in fehlerhafte_termine: + self.assertFalse(is_valid_appointment(json_data)) + + def test_valider_termin(self): + json_data = {"title": "Meeting", "start": "10:00", "end": "12:00"} + is_valid_appointment(json_data) + + @patch('api.is_valid_appointment', return_value = True) + def test_extract_fields_with_valid_appointment(self, mock_is_valid_appointment): + json_data = {"title": "Meeting", "start": "10:00", "end": "12:00"} + result = extract_data_fields(json_data) + mock_is_valid_appointment.assert_called() + self.assertEqual(result, ("Meeting", "10:00", "12:00")) + + @patch('api.is_valid_appointment', return_value = False) + def test_mit_invalidem_spieler_objekt(self, mock_is_valid_appointment): + with self.assertRaises(ValueError) as contextManager: + extract_data_fields({}) + mock_is_valid_appointment.assert_called() + self.assertEqual(contextManager.exception.args[0], "Invalid appointment: wrong or missing fields") + + def test_check_appointment_list(self): + self.assertEqual(len(appointments), 0) + + response = self.client.post("/appointments", json = {"title": "Meeting", "start": "10:00", "end": "12:00"}) + self.assertEqual(response.status_code, 201) + + self.assertEqual(len(appointments), 1) + self.assertEqual(appointments[0]["title"], "Meeting") + self.assertEqual(appointments[0]["start"], "10:00") + self.assertEqual(appointments[0]["end"], "12:00") From 2bdecf0a513994652aab64a96a9f833f3a4f906f Mon Sep 17 00:00:00 2001 From: kl Date: Fri, 26 Sep 2025 15:20:06 +0200 Subject: [PATCH 2/5] api.py feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Kategorie-Feld hinzugefügt - Kategorie-Validierung(vordefinierte Kategorien (health, general, work, social) erlaubt --- api.py | 46 ++++++++++++++---- test_api.py | 135 +++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 134 insertions(+), 47 deletions(-) diff --git a/api.py b/api.py index dd02776..8093ec7 100644 --- a/api.py +++ b/api.py @@ -4,21 +4,47 @@ appointments = [] next_id = 1 +category_types = ["health", "general", "work", "social"] -def extract_data_fields(json_data): - if not is_valid_appointment(json_data): - raise ValueError("Invalid appointment: wrong or missing fields") +def extract_and_validate_data_fields(json_data): + validate_appointment(json_data) + + title = json_data.get("title") + start = json_data.get("start") + end = json_data.get("end") + category = json_data.get("category") + + validate_category_types(category) - return json_data.get("title"), json_data.get("start"), json_data.get("end") + return title, start, end, category -def is_valid_appointment(json_data): - return {"title", "start", "end"} == set(json_data.keys()) +def validate_appointment(json_data): + if {"title", "start", "end", "category"} != set(json_data.keys()): + raise ValueError("Invalid appointment: wrong or missing fields") + + +def validate_category_types(category): + if category not in category_types: + raise ValueError(f"Invalid category. Must be one of {category_types}") @app.route("/appointments", methods = ["GET"]) def list_appointments(): + category_filter = request.args.get("category") + + if category_filter: + try: + validate_category_types(category_filter) + except ValueError as e: + return jsonify({"error": str(e)}) + + filtered = [appt for appt in appointments if appt["category"] == category_filter] + + if not filtered: + return jsonify({"error": "No appointments found for this category"}), 404 + return jsonify(filtered), 200 return jsonify(appointments), 200 @@ -26,7 +52,7 @@ def list_appointments(): def create_appointment(): global next_id data = request.get_json() - title, start, end = extract_data_fields(data) + title, start, end, category = extract_and_validate_data_fields(data) for appointment in appointments: if end >= appointment["start"] and start <= appointment["end"]: @@ -36,7 +62,8 @@ def create_appointment(): "id": next_id, "title": title, "start": start, - "end": end + "end": end, + "category": category, } appointments.append(appointment) next_id += 1 @@ -47,7 +74,7 @@ def create_appointment(): @app.route("/appointments/", methods = ["PUT"]) def update_appointment(appt_id): data = request.get_json() - title, start, end = extract_data_fields(data) + title, start, end, category = extract_and_validate_data_fields(data) for appt in appointments: if end >= appt["start"] and start <= appt["end"]: @@ -57,6 +84,7 @@ def update_appointment(appt_id): appt["title"] = title appt["start"] = start appt["end"] = end + appt["category"] = category return jsonify(appt), 200 return jsonify({"error": "Appointment not found"}), 404 diff --git a/test_api.py b/test_api.py index d3297eb..58e9c5d 100644 --- a/test_api.py +++ b/test_api.py @@ -1,7 +1,6 @@ import unittest -from unittest.mock import patch -from api import app, appointments, is_valid_appointment, extract_data_fields +from api import app, appointments, extract_and_validate_data_fields, validate_category_types, category_types class TestApi(unittest.TestCase): @@ -16,8 +15,12 @@ def test_list_appointments_empty(self): self.assertEqual(response.json, []) def test_list_appointments_with_entries(self): - self.client.post("/appointments", json = {"title": "Meeting", "start": "10:00", "end": "12:00"}) - self.client.post("/appointments", json = {"title": "Meeting2", "start": "13:00", "end": "14:00"}) + self.client.post("/appointments", + json = {"title": "Meeting", "start": "2025-09-26 10:00", "end": "2025-09-26 12:00", + "category": "general"}) + self.client.post("/appointments", + json = {"title": "Meeting2", "start": "2025-09-26 13:00", "end": "2025-09-26 14:00", + "category": "general"}) response = self.client.get("/appointments") self.assertEqual(response.status_code, 200) @@ -25,47 +28,68 @@ def test_list_appointments_with_entries(self): self.assertEqual(len(appointments), 2) def test_create_appointment(self): - response = self.client.post("/appointments", json = {"title": "Meeting", "start": "10:00", "end": "12:00"}) + response = self.client.post("/appointments", + json = {"title": "Meeting", "start": "2025-09-26 10:00", + "end": "2025-09-26 12:00", "category": "general"}) self.assertEqual(response.status_code, 201) self.assertEqual(appointments[0]["title"], "Meeting") - self.assertEqual(appointments[0]["start"], "10:00") - self.assertEqual(appointments[0]["end"], "12:00") + self.assertEqual(appointments[0]["start"], "2025-09-26 10:00") + self.assertEqual(appointments[0]["end"], "2025-09-26 12:00") + self.assertEqual(appointments[0]["category"], "general") def test_create_overlapping_appointment(self): - self.client.post("/appointments", json = {"title": "Meeting", "start": "10:00", "end": "12:00"}) - response = self.client.post("/appointments", json = {"title": "Meeting2", "start": "09:00", "end": "13:00"}) + self.client.post("/appointments", + json = {"title": "Meeting", "start": "2025-09-26 10:00", "end": "2025-09-26 12:00", + "category": "general"}) + response = self.client.post("/appointments", + json = {"title": "Meeting2", "start": "2025-09-26 09:00", "end": "2025-09-26 13:00", + "category": "general"}) + self.assertEqual(response.status_code, 409) self.assertIn("error", response.json) self.assertEqual(response.json["error"], "Overlapping appointment") def test_update_appointment(self): - self.client.post("/appointments", json = {"title": "Meeting", "start": "10:00", "end": "12:00"}) + self.client.post("/appointments", + json = {"title": "Meeting", "start": "2025-09-26 10:00", "end": "2025-09-26 12:00", + "category": "general"}) appt_id = appointments[0]["id"] response = self.client.put(f"/appointments/{appt_id}", - json = {"title": "Sommerfest Meeting", "start": "13:00", "end": "15:00"}) + json = {"title": "Sommerfest Meeting", "start": "2025-09-26 13:00", + "end": "2025-09-26 15:00", "category": "general"}) self.assertEqual(response.status_code, 200) self.assertEqual(appointments[0]["title"], "Sommerfest Meeting") - self.assertEqual(appointments[0]["start"], "13:00") - self.assertEqual(appointments[0]["end"], "15:00") + self.assertEqual(appointments[0]["start"], "2025-09-26 13:00") + self.assertEqual(appointments[0]["end"], "2025-09-26 15:00") + self.assertEqual(appointments[0]["category"], "general") def test_update_overlapping_appointment(self): self.client.post("/appointments", - json = {"title": "Meeting", "start": "10:00", "end": "12:00"}) - self.client.post("/appointments", json = {"title": "Meeting2", "start": "13:00", "end": "15:00"}) + json = {"title": "Meeting", "start": "2025-09-26 10:00", "end": "2025-09-26 12:00", + "category": "general"}) + self.client.post("/appointments", + json = {"title": "Meeting2", "start": "2025-09-26 13:00", "end": "2025-09-26 15:00", + "category": "general"}) response = self.client.put("/appointments/2", - json = {"title": "Meeting2", "start": "11:00", "end": "14:00"}) + json = {"title": "Meeting2", "start": "2025-09-26 11:00", "end": "2025-09-26 14:00", + "category": "general"}) self.assertEqual(response.status_code, 409) self.assertEqual(response.json["error"], "Overlapping appointment") def test_update_overlapping_appointment_id_error(self): - self.client.post("/appointments", json = {"title": "Meeting", "start": "10:00", "end": "12:00"}) + self.client.post("/appointments", + json = {"title": "Meeting", "start": "2025-09-26 10:00", "end": "2025-09-26 12:00", + "category": "general"}) response = self.client.put("/appointments/2", - json = {"title": "Meeting", "start": "13:00", "end": "14:00"}) + json = {"title": "Meeting", "start": "2025-09-26 13:00", "end": "2025-09-26 14:00", + "category": "general"}) self.assertEqual(response.status_code, 404) self.assertEqual(response.json["error"], "Appointment not found") def test_delete_appointment(self): - self.client.post("/appointments", json = {"title": "Meeting", "start": "10:00", "end": "12:00"}) + self.client.post("/appointments", + json = {"title": "Meeting", "start": "2025-09-26 10:00", "end": "2025-09-26 12:00", + "category": "general"}) appt_id = appointments[0]["id"] response = self.client.delete(f"/appointments/{appt_id}") self.assertEqual(response.status_code, 200) @@ -73,13 +97,16 @@ def test_delete_appointment(self): self.assertEqual(len(appointments), 0) def test_delete_appointment_not_found(self): - self.client.post("/appointments", json = {"title": "Meeting", "start": "10:00", "end": "12:00"}) + self.client.post("/appointments", + json = {"title": "Meeting", "start": "2025-09-26 10:00", "end": "2025-09-26 12:00", + "category": "general"}) response = self.client.delete("/appointments/8") self.assertEqual(response.status_code, 404) self.assertEqual(response.json["error"], "Appointment not found") self.assertEqual(len(appointments), 1) - def test_invalide_termine(self): + def test_invalid_appointments(self): + categorys = {"title", "start", "end", "category"} fehlerhafte_termine = [ {}, {"title": "1"}, @@ -90,33 +117,65 @@ def test_invalide_termine(self): ] for json_data in fehlerhafte_termine: - self.assertFalse(is_valid_appointment(json_data)) + self.assertNotEqual(categorys, set(json_data.keys())) - def test_valider_termin(self): - json_data = {"title": "Meeting", "start": "10:00", "end": "12:00"} - is_valid_appointment(json_data) + def test_valid_appointment(self): + json_data = {"title": "Meeting", "start": "2025-09-26 10:00", "end": "2025-09-26 12:00", "category": "general"} + self.assertEqual({"title", "start", "end", "category"}, set(json_data.keys())) - @patch('api.is_valid_appointment', return_value = True) - def test_extract_fields_with_valid_appointment(self, mock_is_valid_appointment): - json_data = {"title": "Meeting", "start": "10:00", "end": "12:00"} - result = extract_data_fields(json_data) - mock_is_valid_appointment.assert_called() - self.assertEqual(result, ("Meeting", "10:00", "12:00")) + def test_extract_fields_with_valid_appointment(self): + json_data = {"title": "Meeting", "start": "2025-09-26 10:00", "end": "2025-09-26 12:00", "category": "general"} + result = extract_and_validate_data_fields(json_data) + self.assertEqual(result, ("Meeting", "2025-09-26 10:00", "2025-09-26 12:00", "general")) - @patch('api.is_valid_appointment', return_value = False) - def test_mit_invalidem_spieler_objekt(self, mock_is_valid_appointment): + def test_extract_fields_with_invalid_appointment(self): with self.assertRaises(ValueError) as contextManager: - extract_data_fields({}) - mock_is_valid_appointment.assert_called() + extract_and_validate_data_fields({}) + self.assertEqual(contextManager.exception.args[0], "Invalid appointment: wrong or missing fields") def test_check_appointment_list(self): self.assertEqual(len(appointments), 0) - response = self.client.post("/appointments", json = {"title": "Meeting", "start": "10:00", "end": "12:00"}) + response = self.client.post("/appointments", + json = {"title": "Meeting", "start": "2025-09-26 10:00", "end": "2025-09-26 12:00", + "category": "general"}) self.assertEqual(response.status_code, 201) self.assertEqual(len(appointments), 1) self.assertEqual(appointments[0]["title"], "Meeting") - self.assertEqual(appointments[0]["start"], "10:00") - self.assertEqual(appointments[0]["end"], "12:00") + self.assertEqual(appointments[0]["start"], "2025-09-26 10:00") + self.assertEqual(appointments[0]["end"], "2025-09-26 12:00") + + def test_list_appointments_with_category_filter(self): + appointments.append({"title": "Arzt", "start": "13:00", "end": "14:00", "category": "health"}) + appointments.append({"title": "Meeting", "start": "15:00", "end": "16:00", "category": "work"}) + appointments.append({"title": "Zahnarzt", "start": "18:00", "end": "18:30", "category": "health"}) + + response = self.client.get("/appointments?category=health") + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.json), 2) + self.assertEqual(response.json[0]["category"], "health") + self.assertEqual(response.json[1]["category"], "health") + + def test_list_appointments_with_invalid_category(self): + appointments.append({"title": "Meeting", "start": "10:00", "end": "12:00", "category": "doesnotexist"}) + + response = self.client.get("/appointments?category=doesnotexist") + self.assertIn("error", response.json) + self.assertEqual(response.json["error"], + "Invalid category. Must be one of ['health', 'general', 'work', 'social']") + + def test_no_valid_category_type(self): + category = {"category": "abc"} + with self.assertRaises(ValueError) as contextManager: + validate_category_types(category) + self.assertEqual(contextManager.exception.args[0], f"Invalid category. Must be one of {category_types}") + + def test_no_appointments_for_category(self): + appointments.append({"title": "Meeting", "start": "10:00", "end": "12:00", "category": "general"}) + + response = self.client.get("/appointments?category=health") + self.assertEqual(response.status_code, 404) + self.assertIn("error", response.json) + self.assertEqual(response.json["error"], "No appointments found for this category") From 19e346a61e2421974d049e38613cc7935200e6b9 Mon Sep 17 00:00:00 2001 From: kl Date: Tue, 30 Sep 2025 15:52:16 +0200 Subject: [PATCH 3/5] api.py korrekte Zeit Verwaltung MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Funktion shift_appointment erstellt - Tests ergänzt und angepasst, um die neuen Änderungen abzudecken --- api.py | 51 ++++++++++++++++++++++++++++++++------ test_api.py | 71 +++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 104 insertions(+), 18 deletions(-) diff --git a/api.py b/api.py index 8093ec7..89426ff 100644 --- a/api.py +++ b/api.py @@ -1,3 +1,5 @@ +from datetime import datetime, timedelta + from flask import Flask, request, jsonify app = Flask(__name__) @@ -11,11 +13,13 @@ def extract_and_validate_data_fields(json_data): validate_appointment(json_data) title = json_data.get("title") - start = json_data.get("start") - end = json_data.get("end") + start_str = json_data.get("start") + end_str = json_data.get("end") category = json_data.get("category") validate_category_types(category) + start = datetime.strptime(start_str, "%Y-%m-%d %H:%M") + end = datetime.strptime(end_str, "%Y-%m-%d %H:%M") return title, start, end, category @@ -30,6 +34,16 @@ def validate_category_types(category): raise ValueError(f"Invalid category. Must be one of {category_types}") +def serialize_datetime_format(appt): + return { + "id": appt["id"], + "title": appt["title"], + "start": appt["start"].strftime("%Y-%m-%d %H:%M"), + "end": appt["end"].strftime("%Y-%m-%d %H:%M"), + "category": appt["category"] + } + + @app.route("/appointments", methods = ["GET"]) def list_appointments(): category_filter = request.args.get("category") @@ -40,12 +54,12 @@ def list_appointments(): except ValueError as e: return jsonify({"error": str(e)}) - filtered = [appt for appt in appointments if appt["category"] == category_filter] + filtered = [serialize_datetime_format(appt) for appt in appointments if appt["category"] == category_filter] if not filtered: return jsonify({"error": "No appointments found for this category"}), 404 return jsonify(filtered), 200 - return jsonify(appointments), 200 + return jsonify([serialize_datetime_format(appt) for appt in appointments]), 200 @app.route("/appointments", methods = ["POST"]) @@ -67,8 +81,7 @@ def create_appointment(): } appointments.append(appointment) next_id += 1 - - return jsonify(appointment), 201 + return jsonify(serialize_datetime_format(appointment)), 201 @app.route("/appointments/", methods = ["PUT"]) @@ -77,7 +90,7 @@ def update_appointment(appt_id): title, start, end, category = extract_and_validate_data_fields(data) for appt in appointments: - if end >= appt["start"] and start <= appt["end"]: + if appt["id"] != appt_id and end >= appt["start"] and start <= appt["end"]: return jsonify({"error": "Overlapping appointment"}), 409 if appt["id"] == appt_id: @@ -85,7 +98,7 @@ def update_appointment(appt_id): appt["start"] = start appt["end"] = end appt["category"] = category - return jsonify(appt), 200 + return jsonify(serialize_datetime_format(appt)), 200 return jsonify({"error": "Appointment not found"}), 404 @@ -100,5 +113,27 @@ def delete_appointment(appt_id): return jsonify({"error": "Appointment not found"}), 404 +@app.route("/appointments/shift/", methods = ["POST"]) +def shift_appointment(appt_id): + amount_str = request.args.get("amount") + + try: + amount = float(amount_str) + except ValueError: + return jsonify({"error": "Invalid amount. Must be a number."}), 400 + + shift = timedelta(days = amount) + + for appt in appointments: + if appt["id"] == appt_id: + appt["start"] = appt["start"] + shift + appt["end"] = appt["end"] + shift + return jsonify(serialize_datetime_format(appt)), 200 + + return jsonify({"error": "Appointment not found"}), 404 + + +app.config['JSON_SORT_KEYS'] = False + if __name__ == "__main__": # pragma: no coverage app.run(debug = True) diff --git a/test_api.py b/test_api.py index 58e9c5d..e79e237 100644 --- a/test_api.py +++ b/test_api.py @@ -1,4 +1,5 @@ import unittest +from datetime import datetime from api import app, appointments, extract_and_validate_data_fields, validate_category_types, category_types @@ -33,8 +34,8 @@ def test_create_appointment(self): "end": "2025-09-26 12:00", "category": "general"}) self.assertEqual(response.status_code, 201) self.assertEqual(appointments[0]["title"], "Meeting") - self.assertEqual(appointments[0]["start"], "2025-09-26 10:00") - self.assertEqual(appointments[0]["end"], "2025-09-26 12:00") + self.assertEqual(appointments[0]["start"], datetime(2025, 9, 26, 10, 0)) + self.assertEqual(appointments[0]["end"], datetime(2025, 9, 26, 12, 0)) self.assertEqual(appointments[0]["category"], "general") def test_create_overlapping_appointment(self): @@ -59,8 +60,8 @@ def test_update_appointment(self): "end": "2025-09-26 15:00", "category": "general"}) self.assertEqual(response.status_code, 200) self.assertEqual(appointments[0]["title"], "Sommerfest Meeting") - self.assertEqual(appointments[0]["start"], "2025-09-26 13:00") - self.assertEqual(appointments[0]["end"], "2025-09-26 15:00") + self.assertEqual(appointments[0]["start"], datetime(2025, 9, 26, 13, 0)) + self.assertEqual(appointments[0]["end"], datetime(2025, 9, 26, 15, 0)) self.assertEqual(appointments[0]["category"], "general") def test_update_overlapping_appointment(self): @@ -126,7 +127,7 @@ def test_valid_appointment(self): def test_extract_fields_with_valid_appointment(self): json_data = {"title": "Meeting", "start": "2025-09-26 10:00", "end": "2025-09-26 12:00", "category": "general"} result = extract_and_validate_data_fields(json_data) - self.assertEqual(result, ("Meeting", "2025-09-26 10:00", "2025-09-26 12:00", "general")) + self.assertEqual(result, ("Meeting", datetime(2025, 9, 26, 10, 0), datetime(2025, 9, 26, 12, 0), "general")) def test_extract_fields_with_invalid_appointment(self): with self.assertRaises(ValueError) as contextManager: @@ -144,13 +145,19 @@ def test_check_appointment_list(self): self.assertEqual(len(appointments), 1) self.assertEqual(appointments[0]["title"], "Meeting") - self.assertEqual(appointments[0]["start"], "2025-09-26 10:00") - self.assertEqual(appointments[0]["end"], "2025-09-26 12:00") + self.assertEqual(appointments[0]["start"], datetime(2025, 9, 26, 10, 0)) + self.assertEqual(appointments[0]["end"], datetime(2025, 9, 26, 12, 0)) def test_list_appointments_with_category_filter(self): - appointments.append({"title": "Arzt", "start": "13:00", "end": "14:00", "category": "health"}) - appointments.append({"title": "Meeting", "start": "15:00", "end": "16:00", "category": "work"}) - appointments.append({"title": "Zahnarzt", "start": "18:00", "end": "18:30", "category": "health"}) + appointments.append( + {"id": 1, "title": "Arzt", "start": datetime(2025, 9, 26, 13, 0), "end": datetime(2025, 9, 26, 14, 0), + "category": "health"}) + appointments.append( + {"id": 2, "title": "Meeting", "start": datetime(2025, 9, 26, 15, 0), "end": datetime(2025, 9, 26, 16, 0), + "category": "work"}) + appointments.append( + {"id": 3, "title": "Zahnarzt", "start": datetime(2025, 9, 26, 18, 0), "end": datetime(2025, 9, 26, 18, 30), + "category": "health"}) response = self.client.get("/appointments?category=health") self.assertEqual(response.status_code, 200) @@ -179,3 +186,47 @@ def test_no_appointments_for_category(self): self.assertEqual(response.status_code, 404) self.assertIn("error", response.json) self.assertEqual(response.json["error"], "No appointments found for this category") + + def test_shift_appointments_various_amounts(self): + test_cases = [ + {"amount": 1,"expected_start": datetime(2025, 1, 1, 10, 0),"expected_end": datetime(2025, 1, 2, 11, 0)}, + {"amount": -1.5,"expected_start": datetime(2024, 12, 29, 22, 0),"expected_end": datetime(2024, 12, 30, 23, 0)}, + { "amount": 2.3,"expected_start": datetime(2025, 1, 2, 17, 12),"expected_end": datetime(2025, 1, 3, 18, 12)}, + ] + + for case in test_cases: + appointments.clear() + appointments.append({ + "id": 1, + "title": "Test Meeting", + "start": datetime(2024, 12, 31, 10, 0), + "end": datetime(2025, 1, 1, 11, 0), + "category": "work" + }) + + response = self.client.post(f"/appointments/shift/1?amount={case['amount']}") + + self.assertEqual(response.status_code, 200) + self.assertEqual(appointments[0]["id"], 1) + self.assertEqual(appointments[0]["start"], case["expected_start"]) + self.assertEqual(appointments[0]["end"], case["expected_end"]) + + def test_shift_appointment_false_id(self): + response = self.client.post("/appointments/shift/0?amount=5") + + self.assertEqual(response.status_code, 404) + self.assertIn("error", response.json) + self.assertEqual(response.json["error"], "Appointment not found") + + def test_shift_appointment_false_amount(self): + appointments.append({ + "id": 1, + "title": "Test Meeting", + "start": datetime(2024, 12, 31, 10, 0), + "end": datetime(2025, 1, 1, 11, 0), + "category": "work" + }) + + response = self.client.post("/appointments/shift/1?amount=xx") + self.assertIn("error", response.json) + self.assertEqual(response.json["error"], "Invalid amount. Must be a number.") From 58232ca817d48f89da392d6f4411dc89937547d7 Mon Sep 17 00:00:00 2001 From: kl Date: Mon, 6 Oct 2025 13:38:39 +0200 Subject: [PATCH 4/5] api.py Weiterentwicklung von shift_appointment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - amount_start und amount_end hinzugefügt für genauere Anpassungen von Terminen - Prüfung "Shift would cause overlapping appointment" eingebaut - sowie Testergänzungen und Anpassungen --- api.py | 39 +++++++++++++++--------- test_api.py | 85 ++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 102 insertions(+), 22 deletions(-) diff --git a/api.py b/api.py index 89426ff..7e89c37 100644 --- a/api.py +++ b/api.py @@ -6,7 +6,8 @@ appointments = [] next_id = 1 -category_types = ["health", "general", "work", "social"] +CATEGORY_TYPES = ["health", "general", "work", "social"] +TIME_FORMAT = "%Y-%m-%d %H:%M" def extract_and_validate_data_fields(json_data): @@ -18,8 +19,8 @@ def extract_and_validate_data_fields(json_data): category = json_data.get("category") validate_category_types(category) - start = datetime.strptime(start_str, "%Y-%m-%d %H:%M") - end = datetime.strptime(end_str, "%Y-%m-%d %H:%M") + start = datetime.strptime(start_str, TIME_FORMAT) + end = datetime.strptime(end_str, TIME_FORMAT) return title, start, end, category @@ -30,16 +31,16 @@ def validate_appointment(json_data): def validate_category_types(category): - if category not in category_types: - raise ValueError(f"Invalid category. Must be one of {category_types}") + if category not in CATEGORY_TYPES: + raise ValueError(f"Invalid category. Must be one of {CATEGORY_TYPES}") def serialize_datetime_format(appt): return { "id": appt["id"], "title": appt["title"], - "start": appt["start"].strftime("%Y-%m-%d %H:%M"), - "end": appt["end"].strftime("%Y-%m-%d %H:%M"), + "start": appt["start"].strftime(TIME_FORMAT), + "end": appt["end"].strftime(TIME_FORMAT), "category": appt["category"] } @@ -115,25 +116,35 @@ def delete_appointment(appt_id): @app.route("/appointments/shift/", methods = ["POST"]) def shift_appointment(appt_id): - amount_str = request.args.get("amount") + amount_start = request.args.get("amount_start", "0") + amount_end = request.args.get("amount_end", "0") try: - amount = float(amount_str) + shift_st = timedelta(days = float(amount_start)) + shift_end = timedelta(days = float(amount_end)) except ValueError: return jsonify({"error": "Invalid amount. Must be a number."}), 400 - shift = timedelta(days = amount) - for appt in appointments: if appt["id"] == appt_id: - appt["start"] = appt["start"] + shift - appt["end"] = appt["end"] + shift + new_start = appt["start"] + shift_st + new_end = appt["end"] + shift_end + + if new_start > new_end: + return jsonify({"error": "Shift would result in start after end"}), 400 + + for other in appointments: + if other["id"] != appt_id and new_end > other["start"] and new_start < other["end"]: + return jsonify({"error": "Shift would cause overlapping appointment"}), 409 + + appt["start"] = new_start + appt["end"] = new_end return jsonify(serialize_datetime_format(appt)), 200 return jsonify({"error": "Appointment not found"}), 404 -app.config['JSON_SORT_KEYS'] = False +app.config['app.json.sort_keys'] = False if __name__ == "__main__": # pragma: no coverage app.run(debug = True) diff --git a/test_api.py b/test_api.py index e79e237..377d940 100644 --- a/test_api.py +++ b/test_api.py @@ -1,7 +1,7 @@ import unittest from datetime import datetime -from api import app, appointments, extract_and_validate_data_fields, validate_category_types, category_types +from api import app, appointments, extract_and_validate_data_fields, validate_category_types, CATEGORY_TYPES class TestApi(unittest.TestCase): @@ -110,11 +110,14 @@ def test_invalid_appointments(self): categorys = {"title", "start", "end", "category"} fehlerhafte_termine = [ {}, + {"category": "1"}, {"title": "1"}, {"title": "1", "end": "1"}, {"start": "1", "end": "1"}, {"title": "1", "start": "1"}, - {"title": "1", "start": "1", "end": "1", "geheim": "1"} + {"title": "1", "start": "1", "category": "1"}, + {"title": "1", "start": "1", "end": "1", "geheim": "1"}, + {"title": "1", "start": "1", "end": "1", "geheim": "1", "category": "1"} ] for json_data in fehlerhafte_termine: @@ -177,7 +180,7 @@ def test_no_valid_category_type(self): category = {"category": "abc"} with self.assertRaises(ValueError) as contextManager: validate_category_types(category) - self.assertEqual(contextManager.exception.args[0], f"Invalid category. Must be one of {category_types}") + self.assertEqual(contextManager.exception.args[0], f"Invalid category. Must be one of {CATEGORY_TYPES}") def test_no_appointments_for_category(self): appointments.append({"title": "Meeting", "start": "10:00", "end": "12:00", "category": "general"}) @@ -189,9 +192,12 @@ def test_no_appointments_for_category(self): def test_shift_appointments_various_amounts(self): test_cases = [ - {"amount": 1,"expected_start": datetime(2025, 1, 1, 10, 0),"expected_end": datetime(2025, 1, 2, 11, 0)}, - {"amount": -1.5,"expected_start": datetime(2024, 12, 29, 22, 0),"expected_end": datetime(2024, 12, 30, 23, 0)}, - { "amount": 2.3,"expected_start": datetime(2025, 1, 2, 17, 12),"expected_end": datetime(2025, 1, 3, 18, 12)}, + {"amount_start": 1, "amount_end": 1, "expected_start": datetime(2025, 1, 1, 10, 0), + "expected_end": datetime(2025, 1, 2, 11, 0)}, + {"amount_start": -1.5, "amount_end": -1.5, "expected_start": datetime(2024, 12, 29, 22, 0), + "expected_end": datetime(2024, 12, 30, 23, 0)}, + {"amount_start": 2.3, "amount_end": 2.3, "expected_start": datetime(2025, 1, 2, 17, 12), + "expected_end": datetime(2025, 1, 3, 18, 12)}, ] for case in test_cases: @@ -204,7 +210,9 @@ def test_shift_appointments_various_amounts(self): "category": "work" }) - response = self.client.post(f"/appointments/shift/1?amount={case['amount']}") + response = self.client.post( + f"/appointments/shift/1?amount_start={case['amount_start']}&amount_end={case['amount_end']}" + ) self.assertEqual(response.status_code, 200) self.assertEqual(appointments[0]["id"], 1) @@ -227,6 +235,67 @@ def test_shift_appointment_false_amount(self): "category": "work" }) - response = self.client.post("/appointments/shift/1?amount=xx") + response = self.client.post("/appointments/shift/1?amount_start=xx") self.assertIn("error", response.json) self.assertEqual(response.json["error"], "Invalid amount. Must be a number.") + + def test_end_shift(self): + appointments.append({ + "id": 1, + "title": "Test Meeting", + "start": datetime(2024, 12, 31, 10, 0), + "end": datetime(2025, 1, 1, 11, 0), + "category": "work" + }) + response = self.client.post("/appointments/shift/1?amount_end=3") + self.assertEqual(response.status_code, 200) + self.assertEqual(appointments[0]["id"], 1) + self.assertEqual(appointments[0]["start"], datetime(2024, 12, 31, 10, 0)) + self.assertEqual(appointments[0]["end"], datetime(2025, 1, 4, 11, 0)) + + def test_start_and_end_shift(self): + appointments.append({ + "id": 1, + "title": "Test Meeting", + "start": datetime(2024, 12, 31, 10, 0), + "end": datetime(2025, 1, 1, 11, 0), + "category": "work" + }) + response = self.client.post("/appointments/shift/1?amount_start=2.5&amount_end=3") + self.assertEqual(response.status_code, 200) + self.assertEqual(appointments[0]["id"], 1) + self.assertEqual(appointments[0]["start"], datetime(2025, 1, 2, 22, 0)) + self.assertEqual(appointments[0]["end"], datetime(2025, 1, 4, 11, 0)) + + def test_end_before_start(self): + appointments.append({ + "id": 1, + "title": "Test Meeting", + "start": datetime(2023, 1, 1, 10, 0), + "end": datetime(2023, 1, 1, 11, 0), + "category": "work" + }) + response = self.client.post("/appointments/shift/1?amount_start=5&amount_end=1") + self.assertEqual(response.status_code, 400) + self.assertIn("error", response.json) + self.assertEqual(response.json["error"], "Shift would result in start after end") + + def test_shift_overlapping_appointment(self): + appointments.append({ + "id": 1, + "title": "Meeting 1", + "start": datetime(2025, 9, 26, 10, 0), + "end": datetime(2025, 9, 26, 12, 0), + "category": "work" + }) + appointments.append({ + "id": 2, + "title": "Meeting 2", + "start": datetime(2025, 9, 26, 13, 0), + "end": datetime(2025, 9, 26, 15, 0), + "category": "work" + }) + + response = self.client.post("/appointments/shift/2?amount_start=-3&amount_end=0") + self.assertEqual(response.status_code, 409) + self.assertEqual(response.json["error"], "Shift would cause overlapping appointment") From e1659df4aa835288be8a2051fd2b4a59ca08b265 Mon Sep 17 00:00:00 2001 From: kl Date: Tue, 7 Oct 2025 11:09:24 +0200 Subject: [PATCH 5/5] =?UTF-8?q?api.py=20Verbesserung=20der=20Fehlerbehandl?= =?UTF-8?q?ung=20-=20Umgang=20mit=20TIME=5FFORMAT=20=3D=20"%Y-%m-%d=20%H:%?= =?UTF-8?q?M"=20besser=20behandelt=20-=20sowie=20Fehlerf=C3=A4lle=20bei=20?= =?UTF-8?q?POST=20und=20PUT=20abgefangen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api.py | 12 +++++++++--- test_api.py | 26 +++++++++++++++++++------- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/api.py b/api.py index 7e89c37..787fc6f 100644 --- a/api.py +++ b/api.py @@ -26,7 +26,7 @@ def extract_and_validate_data_fields(json_data): def validate_appointment(json_data): - if {"title", "start", "end", "category"} != set(json_data.keys()): + if ["category", "end", "start", "title"] != sorted(json_data.keys()): raise ValueError("Invalid appointment: wrong or missing fields") @@ -67,7 +67,10 @@ def list_appointments(): def create_appointment(): global next_id data = request.get_json() - title, start, end, category = extract_and_validate_data_fields(data) + try: + title, start, end, category = extract_and_validate_data_fields(data) + except Exception as e: + return jsonify({"error": str(e)}), 400 for appointment in appointments: if end >= appointment["start"] and start <= appointment["end"]: @@ -88,7 +91,10 @@ def create_appointment(): @app.route("/appointments/", methods = ["PUT"]) def update_appointment(appt_id): data = request.get_json() - title, start, end, category = extract_and_validate_data_fields(data) + try: + title, start, end, category = extract_and_validate_data_fields(data) + except Exception as e: + return jsonify({"error": str(e)}), 400 for appt in appointments: if appt["id"] != appt_id and end >= appt["start"] and start <= appt["end"]: diff --git a/test_api.py b/test_api.py index 377d940..df0a4ea 100644 --- a/test_api.py +++ b/test_api.py @@ -1,7 +1,9 @@ import unittest from datetime import datetime +from unittest.mock import patch -from api import app, appointments, extract_and_validate_data_fields, validate_category_types, CATEGORY_TYPES +from api import app, appointments, extract_and_validate_data_fields, validate_category_types, CATEGORY_TYPES, \ + validate_appointment class TestApi(unittest.TestCase): @@ -15,7 +17,8 @@ def test_list_appointments_empty(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.json, []) - def test_list_appointments_with_entries(self): + @patch("api.extract_and_validate_data_fields", wraps = extract_and_validate_data_fields) + def test_list_appointments_with_entries(self, mock_extract): self.client.post("/appointments", json = {"title": "Meeting", "start": "2025-09-26 10:00", "end": "2025-09-26 12:00", "category": "general"}) @@ -27,6 +30,7 @@ def test_list_appointments_with_entries(self): self.assertEqual(response.status_code, 200) self.assertEqual(len(appointments), 2) + mock_extract.assert_called() def test_create_appointment(self): response = self.client.post("/appointments", @@ -50,7 +54,8 @@ def test_create_overlapping_appointment(self): self.assertIn("error", response.json) self.assertEqual(response.json["error"], "Overlapping appointment") - def test_update_appointment(self): + @patch("api.extract_and_validate_data_fields", wraps = extract_and_validate_data_fields) + def test_update_appointment(self, mock_extract): self.client.post("/appointments", json = {"title": "Meeting", "start": "2025-09-26 10:00", "end": "2025-09-26 12:00", "category": "general"}) @@ -63,6 +68,7 @@ def test_update_appointment(self): self.assertEqual(appointments[0]["start"], datetime(2025, 9, 26, 13, 0)) self.assertEqual(appointments[0]["end"], datetime(2025, 9, 26, 15, 0)) self.assertEqual(appointments[0]["category"], "general") + mock_extract.assert_called() def test_update_overlapping_appointment(self): self.client.post("/appointments", @@ -107,7 +113,6 @@ def test_delete_appointment_not_found(self): self.assertEqual(len(appointments), 1) def test_invalid_appointments(self): - categorys = {"title", "start", "end", "category"} fehlerhafte_termine = [ {}, {"category": "1"}, @@ -119,19 +124,26 @@ def test_invalid_appointments(self): {"title": "1", "start": "1", "end": "1", "geheim": "1"}, {"title": "1", "start": "1", "end": "1", "geheim": "1", "category": "1"} ] - for json_data in fehlerhafte_termine: - self.assertNotEqual(categorys, set(json_data.keys())) + with self.assertRaises(ValueError): + validate_appointment(json_data) def test_valid_appointment(self): json_data = {"title": "Meeting", "start": "2025-09-26 10:00", "end": "2025-09-26 12:00", "category": "general"} - self.assertEqual({"title", "start", "end", "category"}, set(json_data.keys())) + validate_appointment(json_data) def test_extract_fields_with_valid_appointment(self): json_data = {"title": "Meeting", "start": "2025-09-26 10:00", "end": "2025-09-26 12:00", "category": "general"} result = extract_and_validate_data_fields(json_data) self.assertEqual(result, ("Meeting", datetime(2025, 9, 26, 10, 0), datetime(2025, 9, 26, 12, 0), "general")) + def test_extract_fields_with_invalid_datetime_format(self): + json_data = { + "title": "Meeting", "start": "2025-09-26 10:00", "end": "2025-0a-26 12:00", "category": "general"} + + with self.assertRaises(ValueError): + extract_and_validate_data_fields(json_data) + def test_extract_fields_with_invalid_appointment(self): with self.assertRaises(ValueError) as contextManager: extract_and_validate_data_fields({})