Merge branch 'develop' into dashboard-view-fixes

This commit is contained in:
gavin 2022-05-10 17:47:31 +05:30 committed by GitHub
commit 3370d227e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 261 additions and 100 deletions

View file

@ -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"

View file

@ -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)

View file

@ -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"

View file

@ -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
"""

View file

@ -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