diff --git a/rattail/app.py b/rattail/app.py index 9fd74077842bc181abd1ea62905942af5b473d0d..f168797e08a92376413e11df9b975902cce9c8af 100644 --- a/rattail/app.py +++ b/rattail/app.py @@ -104,6 +104,35 @@ class AppHandler(object): """ return self.config.app_title(default=default) + def get_class_prefix(self, default=None): + """ + Returns the "class prefix" for the app, used when naming model + classes etc. + """ + prefix = self.config.get('rattail', 'app_class_prefix', + default=default) + if prefix: + return prefix + + title = self.get_title(default="Rattail") + prefix = title.replace(' ', '') + return prefix + + def get_table_prefix(self, default=None): + """ + Returns the "table prefix" for the app, used when naming + tables etc. + """ + prefix = self.config.get('rattail', 'app_table_prefix', + default=default) + if prefix: + return prefix + + title = self.get_title(default="Rattail") + prefix = title.lower()\ + .replace(' ', '_') + return prefix + def get_timezone(self, key='default'): """ Returns a configured time zone. @@ -595,6 +624,20 @@ class AppHandler(object): self.datasync_handler = Handler(self.config, **kwargs) return self.datasync_handler + def get_db_handler(self, **kwargs): + """ + Get the configured "database" handler. + + :returns: The :class:`~rattail.db.handler.DatabaseHandler` + instance for the app. + """ + if not hasattr(self, 'db_handler'): + spec = self.config.get('rattail.db', 'handler', + default='rattail.db.handler:DatabaseHandler') + Handler = self.load_object(spec) + self.db_handler = Handler(self.config) + return self.db_handler + def get_employment_handler(self, **kwargs): """ Get the configured "employment" handler. diff --git a/rattail/commands/core.py b/rattail/commands/core.py index d21ddc32d62d5630dedc505ea3eec8954c8cbcc3..a0c6dc1f72ee20f1f40fb8e532277a90d9b2b8ce 100644 --- a/rattail/commands/core.py +++ b/rattail/commands/core.py @@ -438,6 +438,87 @@ class Subcommand(object): """ raise NotImplementedError + def require_prompt_toolkit(self): + try: + import prompt_toolkit + except ImportError: + value = input("\nprompt_toolkit is not installed. shall i install it? [Yn] ") + value = value.strip() + if value and not self.config.parse_bool(value): + self.stderr.write("prompt_toolkit is required; aborting\n") + sys.exit(1) + + subprocess.check_call(['pip', 'install', 'prompt_toolkit']) + + def require_rich(self): + try: + import rich + except ImportError: + value = input("\nrich is not installed. shall i install it? [Yn] ") + value = value.strip() + if value and not self.config.parse_bool(value): + self.stderr.write("rich is required; aborting\n") + sys.exit(1) + + subprocess.check_call(['pip', 'install', 'rich']) + + def rprint(self, *args, **kwargs): + self.require_rich() + + from rich import print as rprint + + return rprint(*args, **kwargs) + + def basic_prompt(self, info, default=None, is_password=False, is_bool=False, + required=False): + self.require_prompt_toolkit() + + from prompt_toolkit import prompt + from prompt_toolkit.styles import Style + + # message formatting styles + style = Style.from_dict({ + '': '', + 'bold': 'bold', + }) + + # build prompt message + message = [ + ('', '\n'), + ('class:bold', info), + ] + if default is not None: + if is_bool: + message.append(('', ' [{}]: '.format('Y' if default else 'N'))) + else: + message.append(('', ' [{}]: '.format(default))) + else: + message.append(('', ': ')) + + # prompt user for input + try: + text = prompt(message, style=style, is_password=is_password) + except (KeyboardInterrupt, EOFError): + self.rprint("\n\t[bold yellow]operation canceled by user[/bold yellow]\n", + file=self.stderr) + sys.exit(2) + + if is_bool: + if text == '': + return default + elif text.upper() == 'Y': + return True + elif text.upper() == 'N': + return False + self.rprint("\n\t[bold yellow]ambiguous, please try again[/bold yellow]\n") + return self.basic_prompt(info, default, is_bool=True) + + if required and not text and not default: + return self.basic_prompt(info, default, is_password=is_password, + required=True) + + return text or default + class CheckDatabase(Subcommand): """ diff --git a/rattail/commands/projects.py b/rattail/commands/projects.py new file mode 100644 index 0000000000000000000000000000000000000000..5aaac46b2dba366fba44f45a5aad308efbe6c3bf --- /dev/null +++ b/rattail/commands/projects.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2022 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# Rattail. If not, see . +# +################################################################################ +""" +Project Commands +""" + +from __future__ import unicode_literals, absolute_import + +import os +import subprocess +import sys + +from rattail.commands.core import Subcommand +from rattail.projects.handler import RattailProjectHandler + + +class MakeProject(Subcommand): + """ + Make a new app project + """ + name = 'make-project' + description = __doc__.strip() + + def add_parser_args(self, parser): + parser.add_argument('path', + help="Path to project folder") + + def run(self, args): + + path = os.path.abspath(args.path) + if os.path.exists(path): + self.stderr.write("path already exists: {}\n".format(path)) + sys.exit(1) + + # nb. technically these would be required and auto-installed + # as needed (later), but seems better to do this explicitly + # up-front before any other command output + self.require_prompt_toolkit() + self.require_rich() + + # welcome, continue? + self.rprint("\n\t[blue]Welcome to Rattail[/blue]") + self.rprint("\n\tThis tool will generate source code for a new project.") + if not self.basic_prompt("continue?", True, is_bool=True): + self.rprint() + sys.exit(0) + + # name + name = os.path.basename(path) + + # app_table_prefix + prefix = name + prefix = prefix.replace('-', '_') + prefix = prefix.replace(' ', '_') + app_table_prefix = self.basic_prompt('app_table_prefix', + default=prefix) + app_table_prefix = app_table_prefix.rstrip('_') + + # app_class_prefix + prefix = name + prefix = prefix.replace('-', '_') + prefix = prefix.replace('_', ' ') + app_class_prefix = ''.join([w.capitalize() for w in prefix.split()]) + app_class_prefix = self.basic_prompt('app_class_prefix', + default=app_class_prefix) + + # org_name + org_name = self.basic_prompt('org_name', required=True) + + # pypi_name + pypi_name = name + pypi_name = pypi_name.replace('_', ' ') + pypi_name = pypi_name.replace('-', ' ') + pypi_name = '-'.join([w.capitalize() + for w in org_name.split() + pypi_name.split()]) + pypi_name = self.basic_prompt('pypi_name', default=pypi_name) + + # app_title + app_title = name + app_title = app_title.replace('-', ' ') + app_title = app_title.replace('_', ' ') + app_title = ' '.join([w.capitalize() for w in app_title.split()]) + + # generate project + project_handler = RattailProjectHandler(self.config) + options = { + 'name': app_title, + 'slug': name, + 'organization': org_name, + 'python_project_name': pypi_name, + 'python_name': app_table_prefix, + 'has_db': True, + 'extends_db': True, + 'has_web': True, + + # TODO: these should not be needed..? + 'has_web_api': False, + 'has_datasync': False, + 'integrates_catapult': False, + 'integrates_corepos': False, + 'integrates_locsms': False, + 'uses_fabric': False, + } + project_handler.generate_project('rattail', name, options, path=path) + self.rprint("\n\tproject created at: [bold green]{}[/bold green]".format( + path)) + + # install pkg + if self.basic_prompt("install project package?", is_bool=True, default=True): + subprocess.check_call(['pip', 'install', '-e', path]) + self.rprint("\n\tpackage installed: [bold green]{}[/bold green]".format( + pypi_name)) + + self.rprint("\n\tinstall and configure the app with:") + self.rprint("\n\t[blue]{} -n install[/blue]".format(name)) + + self.rprint() diff --git a/rattail/data/config/web-complete.conf.mako b/rattail/data/config/web-complete.conf.mako index dc671c35263767ec69fe848a48a390328599e007..88dafe00a770a5e1a522c261e21db06500bfd375 100644 --- a/rattail/data/config/web-complete.conf.mako +++ b/rattail/data/config/web-complete.conf.mako @@ -26,7 +26,11 @@ use = egg:${pyramid_egg} pyramid.reload_templates = true pyramid.debug_all = true pyramid.default_locale_name = en -# pyramid.includes = pyramid_debugtoolbar +# TODO: you may want exclog only in production, not dev. +# also you may want debugtoolbar in dev +pyramid.includes = + pyramid_exclog + # pyramid_debugtoolbar beaker.session.type = file beaker.session.data_dir = %(here)s/cache/sessions/data @@ -49,7 +53,7 @@ port = ${pyramid_port} # NOTE: this is needed for local reverse proxy stuff to work with HTTPS # https://docs.pylonsproject.org/projects/waitress/en/latest/reverse-proxy.html # https://docs.pylonsproject.org/projects/waitress/en/latest/arguments.html -# trusted_proxy = 127.0.0.1 +trusted_proxy = 127.0.0.1 # TODO: leave this empty if proxy serves as root site, e.g. http://rattail.example.com/ # url_prefix = diff --git a/rattail/db/handler.py b/rattail/db/handler.py new file mode 100644 index 0000000000000000000000000000000000000000..3877b842488a33f5f67d01683abf33b21b736a1b --- /dev/null +++ b/rattail/db/handler.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2022 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# Rattail. If not, see . +# +################################################################################ +""" +Database Handler +""" + +from __future__ import unicode_literals, absolute_import + +from alembic.config import Config as AlembicConfig +from alembic.script import ScriptDirectory + +from rattail.app import GenericHandler + + +class DatabaseHandler(GenericHandler): + """ + Base class and default implementation for the DB handler. + """ + + def get_alembic_branch_names(self, **kwargs): + """ + Returns a list of Alembic branch names present in the default + database schema. + """ + alembic_config = AlembicConfig() + alembic_config.set_main_option( + 'script_location', + self.config.get('alembic', 'script_location', usedb=False)) + alembic_config.set_main_option( + 'version_locations', + self.config.get('alembic', 'version_locations', usedb=False)) + + script = ScriptDirectory.from_config(alembic_config) + + branches = set() + for rev in script.get_revisions(script.get_heads()): + branches.update(rev.branch_labels) + + return sorted(branches) diff --git a/rattail/projects/handler.py b/rattail/projects/handler.py index 7312026584b3989a6efb3b90e38d6ff44e0da6d5..b09718a201d3083930904fb496b28f217ab06b0f 100644 --- a/rattail/projects/handler.py +++ b/rattail/projects/handler.py @@ -28,6 +28,7 @@ from __future__ import unicode_literals, absolute_import import os import random +import re import shutil import string import subprocess @@ -287,6 +288,9 @@ class RattailProjectHandler(ProjectHandler): """ And here we do some experimentation... """ + from alembic.config import Config as AlembicConfig + from alembic.command import revision as alembic_revision + context = options ############################## @@ -329,8 +333,6 @@ class RattailProjectHandler(ProjectHandler): self.generate('rattail/package/emails.py.mako', os.path.join(package, 'emails.py'), **context) - self.generate('rattail/package/settings.py', os.path.join(package, 'settings.py')) - ############################## # data dir ############################## @@ -380,17 +382,56 @@ class RattailProjectHandler(ProjectHandler): alembic = os.path.join(db, 'alembic') os.makedirs(alembic) - self.generate('rattail/package/db/alembic/README', os.path.join(alembic, 'README')) - self.generate('rattail/package/db/alembic/env.py.mako', os.path.join(alembic, 'env.py'), **context) - self.generate('rattail/package/db/alembic/script.py.mako_', os.path.join(alembic, 'script.py.mako')) - versions = os.path.join(alembic, 'versions') os.makedirs(versions) - self.generate('rattail/package/db/alembic/versions/.keepme', os.path.join(versions, '.keepme')) + # make alembic config, aware of new project versions folder + alembic_config = AlembicConfig() + alembic_config.set_main_option('script_location', + 'rattail.db:alembic') + alembic_config.set_main_option('version_locations', + '{} rattail.db:alembic/versions'.format( + versions)) + + # generate first revision script for new project + script = alembic_revision(alembic_config, + version_path=versions, + head='rattail@head', + splice=True, + branch_label=context['python_name'], + message="add {} branch".format(context['python_name'])) + + # declare `down_revision = None` ..no way to tell alembic + # to do that apparently, so we must rewrite file + with open(script.path, 'rt') as f: + old_contents = f.read() + new_contents = [] + for line in old_contents.split('\n'): + if line.startswith('down_revision ='): + line = re.sub(r"'\w+'", 'None', line) + new_contents.append(line) + with open(script.path, 'wt') as f: + f.write('\n'.join(new_contents)) + + ############################## + # templates + ############################## + + templates = os.path.join(package, 'templates') + os.makedirs(templates) + + installer = os.path.join(templates, 'installer') + os.makedirs(installer) + + self.generate('rattail/package/templates/installer/rattail.conf.mako_tmpl', + os.path.join(installer, 'rattail.conf.mako'), + **context) + + self.generate('rattail/package/templates/installer/upgrade.sh.mako_', + os.path.join(installer, 'upgrade.sh.mako')) ############################## # web package dir @@ -418,10 +459,11 @@ class RattailProjectHandler(ProjectHandler): self.generate('rattail/package/web/static/__init__.py.mako', os.path.join(static, '__init__.py'), **context) - templates = os.path.join(web, 'templates') - os.makedirs(templates) + web_templates = os.path.join(web, 'templates') + os.makedirs(web_templates) - self.generate('rattail/package/web/templates/base_meta.mako_tmpl', os.path.join(templates, 'base_meta.mako'), + self.generate('rattail/package/web/templates/base_meta.mako_tmpl', + os.path.join(web_templates, 'base_meta.mako'), **context) views = os.path.join(web, 'views') diff --git a/rattail/projects/rattail/gitignore.mako b/rattail/projects/rattail/gitignore.mako index 5b4e2a850d70fdd53f6c09b922b06d7d6cbbb2e3..8029460421f27a5d160b438d837e63aa43c7e8d8 100644 --- a/rattail/projects/rattail/gitignore.mako +++ b/rattail/projects/rattail/gitignore.mako @@ -1,4 +1,6 @@ ${egg_name}.egg-info/ +% if uses_fabric: machines/*/.vagrant/ machines/*/fabenv.py machines/*/fabric.yaml +% endif diff --git a/rattail/projects/rattail/package/commands.py.mako b/rattail/projects/rattail/package/commands.py.mako index 9e5804b9e0315401e337b528c42e69033c859f12..99103f35941b4ab3bed58585709418e3432d2a69 100644 --- a/rattail/projects/rattail/package/commands.py.mako +++ b/rattail/projects/rattail/package/commands.py.mako @@ -3,9 +3,15 @@ ${name} commands """ +import os +import stat +import subprocess import sys +from alembic.util.messaging import obfuscate_url_pw + from rattail import commands +from rattail.files import resource_path from ${python_name} import __version__ @@ -38,3 +44,222 @@ class HelloWorld(commands.Subcommand): def run(self, args): self.stdout.write("hello world!\n") + + +class Install(commands.Subcommand): + """ + Install the ${name} app + """ + name = 'install' + description = __doc__.strip() + + def run(self, args): + + # nb. technically these would be required and auto-installed + # as needed (later), but seems better to do this explicitly + # up-front before any other command output + self.require_prompt_toolkit() + self.require_rich() + + self.rprint("\n\t[blue]Welcome to ${name}![/blue]") + self.rprint("\n\tThis tool will install and configure a new app.") + self.rprint("\n\t[italic]NB. You should already have created a new database in PostgreSQL or MySQL.[/italic]") + + # continue? + if not self.basic_prompt("continue?", True, is_bool=True): + self.rprint() + sys.exit(0) + + # appdir must not yet exist + appdir = os.path.join(sys.prefix, 'app') + if os.path.exists(appdir): + self.rprint("\n\t[bold red]appdir already exists:[/bold red] {}\n".format(appdir)) + sys.exit(1) + + # get db info + dbtype = self.basic_prompt('db type', 'postgresql') + dbhost = self.basic_prompt('db host', 'localhost') + dbport = self.basic_prompt('db port', '3306' if dbtype == 'mysql' else '5432') + dbname = self.basic_prompt('db name', '${python_name}') + dbuser = self.basic_prompt('db user', 'rattail') + + # get db password + dbpass = None + while not dbpass: + dbpass = self.basic_prompt('db pass', is_password=True) + + # test db connection + self.rprint("\n\ttesting db connection... ", end='') + dburl = self.make_db_url(dbtype, dbhost, dbport, dbname, dbuser, dbpass) + error = self.test_db_connection(dburl) + if error: + self.rprint("[bold red]cannot connect![/bold red] ..error was:") + self.rprint("\n{}".format(error)) + self.rprint("\n\t[bold yellow]aborting mission[/bold yellow]\n") + sys.exit(1) + self.rprint("[bold green]good[/bold green]") + + # make the appdir + self.app.make_appdir(appdir) + + # shared context for generated app files + context = { + 'envdir': sys.prefix, + 'app_package': '${python_name}', + 'app_title': "${name}", + 'pypi_name': '${python_project_name}', + 'appdir': appdir, + 'db_url': dburl, + 'pyramid_egg': '${egg_name}', + 'beaker_key': '${python_name}', + } + + # make config files + rattail_conf = self.app.make_config_file( + 'rattail', os.path.join(appdir, 'rattail.conf'), + template_path=resource_path('${python_name}:templates/installer/rattail.conf.mako'), + **context) + quiet_conf = self.app.make_config_file('quiet', appdir) + web_conf = self.app.make_config_file( + 'web-complete', os.path.join(appdir, 'web.conf'), + **context) + + # make upgrade script + path = os.path.join(appdir, 'upgrade.sh') + self.app.render_mako_template( + resource_path('${python_name}:templates/installer/upgrade.sh.mako'), + context, output_path=path) + os.chmod(path, stat.S_IRWXU + | stat.S_IRGRP + | stat.S_IXGRP + | stat.S_IROTH + | stat.S_IXOTH) + + self.rprint("\n\tappdir created at: [bold green]{}[/bold green]".format(appdir)) + + bindir = os.path.join(sys.prefix, 'bin') + + schema_installed = False + if self.basic_prompt("install db schema?", True, is_bool=True): + self.rprint() + + # install db schema + alembic = os.path.join(bindir, 'alembic') + cmd = [alembic, '-c', rattail_conf, 'upgrade', 'heads'] + subprocess.check_call(cmd) + schema_installed = True + + rattail = os.path.join(bindir, 'rattail') + + # set falafel theme + cmd = [rattail, '-c', quiet_conf, '--no-versioning', + 'setting-put', 'tailbone.theme', 'falafel'] + subprocess.check_call(cmd) + + # hide theme picker + cmd = [rattail, '-c', quiet_conf, '--no-versioning', + 'setting-put', 'tailbone.themes.expose_picker', 'false'] + subprocess.check_call(cmd) + + # set main image + cmd = [rattail, '-c', quiet_conf, '--no-versioning', + 'setting-put', 'tailbone.main_image_url', '/tailbone/img/home_logo.png'] + subprocess.check_call(cmd) + + # set header image + cmd = [rattail, '-c', quiet_conf, '--no-versioning', + 'setting-put', 'tailbone.header_image_url', '/tailbone/img/rattail.ico'] + subprocess.check_call(cmd) + + # set favicon image + cmd = [rattail, '-c', quiet_conf, '--no-versioning', + 'setting-put', 'tailbone.favicon_url', '/tailbone/img/rattail.ico'] + subprocess.check_call(cmd) + + # set vue version + cmd = [rattail, '-c', quiet_conf, '--no-versioning', + 'setting-put', 'tailbone.vue_version', '2.6.14'] + subprocess.check_call(cmd) + + # set buefy version + cmd = [rattail, '-c', quiet_conf, '--no-versioning', + 'setting-put', 'tailbone.buefy_version', 'latest'] + subprocess.check_call(cmd) + + # set default grid page size + cmd = [rattail, '-c', quiet_conf, '--no-versioning', + 'setting-put', 'tailbone.grid.default_pagesize', '20'] + subprocess.check_call(cmd) + + self.rprint("\n\tdb schema installed to: [bold green]{}[/bold green]".format( + obfuscate_url_pw(dburl))) + + if self.basic_prompt("create admin user?", True, is_bool=True): + + # get admin credentials + username = self.basic_prompt('admin username', 'admin') + password = None + while not password: + password = self.basic_prompt('admin password', is_password=True) + if password: + confirm = self.basic_prompt('confirm password', is_password=True) + if not confirm or confirm != password: + self.rprint("[bold yellow]passwords did not match[/bold yellow]") + password = None + fullname = self.basic_prompt('full name') + + self.rprint() + + # make admin user + rattail = os.path.join(bindir, 'rattail') + cmd = [rattail, '-c', quiet_conf, 'make-user', '-A', username, + '--password', password] + if fullname: + cmd.extend(['--full-name', fullname]) + subprocess.check_call(cmd) + + self.rprint("\n\tadmin user created: [bold green]{}[/bold green]".format( + username)) + + self.rprint("\n\t[bold green]initial setup is complete![/bold green]") + + if schema_installed: + self.rprint("\n\tyou can run the web app with:") + self.rprint("\n\t[blue]cd {}[/blue]".format(sys.prefix)) + self.rprint("\t[blue]bin/pserve file+ini:app/web.conf[/blue]") + + self.rprint() + + def make_db_url(self, dbtype, dbhost, dbport, dbname, dbuser, dbpass): + try: + # newer style + from sqlalchemy.engine import URL + factory = URL.create + except ImportError: + # older style + from sqlalchemy.engine.url import URL + factory = URL + + if dbtype == 'mysql': + drivername = 'mysql+mysqlconnector' + else: + drivername = 'postgresql+psycopg2' + + return factory(drivername=drivername, + username=dbuser, + password=dbpass, + host=dbhost, + port=dbport, + database=dbname) + + def test_db_connection(self, url): + from sqlalchemy import create_engine + + engine = create_engine(url) + + # check for random table; does not matter if it exists, we + # just need to test interaction and this is a neutral way + try: + engine.has_table('whatever') + except Exception as error: + return str(error) diff --git a/rattail/projects/rattail/package/config.py.mako b/rattail/projects/rattail/package/config.py.mako index 6ab12ab4ccf399742ccbea3790fe65fb1615446f..6d8c3ea4796a85f90ebde5a7e968b80dd9a9f6ff 100644 --- a/rattail/projects/rattail/package/config.py.mako +++ b/rattail/projects/rattail/package/config.py.mako @@ -16,7 +16,6 @@ class ${studly_name}Config(ConfigExtension): # set some default config values config.setdefault('rattail.mail', 'emails', '${python_name}.emails') - config.setdefault('rattail', 'settings', '${python_name}.settings') config.setdefault('tailbone', 'menus', '${python_name}.web.menus') config.setdefault('rattail.config', 'templates', '${python_name}:data/config rattail:data/config') diff --git a/rattail/projects/rattail/package/db/alembic/README b/rattail/projects/rattail/package/db/alembic/README deleted file mode 100644 index 98e4f9c44effe479ed38c66ba922e7bcc672916f..0000000000000000000000000000000000000000 --- a/rattail/projects/rattail/package/db/alembic/README +++ /dev/null @@ -1 +0,0 @@ -Generic single-database configuration. \ No newline at end of file diff --git a/rattail/projects/rattail/package/db/alembic/script.py.mako_ b/rattail/projects/rattail/package/db/alembic/script.py.mako_ deleted file mode 100644 index 70d02548df89ff3d52d96cb17fd4e289515954a2..0000000000000000000000000000000000000000 --- a/rattail/projects/rattail/package/db/alembic/script.py.mako_ +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8; mode: python; -*- -# -*- coding: utf-8 -*- -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" - -from __future__ import unicode_literals, absolute_import - -# revision identifiers, used by Alembic. -revision = ${repr(up_revision)} -down_revision = ${repr(down_revision)} -branch_labels = ${repr(branch_labels)} -depends_on = ${repr(depends_on)} - -from alembic import op -import sqlalchemy as sa -import rattail.db.types -${imports if imports else ""} - - -def upgrade(): - ${upgrades if upgrades else "pass"} - - -def downgrade(): - ${downgrades if downgrades else "pass"} diff --git a/rattail/projects/rattail/package/db/alembic/versions/.keepme b/rattail/projects/rattail/package/db/alembic/versions/.keepme deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/rattail/projects/rattail/package/settings.py b/rattail/projects/rattail/package/settings.py deleted file mode 100644 index 5cdeb943817c78a577ac8018d860ad0096999025..0000000000000000000000000000000000000000 --- a/rattail/projects/rattail/package/settings.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8; -*- -""" -App Settings -""" - -from rattail.settings import Setting - - -# bring in some common settings from rattail -from rattail.settings import ( - - # (General) - rattail_app_title, - tailbone_background_color, - - # Email - rattail_mail_record_attempts, - - # Product - rattail_product_key, - rattail_product_key_title, - tailbone_products_show_pod_image, - - # Purchasing / Receiving - rattail_batch_purchase_allow_cases, - rattail_batch_purchase_allow_expired_credits, -) diff --git a/rattail/projects/rattail/package/templates/installer/rattail.conf.mako_tmpl b/rattail/projects/rattail/package/templates/installer/rattail.conf.mako_tmpl new file mode 100644 index 0000000000000000000000000000000000000000..09ac259c44174be76577beee0961bb9b30ee7d5f --- /dev/null +++ b/rattail/projects/rattail/package/templates/installer/rattail.conf.mako_tmpl @@ -0,0 +1,147 @@ +## -*- mode: conf; -*- + +<%%text>############################################################ +# +# ${app_title} core config +# +<%%text>############################################################ + + +<%%text>############################## +# rattail +<%%text>############################## + +[rattail] +app_title = ${app_title} +app_package = ${app_package} +timezone.default = ${timezone} +appdir = ${appdir} +datadir = ${os.path.join(appdir, 'data')} +batch.files = ${os.path.join(appdir, 'data', 'batch')} +workdir = ${os.path.join(appdir, 'work')} +export.files = ${os.path.join(appdir, 'data', 'exports')} + +[rattail.config] +# require = /etc/rattail/rattail.conf +configure_logging = true +usedb = true +preferdb = true + +[rattail.db] +default.url = ${db_url} +versioning.enabled = true + +[rattail.mail] + +# this is the global email shutoff switch +#send_emails = false + +# recommended setup is to always talk to postfix on localhost and then +# it can handle any need complexities, e.g. sending to relay +smtp.server = localhost + +# by default only email templates from rattail proper are used +templates = rattail:templates/mail + +# this is the "default" email profile, from which all others initially +# inherit, but most/all profiles will override these values +default.prefix = [${app_title}] +default.from = rattail@localhost +default.to = root@localhost +# nb. in test environment it can be useful to disable by default, and +# then selectively enable certain (e.g. feedback, upgrade) emails +#default.enabled = false + +[rattail.upgrades] +command = ${os.path.join(appdir, 'upgrade.sh')} --verbose +files = ${os.path.join(appdir, 'data', 'upgrades')} + + +<%%text>############################## +# alembic +<%%text>############################## + +[alembic] +script_location = rattail.db:alembic +version_locations = %(alembic_version_locations)s + + +<%%text>############################## +# logging +<%%text>############################## + +[loggers] +keys = root, exc_logger, beaker, txn, sqlalchemy, django_db, flufl_bounce, requests + +[handlers] +keys = file, console, email + +[formatters] +keys = generic, console + +[logger_root] +handlers = file, console +level = DEBUG + +[logger_exc_logger] +qualname = exc_logger +handlers = email +level = ERROR + +[logger_beaker] +qualname = beaker +handlers = +level = INFO + +[logger_txn] +qualname = txn +handlers = +level = INFO + +[logger_sqlalchemy] +qualname = sqlalchemy.engine +handlers = +# handlers = file +# level = INFO + +[logger_django_db] +qualname = django.db.backends +handlers = +level = INFO +# level = DEBUG + +[logger_flufl_bounce] +qualname = flufl.bounce +handlers = +level = WARNING + +[logger_requests] +qualname = requests +handlers = +# level = WARNING + +[handler_file] +class = handlers.RotatingFileHandler +args = (${repr(os.path.join(appdir, 'log', 'rattail.log'))}, 'a', 1000000, 100, 'utf_8') +formatter = generic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +formatter = console +# formatter = generic +# level = INFO +# level = WARNING + +[handler_email] +class = handlers.SMTPHandler +args = ('localhost', 'rattail@localhost', ['root@localhost'], "[Rattail] Logging") +formatter = generic +level = ERROR + +[formatter_generic] +format = %%(asctime)s %%(levelname)-5.5s [%%(name)s][%%(threadName)s] %%(funcName)s: %%(message)s +datefmt = %%Y-%%m-%%d %%H:%%M:%%S + +[formatter_console] +format = %%(levelname)-5.5s [%%(name)s][%%(threadName)s] %%(funcName)s: %%(message)s diff --git a/rattail/projects/rattail/package/templates/installer/upgrade.sh.mako_ b/rattail/projects/rattail/package/templates/installer/upgrade.sh.mako_ new file mode 100755 index 0000000000000000000000000000000000000000..81c1d0a8050440b8b8f03e675aabb5945e10615e --- /dev/null +++ b/rattail/projects/rattail/package/templates/installer/upgrade.sh.mako_ @@ -0,0 +1,29 @@ +#!/bin/sh -e +<%text>################################################## +# +# upgrade script for ${app_title} app +# +<%text>################################################## + +if [ "$1" = "--verbose" ]; then + VERBOSE='--verbose' + QUIET= +else + VERBOSE= + QUIET='--quiet' +fi + +cd ${envdir} + +PIP='bin/pip' +ALEMBIC='bin/alembic' + +# upgrade pip and friends +$PIP install $QUIET --disable-pip-version-check --upgrade pip +$PIP install $QUIET --upgrade setuptools wheel + +# upgrade app proper +$PIP install $QUIET --upgrade --upgrade-strategy eager ${pypi_name} + +# migrate schema +$ALEMBIC -c app/rattail.conf upgrade heads diff --git a/rattail/projects/rattail/package/web/views/__init__.py.mako b/rattail/projects/rattail/package/web/views/__init__.py.mako index 05f07be49daebf6e6c5f378be0b2947ace2ebeea..ecc896d9c31a26864a38407d22993e87d661cd1f 100644 --- a/rattail/projects/rattail/package/web/views/__init__.py.mako +++ b/rattail/projects/rattail/package/web/views/__init__.py.mako @@ -70,5 +70,5 @@ def includeme(config): config.include('tailbone.views.purchasing') # batch views - config.include('tailbone.views.handheld') + config.include('tailbone.views.batch.handheld') config.include('tailbone.views.batch.inventory') diff --git a/rattail/projects/rattail/setup.py.mako b/rattail/projects/rattail/setup.py.mako index 31e5402ad004332a1c7a670bf995b63f6e0fb35d..bb47e325fd4880d0a4d7bdc5b2386cecffd9d17e 100644 --- a/rattail/projects/rattail/setup.py.mako +++ b/rattail/projects/rattail/setup.py.mako @@ -44,7 +44,9 @@ requires = [ 'invoke', # 1.4.1 'rattail[auth,db,bouncer]', # 0.9.141 + % if uses_fabric: 'rattail-fabric2', # 0.2.1 + % endif % if has_db: 'psycopg2', # 2.8.4 @@ -119,6 +121,7 @@ setup( '${python_name}.commands': [ 'hello = ${python_name}.commands:HelloWorld', + 'install = ${python_name}.commands:Install', ], % if has_web: diff --git a/rattail/settings.py b/rattail/settings.py index 0030a4a12592e6e51fdac83dfdbb796c18072ba8..4fd4dc0257878f46862c6e6b641e853e3368c407 100644 --- a/rattail/settings.py +++ b/rattail/settings.py @@ -30,10 +30,16 @@ from __future__ import unicode_literals, absolute_import class Setting(object): """ Base class for all setting definitions. + + .. attribute:: core + + Boolean indicating if this is a "core setting" - i.e. one which + should be exposed in the App Settings in most/all apps. """ group = "(General)" namespace = None name = None + core = False data_type = str choices = None required = False @@ -49,6 +55,29 @@ class rattail_app_title(Setting): """ namespace = 'rattail' name = 'app_title' + core = True + + +class rattail_app_class_prefix(Setting): + """ + App-specific "capwords-style" prefix, used for naming model + classes and other things. E.g. with prefix of 'Poser', model + might be named 'PoserWidget'. + """ + namespace = 'rattail' + name = 'app_class_prefix' + core = True + + +class rattail_app_table_prefix(Setting): + """ + App-specific "variable-style" prefix, used for naming tables and + other things. E.g. with prefix of 'poser', table might be named + 'poser_widget'. + """ + namespace = 'rattail' + name = 'app_table_prefix' + core = True class rattail_node_title(Setting): @@ -68,6 +97,7 @@ class rattail_production(Setting): namespace = 'rattail' name = 'production' data_type = bool + core = True class tailbone_background_color(Setting): @@ -87,6 +117,16 @@ class tailbone_buefy_version(Setting): """ namespace = 'tailbone' name = 'buefy_version' + core = True + + +class tailbone_favicon_url(Setting): + """ + URL of favicon image. + """ + namespace = 'tailbone' + name = 'favicon_url' + core = True class tailbone_feedback_allows_reply(Setting): @@ -106,6 +146,25 @@ class tailbone_grid_default_pagesize(Setting): namespace = 'tailbone' name = 'grid.default_pagesize' data_type = int + core = True + + +class tailbone_header_image_url(Setting): + """ + URL of smaller logo image, shown in menu header. + """ + namespace = 'tailbone' + name = 'header_image_url' + core = True + + +class tailbone_main_image_url(Setting): + """ + URL of main app logo image, e.g. on home/login page. + """ + namespace = 'tailbone' + name = 'main_image_url' + core = True class tailbone_sticky_headers(Setting): @@ -128,6 +187,7 @@ class tailbone_vue_version(Setting): """ namespace = 'tailbone' name = 'vue_version' + core = True class rattail_single_store(Setting): diff --git a/setup.py b/setup.py index 908f55c662258f6d2e0cdbcaa66ef444e65f962d..3570c12232764ed8b68f8755bd84d49b5d1d53cd 100644 --- a/setup.py +++ b/setup.py @@ -225,6 +225,7 @@ setup( 'make-appdir = rattail.commands.core:MakeAppDir', 'make-batch = rattail.commands.batch:MakeBatch', 'make-config = rattail.commands.core:MakeConfig', + 'make-project = rattail.commands.projects:MakeProject', 'make-user = rattail.commands.core:MakeUser', 'make-uuid = rattail.commands.core:MakeUUID', 'mysql-chars = rattail.commands.mysql:MysqlChars',