diff --git a/messkit/commands.py b/messkit/commands.py index f93f737..eb962fb 100644 --- a/messkit/commands.py +++ b/messkit/commands.py @@ -25,15 +25,10 @@ Messkit commands """ import os -import stat import sys import subprocess -import sqlalchemy as sa -from alembic.util.messaging import obfuscate_url_pw - from rattail import commands -from rattail.files import resource_path from messkit import __version__ @@ -57,203 +52,67 @@ class Command(commands.Command): long_description = '' -class Install(commands.Subcommand): +class Install(commands.InstallSubcommand): """ - Install a Messkit app + Install the Messkit app """ name = 'install' description = __doc__.strip() - def run(self, args): + # nb. these must be explicitly set b/c config is not available + # when running normally, e.g. `messkit -n install` + app_title = "Messkit" + app_package = 'messkit' + app_eggname = 'Messkit' + app_pypiname = 'Messkit' - self.rprint("\n\t[blue]Welcome to Messkit![/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]") + def do_install_steps(self): - # continue? - if not self.basic_prompt("continue?", True, is_bool=True): - self.rprint() - sys.exit(0) + # first all normal steps + super(Install, self).do_install_steps() - # 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) + # we also install poser..for now..? + self.install_poser() - # 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', 'messkit') - dbuser = self.basic_prompt('db user', 'rattail') + def put_settings(self, **kwargs): - # get db password - dbpass = None - while not dbpass: - dbpass = self.basic_prompt('db pass', is_password=True) + rattail = [os.path.join(sys.prefix, 'bin', 'rattail'), + '-c', os.path.join(sys.prefix, 'app', 'silent.conf')] - # 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]") + # set falafel theme + cmd = rattail + ['setting-put', 'tailbone.theme', 'falafel'] + subprocess.check_call(cmd) - # make the appdir - self.app.make_appdir(appdir) + # hide theme picker + cmd = rattail + ['setting-put', 'tailbone.themes.expose_picker', 'false'] + subprocess.check_call(cmd) - # shared context for generated app files - context = { - 'envdir': sys.prefix, - 'app_package': 'messkit', - 'app_title': "Messkit", - 'appdir': appdir, - 'db_url': dburl, - 'pyramid_egg': 'Messkit', - 'beaker_key': 'messkit', - } + # set main image + cmd = rattail + ['setting-put', 'tailbone.main_image_url', '/messkit/img/messkit.png'] + subprocess.check_call(cmd) - # make config files - rattail_conf = self.app.make_config_file( - 'rattail', os.path.join(appdir, 'rattail.conf'), - template_path=resource_path('messkit: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) + # set header image + cmd = rattail + ['setting-put', 'tailbone.header_image_url', '/messkit/img/messkit-small.png'] + subprocess.check_call(cmd) - # make upgrade script - path = os.path.join(appdir, 'upgrade.sh') - self.app.render_mako_template( - resource_path('messkit: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) + # set favicon image + cmd = rattail + ['setting-put', 'tailbone.favicon_url', '/messkit/img/messkit-small.png'] + subprocess.check_call(cmd) - self.rprint("\n\tappdir created at: [bold green]{}[/bold green]".format(appdir)) + # set default grid page size + cmd = rattail + ['setting-put', 'tailbone.grid.default_pagesize', '20'] + subprocess.check_call(cmd) - 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) - - # set main image - cmd = [rattail, '-c', quiet_conf, '--no-versioning', - 'setting-put', 'tailbone.main_image_url', '/messkit/img/messkit.png'] - subprocess.check_call(cmd) - - # set header image - cmd = [rattail, '-c', quiet_conf, '--no-versioning', - 'setting-put', 'tailbone.header_image_url', '/messkit/img/messkit-small.png'] - subprocess.check_call(cmd) - - # set favicon image - cmd = [rattail, '-c', quiet_conf, '--no-versioning', - 'setting-put', 'tailbone.favicon_url', '/messkit/img/messkit-small.png'] - 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)) - - if self.basic_prompt("make poser dir?", True, is_bool=True): - self.rprint() - - # make poser dir - poser_handler = self.app.get_poser_handler() - poserdir = poser_handler.make_poser_dir() - - self.rprint("\n\tposer dir created: [bold green]{}[/bold green]".format( - poserdir)) - - 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 this command:") - pserve = os.path.join(bindir, 'pserve') - self.rprint("\n\t[blue]{} file+ini:{}[/blue]".format(pserve, web_conf)) + def install_poser(self): + if not self.basic_prompt("make poser dir?", True, is_bool=True): + return False self.rprint() - # TODO: somewhere should ask about apache proxy, https etc.? + # make poser dir + poser_handler = self.app.get_poser_handler() + poserdir = poser_handler.make_poser_dir() - 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): - engine = sa.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) + self.rprint("\n\tposer dir created: [bold green]{}[/bold green]".format( + poserdir)) + return True diff --git a/messkit/config.py b/messkit/config.py index 5515456..9a63be3 100644 --- a/messkit/config.py +++ b/messkit/config.py @@ -2,7 +2,7 @@ ###################################################################### # # Messkit -- Generic-ish Data Utility App -# Copyright © 2022 Lance Edgar +# Copyright © 2022-2023 Lance Edgar # # This file is part of Messkit. # @@ -42,7 +42,7 @@ class MesskitConfig(ConfigExtension): config.setdefault('rattail', 'app_title', "Messkit") config.setdefault('rattail', 'app_class_prefix', 'Messkit') config.setdefault('rattail', 'app_table_prefix', 'messkit') - config.setdefault('tailbone', 'menus', 'messkit.web.menus') + config.setdefault('tailbone.menus', 'handler', 'messkit.web.menus:MesskitMenuHandler') config.setdefault('rattail', 'enum', 'messkit.enum') config.setdefault('rattail', 'model', 'messkit.db.model') config.setdefault('rattail.mail', 'emails', 'messkit.emails') diff --git a/messkit/templates/installer/rattail.conf.mako b/messkit/templates/installer/rattail.conf.mako deleted file mode 100644 index 0a2a617..0000000 --- a/messkit/templates/installer/rattail.conf.mako +++ /dev/null @@ -1,146 +0,0 @@ -## -*- mode: conf; -*- - -<%text>############################################################ -# -# ${app_title} core config -# -<%text>############################################################ - - -<%text>############################## -# rattail -<%text>############################## - -[rattail] -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 = rattail.db:alembic/versions - - -<%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/messkit/templates/installer/upgrade.sh.mako b/messkit/templates/installer/upgrade.sh.mako deleted file mode 100755 index 29ff792..0000000 --- a/messkit/templates/installer/upgrade.sh.mako +++ /dev/null @@ -1,29 +0,0 @@ -#!/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 Messkit - -# migrate schema -$ALEMBIC -c app/rattail.conf upgrade heads diff --git a/messkit/web/menus.py b/messkit/web/menus.py index 7071c75..7124e50 100644 --- a/messkit/web/menus.py +++ b/messkit/web/menus.py @@ -2,7 +2,7 @@ ###################################################################### # # Messkit -- Generic-ish Data Utility App -# Copyright © 2022 Lance Edgar +# Copyright © 2022-2023 Lance Edgar # # This file is part of Messkit. # @@ -24,97 +24,36 @@ Web Menus """ +from tailbone import menus as base -def simple_menus(request): - people_menu = { - 'title': "People", - 'type': 'menu', - 'items': [ - { - 'title': "All People", - 'route': 'people', - 'perm': 'people.list', - }, - ], - } +class MesskitMenuHandler(base.MenuHandler): + """ + Messkit menu handler + """ - reports_menu = { - 'title': "Reports", - 'type': 'menu', - 'items': [ - { - 'title': "New Report", - 'route': 'report_output.create', - 'perm': 'report_output.create', - }, - { - 'title': "Generated Reports", - 'route': 'report_output', - 'perm': 'report_output.list', - }, - { - 'title': "Problem Reports", - 'route': 'problem_reports', - 'perm': 'problem_reports.list', - }, - {'type': 'sep'}, - { - 'title': "Poser Reports", - 'route': 'poser_reports', - 'perm': 'poser_reports.list', - }, - ], - } + def make_menus(self, request, **kwargs): - admin_menu = { - 'title': "Admin", - 'type': 'menu', - 'items': [ - { - 'title': "Users", - 'route': 'users', - 'perm': 'users.list', - }, - { - 'title': "Roles", - 'route': 'roles', - 'perm': 'roles.list', - }, - {'type': 'sep'}, - { - 'title': "App Settings", - 'route': 'appsettings', - 'perm': 'settings.list', - }, - { - 'title': "Email Settings", - 'route': 'emailprofiles', - 'perm': 'emailprofiles.list', - }, - { - 'title': "Raw Settings", - 'route': 'settings', - 'perm': 'settings.list', - }, - {'type': 'sep'}, - { - 'title': "Tables", - 'route': 'tables', - 'perm': 'tables.list', - }, - { - 'title': "Messkit Upgrades", - 'route': 'upgrades', - 'perm': 'upgrades.list', - }, - ], - } + people_menu = { + 'title': "People", + 'type': 'menu', + 'items': [ + { + 'title': "All People", + 'route': 'people', + 'perm': 'people.list', + }, + ], + } - menus = [ - people_menu, - reports_menu, - admin_menu, - ] + reports_menu = self.make_reports_menu(request, include_poser=True) - return menus + admin_menu = self.make_admin_menu(request, include_stores=False) + + menus = [ + people_menu, + reports_menu, + admin_menu, + ] + + return menus diff --git a/messkit/web/views/__init__.py b/messkit/web/views/__init__.py index bd7ffa9..378c518 100644 --- a/messkit/web/views/__init__.py +++ b/messkit/web/views/__init__.py @@ -2,7 +2,7 @@ ###################################################################### # # Messkit -- Generic-ish Data Utility App -# Copyright © 2022 Lance Edgar +# Copyright © 2022-2023 Lance Edgar # # This file is part of Messkit. # @@ -30,8 +30,6 @@ from tailbone.util import include_configured_views def includeme(config): config.include('tailbone.views.essentials') - config.include('tailbone.views.poser') - config.include('tailbone.views.reports') include_configured_views(config)