kopia lustrzana https://github.com/rtts/django-simplecms
Add missing docstrings
rodzic
6f02a846a2
commit
6781532448
|
@ -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
|
||||
|
|
14
cms/apps.py
14
cms/apps.py
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
12
cms/forms.py
12
cms/forms.py
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
"""
|
||||
URLs.
|
||||
"""
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from .views import CreatePage, CreateSection, PageView, UpdatePage, UpdateSection
|
||||
|
|
98
cms/views.py
98
cms/views.py
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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")),
|
||||
]
|
||||
|
|
|
@ -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"]
|
||||
|
|
Ładowanie…
Reference in New Issue