Here’s a detailed commit message based on the provided diff:
Commit Message: feat: Implement web calendar application with Flask, Docker, and calendar integration Description: 1. Server Refactor: • Moved application logic from main.py to a structured directory server/app/. • Added server/app/__init__.py for Flask app initialization. • Introduced server/app/views.py to handle routes (dashboard and dashboard_image). • Created server/app/models.py for event modeling, supporting CalDAV and iCalendar events. • Added server/app/weather.py to fetch weather data using OpenWeatherMap API. 2. New Features: • Added an image generation route (/image) to render calendar views as BMP images. • Integrated OpenWeatherMap API for weather data on the dashboard. 3. Environment and Configurations: • Added a Dockerfile to build and deploy the app using uwsgi-nginx-flask. • Introduced compose.yml for running the app with Docker Compose. • Moved uwsgi.ini configuration to server/uwsgi.ini for modular organization. 4. Dependencies: • Updated requirements.txt to include new dependencies: imgkit, pillow, and Werkzeug==2.2.2. 5. Static Assets: • Added placeholder images out.png and test.png. 6. Code Cleanup: • Removed old files (main.py and root-level uwsgi.ini). • Updated .gitignore to include .idea/ folder. Additional Notes: • Enhanced event parsing to handle all-day and time-specific events using server/app/models.py. • Utilized Flask’s render_template for dynamic HTML rendering and imgkit for HTML-to-image conversion. • Integrated multiple calendar sources (CalDAV and public iCal feeds). Let me know if you need further adjustments!
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
from flask import Flask
|
||||
app = Flask(__name__)
|
||||
from app import views
|
||||
@@ -1,98 +0,0 @@
|
||||
from datetime import datetime, date, timedelta, timezone
|
||||
from caldav.objects import Event as CaldavEvent
|
||||
import dataclasses
|
||||
|
||||
from icalendar import Event as IcEvent
|
||||
from icalendar import Calendar as Icalendar
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Event():
|
||||
summary: str
|
||||
dtstart: datetime
|
||||
dtend: datetime
|
||||
is_all_day: bool
|
||||
|
||||
@classmethod
|
||||
def fromCalDavEvents(cls, caldav_events: [CaldavEvent]) -> list['Event']:
|
||||
|
||||
events = []
|
||||
|
||||
for event in caldav_events:
|
||||
date_value = event.vobject_instance.vevent.dtstart.value
|
||||
if isinstance(date_value, datetime):
|
||||
dt_start = date_value
|
||||
is_all_day = False
|
||||
elif isinstance(date_value, date):
|
||||
d_start = date_value
|
||||
dt_start = datetime(d_start.year, d_start.month, d_start.day, tzinfo=datetime.now(timezone.utc).astimezone().tzinfo)
|
||||
is_all_day = True
|
||||
else:
|
||||
raise Exception
|
||||
|
||||
date_value = event.vobject_instance.vevent.dtend.value
|
||||
if isinstance(date_value, datetime):
|
||||
dt_end = date_value
|
||||
elif isinstance(date_value, date):
|
||||
d_end = date_value - timedelta(days=1)
|
||||
dt_end = datetime(d_end.year, d_end.month, d_end.day,tzinfo=datetime.now(timezone.utc).astimezone().tzinfo)
|
||||
else:
|
||||
raise Exception
|
||||
|
||||
events.append(Event(
|
||||
summary=event.vobject_instance.vevent.summary.value,
|
||||
dtstart=dt_start,
|
||||
dtend=dt_end,
|
||||
is_all_day = is_all_day
|
||||
))
|
||||
return events
|
||||
|
||||
@classmethod
|
||||
def fromIcalendar(cls, icalendar: Icalendar) -> list['Event']:
|
||||
events = []
|
||||
for event in [e for e in icalendar.subcomponents if isinstance(e, IcEvent)]:
|
||||
date_value = event['DTSTART'].dt
|
||||
if isinstance(date_value, datetime):
|
||||
dt_start = date_value
|
||||
is_all_day = False
|
||||
elif isinstance(date_value, date):
|
||||
d_start = date_value
|
||||
dt_start = datetime(d_start.year, d_start.month, d_start.day, tzinfo=datetime.now(timezone.utc).astimezone().tzinfo)
|
||||
is_all_day = True
|
||||
else:
|
||||
raise Exception
|
||||
|
||||
date_value = event['DTEND'].dt if event.get('DTEND') else event['DTSTART'].dt
|
||||
if isinstance(date_value, datetime):
|
||||
dt_end = date_value
|
||||
elif isinstance(date_value, date):
|
||||
d_end = date_value
|
||||
dt_end = datetime(d_end.year, d_end.month, d_end.day,tzinfo=datetime.now(timezone.utc).astimezone().tzinfo)
|
||||
else:
|
||||
raise Exception
|
||||
|
||||
events.append(Event(
|
||||
summary=event['summary'],
|
||||
dtstart=dt_start,
|
||||
dtend=dt_end,
|
||||
is_all_day = is_all_day
|
||||
))
|
||||
|
||||
return events
|
||||
|
||||
@property
|
||||
def range_str(self) -> str:
|
||||
start_time_str = self.dtstart.strftime('%-I')
|
||||
end_time_str = self.dtend.strftime('%-I')
|
||||
|
||||
if not(self.dtstart.hour and self.dtend.hour):
|
||||
return ""
|
||||
|
||||
if self.dtstart.minute: start_time_str += self.dtstart.strftime(':%M')
|
||||
if self.dtend.minute: end_time_str += self.dtend.strftime(':%M')
|
||||
|
||||
if not ((self.dtstart.hour < 12 and self.dtend.hour < 12) or (self.dtstart.hour > 12 and self.dtend.hour > 12)):
|
||||
start_time_str += self.dtstart.strftime("%p")
|
||||
|
||||
end_time_str += self.dtend.strftime("%p")
|
||||
|
||||
return f"{start_time_str}-{end_time_str}"
|
||||
@@ -1,95 +0,0 @@
|
||||
@import url('fonts/dinpro/dinpro.css');
|
||||
@import url('fonts/noto-emoji.css');
|
||||
@import url('fonts/openmoji.css');
|
||||
@import url('fonts/weather-icons/weather-icons.css');
|
||||
|
||||
@font-face {
|
||||
font-family: "Hellovetica";
|
||||
src: url("fonts/hellovetica.ttf") format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Helvetica";
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
font-family: "Helvetica", sans-serif;
|
||||
height: 699px;
|
||||
width: 600px;
|
||||
text-align: left;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
position: relative;
|
||||
height: 50%;
|
||||
verical-align: top;
|
||||
/* height: 250px; */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.panel.top {
|
||||
height: 35%;
|
||||
}
|
||||
|
||||
.panel.bottom {
|
||||
height: 65%;
|
||||
}
|
||||
|
||||
.subpanel {
|
||||
position: relative;
|
||||
height:100%;
|
||||
width:48%;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.weather-icon {
|
||||
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: scale-down;
|
||||
}
|
||||
|
||||
.week {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.day {
|
||||
border-style: solid;
|
||||
display: inline-block;
|
||||
min-height: 220px;
|
||||
max-height: 220px;
|
||||
width: 135px;
|
||||
vertical-align: top;
|
||||
margin: 1px;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.day-title{
|
||||
background-color: black;
|
||||
text-align: center;
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
|
||||
}
|
||||
|
||||
.event {
|
||||
margin: 3pt;
|
||||
background-color: white;
|
||||
border-style: solid;
|
||||
border-radius: 5px;
|
||||
padding: 2px;
|
||||
font-family: "Helvetica";
|
||||
}
|
||||
|
||||
.daterange {
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.weather-icon {
|
||||
font-size: 200px;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Dashboard ({{today.strftime('%-m/%-d, %-H:%M')}})</title>
|
||||
<link rel="stylesheet" href="static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="dashboard">
|
||||
<div class="panel top">
|
||||
<div class="subpanel">
|
||||
<div><i class="wi wi-owm-{{ weather.id }} weather-icon"></i></div>
|
||||
<div>{{weather.main}}</div>
|
||||
</div>
|
||||
<div class="subpanel" style="font-size: 60px;font-weight: bold;vertical-align:top; text-align:right;">
|
||||
{{ today.strftime('%a') }}<br>
|
||||
{{ today.strftime('%b %-d') }}<br>
|
||||
{{ today.strftime('%Y') }}<br>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel bottom week">
|
||||
|
||||
{% for day, events in days %}
|
||||
<div class="day">
|
||||
<div id="dotw-1" class="day-title">
|
||||
{{ day.strftime('%A') }}
|
||||
</div>
|
||||
{% for event in events %}
|
||||
<div class="event">
|
||||
{{ event.summary }}<br>
|
||||
{% if not event.is_all_day %}
|
||||
<span class="daterange">{{ event.range_str }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
96
app/views.py
96
app/views.py
@@ -1,96 +0,0 @@
|
||||
from app import app
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import os
|
||||
import caldav
|
||||
import datetime
|
||||
from icalendar import cal, Event
|
||||
from flask import render_template
|
||||
from .models import Event
|
||||
import requests
|
||||
caldav_url = os.getenv('caldav_url')
|
||||
username = os.getenv('username')
|
||||
password = os.getenv('password')
|
||||
cal_id = os.getenv('cal_id')
|
||||
from .weather import weather
|
||||
|
||||
def remove_emoji(string):
|
||||
import re
|
||||
emoji_pattern = re.compile("["
|
||||
u"\U0001F600-\U0001F64F" # emoticons
|
||||
u"\U0001F300-\U0001F5FF" # symbols & pictographs
|
||||
u"\U0001F680-\U0001F6FF" # transport & map symbols
|
||||
u"\U0001F1E0-\U0001F1FF" # flags (iOS)
|
||||
u"\U00002500-\U00002BEF" # chinese char
|
||||
u"\U00002702-\U000027B0"
|
||||
u"\U00002702-\U000027B0"
|
||||
u"\U000024C2-\U0001F251"
|
||||
u"\U0001f926-\U0001f937"
|
||||
u"\U00010000-\U0010ffff"
|
||||
u"\u2640-\u2642"
|
||||
u"\u2600-\u2B55"
|
||||
u"\u200d"
|
||||
u"\u23cf"
|
||||
u"\u23e9"
|
||||
u"\u231a"
|
||||
u"\ufe0f" # dingbats
|
||||
u"\u3030"
|
||||
"]+", flags=re.UNICODE)
|
||||
return emoji_pattern.sub(r'', string)
|
||||
|
||||
def daterange(start_date, end_date):
|
||||
for n in range(int((end_date - start_date).days)):
|
||||
yield datetime.datetime.date(start_date + timedelta(n))
|
||||
|
||||
@app.route('/')
|
||||
def dashboard():
|
||||
today = datetime.datetime.now(tz=datetime.datetime.now(timezone.utc).astimezone().tzinfo)
|
||||
# today = datetime.datetime(2022,6,5, tzinfo=datetime.datetime.now(timezone.utc).astimezone().tzinfo)
|
||||
start_of_week = today - timedelta(days=(today.weekday()+1)) # Monday
|
||||
end_of_week = start_of_week + timedelta(days=8) # Sunday
|
||||
|
||||
events = []
|
||||
|
||||
for url in [
|
||||
'https://www.calendarlabs.com/ical-calendar/ics/76/US_Holidays.ics',
|
||||
]:
|
||||
r = requests.get(url)
|
||||
c = cal.Calendar.from_ical(r.content)
|
||||
|
||||
events += Event.fromIcalendar(c)
|
||||
|
||||
with caldav.DAVClient(url=caldav_url, username=username, password=password) as client:
|
||||
my_principal = client.principal()
|
||||
|
||||
calendars = my_principal.calendars()
|
||||
for id in [cal_id]:
|
||||
calendar = my_principal.calendar(cal_id=id)
|
||||
events += Event.fromCalDavEvents(calendar.date_search(
|
||||
start=start_of_week, end=end_of_week, expand=False))
|
||||
|
||||
for url in [
|
||||
'http://ical-cdn.teamsnap.com/team_schedule/5f1ddc9e-15b0-4912-84a2-11cc70e9e375.ics'
|
||||
]:
|
||||
r = requests.get(url)
|
||||
c = cal.Calendar.from_ical(r.content)
|
||||
|
||||
events += Event.fromIcalendar(c)
|
||||
|
||||
days = []
|
||||
for single_date in daterange(start_of_week, end_of_week):
|
||||
days_events = []
|
||||
for e in events:
|
||||
if (e.dtstart.date() <= single_date <= e.dtend.date()):
|
||||
e.summary = remove_emoji(e.summary)
|
||||
days_events.append(e)
|
||||
|
||||
days.append((single_date, days_events))
|
||||
|
||||
# breakpoint()
|
||||
pass
|
||||
# r = "<br>".join([event.vobject_instance.vevent.summary.value for event in events_fetched if event.vobject_instance.vevent.dtstart.value < datetime.now()])
|
||||
return render_template("dashboard.html",
|
||||
days=days,
|
||||
today=today,
|
||||
weather=weather()
|
||||
)
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import json
|
||||
import requests
|
||||
import os
|
||||
|
||||
#based on
|
||||
URL = "https://worldweather.wmo.int/en/json/{city_id}_en.json"
|
||||
URL = "https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={api_key}"
|
||||
|
||||
|
||||
def weather(api_key=os.getenv('openweathermap_api_key'), lat="41.98", lon="-87.90"):
|
||||
url = URL.format(api_key=api_key, lat=lat, lon=lon)
|
||||
data = json.loads(requests.get(url).content)
|
||||
|
||||
return data['weather'][0]
|
||||
|
||||
f=weather()
|
||||
pass
|
||||
Reference in New Issue
Block a user