Skip to content

Commit 7f0ad2f

Browse files
authored
Merge pull request #1 from mschmid09/copilot/edit-flight-timing-feature
Add flight time editing and manual entry with yyyy-mm-dd date format
2 parents e1b50e9 + ec6e89e commit 7f0ad2f

12 files changed

Lines changed: 2149 additions & 25 deletions

File tree

.github/workflows/tests.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [ main, develop ]
6+
pull_request:
7+
branches: [ main, develop ]
8+
9+
jobs:
10+
code-quality:
11+
name: Lint
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- name: Install uv
18+
uses: astral-sh/setup-uv@v4
19+
with:
20+
version: "latest"
21+
22+
- name: Set up Python
23+
run: uv python install
24+
25+
- name: Install dependencies
26+
run: uv sync
27+
28+
- name: Run linter
29+
run: uv run ruff check .
30+
31+
- name: Run formatter check
32+
run: uv run ruff format --check .
33+
34+
test:
35+
name: Test
36+
runs-on: ubuntu-latest
37+
38+
steps:
39+
- uses: actions/checkout@v4
40+
41+
- name: Install uv
42+
uses: astral-sh/setup-uv@v4
43+
with:
44+
version: "latest"
45+
46+
- name: Set up Python
47+
run: uv python install
48+
49+
- name: Install dependencies
50+
run: uv sync
51+
52+
- name: Run tests
53+
run: uv run pytest tests/ -v

Makefile

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1-
format: ## Run black and isort
2-
black .
3-
isort .
1+
install: ## Install dependencies using uv
2+
uv sync
3+
4+
test: ## Run tests
5+
uv run pytest tests/ -v
6+
7+
lint: ## Run ruff linter
8+
uv run ruff check .
9+
10+
format: ## Run ruff formatter and linter
11+
uv run ruff check --fix .
12+
uv run ruff format .
413

514
update_reqs: ## Update requirements.txt
615
pipreqs --force .

app.py

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import json
21
import os
2+
import re
33

44
import pandas as pd
5+
from datetime import datetime
56
from flask import Flask, render_template, request, send_file, session
67

7-
from core import get_flight, make_ics_from_selected_df_index
8+
from core import get_flight, make_ics_from_selected_df_index, make_ics_from_manual_data
89

910
app = Flask(__name__)
1011

@@ -42,11 +43,83 @@ def create_ical_from_selected(index):
4243
return "No flight data found", 400
4344

4445
df = pd.read_json(df_json, orient="split")
46+
47+
# Check if custom times were provided
48+
custom_departure = request.form.get("custom_departure")
49+
custom_arrival = request.form.get("custom_arrival")
50+
51+
if custom_departure and custom_arrival:
52+
# Update the DataFrame with custom times
53+
df.at[index, "scheduled_departure"] = custom_departure
54+
df.at[index, "scheduled_arrival"] = custom_arrival
55+
4556
ics_data = make_ics_from_selected_df_index(df, index)
4657
flight = df.iloc[index]["flight_number"]
4758

4859
return send_file(ics_data, as_attachment=True, download_name=f"{flight}.ics")
4960

5061

62+
@app.route("/manual_entry")
63+
def manual_entry():
64+
return render_template("manual_entry.html")
65+
66+
67+
@app.route("/create_manual_event", methods=["POST"])
68+
def create_manual_event():
69+
try:
70+
# Get all form data
71+
flight_data = {
72+
"flight_number": request.form.get("flight_number"),
73+
"airline_name": request.form.get("airline_name"),
74+
"origin_airport": request.form.get("origin_airport"),
75+
"origin_airport_code": request.form.get("origin_airport_code"),
76+
"destination_airport": request.form.get("destination_airport"),
77+
"destination_airport_code": request.form.get("destination_airport_code"),
78+
"scheduled_departure": request.form.get("scheduled_departure"),
79+
"scheduled_arrival": request.form.get("scheduled_arrival"),
80+
"origin_timezone": request.form.get("origin_timezone"),
81+
"destination_timezone": request.form.get("destination_timezone"),
82+
}
83+
84+
# Validate required fields
85+
required_fields = [
86+
"flight_number",
87+
"airline_name",
88+
"origin_airport",
89+
"origin_airport_code",
90+
"destination_airport",
91+
"destination_airport_code",
92+
"scheduled_departure",
93+
"scheduled_arrival",
94+
"origin_timezone",
95+
"destination_timezone",
96+
]
97+
for field in required_fields:
98+
if not flight_data.get(field):
99+
raise ValueError(f"Missing required field: {field}")
100+
101+
# Validate airport codes (should be 3 uppercase letters)
102+
if not re.match(r"^[A-Z]{3}$", flight_data["origin_airport_code"]):
103+
raise ValueError("Origin airport code must be 3 uppercase letters")
104+
if not re.match(r"^[A-Z]{3}$", flight_data["destination_airport_code"]):
105+
raise ValueError("Destination airport code must be 3 uppercase letters")
106+
107+
# Validate datetime format (YYYY-MM-DD HH:MM)
108+
try:
109+
datetime.strptime(flight_data["scheduled_departure"], "%Y-%m-%d %H:%M")
110+
datetime.strptime(flight_data["scheduled_arrival"], "%Y-%m-%d %H:%M")
111+
except ValueError:
112+
raise ValueError("Invalid datetime format. Use: yyyy-mm-dd hh:mm")
113+
114+
# Create iCal file from manual data
115+
ics_data = make_ics_from_manual_data(flight_data)
116+
flight = flight_data["flight_number"]
117+
118+
return send_file(ics_data, as_attachment=True, download_name=f"{flight}.ics")
119+
except Exception as e:
120+
error_message = str(e)
121+
return render_template("manual_entry.html", error=error_message)
122+
123+
51124
if __name__ == "__main__":
52125
app.run()

core.py

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import io
22
import re
3-
from datetime import datetime, timedelta
3+
from datetime import datetime, timedelta, timezone as dt_timezone
44

55
import icalendar
66
import pandas as pd
@@ -207,8 +207,8 @@ def make_ical_event(data: dict):
207207
event = icalendar.Event()
208208
event.add(
209209
"summary",
210-
f'🛫 {data["airline_name"]} {data["origin_airport_code"]} ➡️ '
211-
f'{data["destination_airport_code"]} {data["flight_number"]}',
210+
f"🛫 {data['airline_name']} {data['origin_airport_code']} ➡️ "
211+
f"{data['destination_airport_code']} {data['flight_number']}",
212212
)
213213
origin_tz = timezone(data["origin_timezone"])
214214
destination_tz = timezone(data["destination_timezone"])
@@ -222,12 +222,12 @@ def make_ical_event(data: dict):
222222

223223
event.add("dtstart", dtstart)
224224
event.add("dtend", dtend)
225-
event.add("location", f'{data["origin_airport"]}')
225+
event.add("location", f"{data['origin_airport']}")
226226
event.add(
227227
"description",
228-
f'{data["airline_name"]} flight {data["flight_number"]} / Departs {data["origin_airport"]}, {data["origin_airport_code"]}',
228+
f"{data['airline_name']} flight {data['flight_number']} / Departs {data['origin_airport']}, {data['origin_airport_code']}",
229229
)
230-
event.add("dtstamp", datetime.now())
230+
event.add("dtstamp", datetime.now(dt_timezone.utc))
231231

232232
event.add("status", "CONFIRMED")
233233

@@ -238,3 +238,44 @@ def make_ical_event(data: dict):
238238
def save_ical_event(ical_event: bytes):
239239
ical_bytes = io.BytesIO(ical_event)
240240
return ical_bytes
241+
242+
243+
def make_ics_from_manual_data(data: dict):
244+
"""Create iCal event from manually entered data."""
245+
cal = icalendar.Calendar()
246+
cal.add("prodid", "-//eluceo/ical//2.0/EN")
247+
cal.add("version", "2.0")
248+
cal.add("calscale", "GREGORIAN")
249+
cal.add("method", "REQUEST")
250+
251+
event = icalendar.Event()
252+
event.add(
253+
"summary",
254+
f"🛫 {data['airline_name']} {data['origin_airport_code']} ➡️ "
255+
f"{data['destination_airport_code']} {data['flight_number']}",
256+
)
257+
258+
origin_tz = timezone(data["origin_timezone"])
259+
destination_tz = timezone(data["destination_timezone"])
260+
261+
# Parse datetime format "YYYY-MM-DD HH:MM" to datetime
262+
dtstart = origin_tz.localize(
263+
datetime.strptime(data["scheduled_departure"], "%Y-%m-%d %H:%M")
264+
)
265+
dtend = destination_tz.localize(
266+
datetime.strptime(data["scheduled_arrival"], "%Y-%m-%d %H:%M")
267+
)
268+
269+
event.add("dtstart", dtstart)
270+
event.add("dtend", dtend)
271+
event.add("location", f"{data['origin_airport']}")
272+
event.add(
273+
"description",
274+
f"{data['airline_name']} flight {data['flight_number']} / Departs {data['origin_airport']}, {data['origin_airport_code']}",
275+
)
276+
event.add("dtstamp", datetime.now(dt_timezone.utc))
277+
event.add("status", "CONFIRMED")
278+
279+
cal.add_component(event)
280+
ical_event = cal.to_ical()
281+
return save_ical_event(ical_event)

pyproject.toml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[project]
2+
name = "flightcal"
3+
version = "0.1.0"
4+
description = "Flight calendar event generator"
5+
requires-python = ">=3.9"
6+
dependencies = [
7+
"Flask==3.0.3",
8+
"icalendar==6.0.1",
9+
"pandas>=2.2.0",
10+
"pyflightdata==0.8.6.2",
11+
"pytz==2024.1",
12+
"gunicorn==23.0.0",
13+
"numpy>=1.26.0",
14+
]
15+
16+
[dependency-groups]
17+
dev = [
18+
"ruff>=0.8.0",
19+
"pytest>=8.0.0",
20+
"pytest-mock>=3.12.0",
21+
]

requirements.txt

Lines changed: 0 additions & 7 deletions
This file was deleted.

templates/index.html

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
13
<head>
24
<meta charset="UTF-8">
35
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -64,6 +66,20 @@
6466
input[type="submit"]:hover {
6567
background-color: #0056b3;
6668
}
69+
.manual-entry-button {
70+
margin-top: 15px;
71+
padding: 10px 20px;
72+
border: none;
73+
border-radius: 5px;
74+
background-color: #28a745;
75+
color: white;
76+
font-size: 16px;
77+
cursor: pointer;
78+
transition: background-color 0.3s ease;
79+
}
80+
.manual-entry-button:hover {
81+
background-color: #218838;
82+
}
6783
.emoji {
6884
font-size: 5rem;
6985
cursor: pointer;
@@ -95,6 +111,7 @@ <h1>Create Flight Calendar Event</h1>
95111
<div class="error-message">
96112
{{ error }}
97113
</div>
114+
<button class="manual-entry-button" onclick="window.location.href='/manual_entry'">Enter Flight Details Manually</button>
98115
{% endif %}
99116

100117
<p>Enter your flight number and date to create an iCal event.</p>
@@ -103,7 +120,7 @@ <h1>Create Flight Calendar Event</h1>
103120
<input type="text" id="flight_number" name="flight_number" required oninput="this.value = this.value.toUpperCase().replace(/[^A-Z0-9]/g, '')"><br><br>
104121

105122
<label for="flight_date">Flight Date:</label>
106-
<input type="date" id="flight_date" name="flight_date" required><br><br>
123+
<input type="text" id="flight_date" name="flight_date" required placeholder="yyyy-mm-dd" pattern="\d{4}-\d{2}-\d{2}" title="Date format: yyyy-mm-dd"><br><br>
107124

108125
<input type="submit" value="Create Event">
109126
</form>
@@ -113,8 +130,9 @@ <h1>Create Flight Calendar Event</h1>
113130
Made with ❤️ by <a href="https://mschm.id" target="_blank">Michael Schmid</a>
114131
</footer>
115132
<script>
116-
document.querySelector('.emoji').addEventListener('click', function() {
133+
document.querySelector('.emoji').addEventListener('click', function() {
117134
this.classList.toggle('clicked');
118135
});
119136
</script>
120-
</body>
137+
</body>
138+
</html>

0 commit comments

Comments
 (0)