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

Wyświetl plik

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

Wyświetl plik

@ -1,20 +1,33 @@
"""
Decorators.
"""
from cms import registry
def page_model(cls):
"""Decorator to register the Page model"""
"""
Decorator to register the Page model.
"""
registry.page_class = cls
return cls
def section_model(cls):
"""Decorator to register the Section model"""
"""
Decorator to register the Section model.
"""
registry.section_class = cls
return 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.section_types.append((cls.__name__.lower(), cls.verbose_name))
return cls

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -1,3 +1,7 @@
"""
Optional but useful middleware classes.
"""
import os
from django.conf import settings
@ -5,18 +9,11 @@ from django.middleware import cache
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):
"""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
Django default, IMHO.
"""
def process_request(self, request):
@ -25,9 +22,11 @@ class FetchFromCacheMiddleware(cache.FetchFromCacheMiddleware):
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.
In production this does nothing, so commit your files!
"""
def __init__(self, get_response):
@ -53,3 +52,14 @@ class SassMiddleware:
response = self.get_response(request)
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:
"""
Mixin for model fields. Prevents the generation of migrations that
@ -37,7 +42,9 @@ class Numbered:
return self.__class__._meta.ordering[-1].lstrip("-")
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()
field_name = self.get_field_name()
@ -48,7 +55,7 @@ class Numbered:
# The algorithm: loop over the queryset and set each object's
# number to the counter. When an object's number equals 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
inserted = False
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.urls import reverse
from django.utils.text import slugify
@ -8,7 +12,9 @@ from . import fields, mixins
class BasePage(mixins.Numbered, models.Model):
"""Abstract base model for pages."""
"""
Abstract base model for pages.
"""
title = fields.CharField(_("page"))
slug = fields.SlugField(_("slug"), blank=True, unique=True)
@ -33,7 +39,9 @@ class BasePage(mixins.Numbered, models.Model):
class BaseSection(mixins.Numbered, models.Model):
"""Abstract base model for sections"""
"""
Abstract base model for sections.
"""
TYPES = []
title = fields.CharField(_("section"))

Wyświetl plik

@ -1,3 +1,7 @@
"""
Registry that is populated at startup time by the decorators.
"""
page_class = None
section_class = None
section_types = []
@ -5,14 +9,29 @@ view_per_type = {}
def get_types():
"""
Return the available section types as tuples to be used for
form field choices.
"""
return section_types
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)
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 = {}
for name, view in view_per_type.items():
fields_per_type[name] = ["title", "type", "number"] + view.fields

Wyświetl plik

@ -1,3 +1,7 @@
"""
URLs.
"""
from django.urls import path
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
from django.contrib.auth.mixins import UserPassesTestMixin
@ -17,7 +22,9 @@ from .forms import ContactForm, PageForm, SectionForm
class SectionView:
"""Generic section view"""
"""
Generic section view.
"""
template_name = "cms/sections/section.html"
@ -29,7 +36,9 @@ class SectionView:
class SectionFormView(SectionView):
"""Generic section with associated form"""
"""
Generic section with associated form.
"""
form_class = None
success_url = None
@ -60,28 +69,38 @@ class SectionFormView(SectionView):
class ContactSectionFormView(SectionFormView):
"""Contact section with bogus contact form"""
"""
Generic section with a contact form.
"""
form_class = ContactForm
def form_valid(self, form):
response = HttpResponse(status=302)
response["Location"] = form.save(self.object.href)
response["Location"] = form.save(self.object.href, self.object.subject)
return response
class PageView(detail.DetailView):
"""View of a page with heterogeneous sections"""
"""
View of a page with heterogeneous sections.
"""
model = registry.page_class
template_name = "cms/page.html"
def setup(self, *args, slug="", **kwargs):
"""Supply a default argument for slug"""
"""
Supply a default argument for slug.
"""
super().setup(*args, slug=slug, **kwargs)
def get(self, request, *args, **kwargs):
"""Instantiate section views and render final response"""
"""
Instantiate section views and render final response.
"""
try:
page = self.object = self.get_object()
except Http404:
@ -98,6 +117,7 @@ class PageView(detail.DetailView):
return redirect("cms:updatepage", slug=self.kwargs["slug"])
else:
raise
context = self.get_context_data(**kwargs)
sections = page.sections.all()
context.update(
@ -109,7 +129,10 @@ class PageView(detail.DetailView):
return self.render_to_response(context)
def post(self, request, **kwargs):
"""Call the post() method of the correct section view"""
"""
Call the post() method of the correct section view.
"""
try:
pk = int(self.request.POST.get("section"))
except Exception:
@ -150,45 +173,65 @@ class PageView(detail.DetailView):
class EditPage(
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
form_class = PageForm
template_name = "cms/edit.html"
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
model_name = registry.page_class._meta.model_name
return self.request.user.has_perm(f"{app_label}.change_{model_name}")
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()
if "slug" in self.kwargs:
kwargs.update({"initial": {"slug": self.kwargs["slug"]}})
return 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["fields_per_type"] = json.dumps(registry.get_fields_per_type())
return context
def get_object(self):
"""Prevent 404 by serving the new object form"""
"""
Prevent 404 by serving the new object form.
"""
try:
return super().get_object()
except Http404:
return None
def get(self, *args, **kwargs):
"""Handle GET requests"""
"""
Handle GET requests.
"""
self.object = self.get_object()
return self.render_to_response(self.get_context_data(**kwargs))
def post(self, *args, **kwargs):
"""Handle POST requests"""
"""
Handle POST requests.
"""
self.object = self.get_object()
form = self.get_form()
@ -201,26 +244,37 @@ class EditPage(
class CreatePage(EditPage):
"""View for creating new pages"""
"""
View for creating new pages.
"""
def get_object(self):
return registry.page_class()
class UpdatePage(EditPage):
"""View for editing existing pages"""
"""
View for editing existing pages.
"""
@method_decorator(never_cache, name="dispatch")
class EditSection(
UserPassesTestMixin, edit.ModelFormMixin, base.TemplateResponseMixin, base.View
):
"""
Base view to edit a specific section.
"""
model = registry.section_class
form_class = SectionForm
template_name = "cms/edit.html"
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
model_name = registry.section_class._meta.model_name
return self.request.user.has_perm(f"{app_label}.change_{model_name}")
@ -273,9 +327,15 @@ class EditSection(
class CreateSection(EditSection):
"""
View for creating new sections.
"""
def get_section(self):
return registry.section_class(page=self.page)
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.models import BasePage, BaseSection
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 random
import string
@ -84,6 +89,7 @@ DATABASES = {
"NAME": PROJECT_NAME,
}
}
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.memcached.PyLibMCCache",

Wyświetl plik

@ -1,20 +1,19 @@
"""
URLs.
"""
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import include, path
from django.views.generic import RedirectView
admin.site.site_header = admin.site.site_title = settings.PROJECT_NAME.replace(
"_", " "
).title()
urlpatterns = staticfiles_urlpatterns() + 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.views import ContactSectionFormView, SectionView
from django.utils.translation import gettext_lazy as _
@ -5,6 +9,10 @@ from django.utils.translation import gettext_lazy as _
@section_view
class Text(SectionView):
"""
A section that displays text.
"""
verbose_name = _("Text")
template_name = "text.html"
fields = ["content"]
@ -12,6 +20,10 @@ class Text(SectionView):
@section_view
class Images(SectionView):
"""
A section that displays images.
"""
verbose_name = _("Image(s)")
template_name = "images.html"
fields = ["images"]
@ -19,6 +31,10 @@ class Images(SectionView):
@section_view
class Video(SectionView):
"""
A section that displays a video.
"""
verbose_name = _("Video")
template_name = "video.html"
fields = ["video"]
@ -26,6 +42,10 @@ class Video(SectionView):
@section_view
class Contact(ContactSectionFormView):
"""
A section that displays a contact form.
"""
verbose_name = _("Contact")
template_name = "contact.html"
fields = ["content", "href"]