kalo-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +47 -0
  2. package/bin/kalo.ts +17 -0
  3. package/generators/ai-enhancer/index.ts +281 -0
  4. package/generators/ai-enhancer/keywords.json +1158 -0
  5. package/generators/constants.ts +52 -0
  6. package/generators/django-app/index.ts +67 -0
  7. package/generators/django-app/templates/admin.py.hbs +6 -0
  8. package/generators/django-app/templates/apps.py.hbs +9 -0
  9. package/generators/django-app/templates/init.py.hbs +0 -0
  10. package/generators/django-app/templates/models_init.py.hbs +2 -0
  11. package/generators/django-app/templates/urls.py.hbs +8 -0
  12. package/generators/django-app/templates/views.py.hbs +5 -0
  13. package/generators/django-channel/index.ts +78 -0
  14. package/generators/django-channel/templates/consumer.py.hbs +47 -0
  15. package/generators/django-channel/templates/routing.py.hbs +8 -0
  16. package/generators/django-form/index.ts +62 -0
  17. package/generators/django-form/templates/form.py.hbs +12 -0
  18. package/generators/django-form/templates/forms_file.py.hbs +6 -0
  19. package/generators/django-form/templates/model_form.py.hbs +18 -0
  20. package/generators/django-view/index.ts +95 -0
  21. package/generators/django-view/templates/view_cbv.py.hbs +11 -0
  22. package/generators/django-view/templates/view_fbv.py.hbs +7 -0
  23. package/generators/django-view/templates/view_template.html.hbs +8 -0
  24. package/generators/docs/index.ts +36 -0
  25. package/generators/help/index.ts +84 -0
  26. package/generators/main/index.ts +429 -0
  27. package/generators/utils/ai/common.ts +141 -0
  28. package/generators/utils/ai/index.ts +2 -0
  29. package/generators/utils/analysis.ts +82 -0
  30. package/generators/utils/code-manipulation.ts +119 -0
  31. package/generators/utils/filesystem.ts +64 -0
  32. package/generators/utils/index.ts +47 -0
  33. package/generators/utils/plop-actions.ts +61 -0
  34. package/generators/utils/search.ts +24 -0
  35. package/generators/wagtail-admin/index.ts +122 -0
  36. package/generators/wagtail-admin/templates/admin_view.html.hbs +21 -0
  37. package/generators/wagtail-admin/templates/admin_view.py.hbs +15 -0
  38. package/generators/wagtail-admin/templates/component.html.hbs +6 -0
  39. package/generators/wagtail-admin/templates/component.py.hbs +11 -0
  40. package/generators/wagtail-admin/templates/wagtail_hooks.py.hbs +18 -0
  41. package/generators/wagtail-block/index.ts +55 -0
  42. package/generators/wagtail-block/templates/block_class.py.hbs +13 -0
  43. package/generators/wagtail-block/templates/block_template.html.hbs +5 -0
  44. package/generators/wagtail-page/actions/model.ts +18 -0
  45. package/generators/wagtail-page/actions/orderable.ts +21 -0
  46. package/generators/wagtail-page/actions/page.ts +40 -0
  47. package/generators/wagtail-page/actions/snippet.ts +19 -0
  48. package/generators/wagtail-page/index.ts +63 -0
  49. package/generators/wagtail-page/templates/django_model.py.hbs +18 -0
  50. package/generators/wagtail-page/templates/orderable_model.py.hbs +21 -0
  51. package/generators/wagtail-page/templates/page_pair_model.py.hbs +62 -0
  52. package/generators/wagtail-page/templates/page_template.html.hbs +14 -0
  53. package/generators/wagtail-page/templates/snippet_model.py.hbs +24 -0
  54. package/package.json +47 -0
  55. package/plopfile.ts +26 -0
  56. package/tsconfig.json +29 -0
@@ -0,0 +1,119 @@
1
+
2
+ export const getMarkerStart = (className: string, context: string = 'AI_GENERATED_SNIPPET') => `# <!-- CLASS = ${className} START ${context} -->`;
3
+ export const getMarkerEnd = (className: string, context: string = 'AI_GENERATED_SNIPPET') => `# <!-- CLASS = ${className} END ${context} -->`;
4
+
5
+ /**
6
+ * Injects markers into the file content to guide AI code generation.
7
+ * Tries to place markers inside the specified class, preferably before 'class Meta' or at the top of the class.
8
+ */
9
+ export const injectMarkers = (fileContent: string, className: string, context?: string): string => {
10
+ const markerStart = getMarkerStart(className, context);
11
+ const markerEnd = getMarkerEnd(className, context);
12
+
13
+ // Check if markers exist
14
+ if (fileContent.includes(markerStart) || fileContent.includes(markerEnd)) {
15
+ return fileContent;
16
+ }
17
+
18
+ console.log(`Markers for class ${className} not found, attempting to inject...`);
19
+
20
+ // Regex to find class definition
21
+ const classRegex = new RegExp(`^(\\s*)class\\s+${className}\\b.*?:`, 'm');
22
+ const classMatch = fileContent.match(classRegex);
23
+
24
+ if (classMatch) {
25
+ const classIndentation = classMatch[1];
26
+ const standardIndentation = ' '; // Assume 4 spaces
27
+ const injectionIndentation = classIndentation + standardIndentation;
28
+
29
+ const classStartIndex = classMatch.index! + classMatch[0].length;
30
+ const contentAfterClassStart = fileContent.substring(classStartIndex);
31
+
32
+ // Search for 'class Meta' indented relative to our class
33
+ const metaRegex = new RegExp(`^${classIndentation}${standardIndentation}class Meta:`, 'm');
34
+ const metaMatch = contentAfterClassStart.match(metaRegex);
35
+
36
+ if (metaMatch) {
37
+ // Inject before class Meta
38
+ const absoluteMetaIndex = classStartIndex + metaMatch.index!;
39
+ const beforeMeta = fileContent.substring(0, absoluteMetaIndex);
40
+ const afterMeta = fileContent.substring(absoluteMetaIndex);
41
+
42
+ return `${beforeMeta}${injectionIndentation}${markerStart}\n${injectionIndentation}${markerEnd}\n\n${afterMeta}`;
43
+ } else {
44
+ // Inject at the top of the class body
45
+ const injection = `\n${injectionIndentation}${markerStart}\n${injectionIndentation}${markerEnd}\n`;
46
+ const beforeClassBody = fileContent.substring(0, classStartIndex);
47
+ const afterClassBody = fileContent.substring(classStartIndex);
48
+
49
+ return beforeClassBody + injection + afterClassBody;
50
+ }
51
+ } else {
52
+ console.warn(`Class "${className}" not found in file. Appending markers to end of file.`);
53
+ return fileContent + `\n\n ${markerStart}\n ${markerEnd}\n`;
54
+ }
55
+ };
56
+
57
+ /**
58
+ * Applies the AI-generated JSON response to the content.
59
+ * Handles imports, replacements, and code injection between markers.
60
+ */
61
+ export const applyGeneratedCode = (contentWithMarkers: string, generatedData: any, className: string, context?: string): string => {
62
+ const markerStart = getMarkerStart(className, context);
63
+ const markerEnd = getMarkerEnd(className, context);
64
+
65
+ let newContent = contentWithMarkers;
66
+ const { imports = [], replacements = [], code = '' } = generatedData;
67
+
68
+ // 1. Handle Imports
69
+ if (imports.length > 0) {
70
+ const newImports = imports.filter((imp: string) => !newContent.includes(imp.trim()));
71
+ if (newImports.length > 0) {
72
+ // Add imports at the top, but try to be smart (after existing imports?)
73
+ // For simplicity, prepend.
74
+ newContent = newImports.join('\n') + '\n' + newContent;
75
+ }
76
+ }
77
+
78
+ // 2. Handle Replacements
79
+ if (replacements.length > 0) {
80
+ for (const rep of replacements) {
81
+ if (rep && rep.search && rep.replace) {
82
+ if (newContent.includes(rep.search)) {
83
+ newContent = newContent.replace(rep.search, rep.replace);
84
+ } else {
85
+ console.warn(`Replacement pattern not found: "${rep.search.substring(0, 40)}..."`);
86
+ }
87
+ }
88
+ }
89
+ }
90
+
91
+ // 3. Handle Code Injection
92
+ let newCode = code;
93
+ if (Array.isArray(newCode)) newCode = newCode.join('\n');
94
+
95
+ if (typeof newCode === 'string' && newCode.trim().length > 0) {
96
+ const startIdx = newContent.indexOf(markerStart);
97
+ const endIdx = newContent.indexOf(markerEnd);
98
+
99
+ if (startIdx !== -1 && endIdx !== -1) {
100
+ const prefix = newContent.substring(0, startIdx + markerStart.length);
101
+ const suffix = newContent.substring(endIdx);
102
+
103
+ // Detect indentation from the line before start marker
104
+ const lastNewLine = newContent.lastIndexOf('\n', startIdx);
105
+ const indentation = (lastNewLine !== -1)
106
+ ? newContent.substring(lastNewLine + 1, startIdx)
107
+ : ' ';
108
+
109
+ // Indent new code to match marker indentation
110
+ const indentedCode = newCode.split('\n')
111
+ .map(line => line.trim() ? indentation + line : line)
112
+ .join('\n');
113
+
114
+ return `${prefix}\n${indentedCode}\n${indentation}${suffix}`;
115
+ }
116
+ }
117
+
118
+ return newContent;
119
+ };
@@ -0,0 +1,64 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ /**
5
+ * Retrieves the list of existing Django applications in the project.
6
+ * Scans the 'app' directory in the current working directory.
7
+ *
8
+ * @returns {string[]} An array of application names (directory names).
9
+ * @throws {Error} If filesystem access fails (though basic read errors might propagate).
10
+ */
11
+ export const getAppList = (): string[] => {
12
+ const appDir = path.join(process.cwd(), 'app');
13
+ if (!fs.existsSync(appDir)) {
14
+ return [];
15
+ }
16
+ return fs.readdirSync(appDir, { withFileTypes: true })
17
+ .filter(dirent => dirent.isDirectory())
18
+ .map(dirent => dirent.name);
19
+ };
20
+
21
+ /**
22
+ * Recursively lists files in an application directory with filtering.
23
+ * Excludes test files, __init__.py, and specific directories.
24
+ *
25
+ * @param {string} appName - The name of the application.
26
+ * @returns {string[]} List of relative file paths from the app root.
27
+ */
28
+ export const getAppFiles = (appName: string): string[] => {
29
+ const appDir = path.join(process.cwd(), 'app', appName);
30
+ if (!fs.existsSync(appDir)) {
31
+ return [];
32
+ }
33
+
34
+ const files: string[] = [];
35
+
36
+ const walk = (dir: string, relativePath: string) => {
37
+ const list = fs.readdirSync(dir, { withFileTypes: true });
38
+
39
+ for (const dirent of list) {
40
+ const currentRelPath = path.join(relativePath, dirent.name);
41
+ const currentAbsPath = path.join(dir, dirent.name);
42
+
43
+ if (dirent.isDirectory()) {
44
+ // Exclude test directories and cache
45
+ if (!['__tests__', 'test', 'tests', '__pycache__', 'migrations'].includes(dirent.name)) {
46
+ walk(currentAbsPath, currentRelPath);
47
+ }
48
+ } else {
49
+ // Exclude test files and __init__.py
50
+ if (
51
+ dirent.name !== '__init__.py' &&
52
+ !dirent.name.includes('.test.') &&
53
+ !dirent.name.includes('.spec.') &&
54
+ dirent.name.endsWith('.py') // Assume we only care about Python files for now based on context
55
+ ) {
56
+ files.push(currentRelPath);
57
+ }
58
+ }
59
+ }
60
+ };
61
+
62
+ walk(appDir, '');
63
+ return files;
64
+ };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Utility functions for file path resolution and naming conventions.
3
+ * Pure functions only.
4
+ */
5
+
6
+ /**
7
+ * Formats a string into a URL-friendly slug.
8
+ * Converts to lowercase, replaces spaces with hyphens, and removes non-word characters.
9
+ *
10
+ * @param {string} text - The input string to format.
11
+ * @returns {string} The formatted slug string.
12
+ */
13
+ export const formatSlug = (text: string): string =>
14
+ text.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, '');
15
+
16
+ /**
17
+ * Formats a string into snake_case.
18
+ * Converts PascalCase or camelCase to snake_case.
19
+ *
20
+ * @param {string} text - The input string to format.
21
+ * @returns {string} The string in snake_case.
22
+ */
23
+ export const formatSnakeCase = (text: string): string =>
24
+ text.replace(/\.?([A-Z])/g, (x, y) => "_" + y.toLowerCase()).replace(/^_/, "");
25
+
26
+ /**
27
+ * Formats a string into PascalCase.
28
+ * Capitalizes the first letter of each word and removes non-alphanumeric characters.
29
+ *
30
+ * @param {string} text - The input string to format.
31
+ * @returns {string} The string in PascalCase.
32
+ */
33
+ export const formatPascalCase = (text: string): string =>
34
+ text.replace(/(\w)(\w*)/g, (g0, g1, g2) => g1.toUpperCase() + g2.toLowerCase()).replace(/\W/g, "");
35
+
36
+ /**
37
+ * Resolves the filesystem path for a given Django application.
38
+ * Assumes apps are located at the root or within an 'apps' structure.
39
+ *
40
+ * @param {string} appName - The name of the Django app.
41
+ * @returns {string} The resolved relative path to the app directory.
42
+ */
43
+ export const resolveAppPath = (appName: string): string => {
44
+ // In a real scenario, this might check for an 'apps' directory existence
45
+ // For this generator, we default to root level apps for simplicity (KISS)
46
+ return `${appName}`;
47
+ };
@@ -0,0 +1,61 @@
1
+ import { ActionType } from 'plop';
2
+
3
+ /**
4
+ * Ensures a suffix is present on a name.
5
+ *
6
+ * @param {string} name - The name to check.
7
+ * @param {string} suffix - The suffix to ensure.
8
+ * @returns {string} The name with the suffix.
9
+ */
10
+ export const ensureSuffix = (name: string, suffix: string): string => {
11
+ if (name.endsWith(suffix)) return name;
12
+ return `${name}${suffix}`;
13
+ };
14
+
15
+ interface CreateAppendOptions {
16
+ path: string;
17
+ templateFile: string;
18
+ dumbData?: any;
19
+ appendData?: any;
20
+ dumbTemplateFile?: string;
21
+ dumbTemplate?: string;
22
+ }
23
+
24
+ /**
25
+ * Creates a pair of actions: one to ensure the file exists (using a "dumb" template rendering or specific init template),
26
+ * and one to append the actual content.
27
+ *
28
+ * @param {CreateAppendOptions} options - Configuration options.
29
+ * @returns {ActionType[]} An array containing the add and append actions.
30
+ */
31
+ export const createAppendActions = ({
32
+ path,
33
+ templateFile,
34
+ dumbData = { name: 'Dumb' },
35
+ appendData,
36
+ dumbTemplateFile,
37
+ dumbTemplate
38
+ }: CreateAppendOptions): ActionType[] => {
39
+ const addAction: any = {
40
+ type: 'add',
41
+ path,
42
+ skipIfExists: true,
43
+ data: dumbData,
44
+ };
45
+
46
+ if (dumbTemplate) {
47
+ addAction.template = dumbTemplate;
48
+ } else {
49
+ addAction.templateFile = dumbTemplateFile || templateFile;
50
+ }
51
+
52
+ return [
53
+ addAction,
54
+ {
55
+ type: 'append',
56
+ path,
57
+ templateFile,
58
+ data: appendData,
59
+ }
60
+ ];
61
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Utility for fuzzy searching/filtering choices in autocomplete prompts.
3
+ */
4
+
5
+ /**
6
+ * Filters a list of choices based on the input string.
7
+ * Uses simple case-insensitive substring matching.
8
+ *
9
+ * @param {string | null} input - The search input from the user.
10
+ * @param {Array<{ name: string; value: any }>} choices - The list of choices to filter.
11
+ * @returns {Promise<Array<{ name: string; value: any }>>} The filtered list of choices.
12
+ */
13
+ export const searchChoices = (input: string | null, choices: Array<{ name: string; value: any }>) => {
14
+ if (!input) {
15
+ return Promise.resolve(choices);
16
+ }
17
+
18
+ const lowerInput = input.toLowerCase();
19
+ const filtered = choices.filter((choice) =>
20
+ choice.name.toLowerCase().includes(lowerInput)
21
+ );
22
+
23
+ return Promise.resolve(filtered);
24
+ };
@@ -0,0 +1,122 @@
1
+ import { PlopGeneratorConfig } from 'plop';
2
+ import { ensureSuffix } from '../utils/plop-actions';
3
+ import { ADMIN_EXT_TYPES } from '../constants';
4
+ import { searchChoices } from '../utils/search';
5
+
6
+ /**
7
+ * Generator configuration for Wagtail Admin extensions.
8
+ * Allows creation of custom Admin Views or Template Components (Panels).
9
+ * Automatically registers hooks and creates template files.
10
+ *
11
+ * @type {PlopGeneratorConfig}
12
+ */
13
+ export const wagtailAdminGenerator: PlopGeneratorConfig = {
14
+ description: 'Créer une extension Wagtail Admin (Vue ou Composant)',
15
+ prompts: [
16
+ {
17
+ type: 'autocomplete',
18
+ name: 'type',
19
+ message: 'Quel type d\'extension voulez-vous créer ?',
20
+ source: (answers, input) => searchChoices(input, ADMIN_EXT_TYPES),
21
+ },
22
+ {
23
+ type: 'input',
24
+ name: 'name',
25
+ message: 'Nom de l\'extension (ex: DashboardAdminView, WelcomePanel) :',
26
+ validate: (value, answers) => {
27
+ return true;
28
+ }
29
+ },
30
+ {
31
+ type: 'input',
32
+ name: 'app',
33
+ message: 'Nom de l\'application cible',
34
+ },
35
+ ],
36
+ actions: (data) => {
37
+ const actions = [];
38
+ const appPath = `app/${data.app}`;
39
+ const isView = data.type === 'view';
40
+
41
+ // Ensure suffixes
42
+ if (isView) {
43
+ if (!data.name.endsWith('AdminView')) {
44
+ data.name = `${data.name}AdminView`;
45
+ }
46
+ } else {
47
+ // Component or Panel
48
+ if (!data.name.endsWith('Component') && !data.name.endsWith('Panel')) {
49
+ data.name = `${data.name}Panel`;
50
+ }
51
+ }
52
+
53
+ if (isView) {
54
+ // --- Admin View ---
55
+
56
+ // 1. Add View to views.py
57
+ actions.push({
58
+ type: 'append',
59
+ path: `${appPath}/views.py`,
60
+ templateFile: 'generators/wagtail-admin/templates/admin_view.py.hbs',
61
+ });
62
+
63
+ // 2. Add/Append wagtail_hooks.py
64
+ actions.push({
65
+ type: 'add',
66
+ path: `${appPath}/wagtail_hooks.py`,
67
+ templateFile: 'generators/wagtail-admin/templates/wagtail_hooks.py.hbs',
68
+ skipIfExists: true,
69
+ });
70
+
71
+ // If file existed, we might need to append.
72
+ // For simplicity, we just append the registration code.
73
+ // Ideally we would check for imports, but appending is safer than overwriting.
74
+ // We use a different template for appending to avoid duplicate imports if possible,
75
+ // but for now re-using the file with imports is "okay" in Python (just messy)
76
+ // or we can rely on user to clean up.
77
+ // BETTER STRATEGY: Create a separate partial for appending without top-level imports if feasible.
78
+ // But for this MVP, let's just append the same content.
79
+ // The user can deduplicate imports.
80
+
81
+ actions.push({
82
+ type: 'append',
83
+ path: `${appPath}/wagtail_hooks.py`,
84
+ templateFile: 'generators/wagtail-admin/templates/wagtail_hooks.py.hbs',
85
+ unique: true, // Try to avoid exact duplicates
86
+ });
87
+
88
+ // 3. Create Template
89
+ actions.push({
90
+ type: 'add',
91
+ path: `${appPath}/templates/${data.app}/admin/{{snakeCase name}}.html`,
92
+ templateFile: 'generators/wagtail-admin/templates/admin_view.html.hbs',
93
+ });
94
+
95
+ } else {
96
+ // --- Template Component ---
97
+
98
+ // 1. Add Component to components.py
99
+ actions.push({
100
+ type: 'add',
101
+ path: `${appPath}/components.py`,
102
+ template: 'from wagtail.admin.ui.components import Component\n\n',
103
+ skipIfExists: true,
104
+ });
105
+
106
+ actions.push({
107
+ type: 'append',
108
+ path: `${appPath}/components.py`,
109
+ templateFile: 'generators/wagtail-admin/templates/component.py.hbs',
110
+ });
111
+
112
+ // 2. Create Template
113
+ actions.push({
114
+ type: 'add',
115
+ path: `${appPath}/templates/${data.app}/components/${data.name.toLowerCase().replace(/ /g, '_')}.html`,
116
+ templateFile: 'generators/wagtail-admin/templates/component.html.hbs',
117
+ });
118
+ }
119
+
120
+ return actions;
121
+ },
122
+ };
@@ -0,0 +1,21 @@
1
+
2
+ {% extends "wagtailadmin/base.html" %}
3
+ {% block titletag %}{{ title }}{% endblock %}
4
+
5
+ {% block extra_css %}
6
+ {{ block.super }}
7
+ <style>
8
+ /* Add custom styles here */
9
+ </style>
10
+ {% endblock %}
11
+
12
+ {% block content %}
13
+ {% include "wagtailadmin/shared/header.html" with title=title icon="folder-open-inverse" %}
14
+
15
+ <div class="nice-padding">
16
+ <p>Content for {{ title }} goes here.</p>
17
+ </div>
18
+ {% endblock %}
19
+
20
+ <!-- CLASS = {{pascalCase name}} START AI_GENERATED_ADMIN_VIEW -->
21
+ <!-- CLASS = {{pascalCase name}} END AI_GENERATED_ADMIN_VIEW -->
@@ -0,0 +1,15 @@
1
+
2
+
3
+ from django.shortcuts import render
4
+
5
+ def {{snakeCase name}}(request):
6
+ """
7
+ Wagtail Admin View for {{name}}
8
+ """
9
+ # <!-- CLASS = {{pascalCase name}} START AI_GENERATED_ADMIN_VIEW -->
10
+ # <!-- CLASS = {{pascalCase name}} END AI_GENERATED_ADMIN_VIEW -->
11
+
12
+ context = {
13
+ 'title': '{{titleCase name}}',
14
+ }
15
+ return render(request, '{{app}}/admin/{{snakeCase name}}.html', context)
@@ -0,0 +1,6 @@
1
+ <!-- CLASS = {{pascalCase name}} START AI_GENERATED_COMPONENT -->
2
+ <!-- CLASS = {{pascalCase name}} END AI_GENERATED_COMPONENT -->
3
+ <div class="help-block help-info">
4
+ <h2>{{pascalCase name}} Component</h2>
5
+ <p>This is a custom Wagtail Admin Component.</p>
6
+ </div>
@@ -0,0 +1,11 @@
1
+ from wagtail.admin.ui.components import Component
2
+
3
+ class {{pascalCase name}}(Component):
4
+ # <!-- CLASS = {{pascalCase name}} START AI_GENERATED_COMPONENT -->
5
+ # <!-- CLASS = {{pascalCase name}} END AI_GENERATED_COMPONENT -->
6
+ template_name = '{{app}}/components/{{snakeCase name}}.html'
7
+
8
+ def get_context_data(self, parent_context):
9
+ context = super().get_context_data(parent_context)
10
+ # context['username'] = parent_context['request'].user.username
11
+ return context
@@ -0,0 +1,18 @@
1
+
2
+ from django.urls import path, reverse
3
+ from wagtail import hooks
4
+ from .views import {{snakeCase name}}
5
+
6
+ # <!-- CLASS = {{pascalCase name}}Hooks START AI_GENERATED_WAGTAIL_HOOKS -->
7
+ # <!-- CLASS = {{pascalCase name}}Hooks END AI_GENERATED_WAGTAIL_HOOKS -->
8
+
9
+ @hooks.register('register_admin_urls')
10
+ def register_{{snakeCase name}}_url():
11
+ return [
12
+ path('{{snakeCase name}}/', {{snakeCase name}}, name='{{snakeCase name}}'),
13
+ ]
14
+
15
+ @hooks.register('register_admin_menu_item')
16
+ def register_{{snakeCase name}}_menu_item():
17
+ from wagtail.admin.menu import MenuItem
18
+ return MenuItem('{{titleCase name}}', reverse('{{snakeCase name}}'), icon_name='folder-open-inverse')
@@ -0,0 +1,55 @@
1
+ import { PlopGeneratorConfig } from 'plop';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { ensureSuffix, createAppendActions } from '../utils/plop-actions';
5
+
6
+ /**
7
+ * Generator configuration for Wagtail StreamField Blocks.
8
+ * Creates a new StructBlock and its corresponding template.
9
+ * Automatically suffixes block names with 'Block'.
10
+ *
11
+ * @type {PlopGeneratorConfig}
12
+ */
13
+ export const wagtailBlockGenerator: PlopGeneratorConfig = {
14
+ description: 'Créer un nouveau Block Wagtail StreamField',
15
+ prompts: [
16
+ {
17
+ type: 'input',
18
+ name: 'name',
19
+ message: 'Nom du Block (ex: Image, Quote) - Sera suffixé par "Block", pour construire du contenu flexible dans StreamField :',
20
+ validate: (value) => {
21
+ return true;
22
+ }
23
+ },
24
+ {
25
+ type: 'input',
26
+ name: 'app',
27
+ message: 'Nom de l\'application cible',
28
+ },
29
+ ],
30
+ actions: (data) => {
31
+ const actions = [];
32
+ const appPath = data?.app || '';
33
+
34
+ // Ensure suffix
35
+ data.name = ensureSuffix(data.name, 'Block');
36
+
37
+ // This variable blocksFile is not really used by Plop actions directly but was here for calculation
38
+ // const blocksFile = path.join('app', appPath, 'blocks.py');
39
+
40
+ // 1. Ensure blocks.py exists and Append the block
41
+ actions.push(...createAppendActions({
42
+ path: `app/${data.app}/blocks.py`,
43
+ templateFile: 'generators/wagtail-block/templates/block_class.py.hbs'
44
+ }));
45
+
46
+ // 3. Create the template
47
+ actions.push({
48
+ type: 'add',
49
+ path: 'app/{{app}}/templates/{{app}}/blocks/{{snakeCase name}}.html',
50
+ templateFile: 'generators/wagtail-block/templates/block_template.html.hbs',
51
+ });
52
+
53
+ return actions;
54
+ },
55
+ };
@@ -0,0 +1,13 @@
1
+
2
+ class {{pascalCase name}}(blocks.StructBlock):
3
+ """
4
+ {{pascalCase name}} block configuration.
5
+ """
6
+ title = blocks.CharBlock(required=True, help_text="Add your title")
7
+
8
+ # <!-- CLASS = {{pascalCase name}} START AI_GENERATED_BLOCK -->
9
+ # <!-- CLASS = {{pascalCase name}} END AI_GENERATED_BLOCK -->
10
+ class Meta:
11
+ template = "{{app}}/blocks/{{snakeCase name}}.html"
12
+ icon = "placeholder"
13
+ label = "{{titleCase name}}"
@@ -0,0 +1,5 @@
1
+ <!-- CLASS = {{pascalCase name}} START AI_GENERATED_BLOCK -->
2
+ <div class="block-{{dashCase name}}">
3
+ <h3>\{{ self.title }}</h3>
4
+ </div>
5
+ <!-- CLASS = {{pascalCase name}} END AI_GENERATED_BLOCK -->
@@ -0,0 +1,18 @@
1
+
2
+ import { ActionType } from 'plop';
3
+ import { createAppendActions } from '../../utils/plop-actions';
4
+
5
+ /**
6
+ * Generates Plop actions for creating a standard Django model.
7
+ *
8
+ * @param {any} data - The prompt answers data containing 'name' and 'app'.
9
+ * @returns {ActionType[]} An array of Plop actions to create/update model files.
10
+ */
11
+ export const getModelActions = (data: any): ActionType[] => {
12
+ const appPath = `app/${data.app}`;
13
+
14
+ return createAppendActions({
15
+ path: `${appPath}/models/models.py`,
16
+ templateFile: 'generators/wagtail-page/templates/django_model.py.hbs'
17
+ });
18
+ };
@@ -0,0 +1,21 @@
1
+
2
+ import { ActionType } from 'plop';
3
+ import { ensureSuffix, createAppendActions } from '../../utils/plop-actions';
4
+
5
+ /**
6
+ * Generates Plop actions for creating a Wagtail Orderable model.
7
+ * Automatically ensures the name ends with 'Orderable'.
8
+ *
9
+ * @param {any} data - The prompt answers data.
10
+ * @returns {ActionType[]} An array of Plop actions to create/update orderable files.
11
+ */
12
+ export const getOrderableActions = (data: any): ActionType[] => {
13
+ const appPath = `app/${data.app}`;
14
+
15
+ data.name = ensureSuffix(data.name, 'Orderable');
16
+
17
+ return createAppendActions({
18
+ path: `${appPath}/models/orderables.py`,
19
+ templateFile: 'generators/wagtail-page/templates/orderable_model.py.hbs'
20
+ });
21
+ };
@@ -0,0 +1,40 @@
1
+
2
+ import { ActionType } from 'plop';
3
+ import { createAppendActions } from '../../utils/plop-actions';
4
+
5
+ /**
6
+ * Generates Plop actions for creating a pair of Wagtail Pages (Index + Detail).
7
+ * Calculates a 'baseName' by removing 'Page' suffix if present.
8
+ *
9
+ * @param {any} data - The prompt answers data.
10
+ * @returns {ActionType[]} An array of Plop actions to create models, templates, and tests.
11
+ */
12
+ export const getPageActions = (data: any): ActionType[] => {
13
+ const actions: ActionType[] = [];
14
+ const appPath = `app/${data.app}`;
15
+
16
+ // Clean base name: Remove 'Page' if present to avoid BlogPagePage
17
+ const baseName = data.name.replace(/Page$/, '');
18
+ data.baseName = baseName; // Store for templates
19
+
20
+ // Ensure models/pages.py exists and append models
21
+ actions.push(...createAppendActions({
22
+ path: `${appPath}/models/pages.py`,
23
+ templateFile: 'generators/wagtail-page/templates/page_pair_model.py.hbs',
24
+ dumbData: { baseName: 'Dumb' }
25
+ }));
26
+
27
+ // Create HTML templates for both
28
+ actions.push({
29
+ type: 'add',
30
+ path: `${appPath}/templates/${data.app}/{{snakeCase baseName}}_index_page.html`,
31
+ templateFile: 'generators/wagtail-page/templates/page_template.html.hbs',
32
+ });
33
+ actions.push({
34
+ type: 'add',
35
+ path: `${appPath}/templates/${data.app}/{{snakeCase baseName}}_page.html`,
36
+ templateFile: 'generators/wagtail-page/templates/page_template.html.hbs',
37
+ });
38
+
39
+ return actions;
40
+ };