Merge branch 'develop' into dashboard-view-fixes
This commit is contained in:
commit
3370d227e8
5 changed files with 261 additions and 100 deletions
|
|
@ -4,8 +4,6 @@ app_name = "frappe"
|
|||
app_title = "Frappe Framework"
|
||||
app_publisher = "Frappe Technologies"
|
||||
app_description = "Full stack web framework with Python, Javascript, MariaDB, Redis, Node"
|
||||
app_icon = "octicon octicon-circuit-board"
|
||||
app_color = "orange"
|
||||
source_link = "https://github.com/frappe/frappe"
|
||||
app_license = "MIT"
|
||||
app_logo_url = "/assets/frappe/images/frappe-framework-logo.svg"
|
||||
|
|
|
|||
|
|
@ -1,38 +1,52 @@
|
|||
import ast
|
||||
import copy
|
||||
import glob
|
||||
import os
|
||||
import shutil
|
||||
import unittest
|
||||
from io import StringIO
|
||||
from unittest.mock import patch
|
||||
|
||||
import yaml
|
||||
|
||||
import frappe
|
||||
from frappe.utils.boilerplate import make_boilerplate
|
||||
from frappe.utils.boilerplate import (
|
||||
_create_app_boilerplate,
|
||||
_get_user_inputs,
|
||||
github_workflow_template,
|
||||
)
|
||||
|
||||
|
||||
class TestBoilerPlate(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
title = "Test App"
|
||||
description = "This app's description contains 'single quotes' and \"double quotes\"."
|
||||
publisher = "Test Publisher"
|
||||
email = "example@example.org"
|
||||
icon = "" # empty -> default
|
||||
color = ""
|
||||
app_license = "MIT"
|
||||
cls.default_hooks = frappe._dict(
|
||||
{
|
||||
"app_name": "test_app",
|
||||
"app_title": "Test App",
|
||||
"app_description": "This app's description contains 'single quotes' and \"double quotes\".",
|
||||
"app_publisher": "Test Publisher",
|
||||
"app_email": "example@example.org",
|
||||
"app_license": "MIT",
|
||||
"create_github_workflow": False,
|
||||
}
|
||||
)
|
||||
|
||||
cls.user_input = [
|
||||
title,
|
||||
description,
|
||||
publisher,
|
||||
email,
|
||||
icon,
|
||||
color,
|
||||
app_license,
|
||||
]
|
||||
cls.default_user_input = frappe._dict(
|
||||
{
|
||||
"title": "Test App",
|
||||
"description": "This app's description contains 'single quotes' and \"double quotes\".",
|
||||
"publisher": "Test Publisher",
|
||||
"email": "example@example.org",
|
||||
"icon": "", # empty -> default
|
||||
"color": "",
|
||||
"app_license": "MIT",
|
||||
"github_workflow": "n",
|
||||
}
|
||||
)
|
||||
|
||||
cls.bench_path = frappe.utils.get_bench_path()
|
||||
cls.apps_dir = os.path.join(cls.bench_path, "apps")
|
||||
cls.app_names = ("test_app", "test_app_no_git")
|
||||
cls.gitignore_file = ".gitignore"
|
||||
cls.git_folder = ".git"
|
||||
|
||||
|
|
@ -55,39 +69,90 @@ class TestBoilerPlate(unittest.TestCase):
|
|||
"public",
|
||||
]
|
||||
|
||||
def create_app(self, hooks, no_git=False):
|
||||
self.addCleanup(self.delete_test_app, hooks.app_name)
|
||||
_create_app_boilerplate(self.apps_dir, hooks, no_git)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
test_app_dirs = (os.path.join(cls.bench_path, "apps", app_name) for app_name in cls.app_names)
|
||||
for test_app_dir in test_app_dirs:
|
||||
if os.path.exists(test_app_dir):
|
||||
shutil.rmtree(test_app_dir)
|
||||
def delete_test_app(cls, app_name):
|
||||
test_app_dir = os.path.join(cls.bench_path, "apps", app_name)
|
||||
if os.path.exists(test_app_dir):
|
||||
shutil.rmtree(test_app_dir)
|
||||
|
||||
@staticmethod
|
||||
def get_user_input_stream(inputs):
|
||||
user_inputs = []
|
||||
for value in inputs.values():
|
||||
if isinstance(value, list):
|
||||
user_inputs.extend(value)
|
||||
else:
|
||||
user_inputs.append(value)
|
||||
return StringIO("\n".join(user_inputs))
|
||||
|
||||
def test_simple_input_to_boilerplate(self):
|
||||
with patch("sys.stdin", self.get_user_input_stream(self.default_user_input)):
|
||||
hooks = _get_user_inputs(self.default_hooks.app_name)
|
||||
self.assertDictEqual(hooks, self.default_hooks)
|
||||
|
||||
def test_invalid_inputs(self):
|
||||
invalid_inputs = copy.copy(self.default_user_input).update(
|
||||
{
|
||||
"title": ["1nvalid Title", "valid title"],
|
||||
}
|
||||
)
|
||||
with patch("sys.stdin", self.get_user_input_stream(invalid_inputs)):
|
||||
hooks = _get_user_inputs(self.default_hooks.app_name)
|
||||
self.assertEqual(hooks.app_title, "valid title")
|
||||
|
||||
def test_valid_ci_yaml(self):
|
||||
yaml.safe_load(github_workflow_template.format(**self.default_hooks))
|
||||
|
||||
def test_create_app(self):
|
||||
with patch("builtins.input", side_effect=self.user_input):
|
||||
make_boilerplate(self.apps_dir, self.app_names[0])
|
||||
app_name = "test_app"
|
||||
|
||||
new_app_dir = os.path.join(self.bench_path, self.apps_dir, self.app_names[0])
|
||||
hooks = frappe._dict(
|
||||
{
|
||||
"app_name": app_name,
|
||||
"app_title": "Test App",
|
||||
"app_description": "This app's description contains 'single quotes' and \"double quotes\".",
|
||||
"app_publisher": "Test Publisher",
|
||||
"app_email": "example@example.org",
|
||||
"app_license": "MIT",
|
||||
}
|
||||
)
|
||||
|
||||
paths = self.get_paths(new_app_dir, self.app_names[0])
|
||||
self.create_app(hooks)
|
||||
new_app_dir = os.path.join(self.bench_path, self.apps_dir, app_name)
|
||||
|
||||
paths = self.get_paths(new_app_dir, app_name)
|
||||
for path in paths:
|
||||
self.assertTrue(os.path.exists(path), msg=f"{path} should exist in {self.app_names[0]} app")
|
||||
self.assertTrue(os.path.exists(path), msg=f"{path} should exist in {app_name} app")
|
||||
|
||||
self.check_parsable_python_files(new_app_dir)
|
||||
|
||||
def test_create_app_without_git_init(self):
|
||||
with patch("builtins.input", side_effect=self.user_input):
|
||||
make_boilerplate(self.apps_dir, self.app_names[1], no_git=True)
|
||||
app_name = "test_app_no_git"
|
||||
|
||||
new_app_dir = os.path.join(self.apps_dir, self.app_names[1])
|
||||
hooks = frappe._dict(
|
||||
{
|
||||
"app_name": app_name,
|
||||
"app_title": "Test App",
|
||||
"app_description": "This app's description contains 'single quotes' and \"double quotes\".",
|
||||
"app_publisher": "Test Publisher",
|
||||
"app_email": "example@example.org",
|
||||
"app_license": "MIT",
|
||||
}
|
||||
)
|
||||
self.create_app(hooks, no_git=True)
|
||||
|
||||
paths = self.get_paths(new_app_dir, self.app_names[1])
|
||||
new_app_dir = os.path.join(self.apps_dir, app_name)
|
||||
|
||||
paths = self.get_paths(new_app_dir, app_name)
|
||||
for path in paths:
|
||||
if os.path.basename(path) in (self.git_folder, self.gitignore_file):
|
||||
self.assertFalse(
|
||||
os.path.exists(path), msg=f"{path} shouldn't exist in {self.app_names[1]} app"
|
||||
)
|
||||
self.assertFalse(os.path.exists(path), msg=f"{path} shouldn't exist in {app_name} app")
|
||||
else:
|
||||
self.assertTrue(os.path.exists(path), msg=f"{path} should exist in {self.app_names[1]} app")
|
||||
self.assertTrue(os.path.exists(path), msg=f"{path} should exist in {app_name} app")
|
||||
|
||||
self.check_parsable_python_files(new_app_dir)
|
||||
|
||||
|
|
|
|||
|
|
@ -413,26 +413,6 @@ class TestCommands(BaseTestCommands):
|
|||
self.assertEqual(self.returncode, 0)
|
||||
self.assertEqual(check_password("Administrator", "test2"), "Administrator")
|
||||
|
||||
def test_make_app(self):
|
||||
user_input = [
|
||||
b"Test App", # title
|
||||
b"This app's description contains 'single quotes' and \"double quotes\".", # description
|
||||
b"Test Publisher", # publisher
|
||||
b"example@example.org", # email
|
||||
b"", # icon
|
||||
b"", # color
|
||||
b"MIT", # app_license
|
||||
]
|
||||
app_name = "testapp0"
|
||||
apps_path = os.path.join(get_bench_path(), "apps")
|
||||
test_app_path = os.path.join(apps_path, app_name)
|
||||
self.execute(f"bench make-app {apps_path} {app_name}", {"cmd_input": b"\n".join(user_input)})
|
||||
self.assertEqual(self.returncode, 0)
|
||||
self.assertTrue(os.path.exists(test_app_path))
|
||||
|
||||
# cleanup
|
||||
shutil.rmtree(test_app_path)
|
||||
|
||||
@skipIf(
|
||||
not (
|
||||
frappe.conf.root_password and frappe.conf.admin_password and frappe.conf.db_type == "mariadb"
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@
|
|||
# License: MIT. See LICENSE
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
|
||||
import click
|
||||
import git
|
||||
|
||||
import frappe
|
||||
from frappe.utils import cstr, touch_file
|
||||
from frappe.utils import touch_file
|
||||
|
||||
|
||||
def make_boilerplate(dest, app_name, no_git=False):
|
||||
|
|
@ -17,47 +19,63 @@ def make_boilerplate(dest, app_name, no_git=False):
|
|||
|
||||
# app_name should be in snake_case
|
||||
app_name = frappe.scrub(app_name)
|
||||
hooks = _get_user_inputs(app_name)
|
||||
_create_app_boilerplate(dest, hooks, no_git=no_git)
|
||||
|
||||
|
||||
def _get_user_inputs(app_name):
|
||||
"""Prompt user for various inputs related to new app and return config."""
|
||||
app_name = frappe.scrub(app_name)
|
||||
|
||||
hooks = frappe._dict()
|
||||
hooks.app_name = app_name
|
||||
app_title = hooks.app_name.replace("_", " ").title()
|
||||
for key in (
|
||||
"App Title (default: {0})".format(app_title),
|
||||
"App Description",
|
||||
"App Publisher",
|
||||
"App Email",
|
||||
"App Icon (default 'octicon octicon-file-directory')",
|
||||
"App Color (default 'grey')",
|
||||
"App License (default 'MIT')",
|
||||
):
|
||||
hook_key = key.split(" (")[0].lower().replace(" ", "_")
|
||||
hook_val = None
|
||||
while not hook_val:
|
||||
hook_val = cstr(input(key + ": "))
|
||||
|
||||
if not hook_val:
|
||||
defaults = {
|
||||
"app_title": app_title,
|
||||
"app_icon": "octicon octicon-file-directory",
|
||||
"app_color": "grey",
|
||||
"app_license": "MIT",
|
||||
}
|
||||
if hook_key in defaults:
|
||||
hook_val = defaults[hook_key]
|
||||
new_app_config = {
|
||||
"app_title": {
|
||||
"prompt": "App Title",
|
||||
"default": app_title,
|
||||
"validator": is_valid_title,
|
||||
},
|
||||
"app_description": {"prompt": "App Description"},
|
||||
"app_publisher": {"prompt": "App Publisher"},
|
||||
"app_email": {"prompt": "App Email"},
|
||||
"app_license": {"prompt": "App License", "default": "MIT"},
|
||||
"create_github_workflow": {
|
||||
"prompt": "Create GitHub Workflow action for unittests",
|
||||
"default": False,
|
||||
"type": bool,
|
||||
},
|
||||
}
|
||||
|
||||
if hook_key == "app_name" and hook_val.lower().replace(" ", "_") != hook_val:
|
||||
print("App Name must be all lowercase and without spaces")
|
||||
hook_val = ""
|
||||
elif hook_key == "app_title" and not re.match(
|
||||
r"^(?![\W])[^\d_\s][\w -]+$", hook_val, re.UNICODE
|
||||
):
|
||||
print(
|
||||
"App Title should start with a letter and it can only consist of letters, numbers, spaces and underscores"
|
||||
)
|
||||
hook_val = ""
|
||||
for property, config in new_app_config.items():
|
||||
value = None
|
||||
input_type = config.get("type", str)
|
||||
|
||||
hooks[hook_key] = hook_val
|
||||
while value is None:
|
||||
if input_type == bool:
|
||||
value = click.confirm(config["prompt"], default=config.get("default"))
|
||||
else:
|
||||
value = click.prompt(config["prompt"], default=config.get("default"), type=input_type)
|
||||
|
||||
if validator_function := config.get("validator"):
|
||||
if not validator_function(value):
|
||||
value = None
|
||||
hooks[property] = value
|
||||
|
||||
return hooks
|
||||
|
||||
|
||||
def is_valid_title(title) -> bool:
|
||||
if not re.match(r"^(?![\W])[^\d_\s][\w -]+$", title, re.UNICODE):
|
||||
print(
|
||||
"App Title should start with a letter and it can only consist of letters, numbers, spaces and underscores"
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _create_app_boilerplate(dest, hooks, no_git=False):
|
||||
frappe.create_folder(
|
||||
os.path.join(dest, hooks.app_name, hooks.app_name, frappe.scrub(hooks.app_title)), with_init=True
|
||||
)
|
||||
|
|
@ -123,6 +141,9 @@ def make_boilerplate(dest, app_name, no_git=False):
|
|||
|
||||
app_directory = os.path.join(dest, hooks.app_name)
|
||||
|
||||
if hooks.create_github_workflow:
|
||||
_create_github_workflow_files(dest, hooks)
|
||||
|
||||
if not no_git:
|
||||
with open(os.path.join(dest, hooks.app_name, ".gitignore"), "w") as f:
|
||||
f.write(frappe.as_unicode(gitignore_template.format(app_name=hooks.app_name)))
|
||||
|
|
@ -132,7 +153,16 @@ def make_boilerplate(dest, app_name, no_git=False):
|
|||
app_repo.git.add(A=True)
|
||||
app_repo.index.commit("feat: Initialize App")
|
||||
|
||||
print("'{app}' created at {path}".format(app=app_name, path=app_directory))
|
||||
print(f"'{hooks.app_name}' created at {app_directory}")
|
||||
|
||||
|
||||
def _create_github_workflow_files(dest, hooks):
|
||||
workflows_path = pathlib.Path(dest) / hooks.app_name / ".github" / "workflows"
|
||||
workflows_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
ci_workflow = workflows_path / "ci.yml"
|
||||
with open(ci_workflow, "w") as f:
|
||||
f.write(github_workflow_template.format(**hooks))
|
||||
|
||||
|
||||
manifest_template = """include MANIFEST.in
|
||||
|
|
@ -165,8 +195,6 @@ app_name = "{app_name}"
|
|||
app_title = "{app_title}"
|
||||
app_publisher = "{app_publisher}"
|
||||
app_description = "{app_description}"
|
||||
app_icon = "{app_icon}"
|
||||
app_color = "{app_color}"
|
||||
app_email = "{app_email}"
|
||||
app_license = "{app_license}"
|
||||
|
||||
|
|
@ -364,8 +392,6 @@ def get_data():
|
|||
return [
|
||||
{{
|
||||
"module_name": "{app_title}",
|
||||
"color": "{app_color}",
|
||||
"icon": "{app_icon}",
|
||||
"type": "module",
|
||||
"label": _("{app_title}")
|
||||
}}
|
||||
|
|
@ -398,7 +424,8 @@ gitignore_template = """.DS_Store
|
|||
*.egg-info
|
||||
*.swp
|
||||
tags
|
||||
{app_name}/docs/current"""
|
||||
{app_name}/docs/current
|
||||
node_modules/"""
|
||||
|
||||
docs_template = '''"""
|
||||
Configuration for docs
|
||||
|
|
@ -411,3 +438,96 @@ Configuration for docs
|
|||
def get_context(context):
|
||||
context.brand_html = "{app_title}"
|
||||
'''
|
||||
|
||||
|
||||
github_workflow_template = """
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: develop-{app_name}-${{{{ github.event.number }}}}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
name: Server
|
||||
|
||||
services:
|
||||
mariadb:
|
||||
image: mariadb:10.3
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
ports:
|
||||
- 3306:3306
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
check-latest: true
|
||||
|
||||
- name: Cache pip
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{{{ runner.os }}}}-pip-${{{{ hashFiles('**/*requirements.txt') }}}}
|
||||
restore-keys: |
|
||||
${{{{ runner.os }}}}-pip-
|
||||
${{{{ runner.os }}}}-
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: 'echo "::set-output name=dir::$(yarn cache dir)"'
|
||||
|
||||
- uses: actions/cache@v2
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{{{ steps.yarn-cache-dir-path.outputs.dir }}}}
|
||||
key: ${{{{ runner.os }}}}-yarn-${{{{ hashFiles('**/yarn.lock') }}}}
|
||||
restore-keys: |
|
||||
${{{{ runner.os }}}}-yarn-
|
||||
|
||||
- name: Setup
|
||||
run: |
|
||||
pip install frappe-bench
|
||||
bench init --skip-redis-config-generation --skip-assets --python "$(which python)" ~/frappe-bench
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
|
||||
|
||||
- name: Install
|
||||
working-directory: /home/runner/frappe-bench
|
||||
run: |
|
||||
bench get-app {app_name} $GITHUB_WORKSPACE
|
||||
bench setup requirements --dev
|
||||
bench new-site --db-root-password root --admin-password admin test_site
|
||||
bench --site test_site install-app {app_name}
|
||||
bench build
|
||||
env:
|
||||
CI: 'Yes'
|
||||
|
||||
- name: Run Tests
|
||||
working-directory: /home/runner/frappe-bench
|
||||
run: |
|
||||
bench --site test_site set-config allow_tests true
|
||||
bench --site test_site run-tests --app {app_name}
|
||||
env:
|
||||
TYPE: server
|
||||
"""
|
||||
|
|
|
|||
2
hooks.md
2
hooks.md
|
|
@ -7,8 +7,6 @@
|
|||
1. `app_publisher`
|
||||
1. `app_description`
|
||||
1. `app_version`
|
||||
1. `app_icon` - font-awesome icon or image url
|
||||
1. `app_color` - hex colour background of the app icon
|
||||
|
||||
#### Install
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue