This commit is contained in:
Yannik Schmidt
2020-07-26 22:50:45 +02:00
parent f21a979467
commit c9838954eb
21 changed files with 194 additions and 170 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
js/
__pycache__/
css/
*.swp
*.jpg

View File

@@ -1,50 +1,14 @@
# Requirements
This Softwares runs python3-flask with markdown, json and caldav.
python3 -m pip install flask, json, caldav, markdown2
python3 -m pip install flask, caldav, markdown2
This Software requires bootstrap > 4.13 which can be downloaded [here](https://getbootstrap.com/docs/4.3/getting-started/download/). It must be unpacked into the *static/*-directory into *js* and *css* respectively.
Additionally bootstrap depends on [jquery](https://code.jquery.com) which you have to download into a file called *jquery.min.js* in *static/js/*.
# Usage
./server.py -h
usage: server.py [-h] [-i INTERFACE] [-p PORT] --cal-info CAL_INFO
[--no-update-on-start]
optional arguments:
-h, --help show this help message and exit
-i INTERFACE Interface to listen on (default: 0.0.0.0)
-p PORT, --port PORT Port to listen on (default: 5000)
--cal-info CAL_INFO File Containing a public calendar link (default: None)
--no-update-on-start Don't update the calendar on start (default: False)
# Configuration
The page and most of it's content is configured via json. To use the CalDav-events section, you need to add a comma seperated file with the following format format/information:
URL,USER,PASSWORD
## Main Config
The main Config ``config.json`` which must be placed in the project-root must contain the following values:
{
"siteTitle" : "the default site title",
"siteDescription" : "a description for this site",
"siteLogo" : "url to logo",
"siteURL": "the url of this site"
}
Additionally it may contain the following information:
"teamspeak-server" : "TS_SERVER",
"discord-server" : "DISCORD_LINK",
"facebook" : "FACEBOOK_LINK",
"instagram" : "INSTAGRAM_LINK",
"twitter" : "TWITTER_LINK",
"twitch-channel" : "TWITCH_CHANNEL_NAME",
"twitch-placeholder-img" : "PLACEHOLDER_IMG"
The page and most of it's content is configured via json, basic configuration is done in *config.py*.
## Startpage Sections
### Events
@@ -108,4 +72,4 @@ New subpages must be added as a new location in the *server.py* like this:
def subpage():
return flask.render_template("subpage.html", conf=mainConfig)
See the example subpage-templates in *templates/*.
See the example *subpage\_example.html* in *templates/*.

4
app.py Normal file
View File

@@ -0,0 +1,4 @@
import server as moduleContainingApp
def createApp(envivorment=None, start_response=None):
return moduleContainingApp.app

31
config.py Normal file
View File

@@ -0,0 +1,31 @@
# calendar configuration #
SHOW_CALENDAR=False
CALENDAR_URL=""
CALENDAR_USERNAME=""
CALENDAR_PASSWORD=""
# content directory #
CONTENT_DIR="content.example"
# reload calendar on start #
RELOAD_CALENDAR_ON_START=True
# other
NEWS_MAX_AGE=90
SITEMAP_IGNORE = ["icon", "siteMap", "invalidate", "news"]
# site parameters
SITE_TITLE = "Site Title"
SITE_DESCRIPTION = "Site Description"
SITE_AUTHOR = "Site Author"
SITE_LOGO_URL = "Site Logo URL"
SITE_BASE_URL = "Site Base URL"
TEAMSPEAK_SERVER = "teamspeak.com"
DISCORD_SERVER = "https://discord.gg/",
FACEBOOK = "https://www.facebook.com/",
INSTAGRAM = "https://www.instagram.com/",
TWITTER = "https://twitter.com/its_a_sheppy",
TWITCH_CHANNEL = "esports_erlangen",
TWITCH_PLACEHOLDER_IMG = "placeholder.png"

View File

@@ -1,5 +1,5 @@
{
"picture" : "/static/pictures/placeholder.png",
"picture" : "/pictures/placeholder.png",
"title" : "Section Title",
"text" : "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
"moreInfoButtonText" : "Mehr..",

View File

@@ -1,5 +1,5 @@
{
"picture" : "/static/pictures/placeholder.png",
"picture" : "/pictures/placeholder.png",
"title" : "Section Title",
"text" : "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
}

View File

@@ -1,5 +1,5 @@
{
"picture" : "/static/pictures/placeholder.png",
"picture" : "/pictures/placeholder.png",
"title" : "Section Title",
"text" : "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
"moreInfoButtonText" : "Mehr..",

3
req.txt Normal file
View File

@@ -0,0 +1,3 @@
flask
caldav
markdown2

189
server.py
View File

@@ -8,40 +8,55 @@ import caldav
import datetime as dt
import markdown2
# sitemap utilities #
# sitemap utilities
from werkzeug.routing import BuildError
import xml.etree.ElementTree as et
VEREIN_SECTIONS_DIR = "sections/"
MAIN_LINKS_DIR = "main-links/"
# paths
SECTIONS_DIR = "sections/"
NEWS_DIR = "news/"
PICTURES_DIR = "pictures/"
app = flask.Flask("athq-landing-page", static_folder=None)
mainConfig = dict()
with open("config.json") as f:
mainConfig = json.load(f)
# json config keys
TIMEOUT_RELATIVE = "timeout-relative-weeks"
TIMEOUT_FIXED = "timeout-fixed"
PARSED_TIME = "parsed-time"
ACTIVE = "active"
DATE = "date"
UID = "uid"
caldavUrl = None
caldavPassword = None
caldavUsername = None
MARKDOWN_FILE_KEY = "markdown-file"
MARKDOWN_CONTENT_KEY = "markdown-content"
# sitemap
PRIORITY_PRIMARY = 1.0
PRIORITY_SECONDARY = 0.8
# other
HTTP_NOT_FOUND = 404
EMPTY_STRING = ""
CACHE_FILE = "cache.json"
READ = "r"
WRITE = "w"
app = flask.Flask("FLASK_JSON_DREAM_WEBSITE", static_folder=None)
app.config.from_object("config")
def updateEventsFromCalDav():
'''Load event from a remote calendar'''
if app.config["USE_CALENDAR"]:
if app.config["SHOW_CALENDAR"]:
client = caldav.DAVClient(url=caldavUrl, username=caldavUsername, password=caldavPassword)
authenticatedClient = client.principal()
defaultCal = authenticatedClient.calendars()[0]
start = dt.datetime.now()
start = start - dt.timedelta(seconds=start.timestamp() % dt.timedelta(days=1).total_seconds())
end = start + dt.timedelta(days=90)
# TODO remove this
# start = start - dt.timedelta(days=90)
start -= dt.timedelta(seconds=start.timestamp() % dt.timedelta(days=1).total_seconds())
end = start + dt.timedelta(days=app.config["NEWS_MAX_AGE"])
events = sorted(defaultCal.date_search(start, end),
key=lambda e: e.vobject_instance.vevent.dtstart.value)
eventsDictList = []
for e in events:
date = e.vobject_instance.vevent.dtstart.value
@@ -59,17 +74,20 @@ def updateEventsFromCalDav():
else:
eventsDictList = []
with open("cache.json", "w") as f:
# dump to cache file #
with open(CACHE_FILE, WRITE) as f:
json.dump(eventsDictList, f)
def getEventsCache():
with open("cache.json", "r") as f:
'''Return the cached events'''
with open(CACHE_FILE, READ) as f:
return json.load(f)
def readJsonDir(basedir):
'''Read a directory containing json information'''
# load json files from projects/ dir #
jsonDictList =[]
for root, dirs, files in os.walk(basedir):
for filename in sorted(files):
@@ -80,14 +98,10 @@ def readJsonDir(basedir):
return jsonDictList
def parseNewsDirWithTimeout():
'''Parse a directory containing news-json structs and filter out
entries that have exceeded the max age'''
TIMEOUT_RELATIVE = "timeout-relative-weeks"
TIMEOUT_FIXED = "timeout-fixed"
PARSED_TIME = "parsed-time"
ACTIVE = "active"
DATE = "date"
news = readJsonDir(NEWS_DIR)
news = readJsonDir(app.config["NEWS_DIR"])
now = dt.datetime.now()
for n in news:
n.update( { PARSED_TIME : dt.datetime.fromtimestamp(n[DATE]) } )
@@ -104,96 +118,107 @@ def parseNewsDirWithTimeout():
return sorted(news, key=lambda n: n[PARSED_TIME], reverse=True)
@app.route("/invalidate")
def invalidateEventCache():
'''Reload the calendar events'''
updateEventsFromCalDav();
return ("", 204)
return (EMPTY_STRING, 204)
@app.route("/")
def root():
announcements = parseNewsDirWithTimeout()
return flask.render_template("index.html", mainLinks=readJsonDir(MAIN_LINKS_DIR),
siteTitle=mainConfig["siteTitle"],
conf=mainConfig,
return flask.render_template("index.html", conf=app.config,
events=getEventsCache(),
moreEvents=len(getEventsCache())>3,
vereinSections=readJsonDir(VEREIN_SECTIONS_DIR),
announcements=announcements)
sections=readJsonDir(app.config["SECTIONS_DIR"]),
announcements=parseNewsDirWithTimeout())
@app.route("/impressum")
def impressum():
return flask.render_template("impressum.html", conf=mainConfig)
@app.route("/verein")
def verein():
return flask.render_template("verein.html", conf=mainConfig)
@app.route("/stammtisch")
def stammtisch():
return flask.render_template("stammtisch.html", conf=mainConfig)
return flask.render_template("impressum.html", conf=app.config)
@app.route("/people")
def people():
return flask.render_template("people.html", conf=mainConfig,
return flask.render_template("people.html", conf=app.config,
people=readJsonDir("people/"))
@app.route("/news")
def news():
'''Display news-articles based on a UID-parameter'''
uid = flask.request.args.get("uid")
requestedId = flask.request.args.get(UID)
# load news and map UIDs #
news = parseNewsDirWithTimeout()
newsDict = dict()
for n in news:
newsDict.update( { n["uid"] : n } )
newsDict.update( { n[UID] : n } )
if not uid:
article = sorted(news, key=lambda n: n["parsed-time"])[-1]
elif not newsDict[int(uid)]:
return ("", 404)
# set newest article config if there is not UID #
# return 404 if the UID doesnt exist #
# set article config of matching article otherwiese #
if not requestedId:
article = sorted(news, key=lambda n: n[PARSED_TIME])[-1]
elif not newsDict[int(requestedId)]:
return (EMPTY_STRING, HTTP_NOT_FOUND)
else:
article = newsDict[int(uid)]
try:
with open(article["markdown-file"]) as f:
article.update( { "markdown-content" : markdown2.markdown(f.read()) } )
except FileNotFoundError as e:
return ("File not found Error ({})".format(e), 404)
article = newsDict[int(requestedId)]
return flask.render_template("news.html", conf=mainConfig, article=article)
# load article based on config #
try:
with open(article[MARKDOWN_FILE_KEY]) as f:
article.update( { MARKDOWN_CONTENT_KEY : markdown2.markdown(f.read()) } )
except FileNotFoundError as e:
return ("File not found Error ({})".format(e), HTTP_NOT_FOUND)
return flask.render_template("news.html", conf=app.config, article=article)
@app.route("/static/<path:path>")
def sendStatic(path):
if "pictures" in path:
cache_timeout = 2592000
else:
cache_timeout = None
return flask.send_from_directory('static', path, cache_timeout=cache_timeout)
@app.route("/picture/<path:path>")
def sendPicture(path):
cache_timeout = 2592000
return flask.send_from_directory(PICTURES_DIR, path, cache_timeout=cache_timeout)
@app.route('/defaultFavicon.ico')
def icon():
return flask.send_from_directory('static', 'defaultFavicon.ico')
@app.route("/sitemap.xml")
def siteMap():
'''Return an XML-sitemap for SEO'''
# search for urls to add to sitemap #
urls = []
# iterate through all endpoints #
for rule in app.url_map.iter_rules():
skips = ["icon", "siteMap", "invalidate", "news"]
if any([s in rule.endpoint for s in skips]):
# skip all endpoints #
if any([s in rule.endpoint for s in app.config["SITEMAP_IGNORE"]]):
continue
if "GET" in rule.methods:
# skip all non-GET endpoints #
if not "GET" in rule.methods:
continue
# get url for endpoint, get start time and set priority #
try:
url = flask.url_for(rule.endpoint, **(rule.defaults or {}))
priority = 0.8
priority = PRIORITY_SECONDARY
if rule.endpoint == "root":
priority = 1.0
priority = PRIORITY_PRIMARY
urls += [(url, app.config["START_TIME"], priority)]
except BuildError:
pass
# add news articles to sitemap #
news = parseNewsDirWithTimeout()
for n in filter(lambda x: x["active"], news):
urls += [("/news?uid={}".format(n["uid"]), n["parsed-time"], 0.8)]
urls += [("/news?uid={}".format(n[UID]), n[PARSED_TIME], PRIORITY_SECONDARY)]
hostname = flask.request.headers.get("X-REAL-HOSTNAME")
if not hostname:
@@ -215,32 +240,30 @@ def siteMap():
xmlDump += et.tostring(top, encoding='UTF-8', method='xml').decode()
return flask.Response(xmlDump, mimetype='application/xml')
@app.before_first_request
def init():
app.config["SECTIONS_DIR"] = os.path.join(app.config["CONTENT_DIR"], SECTIONS_DIR)
app.config["NEWS_DIR"] = os.path.join(app.config["CONTENT_DIR"], NEWS_DIR)
if app.config["RELOAD_CALENDAR_ON_START"]:
updateEventsFromCalDav()
app.config["START_TIME"] = dt.datetime.now()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Projects Showcase',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
# general parameters #
parser.add_argument("-i", "--interface", default="0.0.0.0", help="Interface to listen on")
parser.add_argument("-i", "--interface", default="127.0.0.1", help="Interface to listen on")
parser.add_argument("-p", "--port", default="5000", help="Port to listen on")
parser.add_argument("--cal-info", help="File Containing a public calendar link")
parser.add_argument("--auto-reload", action="store_const", default=False, const=True,
help="Automaticly reload HTTP templates (impacts performance)")
parser.add_argument("--no-update-on-start", action="store_const", const=True, default=False,
help="Don't update the calendar on start")
# startup #
app.config['TEMPLATES_AUTO_RELOAD'] = True
args = parser.parse_args()
if args.cal_info:
app.config["USE_CALENDAR"] = True
with open(args.cal_info) as f:
caldavUrl, caldavUsername, caldavPassword = f.read().strip().split(",")
else:
app.config["USE_CALENDAR"] = False
if not args.no_update_on_start:
updateEventsFromCalDav()
app.config["START_TIME"] = dt.datetime.now()
app.config['TEMPLATES_AUTO_RELOAD'] = args.auto_reload
app.run(host=args.interface, port=args.port)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1 +0,0 @@
placeholder.png

View File

@@ -1,7 +1,10 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<meta name="description" content="{{ conf['SITE_DESCRIPTION'] }}">
<meta name="author" content="{{ conf['SITE_AUTHOR'] }}">
<meta name="title" content="{{ conf['SITE_TITLE'] }}">
<link rel="shortcut icon" href="/defaultFavicon.ico">
<!-- Bootstrap core CSS -->
@@ -14,16 +17,16 @@
<script src="/static/js/bootstrap.min.js"></script>
<meta property="og:type" content="website" />
<meta property="og:title" content="{{ conf['siteTitle'] }}" />
<meta property="og:description" content="{{ conf['siteDescription'] }}" />
<meta property="og:title" content="{{ conf['SITE_TITLE'] }}" />
<meta property="og:description" content="{{ conf['SITE_DESCRIPTION'] }}" />
<meta property="og:url" content="{{ url_for(request.endpoint) }}" />
<meta property="og:image" content="{{ conf['siteLogo'] }}">
<meta property="og:image" content="{{ conf['SITE_LOG_URL'] }}">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"url": "{{ conf['siteURL'] }}",
"logo": "{{ conf['siteLogo'] }}"
"url": "{{ conf['SITE_BASE_URL'] }}",
"logo": "{{ conf['SITE_LOGO_URL'] }}"
}
</script>

View File

@@ -4,8 +4,6 @@
{% include 'head.html' %}
<title>{{ conf["siteTitle"] }}</title>
</head>
<body style="background-color: #eae9e9">

View File

@@ -5,8 +5,6 @@
{% include 'head.html' %}
<title>{{ conf["siteTitle"] }}</title>
<!-- Load the Twitch embed script -->
<!-- <script src="https://embed.twitch.tv/embed/v1.js"></script> -->
<script src="https://sslrelay.atlantishq.de/twitch"></script>
@@ -55,7 +53,7 @@
<div id="twitch-consent-placeholder" class="card bg-dark text-white">
<img style="min-width: 80%; min-height: 200px;"
class="card-img" src="/static/pictures/{{ conf['twitch-placeholder-img'] }}" >
class="card-img" src="/pictures/{{ conf['twitch-placeholder-img'] }}" >
<div class="card-img-overlay">
<label class="switch mt-3 mt-0-u440">
<input id="toogle-twitch" class="custom-control-input"
@@ -97,7 +95,7 @@
</div>
{% endif %}
{% for section in vereinSections %}
{% for section in sections %}
<div class="{% if loop.index %2 == 1 %} bg-secondary {% else %} bg-dark {% endif %} pt-2 pb-2">
<div class="container text-color-special">
<div class="row" {% if loop.index %2 == 1 %} style="flex-direction: row-reverse;" {% endif %}>

View File

@@ -15,15 +15,15 @@
<a class="nav-link" href="/">Home</a>
</li>
{% if conf["teamspeak-server"] %}
{% if conf["TEAMSPEAK_SERVER"] %}
<li class="nav-item right">
<a class="nav-link" href="ts3server://{{ conf['teamspeak-server'] }}">Teamspeak</a>
<a class="nav-link" href="ts3server://{{ conf['TEAMSPEAK_SERVER'] }}">Teamspeak</a>
</li>
{% endif %}
{% if conf["discord-server"] %}
{% if conf["DISCORD_SERVER"] %}
<li class="nav-item right">
<a class="nav-link" href="{{ conf['discord-server'] }}">Discord</a>
<a class="nav-link" href="{{ conf['DISCORD_SERVER'] }}">Discord</a>
</li>
{% endif %}
@@ -32,21 +32,21 @@
<!-- right side -->
<ul class="navbar-nav">
{% if conf["instagram"] %}
{% if conf["INSTAGRAM"] %}
<li class="nav-item right">
<a class="nav-link" href="{{ conf['instagram'] }}">Instagram</a>
<a class="nav-link" href="{{ conf['INSTAGRAM'] }}">Instagram</a>
</li>
{% endif %}
{% if conf["facebook"] %}
{% if conf["FACEBOOK"] %}
<li class="nav-item right">
<a class="nav-link" href="{{ conf['facebook'] }}">Facebook</a>
<a class="nav-link" href="{{ conf['FACEBOOK'] }}">Facebook</a>
</li>
{% endif %}
{% if conf["twitter"] %}
{% if conf["TWITTER"] %}
<li>
<a class="nav-link" href="{{ conf['twitter'] }}">Twitter</a>
<a class="nav-link" href="{{ conf['TWITTER'] }}">Twitter</a>
</li>
{% endif %}

View File

@@ -22,7 +22,7 @@
</div>
<div class="col image-min-dimensions">
<img class="img-responsive w-100 image-max-dimensions"
src="/static/pictures/{{ p['image'] }}"></img>
src="/pictures/{{ p['image'] }}"></img>
</div>
</div>
{% endfor %}