Add missing docstrings

main
Jaap Joris Vens 2024-12-10 23:05:36 +01:00
rodzic 6f02a846a2
commit 6781532448
15 zmienionych plików z 253 dodań i 59 usunięć

Wyświetl plik

@ -1,15 +1,22 @@
"""
Main entry point.
"""
import argparse import argparse
import os import os
import re import re
import shutil import shutil
import cms import cms
from pip._internal.operations import freeze as pip
import example import example
def main(): def main():
"""
Ask the user to confirm, then call create_project().
"""
parser = argparse.ArgumentParser(description="SimpleCMS") parser = argparse.ArgumentParser(description="SimpleCMS")
parser.add_argument("project_name", nargs="?", default=".") parser.add_argument("project_name", nargs="?", default=".")
project_name = parser.parse_args().project_name project_name = parser.parse_args().project_name
@ -31,12 +38,13 @@ def main():
def create_project(project_name, project_dir): def create_project(project_name, project_dir):
"""
Populate project directory with a minimal, working project.
"""
os.makedirs(project_dir, exist_ok=True) os.makedirs(project_dir, exist_ok=True)
with open(os.path.join(project_dir, "requirements.txt"), "w") as f: with open(os.path.join(project_dir, "requirements.txt"), "w") as f:
for line in pip.freeze(): print(f"django-simplecms=={cms.__version__}", file=f)
if "django_simplecms" in line:
line = f"django-simplecms=={cms.__version__}"
print(line, file=f)
example_dir = os.path.dirname(example.__file__) example_dir = os.path.dirname(example.__file__)
app_dir = os.path.join(project_dir, project_name) app_dir = os.path.join(project_dir, project_name)
@ -49,7 +57,7 @@ def create_project(project_name, project_dir):
"w", "w",
) as f: ) as f:
print( print(
f"""#!/usr/bin/env python f"""#!/usr/bin/env python3
import os import os
import sys import sys
@ -74,7 +82,7 @@ application = get_wsgi_application()""",
print( print(
f""" f"""
Successfully created project "{project_name}" Successfully created project "{project_name}"!
Things to do next: Things to do next:
- create a database - create a database

Wyświetl plik

@ -1,11 +1,21 @@
"""
Metadata for the application registry.
"""
from django.apps import AppConfig from django.apps import AppConfig
from django.utils.module_loading import autodiscover_modules from django.utils.module_loading import autodiscover_modules
from django.utils.translation import gettext_lazy as _
class CmsConfig(AppConfig): class CmsConfig(AppConfig):
"""
The `cms` app.
"""
name = "cms" name = "cms"
verbose_name = _("Content Management System")
def ready(self): def ready(self):
"""
When ready, populate the section type registry.
"""
autodiscover_modules("views") autodiscover_modules("views")

Wyświetl plik

@ -1,20 +1,33 @@
"""
Decorators.
"""
from cms import registry from cms import registry
def page_model(cls): def page_model(cls):
"""Decorator to register the Page model""" """
Decorator to register the Page model.
"""
registry.page_class = cls registry.page_class = cls
return cls return cls
def section_model(cls): def section_model(cls):
"""Decorator to register the Section model""" """
Decorator to register the Section model.
"""
registry.section_class = cls registry.section_class = cls
return cls return cls
def section_view(cls): def section_view(cls):
"""Decorator to register a view for a specific section""" """
Decorator to register a view for a specific section.
"""
registry.view_per_type[cls.__name__.lower()] = cls registry.view_per_type[cls.__name__.lower()] = cls
registry.section_types.append((cls.__name__.lower(), cls.verbose_name)) registry.section_types.append((cls.__name__.lower(), cls.verbose_name))
return cls return cls

Wyświetl plik

@ -1,3 +1,7 @@
"""
Commonly used Django model fields.
"""
from django.db import models from django.db import models
from django.forms import TextInput from django.forms import TextInput
@ -5,9 +9,15 @@ from .mixins import EasilyMigratable
class CharField(EasilyMigratable, models.TextField): class CharField(EasilyMigratable, models.TextField):
"""Variable width CharField.""" """
Variable width CharField.
"""
def formfield(self, **kwargs): def formfield(self, **kwargs):
"""
Use TextInput instead of the default TextArea.
"""
if not self.choices: if not self.choices:
kwargs.update({"widget": TextInput}) kwargs.update({"widget": TextInput})
return super().formfield(**kwargs) return super().formfield(**kwargs)
@ -58,6 +68,10 @@ class ImageField(EasilyMigratable, models.ImageField):
class ForeignKey(EasilyMigratable, models.ForeignKey): class ForeignKey(EasilyMigratable, models.ForeignKey):
"""
A foreign key that does not create a reverse relation by default.
"""
def __init__(self, *args, related_name="+", **kwargs): def __init__(self, *args, related_name="+", **kwargs):
super().__init__( super().__init__(
*args, *args,

Wyświetl plik

@ -1,3 +1,7 @@
"""
Some not-so-simple forms.
"""
from urllib.parse import quote from urllib.parse import quote
from django import forms from django import forms
@ -8,6 +12,10 @@ from . import registry
class PageForm(forms.ModelForm): class PageForm(forms.ModelForm):
"""
Form to edit a page, including all its sections.
"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.label_suffix = "" self.label_suffix = ""
@ -50,6 +58,10 @@ class PageForm(forms.ModelForm):
class SectionForm(forms.ModelForm): class SectionForm(forms.ModelForm):
"""
Form to edit a section.
"""
type = forms.ChoiceField() type = forms.ChoiceField()
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

Wyświetl plik

@ -1,3 +1,7 @@
"""
Optional but useful middleware classes.
"""
import os import os
from django.conf import settings from django.conf import settings
@ -5,18 +9,11 @@ from django.middleware import cache
from sass import compile from sass import compile
def locate(filename):
for path, dirs, files in os.walk(os.getcwd(), followlinks=True):
for f in files:
if f == filename:
yield os.path.join(path, filename)
class FetchFromCacheMiddleware(cache.FetchFromCacheMiddleware): class FetchFromCacheMiddleware(cache.FetchFromCacheMiddleware):
"""Minor change to the original middleware that prevents caching of """
Minor change to the original middleware that prevents caching of
requests that have a `sessionid` cookie. This should be the requests that have a `sessionid` cookie. This should be the
Django default, IMHO. Django default, IMHO.
""" """
def process_request(self, request): def process_request(self, request):
@ -25,9 +22,11 @@ class FetchFromCacheMiddleware(cache.FetchFromCacheMiddleware):
class SassMiddleware: class SassMiddleware:
"""Simple SASS middleware that intercepts requests for .css files and """
SASS middleware that intercepts requests for .css files and
tries to compile the corresponding SCSS file. tries to compile the corresponding SCSS file.
In production this does nothing, so commit your files!
""" """
def __init__(self, get_response): def __init__(self, get_response):
@ -53,3 +52,14 @@ class SassMiddleware:
response = self.get_response(request) response = self.get_response(request)
return response return response
def locate(filename):
"""
Locate a file beneath the current directory.
"""
for path, dirs, files in os.walk(os.getcwd(), followlinks=True):
for f in files:
if f == filename:
yield os.path.join(path, filename)

Wyświetl plik

@ -1,3 +1,8 @@
"""
Some handy mixins.
"""
class EasilyMigratable: class EasilyMigratable:
""" """
Mixin for model fields. Prevents the generation of migrations that Mixin for model fields. Prevents the generation of migrations that
@ -37,7 +42,9 @@ class Numbered:
return self.__class__._meta.ordering[-1].lstrip("-") return self.__class__._meta.ordering[-1].lstrip("-")
def _renumber(self): def _renumber(self):
"""Renumbers the queryset while preserving the instance's number""" """
Renumber the queryset while preserving the instance's number.
"""
queryset = self.number_with_respect_to() queryset = self.number_with_respect_to()
field_name = self.get_field_name() field_name = self.get_field_name()
@ -48,7 +55,7 @@ class Numbered:
# The algorithm: loop over the queryset and set each object's # The algorithm: loop over the queryset and set each object's
# number to the counter. When an object's number equals the # number to the counter. When an object's number equals the
# number of this instance, set this instance's number to the # number of this instance, set this instance's number to the
# counter, increment the counter by 1, and finish the loop # counter, increment the counter by 1, and finish the loop.
counter = 1 counter = 1
inserted = False inserted = False
for other in queryset.exclude(pk=self.pk): for other in queryset.exclude(pk=self.pk):

Wyświetl plik

@ -1,3 +1,7 @@
"""
Base page and section models.
"""
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.text import slugify from django.utils.text import slugify
@ -8,7 +12,9 @@ from . import fields, mixins
class BasePage(mixins.Numbered, models.Model): class BasePage(mixins.Numbered, models.Model):
"""Abstract base model for pages.""" """
Abstract base model for pages.
"""
title = fields.CharField(_("page")) title = fields.CharField(_("page"))
slug = fields.SlugField(_("slug"), blank=True, unique=True) slug = fields.SlugField(_("slug"), blank=True, unique=True)
@ -33,7 +39,9 @@ class BasePage(mixins.Numbered, models.Model):
class BaseSection(mixins.Numbered, models.Model): class BaseSection(mixins.Numbered, models.Model):
"""Abstract base model for sections""" """
Abstract base model for sections.
"""
TYPES = [] TYPES = []
title = fields.CharField(_("section")) title = fields.CharField(_("section"))

Wyświetl plik

@ -1,3 +1,7 @@
"""
Registry that is populated at startup time by the decorators.
"""
page_class = None page_class = None
section_class = None section_class = None
section_types = [] section_types = []
@ -5,14 +9,29 @@ view_per_type = {}
def get_types(): def get_types():
"""
Return the available section types as tuples to be used for
form field choices.
"""
return section_types return section_types
def get_view(section, request): def get_view(section, request):
"""
Given a section instance and a request, return the view class
that is registered to render that section.
"""
return view_per_type[section.type](request) return view_per_type[section.type](request)
def get_fields_per_type(): def get_fields_per_type():
"""
Return a dictionary with the editable fields of each section.
This is used by the JS to show the the relevant form fields.
"""
fields_per_type = {} fields_per_type = {}
for name, view in view_per_type.items(): for name, view in view_per_type.items():
fields_per_type[name] = ["title", "type", "number"] + view.fields fields_per_type[name] = ["title", "type", "number"] + view.fields

Wyświetl plik

@ -1,3 +1,7 @@
"""
URLs.
"""
from django.urls import path from django.urls import path
from .views import CreatePage, CreateSection, PageView, UpdatePage, UpdateSection from .views import CreatePage, CreateSection, PageView, UpdatePage, UpdateSection

Wyświetl plik

@ -1,3 +1,8 @@
"""
Some simple section views, as well as the "real" Django views that
make the simple views possible.
"""
import json import json
from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth.mixins import UserPassesTestMixin
@ -17,7 +22,9 @@ from .forms import ContactForm, PageForm, SectionForm
class SectionView: class SectionView:
"""Generic section view""" """
Generic section view.
"""
template_name = "cms/sections/section.html" template_name = "cms/sections/section.html"
@ -29,7 +36,9 @@ class SectionView:
class SectionFormView(SectionView): class SectionFormView(SectionView):
"""Generic section with associated form""" """
Generic section with associated form.
"""
form_class = None form_class = None
success_url = None success_url = None
@ -60,28 +69,38 @@ class SectionFormView(SectionView):
class ContactSectionFormView(SectionFormView): class ContactSectionFormView(SectionFormView):
"""Contact section with bogus contact form""" """
Generic section with a contact form.
"""
form_class = ContactForm form_class = ContactForm
def form_valid(self, form): def form_valid(self, form):
response = HttpResponse(status=302) response = HttpResponse(status=302)
response["Location"] = form.save(self.object.href) response["Location"] = form.save(self.object.href, self.object.subject)
return response return response
class PageView(detail.DetailView): class PageView(detail.DetailView):
"""View of a page with heterogeneous sections""" """
View of a page with heterogeneous sections.
"""
model = registry.page_class model = registry.page_class
template_name = "cms/page.html" template_name = "cms/page.html"
def setup(self, *args, slug="", **kwargs): def setup(self, *args, slug="", **kwargs):
"""Supply a default argument for slug""" """
Supply a default argument for slug.
"""
super().setup(*args, slug=slug, **kwargs) super().setup(*args, slug=slug, **kwargs)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
"""Instantiate section views and render final response""" """
Instantiate section views and render final response.
"""
try: try:
page = self.object = self.get_object() page = self.object = self.get_object()
except Http404: except Http404:
@ -98,6 +117,7 @@ class PageView(detail.DetailView):
return redirect("cms:updatepage", slug=self.kwargs["slug"]) return redirect("cms:updatepage", slug=self.kwargs["slug"])
else: else:
raise raise
context = self.get_context_data(**kwargs) context = self.get_context_data(**kwargs)
sections = page.sections.all() sections = page.sections.all()
context.update( context.update(
@ -109,7 +129,10 @@ class PageView(detail.DetailView):
return self.render_to_response(context) return self.render_to_response(context)
def post(self, request, **kwargs): def post(self, request, **kwargs):
"""Call the post() method of the correct section view""" """
Call the post() method of the correct section view.
"""
try: try:
pk = int(self.request.POST.get("section")) pk = int(self.request.POST.get("section"))
except Exception: except Exception:
@ -150,45 +173,65 @@ class PageView(detail.DetailView):
class EditPage( class EditPage(
UserPassesTestMixin, edit.ModelFormMixin, base.TemplateResponseMixin, base.View UserPassesTestMixin, edit.ModelFormMixin, base.TemplateResponseMixin, base.View
): ):
"""Base view with nested forms for editing the page and all its sections""" """
Base view with nested forms for editing the page and all its sections.
"""
model = registry.page_class model = registry.page_class
form_class = PageForm form_class = PageForm
template_name = "cms/edit.html" template_name = "cms/edit.html"
def test_func(self): def test_func(self):
"""Only allow users with the correct permissions""" """
Only allow users with the correct permissions.
"""
app_label = registry.page_class._meta.app_label app_label = registry.page_class._meta.app_label
model_name = registry.page_class._meta.model_name model_name = registry.page_class._meta.model_name
return self.request.user.has_perm(f"{app_label}.change_{model_name}") return self.request.user.has_perm(f"{app_label}.change_{model_name}")
def get_form_kwargs(self): def get_form_kwargs(self):
"""Set the default slug to the current URL for new pages""" """
Set the default slug to the current URL for new pages.
"""
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
if "slug" in self.kwargs: if "slug" in self.kwargs:
kwargs.update({"initial": {"slug": self.kwargs["slug"]}}) kwargs.update({"initial": {"slug": self.kwargs["slug"]}})
return kwargs return kwargs
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Populate the fields_per_type dict for use in javascript""" """
Populate the fields_per_type dict for use in JS.
"""
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["fields_per_type"] = json.dumps(registry.get_fields_per_type()) context["fields_per_type"] = json.dumps(registry.get_fields_per_type())
return context return context
def get_object(self): def get_object(self):
"""Prevent 404 by serving the new object form""" """
Prevent 404 by serving the new object form.
"""
try: try:
return super().get_object() return super().get_object()
except Http404: except Http404:
return None return None
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
"""Handle GET requests""" """
Handle GET requests.
"""
self.object = self.get_object() self.object = self.get_object()
return self.render_to_response(self.get_context_data(**kwargs)) return self.render_to_response(self.get_context_data(**kwargs))
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
"""Handle POST requests""" """
Handle POST requests.
"""
self.object = self.get_object() self.object = self.get_object()
form = self.get_form() form = self.get_form()
@ -201,26 +244,37 @@ class EditPage(
class CreatePage(EditPage): class CreatePage(EditPage):
"""View for creating new pages""" """
View for creating new pages.
"""
def get_object(self): def get_object(self):
return registry.page_class() return registry.page_class()
class UpdatePage(EditPage): class UpdatePage(EditPage):
"""View for editing existing pages""" """
View for editing existing pages.
"""
@method_decorator(never_cache, name="dispatch") @method_decorator(never_cache, name="dispatch")
class EditSection( class EditSection(
UserPassesTestMixin, edit.ModelFormMixin, base.TemplateResponseMixin, base.View UserPassesTestMixin, edit.ModelFormMixin, base.TemplateResponseMixin, base.View
): ):
"""
Base view to edit a specific section.
"""
model = registry.section_class model = registry.section_class
form_class = SectionForm form_class = SectionForm
template_name = "cms/edit.html" template_name = "cms/edit.html"
def test_func(self): def test_func(self):
"""Only allow users with the correct permissions""" """
Only allow users with the correct permissions.
"""
app_label = registry.section_class._meta.app_label app_label = registry.section_class._meta.app_label
model_name = registry.section_class._meta.model_name model_name = registry.section_class._meta.model_name
return self.request.user.has_perm(f"{app_label}.change_{model_name}") return self.request.user.has_perm(f"{app_label}.change_{model_name}")
@ -273,9 +327,15 @@ class EditSection(
class CreateSection(EditSection): class CreateSection(EditSection):
"""
View for creating new sections.
"""
def get_section(self): def get_section(self):
return registry.section_class(page=self.page) return registry.section_class(page=self.page)
class UpdateSection(EditSection): class UpdateSection(EditSection):
pass """
View for editing existing sections.
"""

Wyświetl plik

@ -1,3 +1,7 @@
"""
Page and section models.
"""
from cms.decorators import page_model, section_model from cms.decorators import page_model, section_model
from cms.models import BasePage, BaseSection from cms.models import BasePage, BaseSection
from django.db import models from django.db import models

Wyświetl plik

@ -1,3 +1,8 @@
"""
Django settings file. For the available options, see:
https://docs.djangoproject.com/en/stable/ref/settings/
"""
import os import os
import random import random
import string import string
@ -84,6 +89,7 @@ DATABASES = {
"NAME": PROJECT_NAME, "NAME": PROJECT_NAME,
} }
} }
CACHES = { CACHES = {
"default": { "default": {
"BACKEND": "django.core.cache.backends.memcached.PyLibMCCache", "BACKEND": "django.core.cache.backends.memcached.PyLibMCCache",

Wyświetl plik

@ -1,20 +1,19 @@
"""
URLs.
"""
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import include, path from django.urls import include, path
from django.views.generic import RedirectView
admin.site.site_header = admin.site.site_title = settings.PROJECT_NAME.replace( urlpatterns = (
"_", " " staticfiles_urlpatterns()
).title() + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns = staticfiles_urlpatterns() + static( + [
settings.MEDIA_URL, document_root=settings.MEDIA_ROOT path("admin/", admin.site.urls),
path("accounts/", include("django.contrib.auth.urls")),
path("", include("cms.urls", namespace="cms")),
]
) )
urlpatterns += [
path("admin/", admin.site.urls),
path("accounts/", include("django.contrib.auth.urls")),
path("login/", RedirectView.as_view(url="/accounts/login/")),
path("logout/", RedirectView.as_view(url="/accounts/logout/")),
path("", include("cms.urls", namespace="cms")),
]

Wyświetl plik

@ -1,3 +1,7 @@
"""
View classes for all possible section types.
"""
from cms.decorators import section_view from cms.decorators import section_view
from cms.views import ContactSectionFormView, SectionView from cms.views import ContactSectionFormView, SectionView
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -5,6 +9,10 @@ from django.utils.translation import gettext_lazy as _
@section_view @section_view
class Text(SectionView): class Text(SectionView):
"""
A section that displays text.
"""
verbose_name = _("Text") verbose_name = _("Text")
template_name = "text.html" template_name = "text.html"
fields = ["content"] fields = ["content"]
@ -12,6 +20,10 @@ class Text(SectionView):
@section_view @section_view
class Images(SectionView): class Images(SectionView):
"""
A section that displays images.
"""
verbose_name = _("Image(s)") verbose_name = _("Image(s)")
template_name = "images.html" template_name = "images.html"
fields = ["images"] fields = ["images"]
@ -19,6 +31,10 @@ class Images(SectionView):
@section_view @section_view
class Video(SectionView): class Video(SectionView):
"""
A section that displays a video.
"""
verbose_name = _("Video") verbose_name = _("Video")
template_name = "video.html" template_name = "video.html"
fields = ["video"] fields = ["video"]
@ -26,6 +42,10 @@ class Video(SectionView):
@section_view @section_view
class Contact(ContactSectionFormView): class Contact(ContactSectionFormView):
"""
A section that displays a contact form.
"""
verbose_name = _("Contact") verbose_name = _("Contact")
template_name = "contact.html" template_name = "contact.html"
fields = ["content", "href"] fields = ["content", "href"]