Skip to content

Commit

Permalink
Finished 'initial loading' feature with tests and django command, close
Browse files Browse the repository at this point in the history
  • Loading branch information
sveetch committed Nov 19, 2022
1 parent b5dd281 commit b845f74
Show file tree
Hide file tree
Showing 9 changed files with 259 additions and 160 deletions.
2 changes: 1 addition & 1 deletion cookiecutter.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

"version": "0.1.0",

"_bireli_version": "0.2.0-pre.13",
"_bireli_version": "0.2.0-pre.14",
"_python_version": ">=3.8",
"_django_version": ">=4.0,<4.1",
"_project_composer_version": ">=0.6.0,<0.7.0",
Expand Down
8 changes: 8 additions & 0 deletions {{ cookiecutter.project_name }}/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ help:
@echo " check-migrations -- to check for pending application migrations (do not write anything)"
@echo " migrate -- to apply demo database migrations"
@echo " superuser -- to create a superuser for Django admin"
@echo " initial-data -- to load initial data"
@echo
@echo " css -- to build CSS with default environnement"
@echo " watch-css -- to launch watcher CSS with default environnement"
Expand Down Expand Up @@ -174,6 +175,13 @@ superuser:
$(DJANGO_MANAGE) createsuperuser
.PHONY: superuser

initial-data:
@echo ""
@printf "$(FORMATBLUE)$(FORMATBOLD)---> Loading initial data <---$(FORMATRESET)\n"
@echo ""
$(DJANGO_MANAGE) emencia_initial django-apps/project_utils/initial.json
.PHONY: initial-data

run:
@echo ""
@printf "$(FORMATBLUE)$(FORMATBOLD)---> Running development server <---$(FORMATRESET)\n"
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ class ProjectUtilsException(Exception):
pass


class DemoMakerException(ProjectUtilsException):
class InitialDataLoaderException(ProjectUtilsException):
"""
Custom explicit exception from a DemoMaker process.
Custom explicit exception from a InitialDataLoader process.
"""
pass
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,29 @@
"password": "django324"
}
},
"pages": {
"key": "homepage",
"name": "Demo homepage",
"children": [
{
"key": "annexes",
"name": "Annexes",
"children": [
{
"key": "contact",
"name": "Contact"
},
{
"key": "legal-notice",
"name": "Legal notice"
},
{
"key": "privacy_policy",
"name": "Privacy policy"
}
]
}
]
}
"pages": [
{
"key": "homepage",
"name": "Demo homepage",
"is_homepage": true
},
{
"key": "annexes",
"name": "Annexes",
"children": [
{
"key": "contact",
"name": "Contact"
},
{
"key": "legal-notice",
"name": "Legal notice"
},
{
"key": "privacy_policy",
"name": "Privacy policy"
}
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
Currently this is hardly coupled to Django site, Django auth and CMS. This is not
really flexible and will be broken if CMS is not enabled.
This can be improved by splitting DemoMaker into application parts managed by
This could be improved by splitting InitialDataLoader into application parts managed by
project-composer.
"""
Expand All @@ -98,12 +98,12 @@

from cms.utils import get_current_site

from .exceptions import DemoMakerException
from .exceptions import InitialDataLoaderException
from .factories import PageFactory, UserFactory
from .logging import BaseOutput


class DemoMaker:
class InitialDataLoader:
"""
Helper class that will create implemented objects from a given structure.
Expand All @@ -127,11 +127,68 @@ def validate_global_author(self):
"""
if not self.global_author:
msg = "No 'global_author' have been given despite it is required."
raise DemoMakerException(msg)
raise InitialDataLoaderException(msg)

if isinstance(self.global_author, str):
msg = "Unable to find created user for global author username '{}'."
raise DemoMakerException(msg.format(self.global_author))
raise InitialDataLoaderException(msg.format(self.global_author))

return True

def _recursive_get_pages(self, pages, collected=[]):
"""
Collect a flat list of page tree.
Note than all page items won't have the children item since the collection
is definitively flat. Obviously the collection drop all the tree hierarchy
informations (the 'children' key).
Arguments:
pages (list): The tree list of page to recursively dig for pages.
Keyword Arguments:
collected (list):
"""
for page in pages:
collected.append({k: v for k, v in page.items() if k != "children"})

if page.get("children"):
self._recursive_get_pages(page.get("children"), collected=collected)

def validate_page_tree(self, pages):
"""
Recursively validate page tree structure.
Arguments:
pages (list): The tree list of page to recursively dig for pages.
Returns:
boolean:
"""

flatten_tree = []
self._recursive_get_pages(pages, collected=flatten_tree)

homepages = [
item
for item in flatten_tree
if item.get("is_homepage") is True
]

if len(homepages) == 0:
msg = (
"At least one homepage is required, given structure got none."
)
raise InitialDataLoaderException(msg.format(len(homepages)))
elif len(homepages) > 1:
msg = (
"Only a single page with 'is_homepage' is allowed but given structure "
"got {} pages with this option enabled."
)
raise InitialDataLoaderException(msg.format(len(homepages)))

for item in flatten_tree:
self.validate_page_data(item)

return True

Expand All @@ -144,18 +201,18 @@ def validate_page_data(self, data):
"""
if not data.get("key"):
msg = "A page must define a non empty 'key' item."
raise DemoMakerException(msg)
raise InitialDataLoaderException(msg)

if not slug_re.match(data["key"]):
msg = (
"The 'key' item must be a valid identifier consisting of letters, "
"numbers, underscores or hyphens. Given one is invalid: {}"
)
raise DemoMakerException(msg.format(data["key"]))
raise InitialDataLoaderException(msg.format(data["key"]))

if not data.get("name"):
msg = "A page must define a non empty 'name' item."
raise DemoMakerException(msg)
raise InitialDataLoaderException(msg)

return True

Expand All @@ -172,14 +229,14 @@ def validate_page_template(self, template, can_be_empty=False):
"""
if not can_be_empty and not template:
msg = "A template path cannot be empty."
raise DemoMakerException(msg)
raise InitialDataLoaderException(msg)

if template not in [k for k, v in settings.CMS_TEMPLATES]:
msg = (
"Given template path is not registered from "
"'settings.CMS_TEMPLATES': {}"
)
raise DemoMakerException(msg.format(template))
raise InitialDataLoaderException(msg.format(template))

return True

Expand Down Expand Up @@ -241,8 +298,6 @@ def page_creation(self, data, parent=None, level=0, created=[]):
root page.
created (list): List where to append all created page objects.
"""
self.validate_page_data(data)

key = data["key"]
self.log.info("- Creating page: {name}".format(
name=key,
Expand All @@ -255,7 +310,7 @@ def page_creation(self, data, parent=None, level=0, created=[]):
"user": self.global_author,
"parent": parent,
"reverse_id": key,
"set_homepage": True if level == 0 else False,
"set_homepage": True if data.get("is_homepage") else False,
"should_publish": True,
"in_navigation": True,
"title__language": settings.LANGUAGE_CODE,
Expand All @@ -270,7 +325,7 @@ def page_creation(self, data, parent=None, level=0, created=[]):

level += 1
for child in data.get("children", []):
self.page_creation(child, parent=page, level=level)
self.page_creation(child, parent=page, level=level, created=created)

return created

Expand Down Expand Up @@ -302,8 +357,16 @@ def create(self, structure):
self.validate_global_author()

if structure.get("pages"):
# Validate the default template
self.validate_page_template(self.default_template)
pages = self.page_creation(structure["pages"])

# Validate the whole tree
self.validate_page_tree(structure["pages"])

# Create pages
pages = []
for item in structure["pages"]:
self.page_creation(item, created=pages)
self.log.info("* Created {} page(s)".format(len(pages)))

return {
Expand All @@ -321,7 +384,8 @@ def load(self, path):
path (string or pathlib.Path): Path to the JSON file to load.
Returns:
dict: Dictionnary of created objets as returned by ``DemoMaker.create()``.
dict: Dictionnary of created objets as returned
by ``InitialDataLoader.create()``.
"""
return self.create(
json.loads(Path(path).read_text())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from django.core.management.base import CommandError, BaseCommand

from cms.api import Page

from ...initial_loader import InitialDataLoader


class Command(BaseCommand):
"""
Initial data loader.
"""
help = (
"Populate site with initial data loaded from a JSON file."
)

def add_arguments(self, parser):
parser.add_argument(
"dump",
metavar="FILEPATH",
help="Filepath to the JSON file with structure to load."
)

def handle(self, *args, **options):
self.stdout.write(
self.style.SUCCESS("=== Loading initial data ===")
)

# Validate database is empty
existing = Page.objects.all()
if existing.count():
raise CommandError(
"Initial data can only be loaded when the database is empty from any "
"objects. You should (carefuly) use Django command 'flush' before."
)

self.stdout.write("* Opened JSON source: {}".format(options["dump"]))

maker = InitialDataLoader()
maker.load(options["dump"])
Loading

0 comments on commit b845f74

Please sign in to comment.