diff --git a/.gitignore b/.gitignore index 51ef840..9b9f2a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ js/ +__pycache__/ css/ *.swp *.jpg diff --git a/README.md b/README.md index d24b504..55722e2 100644 --- a/README.md +++ b/README.md @@ -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/*. diff --git a/app.py b/app.py new file mode 100644 index 0000000..ee46265 --- /dev/null +++ b/app.py @@ -0,0 +1,4 @@ +import server as moduleContainingApp + +def createApp(envivorment=None, start_response=None): + return moduleContainingApp.app diff --git a/config.py b/config.py new file mode 100644 index 0000000..c984933 --- /dev/null +++ b/config.py @@ -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" diff --git a/config.json b/content.example/config.json similarity index 100% rename from config.json rename to content.example/config.json diff --git a/markdown/example_markdown.md b/content.example/markdown/example_markdown.md similarity index 100% rename from markdown/example_markdown.md rename to content.example/markdown/example_markdown.md diff --git a/news/example_news.json b/content.example/news/example_news.json similarity index 100% rename from news/example_news.json rename to content.example/news/example_news.json diff --git a/people/10_john_doe.json b/content.example/people/10_john_doe.json similarity index 100% rename from people/10_john_doe.json rename to content.example/people/10_john_doe.json diff --git a/people/20_jane_doe.json b/content.example/people/20_jane_doe.json similarity index 100% rename from people/20_jane_doe.json rename to content.example/people/20_jane_doe.json diff --git a/sections/00-example-section.json b/content.example/sections/00-example-section.json similarity index 91% rename from sections/00-example-section.json rename to content.example/sections/00-example-section.json index 5f0d994..b11b9c3 100644 --- a/sections/00-example-section.json +++ b/content.example/sections/00-example-section.json @@ -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..", diff --git a/sections/10-example-section.json b/content.example/sections/10-example-section.json similarity index 90% rename from sections/10-example-section.json rename to content.example/sections/10-example-section.json index 595a094..70bb688 100644 --- a/sections/10-example-section.json +++ b/content.example/sections/10-example-section.json @@ -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." } diff --git a/sections/20-example-section.json b/content.example/sections/20-example-section.json similarity index 91% rename from sections/20-example-section.json rename to content.example/sections/20-example-section.json index 5f0d994..b11b9c3 100644 --- a/sections/20-example-section.json +++ b/content.example/sections/20-example-section.json @@ -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..", diff --git a/req.txt b/req.txt new file mode 100644 index 0000000..234a893 --- /dev/null +++ b/req.txt @@ -0,0 +1,3 @@ +flask +caldav +markdown2 diff --git a/server.py b/server.py index b00838f..5acf1a6 100755 --- a/server.py +++ b/server.py @@ -8,49 +8,64 @@ 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/" -NEWS_DIR = "news/" +# 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 date += dt.timedelta(hours=2) newEventDict = { "description" : e.vobject_instance.vevent.summary.value, - "time" : date.strftime("%H:%M"), - "day" : date.strftime("%d"), - "month" : date.strftime("%b"), - "year" : date.strftime("%Y") } + "time" : date.strftime("%H:%M"), + "day" : date.strftime("%d"), + "month" : date.strftime("%b"), + "year" : date.strftime("%Y")} try: newEventDict.update({ "location" : e.vobject_instance.vevent.location.value }) except AttributeError: @@ -58,18 +73,21 @@ def updateEventsFromCalDav(): eventsDictList += [newEventDict] 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, - events=getEventsCache(), - moreEvents=len(getEventsCache())>3, - vereinSections=readJsonDir(VEREIN_SECTIONS_DIR), - announcements=announcements) + return flask.render_template("index.html", conf=app.config, + events=getEventsCache(), + moreEvents=len(getEventsCache())>3, + 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/") def sendStatic(path): - if "pictures" in path: - cache_timeout = 2592000 - else: - cache_timeout = None + cache_timeout = None return flask.send_from_directory('static', path, cache_timeout=cache_timeout) +@app.route("/picture/") +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(): - urls = [] - for rule in app.url_map.iter_rules(): - skips = ["icon", "siteMap", "invalidate", "news"] - if any([s in rule.endpoint for s in skips]): - continue - if "GET" in rule.methods: - try: - url = flask.url_for(rule.endpoint, **(rule.defaults or {})) - priority = 0.8 - if rule.endpoint == "root": - priority = 1.0 - urls += [(url, app.config["START_TIME"], priority)] - except BuildError: - pass + '''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(): + + # skip all endpoints # + if any([s in rule.endpoint for s in app.config["SITEMAP_IGNORE"]]): + continue + + # 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 = PRIORITY_SECONDARY + if rule.endpoint == "root": + 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("-p", "--port", default="5000", help="Port to listen on") - parser.add_argument("--cal-info", help="File Containing a public calendar link") + 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("--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) diff --git a/static/pictures/placeholder.png b/static/pictures/placeholder.png deleted file mode 100644 index 9260c97..0000000 Binary files a/static/pictures/placeholder.png and /dev/null differ diff --git a/static/pictures/wallpaper.jpg b/static/pictures/wallpaper.jpg deleted file mode 120000 index 65cd91e..0000000 --- a/static/pictures/wallpaper.jpg +++ /dev/null @@ -1 +0,0 @@ -placeholder.png \ No newline at end of file diff --git a/templates/head.html b/templates/head.html index ba8839f..d8127da 100644 --- a/templates/head.html +++ b/templates/head.html @@ -1,7 +1,10 @@ - - - + + + + + + @@ -14,16 +17,16 @@ - - + + - + diff --git a/templates/impressum.html b/templates/impressum.html index 9264649..e3b909c 100644 --- a/templates/impressum.html +++ b/templates/impressum.html @@ -4,8 +4,6 @@ {% include 'head.html' %} - {{ conf["siteTitle"] }} - diff --git a/templates/index.html b/templates/index.html index b0df0b8..0047b0b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -5,8 +5,6 @@ {% include 'head.html' %} - {{ conf["siteTitle"] }} - @@ -55,7 +53,7 @@