Changeset - 4ae2762be562
rattail/app.py
Show inline comments
 
@@ -95,24 +95,53 @@ class AppHandler(object):
 

	
 
        Default logic invokes
 
        :meth:`rattail.config.RattailConfig.app_title()` to obtain the
 
        title.
 

	
 
        :param default: Value to be returned if there is no app title
 
           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.
 

	
 
        :param key: Unique key designating which time zone should be
 
           returned.  Note that most apps have only one ("default"),
 
           but may have others defined.
 
        """
 
        from rattail.time import timezone
 
@@ -586,24 +615,38 @@ class AppHandler(object):
 

	
 
        :returns: The
 
           :class:`~rattail.datasync.handler.DatasyncHandler` instance
 
           for the app.
 
        """
 
        if not hasattr(self, 'datasync_handler'):
 
            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.
 
        """
 
        if not hasattr(self, 'employment_handler'):
 
            from rattail.employment import get_employment_handler
 
            self.employment_handler = get_employment_handler(self.config, **kwargs)
 
        return self.employment_handler
 

	
rattail/commands/core.py
Show inline comments
 
@@ -429,24 +429,105 @@ class Subcommand(object):
 
        return progress_loop(func, items, factory or self.progress, **kwargs)
 
            
 
    def _run(self, *args):
 
        args = self.parser.parse_args(list(args))
 
        return self.run(args)
 

	
 
    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'
 
    description = __doc__.strip()
 

	
 
    def run(self, args):
 
        import sqlalchemy as sa
 

	
 
        try:
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
 
@@ -17,48 +17,52 @@ require = %(here)s/rattail.conf
 

	
 
<%text>##############################</%text>
 
# pyramid
 
<%text>##############################</%text>
 

	
 
[app:main]
 
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}
 

	
 
pyramid_deform.tempdir = %(here)s/data/uploads
 

	
 
exclog.extra_info = true
 

	
 
# required for tailbone
 
rattail.config = %(__file__)s
 

	
 
[server:main]
 
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
 

	
 

	
 
<%text>##############################</%text>
 
# logging
 
<%text>##############################</%text>
 

	
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
 
@@ -19,24 +19,25 @@
 
#  You should have received a copy of the GNU General Public License along with
 
#  Rattail.  If not, see <http://www.gnu.org/licenses/>.
 
#
 
################################################################################
 
"""
 
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
 

	
 

	
 
class ProjectHandler(object):
 
    """
 
    Base class for project handlers.
 
    """
 
@@ -278,24 +279,27 @@ class RattailProjectHandler(ProjectHandler):
 
            return self.generate_fabric_project(slug, options, path)
 
        elif project_type == 'rattail_integration':
 
            return self.generate_rattail_integration_project(slug, options, path)
 
        elif project_type == 'tailbone_integration':
 
            return self.generate_tailbone_integration_project(slug, options, path)
 
        else:
 
            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
 
        ##############################
 

	
 
        self.generate('rattail/gitignore.mako', os.path.join(path, '.gitignore'),
 
                      **context)
 

	
 
        self.generate('rattail/MANIFEST.in.mako', os.path.join(path, 'MANIFEST.in'),
 
                      **context)
 

	
 
@@ -320,26 +324,24 @@ class RattailProjectHandler(ProjectHandler):
 

	
 
        self.generate('rattail/package/_version.py', os.path.join(package, '_version.py'))
 

	
 
        self.generate('rattail/package/config.py.mako', os.path.join(package, 'config.py'),
 
                      **context)
 

	
 
        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)
 

	
 
        config = os.path.join(data, 'config')
 
        os.makedirs(config)
 

	
 
        self.generate('rattail/package/data/config/rattail.conf.mako',
 
                      os.path.join(config, '{}-rattail.conf'.format(slug)),
 
@@ -371,35 +373,74 @@ class RattailProjectHandler(ProjectHandler):
 
                          **context)
 

	
 
            self.generate('rattail/package/db/model/core.py.mako', os.path.join(model, 'core.py'),
 
                          **context)
 

	
 
            ####################
 
            # 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']:
 

	
 
            web = os.path.join(package, 'web')
 
            os.makedirs(web)
 

	
 
            self.generate('rattail/package/web/__init__.py', os.path.join(web, '__init__.py'))
 

	
 
@@ -409,28 +450,29 @@ class RattailProjectHandler(ProjectHandler):
 
            self.generate('rattail/package/web/menus.py.mako', os.path.join(web, 'menus.py'),
 
                          **context)
 

	
 
            self.generate('rattail/package/web/subscribers.py.mako', os.path.join(web, 'subscribers.py'),
 
                          **context)
 

	
 
            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'),
 
                          **context)
 

	
 
            self.generate('rattail/package/web/views/common.py.mako', os.path.join(views, 'common.py'),
 
                          **context)
 

	
 
        ##############################
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):
 
    """
 
    Main entry point for ${name} command system
 
    """
 
    args = list(args or sys.argv[1:])
 
    cmd = Command()
 
    cmd.run(*args)
 

	
 
@@ -29,12 +35,231 @@ class Command(commands.Command):
 
    long_description = ''
 

	
 

	
 
class HelloWorld(commands.Subcommand):
 
    """
 
    The requisite 'hello world' example
 
    """
 
    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
 
@@ -7,21 +7,20 @@ from rattail.config import ConfigExtension
 

	
 

	
 
class ${studly_name}Config(ConfigExtension):
 
    """
 
    Rattail config extension for ${name}
 
    """
 
    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')
 
        config.setdefault('rattail.importing', 'versions.handler', 'rattail_onager.importing.versions:FromRattailToRattailVersions')
 
        % endif
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
 
@@ -61,14 +61,14 @@ def includeme(config):
 
    config.include('tailbone.views.taxes')
 
    config.include('tailbone.views.departments')
 
    config.include('tailbone.views.brands')
 
    config.include('tailbone.views.vendors')
 
    config.include('tailbone.views.products')
 
    % endif
 

	
 
    # 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
 
@@ -35,25 +35,27 @@ requires = [
 
    # when attempting to support a more recent version of the package.  (A
 
    # "hard" low limit should be indicated by a true version requirement
 
    # when a 'high' version is present.)
 
    #
 
    # In any case, developers and other users are encouraged to play
 
    # outside the lines with regard to these soft limits.  If bugs are
 
    # 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:
 
    'Tailbone',                         # 0.8.72
 
    % endif
 

	
 
    % if integrates_catapult:
 
    # TODO: must cap this for now, b/c it breaks Catapult integration?!
 
    # (something about "Syntax error near 'ROWS'" with grid queries)
 
@@ -110,21 +112,22 @@ setup(
 
    entry_points = {
 

	
 
        'rattail.config.extensions': [
 
            '${python_name} = ${python_name}.config:${studly_name}Config',
 
        ],
 

	
 
        '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',
 
        ],
 
        % endif
 
    },
 
)
rattail/settings.py
Show inline comments
 
@@ -21,122 +21,182 @@
 
#
 
################################################################################
 
"""
 
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
 

	
 

	
 
##############################
 
# (General)
 
##############################
 

	
 
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.
 
    """
 
    namespace = 'rattail'
 
    name = 'node_title'
 

	
 

	
 
class rattail_production(Setting):
 
    """
 
    If set, the app is considered to be running in "production" mode, whereas
 
    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.
 
    """
 
    namespace = 'tailbone'
 
    name = 'background_color'
 

	
 

	
 
class tailbone_buefy_version(Setting):
 
    """
 
    Version of the Buefy component JS library to use for "falafel"
 
    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?
 
    """
 
    namespace = 'tailbone'
 
    name = 'feedback_allows_reply'
 
    data_type = bool
 

	
 

	
 
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
 
    through the row/record list; however it isn't perfect yet.  Also
 
    please note, it will only work with Buefy 0.8.13 or newer.
 
    """
 
    namespace = 'tailbone'
 
    name = 'sticky_headers'
 
    data_type = bool
 

	
 

	
 
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.
 
    """
 
    namespace = 'rattail'
 
    name = 'single_store'
 
    data_type = bool
 

	
 

	
setup.py
Show inline comments
 
@@ -216,24 +216,25 @@ setup(
 
            'export-csv = rattail.commands.importing:ExportCSV',
 
            'export-rattail = rattail.commands.importing:ExportRattail',
 
            'filemon = rattail.commands.core:FileMonitorCommand',
 
            'import-csv = rattail.commands.importing:ImportCSV',
 
            'import-ifps = rattail.commands.importing:ImportIFPS',
 
            'import-rattail = rattail.commands.importing:ImportRattail',
 
            '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',
 
            'purge-batches = rattail.commands.batch:PurgeBatches',
 
            'purge-versions = rattail.commands.importing:PurgeVersions',
 
            'refresh-batch = rattail.commands.batch:RefreshBatch',
 
            'run-n-mail = rattail.commands.core:RunAndMail',
 
            'runsql = rattail.commands.core:RunSQL',
 
            'postfix-summary = rattail.commands.postfix:PostfixSummary',
0 comments (0 inline, 0 general)