diff --git a/LICENSE b/LICENSE index 3f5a956..93baf29 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ django-medusa -Copyright (c) 2011-2012 Mike Tigas +Copyright (c) 2011-2016 Mike Tigas , Brandon Taylor All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/MANIFEST.in b/MANIFEST.in index 69b5763..6b9c7c9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ include LICENSE include MANIFEST.in -include README.markdown +include README.md diff --git a/README.markdown b/README.md similarity index 79% rename from README.markdown rename to README.md index 13bf8b5..869c1d9 100644 --- a/README.markdown +++ b/README.md @@ -19,7 +19,7 @@ process by rendering many views at once. **For those uninterested in the nitty-gritty**, there are tutorials/examples in the `docs` dir: -* [Tutorial 1: Hello World](https://github.com/mtigas/django-medusa/blob/master/docs/TUTORIAL-01.markdown) +* [Tutorial 1: Hello World](https://github.com/alsoicode/django-medusa/blob/master/docs/TUTORIAL-01.markdown) ## Renderer classes @@ -97,7 +97,6 @@ Or even: Example settings: INSTALLED_APPS = ( - # ... # ... 'django_medusa', ) @@ -115,7 +114,6 @@ Example settings: Example settings: INSTALLED_APPS = ( - # ... # ... 'django_medusa', ) @@ -143,7 +141,6 @@ application/json in the browser -- and will be accessible from "/foo/json/". Example settings: INSTALLED_APPS = ( - # ... # ... 'django_medusa', ) @@ -166,17 +163,52 @@ paths that don't have an extension and are *not* HTML files (i.e. "/foo/json/", "/feeds/blog/", etc.), the mimetype from the "Content-Type" HTTP header will be manually defined for this URL in the `app.yaml` path. +## Collecting static media +Django Medusa will collect static files for you after the static site code is generated if you add: + + MEDUSA_COLLECT_STATIC = True + +to your `settings.py`. Optionally, you may specify a list of patterns to exclude by adding: + + MEDUSA_COLLECT_STATIC_IGNORE = ['admin', 'less'] + +### Specifying the static media collection directory +By default, static files will be collected to the directory specified by `STATIC_ROOT`. If you wish to provide a different directory, you may do so via a django-medusa specific settings file, in which you can override `STATIC_ROOT`: + +Given the directory structure of: + + your_app/ + build/ <- MEDUSA_DEPLOY_DIRECTORY + + your_app/ + settings.py + medusa_settings.py + +and the following values in `medusa_settings.py`: + + import os + from .settings import * + + STATIC_ROOT = os.path.join(MEDUSA_DEPLOY_DIRECTORY, 'static') + +you can now run: + + $ python manage.py staticsitegen --settings=your_app.medusa_settings + +and static media will be collected to your django-medusa specific directory. + + ## Usage -1. Install `django-medusa` into your python path (TODO: setup.py) and add +1. Install `django-medusa` into your python path via pip: `$ pip install django-medusa` or download and run `python setup.py` and add `django_medusa` to `INSTALLED_APPS`. -2. Select a renderer backend (currently: disk or s3) in your settings. +2. Select a renderer backend (currently: disk or s3) and other options in your settings. 2. Create renderer classes in `renderers.py` under the apps you want to render. -3. `django-admin.py staticsitegen` -4. ??? +3. `django-admin.py staticsitegen` (optionally provide a specific settings file) +4. Deploy the static version of your site. 5. Profit! -#### Example +### Example From the first example in the "**Renderer classes**" section, using the disk-based backend. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..e5822b6 --- /dev/null +++ b/README.rst @@ -0,0 +1,275 @@ +django-medusa +============= + +Allows rendering a Django-powered website into a static website a la +*Jekyll*, *Movable Type*, or other static page generation CMSes or +frameworks. **django-medusa** is designed to be as simple as possible +and allow the easy(ish) conversion of existing dynamic Django-powered +websites -- nearly any existing Django site installation (not relying on +highly-dynamic content) can be converted into a static generator which +mirror's that site's output. + +Given a "renderer" that defines a set of URLs (see below), this uses +Django's built-in ``TestClient`` to render out those views to either +disk, Amazon S3, or to Google App Engine. + +At the moment, this likely does not scale to extremely large websites. + +Optionally utilizes the ``multiprocessing`` library to speed up the +rendering process by rendering many views at once. + +**For those uninterested in the nitty-gritty**, there are +tutorials/examples in the ``docs`` dir: + +- `Tutorial 1: Hello + World `__ + +Renderer classes +---------------- + +Renderers live in ``renderers.py`` in each ``INSTALLED_APP``. + +Simply subclassing the ``StaticSiteRenderer`` class and defining +``get_paths`` works: + +:: + + from django_medusa.renderers import StaticSiteRenderer + + class HomeRenderer(StaticSiteRenderer): + def get_paths(self): + return frozenset([ + "/", + "/about/", + "/sitemap.xml", + ]) + + renderers = [HomeRenderer, ] + +A more complex example: + +:: + + from django_medusa.renderers import StaticSiteRenderer + from myproject.blog.models import BlogPost + + + class BlogPostsRenderer(StaticSiteRenderer): + def get_paths(self): + paths = ["/blog/", ] + + items = BlogPost.objects.filter(is_live=True).order_by('-pubdate') + for item in items: + paths.append(item.get_absolute_url()) + + return paths + + renderers = [BlogPostsRenderer, ] + +Or even: + +:: + + from django_medusa.renderers import StaticSiteRenderer + from myproject.blog.models import BlogPost + from django.core.urlresolvers import reverse + + + class BlogPostsRenderer(StaticSiteRenderer): + def get_paths(self): + # A "set" so we can throw items in blindly and be guaranteed that + # we don't end up with dupes. + paths = set(["/blog/", ]) + + items = BlogPost.objects.filter(is_live=True).order_by('-pubdate') + for item in items: + # BlogPost detail view + paths.add(item.get_absolute_url()) + + # The generic date-based list views. + paths.add(reverse('blog:archive_day', args=( + item.pubdate.year, item.pubdate.month, item.pubdate.day + ))) + paths.add(reverse('blog:archive_month', args=( + item.pubdate.year, item.pubdate.month + ))) + paths.add(reverse('blog:archive_year', args=(item.pubdate.year,))) + + # Cast back to a list since that's what we're expecting. + return list(paths) + + renderers = [BlogPostsRenderer, ] + +Renderer backends +----------------- + +Disk-based static site renderer +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Example settings: + +:: + + INSTALLED_APPS = ( + # ... + 'django_medusa', + ) + # ... + MEDUSA_RENDERER_CLASS = "django_medusa.renderers.DiskStaticSiteRenderer" + MEDUSA_MULTITHREAD = True + MEDUSA_DEPLOY_DIR = os.path.abspath(os.path.join( + REPO_DIR, + 'var', + "html" + )) + +S3-based site renderer +~~~~~~~~~~~~~~~~~~~~~~ + +Example settings: + +:: + + INSTALLED_APPS = ( + # ... + 'django_medusa', + ) + # ... + MEDUSA_RENDERER_CLASS = "django_medusa.renderers.S3StaticSiteRenderer" + MEDUSA_MULTITHREAD = True + AWS_ACCESS_KEY = "" + AWS_SECRET_ACCESS_KEY = "" + MEDUSA_AWS_STORAGE_BUCKET_NAME = "" # (also accepts AWS_STORAGE_BUCKET_NAME) + +Be aware that the S3 renderer will overwrite any existing files that +match URL paths in your site. + +The S3 backend will force "index.html" to be the Default Root Object for +each directory, so that "/about/" would actually be uploaded as +"/about/index.html", but properly loaded by the browser at the "/about/" +URL. + +**BONUS:** Additionally, the S3 renderer keeps the "Content-Type" HTTP +header that the view returns: if "/foo/json/" returns a JSON file +(application/json), the file will be uploaded to "/foo/json/index.html" +but will be served as application/json in the browser -- and will be +accessible from "/foo/json/". + +App Engine-based site renderer +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Example settings: + +:: + + INSTALLED_APPS = ( + # ... + 'django_medusa', + ) + # ... + MEDUSA_RENDERER_CLASS = "django_medusa.renderers.GAEStaticSiteRenderer" + MEDUSA_MULTITHREAD = True + MEDUSA_DEPLOY_DIR = os.path.abspath(os.path.join( + REPO_DIR, + 'var', + "html" + )) + GAE_APP_ID = "" + +This generates a ``app.yaml`` file and a ``deploy`` directory in your +``MEDUSA_DEPLOY_DIR``. The ``app.yaml`` file contains the URL mappings +to upload the entire site as a static files. + +App Engine generally follows filename extensions as the mimetype. If you +have paths that don't have an extension and are *not* HTML files (i.e. +"/foo/json/", "/feeds/blog/", etc.), the mimetype from the +"Content-Type" HTTP header will be manually defined for this URL in the +``app.yaml`` path. + +Collecting static media +----------------------- + +Django Medusa will collect static files for you after the static site +code is generated if you add: + +:: + + MEDUSA_COLLECT_STATIC = True + +to your ``settings.py``. Optionally, you may specify a list of patterns +to exclude by adding: + +:: + + MEDUSA_COLLECT_STATIC_IGNORE = ['admin', 'less'] + +Specifying the static media collection directory +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, static files will be collected to the directory specified by +``STATIC_ROOT``. If you wish to provide a different directory, you may +do so via a django-medusa specific settings file, in which you can +override ``STATIC_ROOT``: + +Given the directory structure of: + +:: + + your_app/ + build/ <- MEDUSA_DEPLOY_DIRECTORY + + your_app/ + settings.py + medusa_settings.py + +and the following values in ``medusa_settings.py``: + +:: + + import os + from .settings import * + + STATIC_ROOT = os.path.join(MEDUSA_DEPLOY_DIRECTORY, 'static') + +you can now run: + +:: + + $ python manage.py staticsitegen --settings=your_app.medusa_settings + +and static media will be collected to your django-medusa specific +directory. + +Usage +----- + +1. Install ``django-medusa`` into your python path via pip: + ``$ pip install django-medusa`` or download and run + ``python setup.py`` and add ``django_medusa`` to ``INSTALLED_APPS``. +2. Select a renderer backend (currently: disk or s3) and other options + in your settings. +3. Create renderer classes in ``renderers.py`` under the apps you want + to render. +4. ``django-admin.py staticsitegen`` (optionally provide a specific + settings file) +5. Deploy the static version of your site. +6. Profit! + +Example +~~~~~~~ + +From the first example in the "**Renderer classes**" section, using the +disk-based backend. + +:: + + $ django-admin.py staticsitegen + Found renderers for 'myproject'... + Skipping app 'django.contrib.syndication'... (No 'renderers.py') + Skipping app 'django.contrib.sitemaps'... (No 'renderers.py') + Skipping app 'typogrify'... (No 'renderers.py') + + Generating with up to 8 processes... + /project_dir/var/html/index.html + /project_dir/var/html/about/index.html + /project_dir/var/html/sitemap.xml diff --git a/django_medusa/__init__.py b/django_medusa/__init__.py index c359ad9..d79aca3 100644 --- a/django_medusa/__init__.py +++ b/django_medusa/__init__.py @@ -1,9 +1,20 @@ -VERSION = (0, 3, 0) - -def get_version(): - version = '%s.%s' % (VERSION[0], VERSION[1]) - if VERSION[2]: - version = '%s.%s' % (version, VERSION[2]) - if VERSION[3:]: - version = '%s.%s' % (version, VERSION[3]) - return version +from pkg_resources import get_distribution, DistributionNotFound +import os.path + + +try: + _dist = get_distribution('django_medusa') + + # Normalize case for Windows systems + dist_loc = os.path.normcase(_dist.location) + here = os.path.normcase(__file__) + + if not here.startswith(os.path.join(dist_loc, 'django_medusa')): + # not installed, but there is another version that *is* + raise DistributionNotFound + +except DistributionNotFound: + __version__ = 'Please install this project with setup.py' + +else: + __version__ = _dist.version diff --git a/django_medusa/management/commands/staticsitegen.py b/django_medusa/management/commands/staticsitegen.py index a592b8e..7a0544e 100644 --- a/django_medusa/management/commands/staticsitegen.py +++ b/django_medusa/management/commands/staticsitegen.py @@ -1,6 +1,10 @@ from django.core.management.base import BaseCommand +from django.core.management import call_command + from django_medusa.renderers import StaticSiteRenderer from django_medusa.utils import get_static_renderers +from django_medusa.settings import (MEDUSA_COLLECT_STATIC_IGNORE, + MEDUSA_COLLECT_STATIC) class Command(BaseCommand): @@ -17,3 +21,8 @@ def handle(self, *args, **options): r.generate() StaticSiteRenderer.finalize_output() + + if MEDUSA_COLLECT_STATIC: + # collect static media for deployment + call_command('collectstatic', interactive=False, + ignore_patterns=MEDUSA_COLLECT_STATIC_IGNORE) diff --git a/django_medusa/renderers/__init__.py b/django_medusa/renderers/__init__.py index 2200369..3f9ec5e 100644 --- a/django_medusa/renderers/__init__.py +++ b/django_medusa/renderers/__init__.py @@ -1,5 +1,11 @@ from django.conf import settings -from django.utils import importlib + +try: + #For Django Versions < 1.9 + from django.utils import importlib +except ImportError: + #For Django versions >= 1.9 + import importlib from .base import BaseStaticSiteRenderer from .disk import DiskStaticSiteRenderer from .appengine import GAEStaticSiteRenderer diff --git a/django_medusa/renderers/appengine.py b/django_medusa/renderers/appengine.py index 59c1675..5251487 100644 --- a/django_medusa/renderers/appengine.py +++ b/django_medusa/renderers/appengine.py @@ -40,7 +40,10 @@ def _gae_render_path(args): resp = client.get(path) if resp.status_code != 200: - raise Exception + raise Exception( + "Request to %s produced response code %d" % + (path, resp.status_code) + ) mimetype = resp['Content-Type'].split(";", 1)[0] @@ -56,7 +59,7 @@ def _gae_render_path(args): "" ) if ((not needs_ext) and path.endswith(STANDARD_EXTENSIONS))\ - or (mimetype == "text/html"): + or (mimetype == "text/html"): # Either has obvious extension OR it's a regular HTML file. return None return "# req since this url does not end in an extension and also\n"\ diff --git a/django_medusa/renderers/disk.py b/django_medusa/renderers/disk.py index 76821e5..b225d89 100644 --- a/django_medusa/renderers/disk.py +++ b/django_medusa/renderers/disk.py @@ -36,7 +36,10 @@ def _disk_render_path(args): resp = client.get(path) if resp.status_code != 200: - raise Exception + raise Exception( + "Request to %s produced response code %d" % + (path, resp.status_code) + ) if needs_ext: mime = resp['Content-Type'] mime = mime.split(';', 1)[0] @@ -69,7 +72,7 @@ def generate(self): print("Generating with up to %d processes..." % cpu_count()) pool = Pool(cpu_count()) - pool.map_async( + pool.map( _disk_render_path, ((None, path, None) for path in self.paths), chunksize=5 diff --git a/django_medusa/renderers/s3.py b/django_medusa/renderers/s3.py index f5c98d8..b85554f 100644 --- a/django_medusa/renderers/s3.py +++ b/django_medusa/renderers/s3.py @@ -36,7 +36,10 @@ def _get_bucket(): aws_access_key_id=settings.AWS_ACCESS_KEY, aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY ) - bucket = (settings.MEDUSA_AWS_STORAGE_BUCKET_NAME if settings.MEDUSA_AWS_STORAGE_BUCKET_NAME else settings.AWS_STORAGE_BUCKET_NAME) + + bucket = settings.AWS_STORAGE_BUCKET_NAME + if settings.MEDUSA_AWS_STORAGE_BUCKET_NAME: + bucket = settings.MEDUSA_AWS_STORAGE_BUCKET_NAME return conn.get_bucket(bucket) @@ -68,7 +71,10 @@ def _s3_render_path(args): # Render the view resp = client.get(path) if resp.status_code != 200: - raise Exception + raise Exception( + "Request to %s produced response code %d" % + (path, resp.status_code) + ) # Default to "index.html" as the upload path if we're in a dir listing. outpath = path @@ -131,7 +137,10 @@ def generate(self): aws_access_key_id=settings.AWS_ACCESS_KEY, aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY ) - self.bucket = (self.conn.get_bucket(settings.MEDUSA_AWS_STORAGE_BUCKET_NAME) if settings.MEDUSA_AWS_STORAGE_BUCKET_NAME else self.conn.get_bucket(settings.AWS_STORAGE_BUCKET_NAME)) + bucket_name = settings.AWS_STORAGE_BUCKET_NAME + if settings.MEDUSA_AWS_STORAGE_BUCKET_NAME: + bucket_name = settings.MEDUSA_AWS_STORAGE_BUCKET_NAME + self.conn.get_bucket(bucket_name) self.bucket.configure_website("index.html", "500.html") self.server_root_path = self.bucket.get_website_endpoint() diff --git a/django_medusa/settings.py b/django_medusa/settings.py new file mode 100644 index 0000000..fcdc36a --- /dev/null +++ b/django_medusa/settings.py @@ -0,0 +1,8 @@ +from django.conf import settings + + +MEDUSA_COLLECT_STATIC_IGNORE = getattr(settings, + 'MEDUSA_COLLECTSTATIC_IGNORE', []) + +MEDUSA_COLLECT_STATIC = getattr(settings, + 'MEDUSA_COLLECT_STATIC', False) diff --git a/django_medusa/utils.py b/django_medusa/utils.py index e276d12..fd814a5 100644 --- a/django_medusa/utils.py +++ b/django_medusa/utils.py @@ -1,9 +1,10 @@ from __future__ import print_function import imp -from django.conf import settings from importlib import import_module import sys +from django.conf import settings + def get_static_renderers(): module_name = 'renderers' @@ -45,7 +46,7 @@ def get_static_renderers(): if hasattr(app_render_module, "renderers"): renderers += getattr(app_render_module, module_name) else: - print("Skipping app '%s'... ('%s.renderers' does not contain "\ + print("Skipping app '%s'... ('%s.renderers' does not contain " "'renderers' var (list of render classes)" % (app, app)) except AttributeError: print("Skipping app '%s'... (Error importing '%s.renderers')" % ( diff --git a/setup.py b/setup.py index dd60c7b..bcd5c1d 100644 --- a/setup.py +++ b/setup.py @@ -1,26 +1,33 @@ from setuptools import setup, find_packages -install_requires = [ - 'django', -] +try: + README = open('README.md').read() +except: + README = None -version = "0.3.0" - -setup(name='django-medusa', - version=version, +setup( + name='django-medusa', + version='0.3.2', description='A Django static website generator.', - author='Mike Tigas', # update this as needed - author_email='mike@tig.as', # update this as needed - url='https://github.com/mtigas/django-medusa/', - download_url='https://github.com/mtigas/django-medusa/releases/tag/v0.3.0', + include_package_data=True, + author='Brandon Taylor', # update this as needed + author_email='alsoicode@gmail.com', # update this as needed + url='https://github.com/alsoicode/django-medusa', + download_url='https://github.com/alsoicode/django-medusa', packages=find_packages(), - install_requires=install_requires, - license='MIT', + install_requires=['django'], + license='APL', + long_description=README, keywords='django static staticwebsite staticgenerator publishing', - classifiers=["Development Status :: 3 - Alpha", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Topic :: Software Development :: Libraries :: Python Modules" + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Environment :: Web Environment', + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 3' ], + zip_safe=False )