Commit ec325cc1 authored by Kreta's avatar Kreta

initial commit

parents
env
output
*.pid
*.pyc
credentials.mk
/content/events/testtimo*
PY?=python2
PELICAN?=pelican
PELICANOPTS=
BASEDIR=$(CURDIR)
INPUTDIR=$(BASEDIR)/content
OUTPUTDIR=$(BASEDIR)/output
CONFFILE=$(BASEDIR)/pelicanconf.py
PUBLISHCONF=$(BASEDIR)/publishconf.py
-include credentials.mk
DEBUG ?= 0
ifeq ($(DEBUG), 1)
PELICANOPTS += -D
endif
RELATIVE ?= 0
ifeq ($(RELATIVE), 1)
PELICANOPTS += --relative-urls
endif
help:
@echo 'Makefile for a pelican Web site '
@echo ' '
@echo 'Usage: '
@echo ' make html (re)generate the web site '
@echo ' make clean remove the generated files '
@echo ' make regenerate regenerate files upon modification '
@echo ' make publish generate using production settings '
@echo ' make serve [PORT=8000] serve site at http://localhost:8000'
@echo ' make serve-global [SERVER=0.0.0.0] serve (as root) to $(SERVER):80 '
@echo ' make devserver [PORT=8000] start/restart develop_server.sh '
@echo ' make stopserver stop local server '
@echo ' make ssh_upload upload the web site via SSH '
@echo ' make rsync_upload upload the web site via rsync+ssh '
@echo ' make dropbox_upload upload the web site via Dropbox '
@echo ' make ftp_upload upload the web site via FTP '
@echo ' make sftp_upload upload the web site via sftp '
@echo ' make s3_upload upload the web site via S3 '
@echo ' make cf_upload upload the web site via Cloud Files'
@echo ' make github upload the web site via gh-pages '
@echo ' '
@echo 'Set the DEBUG variable to 1 to enable debugging, e.g. make DEBUG=1 html '
@echo 'Set the RELATIVE variable to 1 to enable relative urls '
@echo ' '
html:
$(PELICAN) $(INPUTDIR) -o $(OUTPUTDIR) -s $(CONFFILE) $(PELICANOPTS)
clean:
[ ! -d $(OUTPUTDIR) ] || rm -rf $(OUTPUTDIR)
regenerate:
$(PELICAN) -r $(INPUTDIR) -o $(OUTPUTDIR) -s $(CONFFILE) $(PELICANOPTS)
serve:
ifdef PORT
cd $(OUTPUTDIR) && $(PY) -m pelican.server $(PORT)
else
cd $(OUTPUTDIR) && $(PY) -m pelican.server
endif
serve-global:
ifdef SERVER
cd $(OUTPUTDIR) && $(PY) -m pelican.server 80 $(SERVER)
else
cd $(OUTPUTDIR) && $(PY) -m pelican.server 80 0.0.0.0
endif
devserver:
ifdef PORT
$(BASEDIR)/develop_server.sh restart $(PORT)
else
$(BASEDIR)/develop_server.sh restart
endif
stopserver:
$(BASEDIR)/develop_server.sh stop
@echo 'Stopped Pelican and SimpleHTTPServer processes running in background.'
publish:
$(PELICAN) $(INPUTDIR) -o $(OUTPUTDIR) -s $(PUBLISHCONF) $(PELICANOPTS)
ssh_upload: publish
scp -P $(SSH_PORT) -r $(OUTPUTDIR)/* $(SSH_USER)@$(SSH_HOST):$(SSH_TARGET_DIR)
rsync_upload: publish
rsync -e "ssh -p $(SSH_PORT)" -P -rvzc --delete $(OUTPUTDIR)/ $(SSH_USER)@$(SSH_HOST):$(SSH_TARGET_DIR) --cvs-exclude
dropbox_upload: publish
cp -r $(OUTPUTDIR)/* $(DROPBOX_DIR)
ftp_upload: publish
lftp ftp://$(FTP_USER)@$(FTP_HOST) -e "mirror -R $(OUTPUTDIR) $(FTP_TARGET_DIR) ; quit"
sftp-upload: publish
@lftp sftp://$(FTP_USER):$(FTP_PASSWORD)@$(FTP_HOST) -e "mirror -R $(OUTPUTDIR) $(FTP_TARGET_DIR) ; quit"
s3_upload: publish
s3cmd sync $(OUTPUTDIR)/ s3://$(S3_BUCKET) --acl-public --delete-removed --guess-mime-type
cf_upload: publish
cd $(OUTPUTDIR) && swift -v -A https://auth.api.rackspacecloud.com/v1.0 -U $(CLOUDFILES_USERNAME) -K $(CLOUDFILES_API_KEY) upload -c $(CLOUDFILES_CONTAINER) .
github: publish
ghp-import -m "Generate Pelican site" -b $(GITHUB_PAGES_BRANCH) $(OUTPUTDIR)
git push origin $(GITHUB_PAGES_BRANCH)
.PHONY: html help clean regenerate serve serve-global devserver publish ssh_upload rsync_upload dropbox_upload ftp_upload s3_upload cf_upload github
# KRETA Website Generator
The [Kreta Website](https://kreta-dresden.org) is statically generated with `pelican`.
This repository provides all generic code and instructions for others to use and adapt to their needs.
## Overview
`Pelican` is a **static site generator**.
To understand the concept of static websites let's first see how dynamic websites (for example those based on wordpress) work: Dynamic websites are generated again and again for every single user. Once you visit the site, the server reads the actual content (like the content of a news post you want to see) from a database, creates an HTML document from it and returns it to you. When generating the HTML output, it can take into account all kinds of other information, like the current time. Furthermore it can take user input and store it into the database (like comments to a news post).
Many websites actually don't need all those options: they should just display the same pages without taking care of other information (like the current time) and don't have any user input. In those cases, dynamic websites are often used anyways - mostly because people are just used to it.
Static websites on the contrary are just a bunch of HTML documents that the server sends to the client once they visit the page. The actual content is not stored on the server but somewhere else. Every time that content changes, the website is built again and uploaded to the server. `Pelican` is a tool for building all the HTML documents that for a website from your content.
This repository consists of three parts:
- A pelican **event plugin** that allows you the specify events with all the fields you neeed (starting time, location..)
- A pelican **theme** that defines how the website will look
- A **minimal pelican project** around those two
The project is structured als follows:
- **/content**: raw content of the website
- **/output**: generated HTML documents (the finished website)
- **/plugins**: pelican plugins. Currently only the event plugin
- **/themes**: pelican themes. Only the kreta theme
- **credentials.mk.template**: Template file that shows how to set login data for the server
- **pelicanconf.py**: Project Settings
- **publishconf.py**: Settings that will be changed when you upload to the server
- **requirements.txt**: Python packages that need to be installed for everything to work
## Install
- Install requirements `virtualenv, pip, lftp`
- Create Python2 virtualenv in folder `env`: `virtualenv env -p python2`
- Activate virtualenv: `. env/bin/activate`
- Install Python dependencies; `pip install -r requirements.txt`
- Test: `make devserver` and visit `localhost:8000`
## Content
All text content is needs to reside in markdown files with the ending `.md`.
At the top, metadata fields can/need to be set in the form `fieldname: field value`. After that, the main text/description follows.
Some markdown syntax is supported by the theme, other may look crapy.
### Events
| Field | Mandatory | Description |
|-------|-----------|-------------|
| `title` | yes | Complete title |
| `short_title` | no | Title used in schedule. Defaults to complete title |
| `starts` | yes | Timestamp in form YYYY-MM-DD HH:MM |
| `ends` | yes | Same as above. Mandatory for ical file |
| `slug` | no | Internal reference name; no spaces and stuff. Will be auto generated by default |
| `location` | yes | Name of the location |
| `location_link` | no | lLnk target for clicking on location name |
| `location_gps` | no | Format zur Zeit: Breitengrad, Längengrad / aktueller Platzhalter bei TU Dresden: "location_gps: Breitengrad, Längengrad" |
| `summary` | no | Short version of the complete description. Defaults to full description. |
### News
| Field | Mandatory | Description |
|-------|-----------|-------------|
| `title` | see above ||
| `date` | no | Date shown as time of publishing. Format as above. Defaults to timestamp of file |
| `slug` | see above ||
| `summary` | see above ||
### Pages
| Field | Mandatory | Description |
|-------|-----------|-------------|
| `title`| as above ||
| `slug` | as above ||
| `attribute` | no | Defines order in menu in relation to other pages |
| `status` | no | Can be set to hidden to not display page in menu |
Links to all non-hidden pages are displayed in the menu.
The theme treats the page with the slug `impressum` differently: It is linked to at the bottom of every page
## Credentials
Credentials for publishing your site should be stored in 'credentials.mk'.
To avoid pushing passwords to a public repository, only a default file is provided, which needs to be copied:
`cp credentials.mk.template credentials.mk`
title: Example Event
short_title: Example
starts: 2017-01-01 12:30
ends: 2017-01-01 20:00
location: AZ Conni
location_link: http://www.azconni.de
location_gps: 51.07686, 13.75107
summary: Short description of the event
Full description of the event.
**Markdown** *syntax* [supported](http://markdown.de)
Title: Example News
slug: example_news
date: 2017-01-01 22:00
summary: Summary of the news. Can be used for displaying in a news list
This is the body of the the example news.
Here's how to reference files which were placed in the `downloads` folder: [Click me]({filename}/downloads/example_download_file.jpg)
Title: Impressum
slug: impressum
status: hidden
This page is treated differently by the `kreta`-theme. A link to it is placed inside the footer.
Title: Sample Page
attribute: 2
Some example page
# Credentials file
#-----------------
# copy to 'credentials.mk' and
# define hostnames, usernames, passwords etc that are needed for publishing
# inside that file
#FTP_HOST = server.org
#FTP_USER = username
#FTP_PASSWORD = password
#FTP_TARGET_DIR = /www
#!/usr/bin/env bash
##
# This section should match your Makefile
##
PY=${PY:-python}
PELICAN=${PELICAN:-pelican}
PELICANOPTS=
BASEDIR=$(pwd)
INPUTDIR=$BASEDIR/content
OUTPUTDIR=$BASEDIR/output
CONFFILE=$BASEDIR/pelicanconf.py
###
# Don't change stuff below here unless you are sure
###
SRV_PID=$BASEDIR/srv.pid
PELICAN_PID=$BASEDIR/pelican.pid
function usage(){
echo "usage: $0 (stop) (start) (restart) [port]"
echo "This starts Pelican in debug and reload mode and then launches"
echo "an HTTP server to help site development. It doesn't read"
echo "your Pelican settings, so if you edit any paths in your Makefile"
echo "you will need to edit your settings as well."
exit 3
}
function alive() {
kill -0 $1 >/dev/null 2>&1
}
function shut_down(){
PID=$(cat $SRV_PID)
if [[ $? -eq 0 ]]; then
if alive $PID; then
echo "Stopping HTTP server"
kill $PID
else
echo "Stale PID, deleting"
fi
rm $SRV_PID
else
echo "HTTP server PIDFile not found"
fi
PID=$(cat $PELICAN_PID)
if [[ $? -eq 0 ]]; then
if alive $PID; then
echo "Killing Pelican"
kill $PID
else
echo "Stale PID, deleting"
fi
rm $PELICAN_PID
else
echo "Pelican PIDFile not found"
fi
}
function start_up(){
local port=$1
echo "Starting up Pelican and HTTP server"
shift
$PELICAN --debug --autoreload -r $INPUTDIR -o $OUTPUTDIR -s $CONFFILE $PELICANOPTS &
pelican_pid=$!
echo $pelican_pid > $PELICAN_PID
cd $OUTPUTDIR
$PY -m pelican.server $port &
srv_pid=$!
echo $srv_pid > $SRV_PID
cd $BASEDIR
sleep 1
if ! alive $pelican_pid ; then
echo "Pelican didn't start. Is the Pelican package installed?"
return 1
elif ! alive $srv_pid ; then
echo "The HTTP server didn't start. Is there another service using port" $port "?"
return 1
fi
echo 'Pelican and HTTP server processes now running in background.'
}
###
# MAIN
###
[[ ($# -eq 0) || ($# -gt 2) ]] && usage
port=''
[[ $# -eq 2 ]] && port=$2
if [[ $1 == "stop" ]]; then
shut_down
elif [[ $1 == "restart" ]]; then
shut_down
start_up $port
elif [[ $1 == "start" ]]; then
if ! start_up $port; then
shut_down
fi
else
usage
fi
#!/usr/bin/env python
# -*- coding: utf-8 -*- #
from __future__ import unicode_literals
SITENAME = 'KRETA Dresden'
SITEURL = 'http://localhost:8000'
TIMEZONE = 'Europe/Paris'
DEFAULT_LANG = 'de'
PATH = 'content'
ARTICLE_PATHS = ['news']
PLUGIN_PATHS = ['plugins']
PLUGINS = ['events']
# Events
EVENT_DIR="events"
EVENT_URL="program/{slug}.html"
EVENT_SAVE_AS = 'program/{slug}.html'
EVENT_OVERVIEW_URL = "program.html"
EVENT_EXCLUDES=''
EVENT_ICAL_URL = 'kreta.ics'
EVENT_ICAL_TITLE = 'Kreta-Kalender'
DEFAULT_DATE = 'fs'
THEME = 'themes/kreta'
THEME_KRETA = {
'no_articles': "Es gibt noch keine News…",
'no_events': "Es sind noch keine Veranstaltungen bekannt…",
'ical_link': "Kalenderdatei (iCal)"
}
THEME_STATIC_DIR = 'theme'
PAGE_ORDER_BY = 'attribute'
# Include old website and downloads folder
STATIC_PATHS = ['legacy','downloads']
STATIC_EXCLUDE_SOURCES = True
# Clean output directory
DELETE_OUTPUT_DIRECTORY = True
# Remove all unused datatypes
CATEGORY_SAVE_AS = ''
CATEGORIES_SAVE_AS = ''
AUTHOR_SAVE_AS = ''
AUTHORS_SAVE_AS = ''
ARCHIVE_SAVE_AS = ''
ARCHIVES_SAVE_AS = ''
TAG_SAVE_AS = ''
TAGS_SAVE_AS = ''
# Feed generation is usually not desired when developing
FEED_ALL_ATOM = None
CATEGORY_FEED_ATOM = None
TRANSLATION_FEED_ATOM = None
AUTHOR_FEED_ATOM = None
AUTHOR_FEED_RSS = None
DEFAULT_PAGINATION = False
# Uncomment following line if you want document-relative URLs when developing
RELATIVE_URLS = True
#!/usr/bin/env python
from __future__ import print_function
from pelican import signals as core_signals
from .generators import EventsGenerator
def initEvents(generator):
events_dict = {}
try:
events = generator.context['events']
except KeyError:
generator.context['events'] = events = {}
def get_generators(generators):
return EventsGenerator
def register():
core_signals.get_generators.connect(get_generators)
from pelican.contents import Page
class Event(Page):
"""
Define all properties that create an event
"""
base_properties = ('starts','ends','title','short_title','location','location_link')
mandatory_properties = ('starts','ends','title','location')
default_template = 'event'
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function
import logging
from itertools import chain
from pelican.contents import is_valid_content
from pelican.utils import process_translations
from pelican.generators import CachingGenerator
import time
from datetime import datetime
from .contents import Event
from .signals import *
import os
import icalendar
import re
logger = logging.getLogger(__name__)
# In order for the bits below to work we need to define various signals. I'm
# not sure about the details because docs on blinker and how it is used here
# are hazy at best. It appears you simply define them here and use them in the
# code below. YMMV.
class EventsGenerator(CachingGenerator):
"""
Generate Events
This is the code responsible for finding and reading in the JSON files,
ending with ".event" and turning them into objects for Pelican to use in
generating HTML for them.
"""
def __init__(self, *args, **kwargs):
self.events = []
self.hidden_events = []
self.hidden_translations = []
super(EventsGenerator, self).__init__(*args, **kwargs)
event_generator_init.send(self)
self.content_name="Events"
def generate_report(self):
"""
This will report to stdout the number of events and hidden events processed.
"""
print( "Events: {0} published and {1} hidden in {2:0.2f} seconds".format( len(self.events), len(self.hidden_events), self.elapsed ) )
def parse_datetime(self, timestamp):
try:
return datetime.strptime(timestamp, '%Y-%m-%d %H:%M')
except Exception, e:
logger.error("Unable to parse datetime string '%s'" % (timestamp))
raise
def strip_tags(self, input):
"""
Remove all HTML tags
"""
return re.sub('<[^<]+?>', '', input)
def generate_context(self):
"""
Here is the meat of the class - where the heavy lifting occurs. It
generates a list of events and places them in the context object so we
can access them in templates.
Some of this is leftover from the stock Article class. Ideally those aspect
will be removed as it is shown they can be safely done away with.
However, this works.
"""
all_events = []
hidden_events = []
for f in self.get_files(
self.settings['EVENT_DIR'],
exclude=self.settings['EVENT_EXCLUDES']):
event = self.get_cached_data(f, None)
if event is None:
try:
event = self.readers.read_file(
base_path=self.path, path=f, content_class=Event,
context=self.context,
preread_signal=event_generator_preread,
preread_sender=self,
context_signal=event_generator_context,
context_sender=self)
except Exception as e:
logger.warning('Could not process {}\n{}'.format(f, e))
continue
if not is_valid_content(event, f):
continue
# convert start and end to datetime object
event.starts = self.parse_datetime(event.starts)
if event.ends is not None:
event.ends = self.parse_datetime(event.ends)
# short title defaults to title
if 'short_title' not in event.__dict__:
event.short_title = event.title
self.cache_data(f, event)
self.add_source_path(event)
if event.status == "published":
all_events.append(event)
elif event.status == "hidden":
hidden_events.append(event)
else:
logger.warning("Unknown status %s for file %s, skipping it." %
(repr(event.status),
repr(f)))
self.events, self.translations = process_translations(all_events)
self.hidden_events, self.hidden_translations = (
process_translations(hidden_events))
self._update_context(('events', ))
self.context['EVENTS'] = self.events
self.save_cache()
self.readers.save_cache()
event_generator_finalized.send(self)
def generate_ical(self, events):
"""
Generate ics calendar file content from given list of events
"""
cal = icalendar.Calendar()
cal.add('prodid', self.settings['EVENT_ICAL_TITLE'])
cal.add('version', '2.0')
for e in events:
event = icalendar.Event(summary=e.title)
event.add('dtstart', e.starts)
event.add('dtend', e.ends)
event.add('location', e.location)
event.add('description', self.strip_tags(e.content))
cal.add_component(event)
return cal.to_ical()
def generate_output(self, writer):
"""
Here we generate the HTML page form the event(s).
"""
start = time.time()
# Order events by starting time
self.events = sorted(self.events, key=lambda event: event.starts)
# Individual event pages
for event in chain(self.translations, self.events,
self.hidden_translations, self.hidden_events):
writer.write_file(
event.save_as, self.get_template(event.template),
self.context, event=event,
relative_urls=self.settings['RELATIVE_URLS'],
override_output=hasattr(event, 'override_save_as'))
# Group events into days
events_by_day = []
lastDay = None
this_day = None
for e in self.events:
if e.starts.date() != lastDay:
lastDay = e.starts.date()
if this_day is not None:
events_by_day.append(this_day)
this_day = dict()
this_day['events'] = []
this_day['day'] = e.starts.date()
this_day['events'].append(e)
if this_day:
events_by_day.append(this_day)
# Generate ical file
calendar_filename = None
if 'EVENT_ICAL_URL' in self.settings:
calendar_filename = self.settings['EVENT_ICAL_URL']
calendar_file_location = os.path.join(self.settings['OUTPUT_PATH'], calendar_filename)
with open(calendar_file_location, 'wb') as file:
file.write(self.generate_ical(self.events))
# Event overview page
writer.write_file(self.settings['EVENT_OVERVIEW_URL'], self.get_template('events'), self.context, events=self.events, events_by_day=events_by_day, calendar_filename=calendar_filename)
self.elapsed = time.time() - start
self.generate_report()
from blinker import signal
event_generator_init = signal('event_generator_init')
event_generator_finalized = signal('event_generator_finalized')
event_generator_preread = signal('event_generator_preread')
event_generator_context = signal('event_generator_context')
#!/usr/bin/env python
# -*- coding: utf-8 -*- #
from __future__ import unicode_literals
# This file is only used if you use `make publish` or
# explicitly specify it as your config file.
import os
import sys
sys.path.append(os.curdir)
from pelicanconf import *
SITEURL = 'https://kreta-dresden.org'
RELATIVE_URLS = False
Jinja2==2.8
Markdown==2.6.7
MarkupSafe==0.23
Pygments==2.1.3
Unidecode==0.04.19
argparse==1.2.1
blinker==1.4
docutils==0.13.1
feedgenerator==1.9
icalendar==3.11
pelican==3.6.3
python-dateutil==2.6.0
pytz==2016.10
six==1.10.0
wsgiref==0.1.2