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.
- package/README.md +47 -0
- package/bin/kalo.ts +17 -0
- package/generators/ai-enhancer/index.ts +281 -0
- package/generators/ai-enhancer/keywords.json +1158 -0
- package/generators/constants.ts +52 -0
- package/generators/django-app/index.ts +67 -0
- package/generators/django-app/templates/admin.py.hbs +6 -0
- package/generators/django-app/templates/apps.py.hbs +9 -0
- package/generators/django-app/templates/init.py.hbs +0 -0
- package/generators/django-app/templates/models_init.py.hbs +2 -0
- package/generators/django-app/templates/urls.py.hbs +8 -0
- package/generators/django-app/templates/views.py.hbs +5 -0
- package/generators/django-channel/index.ts +78 -0
- package/generators/django-channel/templates/consumer.py.hbs +47 -0
- package/generators/django-channel/templates/routing.py.hbs +8 -0
- package/generators/django-form/index.ts +62 -0
- package/generators/django-form/templates/form.py.hbs +12 -0
- package/generators/django-form/templates/forms_file.py.hbs +6 -0
- package/generators/django-form/templates/model_form.py.hbs +18 -0
- package/generators/django-view/index.ts +95 -0
- package/generators/django-view/templates/view_cbv.py.hbs +11 -0
- package/generators/django-view/templates/view_fbv.py.hbs +7 -0
- package/generators/django-view/templates/view_template.html.hbs +8 -0
- package/generators/docs/index.ts +36 -0
- package/generators/help/index.ts +84 -0
- package/generators/main/index.ts +429 -0
- package/generators/utils/ai/common.ts +141 -0
- package/generators/utils/ai/index.ts +2 -0
- package/generators/utils/analysis.ts +82 -0
- package/generators/utils/code-manipulation.ts +119 -0
- package/generators/utils/filesystem.ts +64 -0
- package/generators/utils/index.ts +47 -0
- package/generators/utils/plop-actions.ts +61 -0
- package/generators/utils/search.ts +24 -0
- package/generators/wagtail-admin/index.ts +122 -0
- package/generators/wagtail-admin/templates/admin_view.html.hbs +21 -0
- package/generators/wagtail-admin/templates/admin_view.py.hbs +15 -0
- package/generators/wagtail-admin/templates/component.html.hbs +6 -0
- package/generators/wagtail-admin/templates/component.py.hbs +11 -0
- package/generators/wagtail-admin/templates/wagtail_hooks.py.hbs +18 -0
- package/generators/wagtail-block/index.ts +55 -0
- package/generators/wagtail-block/templates/block_class.py.hbs +13 -0
- package/generators/wagtail-block/templates/block_template.html.hbs +5 -0
- package/generators/wagtail-page/actions/model.ts +18 -0
- package/generators/wagtail-page/actions/orderable.ts +21 -0
- package/generators/wagtail-page/actions/page.ts +40 -0
- package/generators/wagtail-page/actions/snippet.ts +19 -0
- package/generators/wagtail-page/index.ts +63 -0
- package/generators/wagtail-page/templates/django_model.py.hbs +18 -0
- package/generators/wagtail-page/templates/orderable_model.py.hbs +21 -0
- package/generators/wagtail-page/templates/page_pair_model.py.hbs +62 -0
- package/generators/wagtail-page/templates/page_template.html.hbs +14 -0
- package/generators/wagtail-page/templates/snippet_model.py.hbs +24 -0
- package/package.json +47 -0
- package/plopfile.ts +26 -0
- 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,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,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
|
+
};
|