Skip to content

Commit

Permalink
Add docs
Browse files Browse the repository at this point in the history
  • Loading branch information
orlnub123 committed Dec 12, 2018
1 parent b7278d2 commit 176a065
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .readthedocs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
version: 2

python:
install:
- path: docs/
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ pip install django-class-settings

## Resources

- Documentation: https://django-class-settings.readthedocs.io/
- Releases: https://pypi.org/project/django-class-settings/
- Changelog: https://github.com/orlnub123/django-class-settings/blob/master/CHANGELOG.md
- Code: https://github.com/orlnub123/django-class-settings
Expand Down
211 changes: 211 additions & 0 deletions docs/files/tutorial.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
# Tutorial

This tutorial aims to create a project from the ground up that utilizes the
example code shown in the [README][readme-example]. It assumes that you already
have Django installed.

## Installation

To get started we first need to install the library. This is easily
accomplished using pip:

```bash
pip install django-class-settings
```

Keep in mind that only Django 1.11+ and Python 3.4+ are supported.

## Starting a Project

Before getting started let's also create a new project for the tutorial. We're
going to use Django's startproject command:

```bash
django-admin startproject myproject
```

The resulting layout should look like this:

```
myproject/
manage.py
myproject/
__init__.py
settings.py
urls.py
wsgi.py
```

`myproject/settings.py` is the file we're mostly going to focus on. The next
section assumes that it's empty so let's empty it now.

## Creating the Settings

Now that we've got the library installed and started a project we can get to
the fun part, creating our settings.

First up we need to import the `Settings` class. It will be the foundation we
build our settings on. Open up your `myproject/settings.py` file and import it
like the below:

```python
from class_settings import Settings
```

Next, let's create the actual settings. We're going to subclass the `Settings`
class that we just imported and define all our settings inside of it:

```python
class MySettings(Settings):
SECRET_KEY = '*2#fz@c0w5fe8f-'
DEBUG = True
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
ROOT_URLCONF = 'myproject.urls'
WSGI_APPLICATION = 'myproject.wsgi.application'
```

For the tutorial, we've only populated our settings with the bare minimum. In
the real world you'd need much more than this, possibly with them even
scattered around in different classes.

## Setting It Up

If you tried to run the server now you would've noticed that an error pops up
talking about the SECRET_KEY setting being empty. What gives? We have it
defined right there under MySettings. Well, Django doesn't actually know that
it's supposed to look in MySettings. It's been searching for it in the
top-level namespace where it doesn't exist, hence the error. Let's fix that by
modifying the `manage.py` file a bit:

```diff
#!/usr/bin/env python
import os
import sys

+import class_settings
+
if __name__ == '__main__':
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
+ os.environ.setdefault('DJANGO_SETTINGS_CLASS', 'MySettings')
+ class_settings.setup()
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
```

Going through the changes step-by-step:

1. We import the library.
2. We default the DJANGO_SETTINGS_CLASS environment variable to MySettings.
3. We call `setup`.

The last change is where the magic happens. Internally `setup` imports the
module defined in DJANGO_SETTINGS_MODULE, creates an instance of the class
defined in DJANGO_SETTINGS_CLASS, and calls `django.configure` with it. This
lets Django know that our settings are defined in MySettings.

You'd also want to similarly change the `myproject/wsgi.py` file as it's used
instead of `manage.py` for production but I'll leave that as an exercise to the
reader.

## Configuring from the Environment

The setup we have so far is nice but what if you didn't want to hardcode local
settings, such as DATABASES, or if you're going to publish the project to some
kind of public repository, hide sensitive settings, such as SECRET_KEY? You
might've heard a bit about these problems in the config factor of
[The Twelve-Factor App][12factor-config]. Luckily the library provides an
easy-to-use environment variable parser.

Let's begin our _refactor_ by modifying the imports in `myproject/settings.py`:

```diff
-from class_settings import Settings
+from class_settings import Settings, env
```

Say we want to be able to enable DEBUG locally, making it fall back to the
reasonable default of `False` for production, and hide SECRET_KEY from prying
eyes. We'd do that by calling the newly imported `env`:

```diff
class MySettings(Settings):
- SECRET_KEY = '*2#fz@c0w5fe8f-'
- DEBUG = True
+ SECRET_KEY = env()
+ DEBUG = env(default=False)
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
ROOT_URLCONF = 'myproject.urls'
WSGI_APPLICATION = 'myproject.wsgi.application'
```

Now we can create a `.env` file with our desired environment:

```bash
export DJANGO_SECRET_KEY='*2#fz@c0w5fe8f-'
export DJANGO_DEBUG=true
```

And then modify `manage.py` to read from it:

```diff
#!/usr/bin/env python
import os
import sys

import class_settings
+from class_settings import env

if __name__ == '__main__':
+ env.read_env()
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
os.environ.setdefault('DJANGO_SETTINGS_CLASS', 'MySettings')
class_settings.setup()
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
```

And everything should work. This is great but wait, there's a problem. If we
explicitly set DJANGO_DEBUG to `false`, it still acts as if it were set to
`true`. The reason is that we didn't specify that we wanted a boolean. Let's
fix that by modifying our DEBUG like the below:

```diff
- DEBUG = env(default=False)
+ DEBUG = env.bool(default=False)
```

## Conclusion

Now that the tutorial is over and you've learned the basics you can (hopefully)
apply them to your own project.

[readme-example]: https://github.com/orlnub123/django-class-settings/blob/master/README.md#example
[12factor-config]: https://12factor.net/config
13 changes: 13 additions & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
site_name: django-class-settings
repo_url: https://github.com/orlnub123/django-class-settings
site_description: Effortless class-based settings for Django.
copyright: Copyright © 2018 orlnub123.
theme: readthedocs
docs_dir: files

nav:
- Index: index.md
- Tutorial: tutorial.md

plugins:
- index_generator
Empty file added docs/plugins/__init__.py
Empty file.
70 changes: 70 additions & 0 deletions docs/plugins/generators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import lxml.html
import markdown
from mkdocs.plugins import BasePlugin
from mkdocs.structure.files import File


class IndexGeneratorPlugin(BasePlugin):
"""A MkDocs plugin that generates the index.
Make sure it comes after any other file generators.
"""

def on_config(self, config):
self.config = config

def on_files(self, files, config):
self.files = files
self.exists = any(file.name == "index" for file in files)
if not self.exists:
files.append(
File(
"index.md",
config["docs_dir"],
config["site_dir"],
config["use_directory_urls"],
)
)

def on_page_read_source(self, _, page, config):
if self.exists or page.file.name != "index":
return
source_lines = []
with open("../README.md", encoding="utf-8") as file:
readme_html = markdown.markdown(file.read(), extensions=["mdx_partial_gfm"])
readme_root = lxml.html.fromstring(readme_html)
source_lines.append(self.get_title(readme_root))
source_lines.append(self.get_description(readme_root))

source_lines.append("## Table of Contents")
pages = [file.page for file in self.files.documentation_pages()]
for page in pages[: pages.index(page)]: # Exclude unpopulated pages
source_lines.append(f"### [{page.title}]({page.url})")
root = lxml.html.fromstring(page.content)
source_lines.append(self.get_description(root))
source_lines.append(self.get_toc(page.markdown))
return "\n".join(source_lines)

def get_title(self, root):
title_element = root.xpath("/html/div/h1[1]")[0]
return lxml.html.tostring(title_element, encoding="unicode")

def get_description(self, root):
description_elements = root.xpath(
"/html/div/h2[1]/preceding-sibling::*[not(self::h1)]"
)
return "\n".join(
lxml.html.tostring(element, encoding="unicode")
for element in description_elements
)

def get_toc(self, source):
toc_config = {"toc": {}}
md = markdown.Markdown(
extensions=self.config["markdown_extensions"], # toc is builtin
extension_configs={**self.config["mdx_configs"], **toc_config},
)
md.convert(source)
toc_root = lxml.html.fromstring(md.toc)
toc_element = toc_root.xpath("/html/body/div/ul/li[1]/ul")[0]
return lxml.html.tostring(toc_element, encoding="unicode")
11 changes: 11 additions & 0 deletions docs/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# TODO: Switch to pyproject.toml once pip implements PEP 517
from setuptools import find_packages, setup

setup(
name="docs",
packages=find_packages(),
install_requires=["mkdocs~=1.0", "markdown", "py-gfm"],
entry_points={
"mkdocs.plugins": ["index_generator = plugins.generators:IndexGeneratorPlugin"]
},
)

0 comments on commit 176a065

Please sign in to comment.