From c583a69362f86fcc8e1b35a45a06dd8377d6308f Mon Sep 17 00:00:00 2001 From: mjfernez Date: Thu, 14 Oct 2021 20:14:53 -0400 Subject: Adds RSS auto-generation for files in 'site' This commit adds rss_generator.py which contains the main logic for indexing the site directory and generating a feed on startup. It serves as a sort of ad-hoc database which is accessed when /feed.xml is requested. Also corrects various typos, README nonsense, and expands the config options for RSS. Instances of './templates/site' have been replaced with the general BASE_DIR variable in the siteconfig. --- README | 223 ---------------------------------------------- README.md | 248 ++++++++++++++++++++++++++++++++++++++++++++++++++++ rss_generator.py | 74 ++++++++++++++++ server.py | 6 +- siteconfig.py | 64 ++++++++++---- static/main.css | 2 +- static/rss.svg | 3 + templates/base.html | 2 + templates/feed.xml | 27 ++++++ views.py | 23 ++++- 10 files changed, 423 insertions(+), 249 deletions(-) delete mode 100644 README create mode 100644 README.md create mode 100644 rss_generator.py create mode 100644 static/rss.svg create mode 100644 templates/feed.xml diff --git a/README b/README deleted file mode 100644 index 895d9b0..0000000 --- a/README +++ /dev/null @@ -1,223 +0,0 @@ -# EZCMS - a minimal and simple way to manage a website - -## Requirements - -Python 3.7+ - - -## Huh/What/Why? - -EZCMS (or "Easy CMS" for those of you who call that "zed" instead) is a -minimal and simple way of managing content and serving static files on -a website. It was mostly made for my own website which I wanted to be -as simple as possible, but with some ability to easily add new pages in -a template I like. - -I don't really expect many people to use this, but I hope it might be -useful for someone learning to use Flask, or someone who also likes the -look of websites from the 90s. - - -### Why not just neocities? - -Neocities is awesome! You should definitely host a site there. - -https://neocities.org/ - -It's easy to get a simple static site going there and it's totally -free, but it lacks server side scripting as far as I know. - -## Quick start - -It's recommended to run each server in it's own virtual environment. This -program uses python 3.7, so change `python` to either `python3` or `python3.7` -depending on your needs. First clone this repo (with git clone, or download the -zip), change into the directory, then: - -```bash -$ python -m venv env -$ source env/bin/activate -$ pip install -r requirements.txt -$ python server.py -``` - -Your server will (by default) be hosted on http://127.0.0.1:5000 -and have the `templates/site/` directory delivered to your users when they -access http://127.0.0.1:5000/site/ - -You should see `home.html` render on the root directory. - - -## Adding Pages - -To add a new page, all you need to do is add a new file (or a folder and a -group of files) somewhere under one of the folders in `site`. This folder in -particular is special since it contains the top-level folders which will be -used to navigate your site, but any folders beneath will be automatically -indexed. - -As an exercise, add a file to the `templates/site/thoughts/rants` folder -called `myrant.html` and put the following content: - -`

I don't like spam!

` - -The new page will be rendered with your navbar on top and footer on the bottom -when navigated to in the `rants folder` HTML files will by default be rendered -in page, and all other types of files (like txt) will be returned without -rendering. - -An important note, since these HTML files are being rendered by Flask, -*you can make full use of the Jinja templating language*! So in other words, -any template you've developed for flask is fully usable here--but remember it -will be rendered *inside* the `templates/base.html` template. If you need to -make tweaks to the navbar or footer, you'll want to edit that file instead. - -### Override base template - -Have a page with custom CSS, or need to get rid of the navbar entirely? No -worries! Just add a '!' at the end of you html file and EZCMS will interpret it -as it's own Jinja template without adding everything from the base template. -Tip: if you're just changing the CSS, you can start with the following -boilerplate that I provide in `base.html`: - - -```code -{% extends 'base.html' %} -{% block css%} -//your cool css here -{% endblock %} -{% block content %} -//your cool content here -{% endblock %} -``` - - -## Customization (or things you'll want to change right now) - -To make customization easier, this program comes with a configuration file with -variables to tweak the display of your site call `siteconfig.py`. -For example by default this program makes the navbar out of the directories in -the `templates/site` directory, but you might want include other directories, -or even external sites. Examples of how to change these options are provided -in the comments on that file. Customization is also provided through the use of -specific files. - - -### Navbar customization - -Be default, the top navbar is populated by indexing and sorting the top-level -`templates/site/` directory. You can override this to include any directories -you want in any order, so long as they exist, but it's advised to still keep -them all in the `site` directory to avoid confusion. - - -### Index File Configuration - -This program uses a single master index file which is used when navigating to -any directory--instead of having to put in an 'index.html' in each folder, or -using the default apache/nginx/httpd auto-indexer. In it's place, you can -optionally put a `.description` file to provide a short description of what's -in the directory or a `.links` file - -The `.description` file should just be a text file with no formatting. If you -want to add formatting, you can edit the `templates\index.html` file around the -`{{ description }}` variable (for example, you could wrap it in for -*italics* - -The `.links` file is a pseudo-csv file which should contain a comma separated -list containing a description and a relative or absolute URL to be linked. For -example this line: - -`About,/about` - -Produces the following HTML on your index page: - -`
  • I don't like spam!

    ` + +The new page will be rendered with your navbar on top and footer on the bottom +when navigated to in the `rants folder` HTML files will by default be rendered +in page, and all other types of files (like txt) will be returned without +rendering. + +An important note, since these HTML files are being rendered by Flask, +*you can make full use of the Jinja templating language*! So in other words, +any template you've developed for flask is fully usable here--but remember it +will be rendered *inside* the `templates/base.html` template. If you need to +make tweaks to the navbar or footer, you'll want to edit that file instead. + +### Override base template + +Have a page with custom CSS, or need to get rid of the navbar entirely? No +worries! Just add a '!' at the end of you html file and EZCMS will interpret it +as it's own Jinja template without adding everything from the base template. +Tip: if you're just changing the CSS, you can start with the following +boilerplate that I provide in `base.html`: + + +```code +{% extends 'base.html' %} +{% block css%} +//your cool css here +{% endblock %} +{% block content %} +//your cool content here +{% endblock %} +``` + + +## Configuration + +This program comes with a configuration file with variables to tweak +the display of your site called `siteconfig.py`. For example, by +default this program makes the navbar out of the directories in +the `templates/site` directory, but you might want include other directories, +or even external sites. Examples of how to change these options are provided +in the comments on that file. Customization is also provided through the use of +specific files. + + +### Navbar + +Be default, the top navbar is populated by indexing and sorting the top-level +`templates/site/` directory. You can override this to include any directories +you want in any order, so long as they exist, but it's advised to still keep +them all in the `site` directory to avoid confusion. + + +### Indexing + +This program uses a single master index file which is used when navigating to +any directory--instead of having to put in an 'index.html' in each folder, or +using the default apache/nginx/httpd auto-indexer. In it's place, you can +optionally put a `.description` file to provide a short description of what's +in the directory or a `.links` file + +The `.description` file should just be a text file with no formatting. If you +want to add formatting, you can edit the `templates\index.html` file around the +`{{ description }}` variable (for example, you could wrap it in for +*italics* + +The `.links` file is a pseudo-csv file which should contain a comma separated +list containing a description and a relative or absolute URL to be linked. For +example this line: + +`About,/about` + +Produces the following HTML on your index page: + +`
  • ".format( + self.FULL_PATH, self.TITLE, self.short_timestamp() + ) + + def short_timestamp(self): + return strftime("%Y-%m-%d %H:%M %z", strptime(self.LAST_UPDATE)) + + def parse_file(self): + """ + parse_file - reads the file at FULL_PATH and saves the content + from when the first

    tag is hit up to and including the + closing

    tag. Expects an HTML style file + """ + with open(self.FULL_PATH) as f: + in_body = False + paragraphs = 0 + description = "" + for line in f.readlines(): + if paragraphs >= self.PARAGRAPHS: + break + line = line.strip() + if line.startswith("

    "): + in_body = True + if in_body: + description += line + if line.endswith("

    "): + in_body = False + paragraphs += 1 + + return ''.join(description) + + def file_last_modified(self): + return ctime(os.stat(self.FULL_PATH).st_ctime) + + def get_uri(self): + return '/'.join(self.FULL_PATH.split('/')[2:]) + + +def get_rss_channel(): + items = [] + for root, dirs, files in os.walk(siteconfig.BASE_DIR): + for f in files: + path = os.path.join(root, f) + if ( + path.endswith(".html") or f.endswith(".html!") + ) and path not in siteconfig.RSS_OMIT: + items.append(RSS_Item(path)) + return items diff --git a/server.py b/server.py index 545f4b6..c5522b9 100644 --- a/server.py +++ b/server.py @@ -6,6 +6,7 @@ from flask import Flask from siteconfig import siteconfig from flask_compress import Compress from flask_caching import Cache +from rss_generator import RSS_Item, get_rss_channel app = Flask(__name__) compress = Compress() @@ -41,11 +42,12 @@ def setup(): app.config.update( {'COMPRESS_MIMETYPES': siteconfig.COMPRESS_MIMETYPES} ) + app.config.update({'RSS_CHANNEL': get_rss_channel()}) # Setup needs to come first to be compatible with wsgi setup() +compress.init_app(app) +cache.init_app(app) if __name__ == "__main__": - compress.init_app(app) - cache.init_app(app) app.run() diff --git a/siteconfig.py b/siteconfig.py index 412dfce..f42875b 100644 --- a/siteconfig.py +++ b/siteconfig.py @@ -7,20 +7,30 @@ class siteconfig: # REQUIRED SETTINGS# DOMAIN = "example.net" # Your site here! - HOME_TITLE = "WELCOME" # Goes right under - # your site + HOME_TITLE = "WELCOME" LINKS_FILE = ".links" # ".lnx" if you like DESC_FILE = ".description" # ".desc" DEFAULT_MIMETYPE = "application/octet-stream" # ^This usually prompts a browser to download a file if the mime # type is unknown. A good alternative might be "text/plain" + # This setting is required, don't change it unless you're running + # things in different directories + BASE_DIR = "./templates/site/" # Add your desired mimetypes to the csv file MIMETYPES = {} with open('mimetypes.csv') as f: for line in f.readlines(): ext, mime = line.strip().split(',') MIMETYPES.update({ext: mime}) + # This reads your omit file. + # Entries should be the full path from the site directory. + # For example "dontread.txt" in this project is entered as + # 'thoughts/rants/donread.txt' + RSS_OMIT = [] + with open('omit') as f: + for line in f.readlines(): + RSS_OMIT.append(BASE_DIR + line.strip()) # OPTIONAL SETTINGS # @@ -33,35 +43,51 @@ class siteconfig: # Most of the time, you don't need to set this! SECRET_KEY = None # Something random. - # Option for Flask Compress + # Options for Flask Compress # see here https://pypi.org/project/Flask-Compress/ COMPRESS_MIMETYPES = list(MIMETYPES.values()) - # Option for Flask Caching + # Options for Flask Caching # https://flask-caching.readthedocs.io/en/latest/#configuring-flask-caching cache_config = { - 'CACHE_TYPE' : "SimpleCache", - 'CACHE_DEFAULT_TIMEOUT' : 300, + 'CACHE_TYPE': "SimpleCache", + 'CACHE_DEFAULT_TIMEOUT': 300, # You should only fill ONE of the sections below # uswgi - 'CACHE_UWSGI_NAME' : None, + 'CACHE_UWSGI_NAME': None, ## # memcache - 'CACHE_MEMCACHED_SERVERS' : None, - 'CACHE_MEMCACHED_USERNAME' : None, - 'CACHE_MEMCACHED_PASSWORD' : None, + 'CACHE_MEMCACHED_SERVERS': None, + 'CACHE_MEMCACHED_USERNAME': None, + 'CACHE_MEMCACHED_PASSWORD': None, ## # redis - 'CACHE_REDIS_HOST' : None, - 'CACHE_REDIS_PORT' : None, - 'CACHE_REDIS_PASSWORD' : None, - 'CACHE_REDIS_DB' : None, - 'CACHE_REDIS_URL' : None, - 'CACHE_REDIS_SENTINELS' : None, - 'CACHE_REDIS_SENTINEL_MASTER' : None, - 'CACHE_REDIS_CLUSTER' : None, + 'CACHE_REDIS_HOST': None, + 'CACHE_REDIS_PORT': None, + 'CACHE_REDIS_PASSWORD': None, + 'CACHE_REDIS_DB': None, + 'CACHE_REDIS_URL': None, + 'CACHE_REDIS_SENTINELS': None, + 'CACHE_REDIS_SENTINEL_MASTER': None, + 'CACHE_REDIS_CLUSTER': None, ## # filesystem - 'CACHE_DIR' : None, + 'CACHE_DIR': None, # add more options as needed from the URL above } + + # RSS Settings + rss_channel_config = { + 'TITLE': "RSS Feed for example.net", + 'LINK': "http://127.0.0.1:5000/", + 'DESCRIPTION': "My example feed", + 'LANGUAGE': "en-us", + 'PUBDATE': "", + 'LASTBUILDDATE': "", + 'DOCS': "https://git.mjfer.net/ezcms.git/", + 'GENERATOR': "EZCMS", + 'AUTHOR': "editor@example.net", + 'WEBMASTER': "webmaster@example.net", + # Max amount of paragraphs to print in each description + 'DESCRIPTION_LENGTH': 3, + } diff --git a/static/main.css b/static/main.css index f8837d0..9b8c6db 100644 --- a/static/main.css +++ b/static/main.css @@ -14,7 +14,7 @@ body { body { font-size: 200% } - .loicense { + .license { font-size: 75%; } } diff --git a/static/rss.svg b/static/rss.svg new file mode 100644 index 0000000..39bef06 --- /dev/null +++ b/static/rss.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 6476091..5e71e6b 100644 --- a/templates/base.html +++ b/templates/base.html @@ -15,6 +15,8 @@ {% for dir in navbar %}
    {{ dir }}/ | {% endfor %} + +

    {{ title }}

    diff --git a/templates/feed.xml b/templates/feed.xml new file mode 100644 index 0000000..a252134 --- /dev/null +++ b/templates/feed.xml @@ -0,0 +1,27 @@ + + + + {{ config.TITLE }} + {{ config.LINK }} + {{ config.DESCRIPTION }} + + {{ config.LANGUAGE }} + {{ config.PUBDATE }} + {{ config.LASTBUILDDATE }} + {{ config.DOCS }} + {{ config.GENERATOR }} + {{ config.AUTHOR }} + {{ config.WEBMASTER }} + {% for item in items %} + + {{ item.TITLE }} + {{ item.LINK }} + {{ item.LINK }} + {{ item.DESCRIPTION }} + {{ item.LAST_UPDATE }} + {{ item.SITE_DIR }} + {{ config.AUTHOR }} + + {% endfor %} + + diff --git a/views.py b/views.py index dd37902..7a6f273 100644 --- a/views.py +++ b/views.py @@ -4,7 +4,7 @@ browsing to certain pages """ import os from flask import request, send_from_directory, abort -from flask import render_template, render_template_string +from flask import render_template, render_template_string, make_response from siteconfig import siteconfig from server import app, cache from view_functions import default_context, index_dir, is_hidden_path @@ -19,6 +19,8 @@ CONTENT_BLOCK = ( @app.route("/") @app.route("/site") +@app.route("/site/home.html") +@app.route("/site/index.html") @cache.cached() def home(): """ @@ -48,7 +50,7 @@ def render_file(path): """ if is_hidden_path(path): abort(404) - abs_path = "./templates/site/" + path + abs_path = siteconfig.BASE_DIR + path context = default_context() context.update( { @@ -68,7 +70,7 @@ def render_file(path): else: # not an html file, so don't render it return send_from_directory( - 'templates/site/', + siteconfig.BASE_DIR, path, mimetype=siteconfig.MIMETYPES.get( f".{ path.split('.')[-1] }", @@ -101,7 +103,7 @@ def send_file_from_site(path): as with `render_file`, send the raw file to the user """ return send_from_directory( - 'templates/site/', + siteconfig.BASE_DIR, path, mimetype=siteconfig.MIMETYPES.get( f".{ path.split('.')[-1] }", siteconfig.DEFAULT_MIMETYPE @@ -122,3 +124,16 @@ def send_file_from_static(path): f".{ path.split('.')[-1] }", siteconfig.DEFAULT_MIMETYPE ), ) + + +@app.route("/feed.xml") +@cache.cached() +def render_rss_feed(): + context = { + 'config': siteconfig.rss_channel_config, + 'items': app.config['RSS_CHANNEL'], + } + feed = render_template("feed.xml", **context) + response = make_response(feed) + response.headers['Content-Type'] = siteconfig.MIMETYPES.get(".xml") + return response -- cgit v1.2.3