Changeset - 4ae2762be562
rattail/app.py
Show inline comments
 
@@ -101,12 +101,41 @@ class AppHandler(object):
 
           configured.
 

	
 
        :returns: Title for the app.
 
        """
 
        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.
 

	
 
        Default logic invokes :func:`rattail.time.timezone()` to
 
        obtain the time zone object.
 
@@ -592,12 +621,26 @@ class AppHandler(object):
 
            spec = self.config.get('rattail.datasync', 'handler',
 
                                   default='rattail.datasync.handler:DatasyncHandler')
 
            Handler = self.load_object(spec)
 
            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.
 

	
 
        :returns: The :class:`~rattail.employment.EmploymentHandler`
 
           instance for the app.
rattail/commands/core.py
Show inline comments
 
@@ -435,12 +435,93 @@ class Subcommand(object):
 
    def run(self, args):
 
        """
 
        Run the subcommand logic.
 
        """
 
        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):
 
    """
 
    Do basic sanity checks on a Rattail DB
 
    """
 
    name = 'checkdb'
rattail/commands/projects.py
Show inline comments
 
new file 100644
 
# -*- 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 <http://www.gnu.org/licenses/>.
 
#
 
################################################################################
 
"""
 
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()
rattail/data/config/web-complete.conf.mako
Show inline comments
 
@@ -23,13 +23,17 @@ require = %(here)s/rattail.conf
 
use = egg:${pyramid_egg}
 

	
 
# TODO: you should disable these first two for production
 
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
 
beaker.session.lock_dir = %(here)s/cache/sessions/lock
 
beaker.session.secret = ${beaker_secret}
 
beaker.session.key = ${beaker_key}
 
@@ -46,13 +50,13 @@ use = egg:waitress#main
 
host = ${pyramid_host}
 
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 =
 

	
 
# TODO: or, if proxy serves as subpath of root site, e.g. http://rattail.example.com/backend/
 
# url_prefix = /backend
rattail/db/handler.py
Show inline comments
 
new file 100644
 
# -*- 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 <http://www.gnu.org/licenses/>.
 
#
 
################################################################################
 
"""
 
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)
rattail/projects/handler.py
Show inline comments
 
@@ -25,12 +25,13 @@ Handler for Generating Projects
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
import os
 
import random
 
import re
 
import shutil
 
import string
 
import subprocess
 
import sys
 

	
 
from mako.template import Template
 
@@ -284,12 +285,15 @@ class RattailProjectHandler(ProjectHandler):
 
            return self.generate_rattail_project(slug, options, path)
 

	
 
    def generate_rattail_project(self, slug, options, path):
 
        """
 
        And here we do some experimentation...
 
        """
 
        from alembic.config import Config as AlembicConfig
 
        from alembic.command import revision as alembic_revision
 

	
 
        context = options
 

	
 
        ##############################
 
        # root project dir
 
        ##############################
 

	
 
@@ -326,14 +330,12 @@ class RattailProjectHandler(ProjectHandler):
 
        self.generate('rattail/package/commands.py.mako', os.path.join(package, 'commands.py'),
 
                      **context)
 

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

	
 
        data = os.path.join(package, 'data')
 
        os.makedirs(data)
 
@@ -377,23 +379,62 @@ class RattailProjectHandler(ProjectHandler):
 
            # alembic
 
            ####################
 

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

	
 
        if context['has_web']:
 
@@ -415,16 +456,17 @@ class RattailProjectHandler(ProjectHandler):
 
            static = os.path.join(web, 'static')
 
            os.makedirs(static)
 

	
 
            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')
 
            os.makedirs(views)
 

	
 
            self.generate('rattail/package/web/views/__init__.py.mako', os.path.join(views, '__init__.py'),
rattail/projects/rattail/gitignore.mako
Show inline comments
 
${egg_name}.egg-info/
 
% if uses_fabric:
 
machines/*/.vagrant/
 
machines/*/fabenv.py
 
machines/*/fabric.yaml
 
% endif
rattail/projects/rattail/package/commands.py.mako
Show inline comments
 
# -*- coding: utf-8; mode: python; -*-
 
"""
 
${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__
 

	
 

	
 
def main(*args):
 
    """
 
@@ -35,6 +41,225 @@ class HelloWorld(commands.Subcommand):
 
    """
 
    name = 'hello'
 
    description = __doc__.strip()
 

	
 
    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)
rattail/projects/rattail/package/config.py.mako
Show inline comments
 
@@ -13,13 +13,12 @@ class ${studly_name}Config(ConfigExtension):
 
    key = '${python_name}'
 

	
 
    def configure(self, config):
 

	
 
        # 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')
 

	
 
        ## do we integrate w/ Catapult?
 
        % if integrates_catapult:
 
        config.setdefault('rattail', 'model', '${python_name}.db.model')
rattail/projects/rattail/package/db/alembic/README
Show inline comments
 
deleted file
rattail/projects/rattail/package/db/alembic/script.py.mako_
Show inline comments
 
deleted file
rattail/projects/rattail/package/db/alembic/versions/.keepme
Show inline comments
 
deleted file
rattail/projects/rattail/package/settings.py
Show inline comments
 
deleted file
rattail/projects/rattail/package/templates/installer/rattail.conf.mako_tmpl
Show inline comments
 
new file 100644
 
## -*- mode: conf; -*-
 

	
 
<%%text>############################################################</%%text>
 
#
 
# ${app_title} core config
 
#
 
<%%text>############################################################</%%text>
 

	
 

	
 
<%%text>##############################</%%text>
 
# rattail
 
<%%text>##############################</%%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>##############################</%%text>
 
# alembic
 
<%%text>##############################</%%text>
 

	
 
[alembic]
 
script_location = rattail.db:alembic
 
version_locations = %(alembic_version_locations)s
 

	
 

	
 
<%%text>##############################</%%text>
 
# logging
 
<%%text>##############################</%%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
rattail/projects/rattail/package/templates/installer/upgrade.sh.mako_
Show inline comments
 
new file 100755
 
#!/bin/sh -e
 
<%text>##################################################</%text>
 
#
 
# upgrade script for ${app_title} app
 
#
 
<%text>##################################################</%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
rattail/projects/rattail/package/web/views/__init__.py.mako
Show inline comments
 
@@ -67,8 +67,8 @@ def includeme(config):
 

	
 
    # purchasing / receiving
 
    config.include('tailbone.views.purchases')
 
    config.include('tailbone.views.purchasing')
 

	
 
    # batch views
 
    config.include('tailbone.views.handheld')
 
    config.include('tailbone.views.batch.handheld')
 
    config.include('tailbone.views.batch.inventory')
rattail/projects/rattail/setup.py.mako
Show inline comments
 
@@ -41,13 +41,15 @@ requires = [
 
    # encountered then they should be filed as such.
 
    #
 
    # package                           # low                   high
 

	
 
    '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
 
    % endif
 

	
 
    % if has_web:
 
@@ -116,12 +118,13 @@ setup(
 
        'console_scripts': [
 
            '${python_name} = ${python_name}.commands:main',
 
        ],
 

	
 
        '${python_name}.commands': [
 
            'hello = ${python_name}.commands:HelloWorld',
 
            'install = ${python_name}.commands:Install',
 
        ],
 
        % if has_web:
 

	
 
        'paste.app_factory': [
 
            'main = ${python_name}.web.app:main',
 
        ],
rattail/settings.py
Show inline comments
 
@@ -27,16 +27,22 @@ Common setting definitions
 
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
 

	
 

	
 
##############################
 
@@ -46,12 +52,35 @@ class Setting(object):
 
class rattail_app_title(Setting):
 
    """
 
    Official display title for the app.
 
    """
 
    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):
 
    """
 
    Official display title for the app node.
 
    """
 
@@ -65,12 +94,13 @@ class rattail_production(Setting):
 
    if disabled, the app is considered to be running in development / testing /
 
    staging mode.
 
    """
 
    namespace = 'rattail'
 
    name = 'production'
 
    data_type = bool
 
    core = True
 

	
 

	
 
class tailbone_background_color(Setting):
 
    """
 
    Background color for this app node.  If unset, default color is white.
 
    """
 
@@ -84,12 +114,22 @@ class tailbone_buefy_version(Setting):
 
    based themes.  The old recommendation was to use '0.8.17' but now
 
    anything from 0.9.x or later should be supported.  See what's
 
    available at https://github.com/buefy/buefy/releases
 
    """
 
    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):
 
    """
 
    When user leaves Feedback, should we show them the "Please email
 
    me back" option?
 
@@ -103,12 +143,31 @@ class tailbone_grid_default_pagesize(Setting):
 
    """
 
    Default page size for grids.
 
    """
 
    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):
 
    """
 
    Whether table/grid headers should be "sticky" for *ALL* grids.
 
    This causes the grid header to remain visible as user scrolls down
 
@@ -125,12 +184,13 @@ class tailbone_vue_version(Setting):
 
    Version of the Vue.js library to use for "falafel" (Buefy) based
 
    themes.  The minimum should be '2.6.10' but feel free to
 
    experiment.
 
    """
 
    namespace = 'tailbone'
 
    name = 'vue_version'
 
    core = True
 

	
 

	
 
class rattail_single_store(Setting):
 
    """
 
    If set, the app should assume there is only one Store record, and that all
 
    purchases etc. will pertain to it.
setup.py
Show inline comments
 
@@ -222,12 +222,13 @@ setup(
 
            'import-sample = rattail.commands.importing:ImportSampleData',
 
            'import-versions = rattail.commands.importing:ImportVersions',
 
            'mailmon = rattail.commands.core:MailMonitorCommand',
 
            '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',
 
            'overnight = rattail.commands.luigi:Overnight',
 
            'populate-batch = rattail.commands.batch:PopulateBatch',
 
            'problems = rattail.commands.problems:Problems',
0 comments (0 inline, 0 general)