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
.gitignore
vendored
1
.gitignore
vendored
@@ -58,4 +58,5 @@ ENV/
|
|||||||
.idea/**/uiDesigner.xml
|
.idea/**/uiDesigner.xml
|
||||||
.idea/**/gradle.xml
|
.idea/**/gradle.xml
|
||||||
.idea/**/libraries
|
.idea/**/libraries
|
||||||
|
.idea/**
|
||||||
*.iws /out/
|
*.iws /out/
|
||||||
12
Dockerfile
12
Dockerfile
@@ -1,9 +1,9 @@
|
|||||||
FROM tiangolo/uwsgi-nginx-flask
|
FROM tiangolo/uwsgi-nginx-flask
|
||||||
ENV STATIC_URL /static
|
# Install system dependencies and wkhtmltoimage
|
||||||
ENV STATIC_PATH /var/www/app/static
|
RUN apt-get update && apt-get install -y \
|
||||||
ENV cal_id 9E2AC562-4328-4CA0-B4D1-D730D9F5E9EF
|
wkhtmltopdf \
|
||||||
ENV username a@correa.co
|
&& apt-get clean \
|
||||||
ENV password jyxq-avwz-qxfd-dklo
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
ENV caldav_url https://caldav.icloud.com/
|
|
||||||
COPY ./requirements.txt /var/www/requirements.txt
|
COPY ./requirements.txt /var/www/requirements.txt
|
||||||
|
COPY ./server ./app
|
||||||
RUN pip install -r /var/www/requirements.txt
|
RUN pip install -r /var/www/requirements.txt
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from flask import Flask
|
|
||||||
app = Flask(__name__)
|
|
||||||
from app import views
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
10
compose.yml
Normal file
10
compose.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
services:
|
||||||
|
webcal:
|
||||||
|
build: .
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- "56733:80"
|
||||||
|
volumes:
|
||||||
|
- ./server:/app
|
||||||
|
|
||||||
@@ -2,3 +2,6 @@ caldav~=0.9.0
|
|||||||
requests~=2.27.1
|
requests~=2.27.1
|
||||||
icalendar~=4.0.9
|
icalendar~=4.0.9
|
||||||
Flask~=2.1.2
|
Flask~=2.1.2
|
||||||
|
Werkzeug==2.2.2
|
||||||
|
imgkit
|
||||||
|
pillow
|
||||||
8
server/app/__init__.py
Normal file
8
server/app/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# app/__init__.py
|
||||||
|
from flask import Flask
|
||||||
|
|
||||||
|
# Create Flask app
|
||||||
|
app = Flask(__name__,static_url_path='', static_folder='static')
|
||||||
|
|
||||||
|
# Import views to register routes
|
||||||
|
from app import views
|
||||||
0
server/app/main.py
Normal file
0
server/app/main.py
Normal file
@@ -2,10 +2,12 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Dashboard ({{today.strftime('%-m/%-d, %-H:%M')}})</title>
|
<title>Dashboard ({{today.strftime('%-m/%-d, %-H:%M')}})</title>
|
||||||
<link rel="stylesheet" href="static/style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<meta name="viewport" content="width=600, height=800" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="dashboard">
|
<div class="dashboard">
|
||||||
|
<div style="height:1em"></div>
|
||||||
<div class="panel top">
|
<div class="panel top">
|
||||||
<div class="subpanel">
|
<div class="subpanel">
|
||||||
<div><i class="wi wi-owm-{{ weather.id }} weather-icon"></i></div>
|
<div><i class="wi wi-owm-{{ weather.id }} weather-icon"></i></div>
|
||||||
42
server/app/templates/dashboard_static.html
Normal file
42
server/app/templates/dashboard_static.html
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Dashboard ({{today.strftime('%-m/%-d, %-H:%M')}})</title>
|
||||||
|
<link rel="stylesheet" href="/style.css">
|
||||||
|
<meta name="viewport" content="width=600, height=800" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="dashboard">
|
||||||
|
<div style="height:1em"></div>
|
||||||
|
<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>
|
||||||
@@ -4,9 +4,12 @@ import os
|
|||||||
import caldav
|
import caldav
|
||||||
import datetime
|
import datetime
|
||||||
from icalendar import cal, Event
|
from icalendar import cal, Event
|
||||||
from flask import render_template
|
from flask import render_template, url_for, send_file, send_from_directory
|
||||||
from .models import Event
|
from .models import Event
|
||||||
import requests
|
import requests
|
||||||
|
import imgkit
|
||||||
|
from PIL import Image, ImageOps
|
||||||
|
import io
|
||||||
caldav_url = os.getenv('caldav_url')
|
caldav_url = os.getenv('caldav_url')
|
||||||
username = os.getenv('username')
|
username = os.getenv('username')
|
||||||
password = os.getenv('password')
|
password = os.getenv('password')
|
||||||
@@ -88,9 +91,38 @@ def dashboard():
|
|||||||
# breakpoint()
|
# breakpoint()
|
||||||
pass
|
pass
|
||||||
# r = "<br>".join([event.vobject_instance.vevent.summary.value for event in events_fetched if event.vobject_instance.vevent.dtstart.value < datetime.now()])
|
# 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",
|
return render_template("dashboard_static.html",
|
||||||
days=days,
|
days=days,
|
||||||
today=today,
|
today=today,
|
||||||
weather=weather()
|
weather=weather()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@app.route('/image')
|
||||||
|
def dashboard_image():
|
||||||
|
page = dashboard()
|
||||||
|
out_file = os.path.join(os.path.dirname(__file__),
|
||||||
|
'static', "out.bmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
s = imgkit.from_string(
|
||||||
|
page,
|
||||||
|
out_file,
|
||||||
|
options={
|
||||||
|
|
||||||
|
'width':600,
|
||||||
|
'height':800,
|
||||||
|
"disable-smart-width": "",
|
||||||
|
'enable-local-file-access': "",
|
||||||
|
'allow': os.path.join(os.path.dirname(__file__),
|
||||||
|
'static',
|
||||||
|
)
|
||||||
|
},
|
||||||
|
css=os.path.join(os.path.dirname(__file__),
|
||||||
|
'static',
|
||||||
|
'style.css'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
image_file = Image.open(out_file) # open colour image
|
||||||
|
image_file = ImageOps.grayscale(image_file)
|
||||||
|
image_file.save(out_file, 'BMP')
|
||||||
|
return send_from_directory(directory = 'static', path='out.bmp', as_attachment=False, attachment_filename='out.bmp')
|
||||||
@@ -10,8 +10,7 @@ URL = "https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid
|
|||||||
def weather(api_key=os.getenv('openweathermap_api_key'), lat="41.98", lon="-87.90"):
|
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)
|
url = URL.format(api_key=api_key, lat=lat, lon=lon)
|
||||||
data = json.loads(requests.get(url).content)
|
data = json.loads(requests.get(url).content)
|
||||||
|
|
||||||
return data['weather'][0]
|
return data['weather'][0]
|
||||||
|
|
||||||
f=weather()
|
# f=weather()
|
||||||
pass
|
pass
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
[uwsgi]
|
[uwsgi]
|
||||||
module = main
|
module = app
|
||||||
callable = app
|
callable = app
|
||||||
master = true
|
master = true
|
||||||
Reference in New Issue
Block a user