From 2ed63b1c1a29e2d681beb27d0f62a84ca2b0ca8b Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 5 May 2023 00:18:16 -0500 Subject: [PATCH] Massive overhaul of "generate project" feature previous incarnation was woefully lacking. new feature is much more extensible. still need to remove old POS integration specifics in some places. and a couple of unrelated things that snuck in.. - deprecate `rattail.util.OrderedDict` - deprecate `rattail.util.import_module_path()` - deprecate `rattail.util.import_reload()` --- tailbone/api/common.py | 3 +- tailbone/forms/core.py | 17 +- tailbone/grids/filters.py | 2 +- tailbone/helpers.py | 6 +- tailbone/menus.py | 19 +- tailbone/templates/forms/deform_buefy.mako | 19 +- tailbone/templates/generate_project.mako | 480 ----------------- .../templates/generated-projects/create.mako | 24 + tailbone/views/batch/handheld.py | 5 +- tailbone/views/batch/inventory.py | 3 +- tailbone/views/batch/product.py | 5 +- tailbone/views/common.py | 3 +- tailbone/views/master.py | 20 +- tailbone/views/people.py | 3 +- tailbone/views/principal.py | 2 +- tailbone/views/products.py | 4 +- tailbone/views/projects.py | 493 ++++++++++++------ tailbone/views/purchasing/receiving.py | 3 +- tailbone/views/reports.py | 3 +- tailbone/views/settings.py | 3 +- tailbone/views/tables.py | 5 +- tailbone/views/upgrades.py | 2 +- 22 files changed, 424 insertions(+), 700 deletions(-) delete mode 100644 tailbone/templates/generate_project.mako create mode 100644 tailbone/templates/generated-projects/create.mako diff --git a/tailbone/api/common.py b/tailbone/api/common.py index b82bafd0..6d8e9344 100644 --- a/tailbone/api/common.py +++ b/tailbone/api/common.py @@ -24,10 +24,11 @@ Tailbone Web API - "Common" Views """ +from collections import OrderedDict + import rattail from rattail.db import model from rattail.mail import send_email -from rattail.util import OrderedDict from cornice import Service diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py index 161bfa25..9f30512b 100644 --- a/tailbone/forms/core.py +++ b/tailbone/forms/core.py @@ -26,6 +26,7 @@ Forms Core import json import logging +from collections import OrderedDict import sqlalchemy as sa from sqlalchemy import orm @@ -346,6 +347,7 @@ class Form(object): self.schema = schema if self.fields is None and self.schema: self.set_fields([f.name for f in self.schema]) + self.grouping = None self.request = request self.readonly = readonly self.readonly_fields = set(readonly_fields or []) @@ -371,6 +373,7 @@ class Form(object): self.validators = validators or {} self.required = required or {} self.helptext = helptext or {} + self.dynamic_helptext = {} self.focus_spec = focus_spec self.action_url = action_url self.cancel_url = cancel_url @@ -404,6 +407,9 @@ class Form(object): return get_fieldnames(self.request.rattail_config, self.model_class, columns=True, proxies=True, relations=True) + def set_grouping(self, items): + self.grouping = OrderedDict(items) + def make_renderers(self): """ Return a default set of field renderers, based on :attr:`model_class`. @@ -728,11 +734,15 @@ class Form(object): """ self.defaults[key] = value - def set_helptext(self, key, value): + def set_helptext(self, key, value, dynamic=False): """ Set the help text for a given field. """ self.helptext[key] = value + if value and dynamic: + self.dynamic_helptext[key] = True + else: + self.dynamic_helptext.pop(key, None) def has_helptext(self, key): """ @@ -935,7 +945,10 @@ class Form(object): # TODO: older logic did this only if field was *not* # readonly, perhaps should add that back.. if self.has_helptext(fieldname): - attrs['message'] = self.render_helptext(fieldname) + msgkey = 'message' + if self.dynamic_helptext.get(fieldname): + msgkey = ':message' + attrs[msgkey] = self.render_helptext(fieldname) # show errors if present error_messages = self.get_error_messages(field) if field else None diff --git a/tailbone/grids/filters.py b/tailbone/grids/filters.py index e4b522f5..26ef4f59 100644 --- a/tailbone/grids/filters.py +++ b/tailbone/grids/filters.py @@ -27,11 +27,11 @@ Grid Filters import re import datetime import logging +from collections import OrderedDict import sqlalchemy as sa from rattail.gpc import GPC -from rattail.util import OrderedDict from rattail.core import UNSPECIFIED from rattail.time import localtime, make_utc from rattail.util import prettify diff --git a/tailbone/helpers.py b/tailbone/helpers.py index aeb6aa01..d4065cc5 100644 --- a/tailbone/helpers.py +++ b/tailbone/helpers.py @@ -24,15 +24,13 @@ Template Context Helpers """ -from __future__ import unicode_literals, absolute_import - import os import datetime from decimal import Decimal +from collections import OrderedDict from rattail.time import localtime, make_utc -from rattail.util import (pretty_quantity, pretty_hours, hours_as_decimal, - OrderedDict) +from rattail.util import pretty_quantity, pretty_hours, hours_as_decimal from rattail.db.util import maxlen from webhelpers2.html import * diff --git a/tailbone/menus.py b/tailbone/menus.py index 98006c00..9a0ba066 100644 --- a/tailbone/menus.py +++ b/tailbone/menus.py @@ -667,11 +667,18 @@ class MenuHandler(GenericHandler): 'route': 'appinfo', 'perm': 'appinfo.list', }, - { - 'title': "Label Settings", - 'route': 'labelprofiles', - 'perm': 'labelprofiles.list', - }, + ]) + + if kwargs.get('include_label_settings', False): + items.extend([ + { + 'title': "Label Settings", + 'route': 'labelprofiles', + 'perm': 'labelprofiles.list', + }, + ]) + + items.extend([ { 'title': "Raw Settings", 'route': 'settings', @@ -807,7 +814,7 @@ def make_menu_entry(request, item): try: entry['url'] = request.route_url(entry['route']) except KeyError: # happens if no such route - log.debug("invalid route name for menu entry: %s", entry) + log.warning("invalid route name for menu entry: %s", entry) entry['url'] = entry['route'] entry['key'] = entry['route'] else: diff --git a/tailbone/templates/forms/deform_buefy.mako b/tailbone/templates/forms/deform_buefy.mako index 4ff9c0b5..39633117 100644 --- a/tailbone/templates/forms/deform_buefy.mako +++ b/tailbone/templates/forms/deform_buefy.mako @@ -11,10 +11,23 @@
% if form_body is not Undefined and form_body: ${form_body|n} + % elif form.grouping: + % for group in form.grouping: + + % endfor % else: - % for field in form.fields: - ${form.render_buefy_field(field)} - % endfor + % for field in form.fields: + ${form.render_buefy_field(field)} + % endfor % endif
diff --git a/tailbone/templates/generate_project.mako b/tailbone/templates/generate_project.mako deleted file mode 100644 index f2b67cb3..00000000 --- a/tailbone/templates/generate_project.mako +++ /dev/null @@ -1,480 +0,0 @@ -## -*- coding: utf-8; -*- -<%inherit file="/page.mako" /> - -<%def name="title()">Generate Project - -<%def name="content_title()"> - -<%def name="page_content()"> - - - - - - ## - - - - -
- ${h.form(request.current_route_url(), ref='rattailForm')} - ${h.csrf_token(request)} - ${h.hidden('project_type', value='rattail')} -
-
-
-

Naming

-
-
-
- - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
-
-

Database

-
-
-
- - - - - - - - - - - - - - - - -
-
-
-
-
-
-

Web App

-
-
-
- - - - - - - - - - - -
-
-
-
-
-
-

Integrations

-
-
-
- - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
-
-

Deployment

-
-
-
- - - - - - -
-
-
- ${h.end_form()} -
- -
- ${h.form(request.current_route_url(), ref='rattail_integrationForm')} - ${h.csrf_token(request)} - ${h.hidden('project_type', value='rattail_integration')} -
-
-
-

Naming

-
-
-
- - - - - - - - - - - - - - ${h.hidden('slug', **{'v-model': 'rattail_integration.python_project_name'})} - - - - - -
-
-
-
-
-
-

Options

-
-
-
- - - - - - - - - - - -
-
-
- ${h.end_form()} -
- -
- ${h.form(request.current_route_url(), ref='tailbone_integrationForm')} - ${h.csrf_token(request)} - ${h.hidden('project_type', value='tailbone_integration')} -
-
-
-

Naming

-
-
-
- - - - - - - - - - - - - - ${h.hidden('slug', **{'v-model': 'tailbone_integration.python_project_name'})} - - - - - -
-
-
-
-
-
-

Options

-
-
-
- - - - - - -
-
-
- ${h.end_form()} -
- -
- ${h.form(request.current_route_url(), ref='byjoveForm')} - ${h.csrf_token(request)} - ${h.hidden('project_type', value='byjove')} - -
-
-
-

Naming

-
-
-
- - - - - - - - - -
-
-
- - ${h.end_form()} -
- -
- ${h.form(request.current_route_url(), ref='fabricForm')} - ${h.csrf_token(request)} - ${h.hidden('project_type', value='fabric')} - -
-
-
-

Naming

-
-
-
- - - - - - - - - - - - - - - - - - - - - -
-
-
- -
-
-
-

Theo

-
-
-
- - - - - - - ## - - - -
-
-
- - ${h.end_form()} -
- -
-
- - Generate Project - -
- - - -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - - -${parent.body()} diff --git a/tailbone/templates/generated-projects/create.mako b/tailbone/templates/generated-projects/create.mako new file mode 100644 index 00000000..32d205a0 --- /dev/null +++ b/tailbone/templates/generated-projects/create.mako @@ -0,0 +1,24 @@ +## -*- coding: utf-8; -*- +<%inherit file="/master/create.mako" /> + +<%def name="title()">${index_title} + +<%def name="content_title()"> + +<%def name="page_content()"> + % if project_type: + + + ${project_type} + + + + + % endif + ${parent.page_content()} + + + +${parent.body()} diff --git a/tailbone/views/batch/handheld.py b/tailbone/views/batch/handheld.py index d4f15ffd..03b9a441 100644 --- a/tailbone/views/batch/handheld.py +++ b/tailbone/views/batch/handheld.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2022 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -24,10 +24,9 @@ Views for handheld batches """ -from __future__ import unicode_literals, absolute_import +from collections import OrderedDict from rattail.db import model -from rattail.util import OrderedDict import colander from webhelpers2.html import tags diff --git a/tailbone/views/batch/inventory.py b/tailbone/views/batch/inventory.py index e13dacca..b41a995e 100644 --- a/tailbone/views/batch/inventory.py +++ b/tailbone/views/batch/inventory.py @@ -27,12 +27,13 @@ Views for inventory batches import re import decimal import logging +from collections import OrderedDict from rattail import pod from rattail.db import model from rattail.db.util import make_full_description from rattail.gpc import GPC -from rattail.util import pretty_quantity, OrderedDict +from rattail.util import pretty_quantity import colander from deform import widget as dfwidget diff --git a/tailbone/views/batch/product.py b/tailbone/views/batch/product.py index 50b18953..dfe8d890 100644 --- a/tailbone/views/batch/product.py +++ b/tailbone/views/batch/product.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2020 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -24,10 +24,9 @@ Views for generic product batches """ -from __future__ import unicode_literals, absolute_import +from collections import OrderedDict from rattail.db import model -from rattail.util import OrderedDict import colander from webhelpers2.html import HTML diff --git a/tailbone/views/common.py b/tailbone/views/common.py index 6de6bc2b..3882f357 100644 --- a/tailbone/views/common.py +++ b/tailbone/views/common.py @@ -25,9 +25,10 @@ Various common views """ import os +from collections import OrderedDict from rattail.batch import consume_batch_id -from rattail.util import OrderedDict, simple_error, import_module_path +from rattail.util import simple_error, import_module_path from rattail.files import resource_path from pyramid import httpexceptions diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 4f0411ac..ed0ed009 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -32,6 +32,7 @@ import getpass import shutil import tempfile import logging +from collections import OrderedDict import json import sqlalchemy as sa @@ -41,7 +42,7 @@ from sqlalchemy_utils.functions import get_primary_keys, get_columns from rattail.db import model, Session as RattailSession from rattail.db.continuum import model_transaction_query -from rattail.util import prettify, OrderedDict, simple_error +from rattail.util import prettify, simple_error, get_class_hierarchy from rattail.time import localtime from rattail.threads import Thread from rattail.csvutil import UnicodeDictWriter @@ -268,17 +269,7 @@ class MasterView(View): return labels def get_class_hierarchy(self): - hierarchy = [] - - def traverse(cls): - if cls is not object: - hierarchy.append(cls) - for parent in cls.__bases__: - traverse(parent) - - traverse(self.__class__) - hierarchy.reverse() - return hierarchy + return get_class_hierarchy(self.__class__) def set_row_labels(self, obj): labels = self.collect_row_labels() @@ -2215,8 +2206,9 @@ class MasterView(View): """ Returns the master view's index URL. """ - route = self.get_route_prefix() - return self.request.route_url(route, **kwargs) + if self.listable: + route = self.get_route_prefix() + return self.request.route_url(route, **kwargs) # TODO: this should not be class method, if possible # (pretty sure overriding as instance method works fine) diff --git a/tailbone/views/people.py b/tailbone/views/people.py index 9556f66d..3761941a 100644 --- a/tailbone/views/people.py +++ b/tailbone/views/people.py @@ -26,6 +26,7 @@ Person Views import datetime import logging +from collections import OrderedDict import sqlalchemy as sa from sqlalchemy import orm @@ -33,7 +34,7 @@ from sqlalchemy import orm from rattail.db import model, api from rattail.db.util import maxlen from rattail.time import localtime -from rattail.util import OrderedDict, simple_error +from rattail.util import simple_error import colander from pyramid.httpexceptions import HTTPFound, HTTPNotFound diff --git a/tailbone/views/principal.py b/tailbone/views/principal.py index 9effd2af..5d477677 100644 --- a/tailbone/views/principal.py +++ b/tailbone/views/principal.py @@ -25,9 +25,9 @@ """ import copy +from collections import OrderedDict from rattail.core import Object -from rattail.util import OrderedDict from webhelpers2.html import HTML diff --git a/tailbone/views/products.py b/tailbone/views/products.py index cc474840..ebec578e 100644 --- a/tailbone/views/products.py +++ b/tailbone/views/products.py @@ -26,7 +26,7 @@ Product Views import re import logging - +from collections import OrderedDict import humanize import sqlalchemy as sa from sqlalchemy import orm @@ -37,7 +37,7 @@ from rattail.db import model, api, auth, Session as RattailSession from rattail.gpc import GPC from rattail.threads import Thread from rattail.exceptions import LabelPrintingError -from rattail.util import load_object, pretty_quantity, OrderedDict, simple_error +from rattail.util import load_object, pretty_quantity, simple_error from rattail.time import localtime, make_utc import colander diff --git a/tailbone/views/projects.py b/tailbone/views/projects.py index 60b531c9..0cfcd349 100644 --- a/tailbone/views/projects.py +++ b/tailbone/views/projects.py @@ -24,207 +24,358 @@ Project views """ -import os -import zipfile -# from collections import OrderedDict +from collections import OrderedDict import colander +from deform import widget as dfwidget + +from rattail.projects import PythonProjectGenerator, PoserProjectGenerator from tailbone import forms -from tailbone.views import View +from tailbone.views import MasterView -class GenerateProject(colander.MappingSchema): - """ - Base schema for the "generate project" form - """ - name = colander.SchemaNode(colander.String()) - - slug = colander.SchemaNode(colander.String()) - - organization = colander.SchemaNode(colander.String()) - - python_project_name = colander.SchemaNode(colander.String()) - - python_name = colander.SchemaNode(colander.String()) - - has_db = colander.SchemaNode(colander.Boolean()) - - extends_db = colander.SchemaNode(colander.Boolean()) - - has_batch_schema = colander.SchemaNode(colander.Boolean()) - - has_web = colander.SchemaNode(colander.Boolean()) - - has_web_api = colander.SchemaNode(colander.Boolean()) - - has_datasync = colander.SchemaNode(colander.Boolean()) - - # has_filemon = colander.SchemaNode(colander.Boolean()) - - # has_tempmon = colander.SchemaNode(colander.Boolean()) - - # has_bouncer = colander.SchemaNode(colander.Boolean()) - - integrates_catapult = colander.SchemaNode(colander.Boolean()) - - integrates_corepos = colander.SchemaNode(colander.Boolean()) - - # integrates_instacart = colander.SchemaNode(colander.Boolean()) - - integrates_locsms = colander.SchemaNode(colander.Boolean()) - - # integrates_mailchimp = colander.SchemaNode(colander.Boolean()) - - uses_fabric = colander.SchemaNode(colander.Boolean()) - - -class GenerateRattailIntegrationProject(colander.MappingSchema): - """ - Schema to generate new rattail-integration project - """ - integration_name = colander.SchemaNode(colander.String()) - - integration_url = colander.SchemaNode(colander.String()) - - slug = colander.SchemaNode(colander.String()) - - python_project_name = colander.SchemaNode(colander.String()) - - python_name = colander.SchemaNode(colander.String()) - - extends_config = colander.SchemaNode(colander.Boolean()) - - extends_db = colander.SchemaNode(colander.Boolean()) - - -class GenerateTailboneIntegrationProject(colander.MappingSchema): - """ - Schema to generate new tailbone-integration project - """ - integration_name = colander.SchemaNode(colander.String()) - - integration_url = colander.SchemaNode(colander.String()) - - slug = colander.SchemaNode(colander.String()) - - python_project_name = colander.SchemaNode(colander.String()) - - python_name = colander.SchemaNode(colander.String()) - - has_static_files = colander.SchemaNode(colander.Boolean()) - - -class GenerateByjoveProject(colander.MappingSchema): - """ - Schema for generating a new 'byjove' project - """ - name = colander.SchemaNode(colander.String()) - - slug = colander.SchemaNode(colander.String()) - - -class GenerateFabricProject(colander.MappingSchema): - """ - Schema for generating a new 'fabric' project - """ - name = colander.SchemaNode(colander.String()) - - slug = colander.SchemaNode(colander.String()) - - organization = colander.SchemaNode(colander.String()) - - python_project_name = colander.SchemaNode(colander.String()) - - python_name = colander.SchemaNode(colander.String()) - - integrates_with = colander.SchemaNode(colander.String(), - missing=colander.null) - - -class GenerateProjectView(View): +class GeneratedProjectView(MasterView): """ View for generating new project source code """ + model_title = "Generated Project" + model_key = 'folder' + route_prefix = 'generated_projects' + url_prefix = '/generated-projects' + listable = False + viewable = False + editable = False + deletable = False def __init__(self, request): - super(GenerateProjectView, self).__init__(request) - self.project_handler = self.get_handler() - # TODO: deprecate / remove this - self.handler = self.project_handler + super(GeneratedProjectView, self).__init__(request) + self.project_handler = self.get_project_handler() - def get_handler(self): - from rattail.projects.handler import RattailProjectHandler - return RattailProjectHandler(self.rattail_config) + def get_project_handler(self): + app = self.get_rattail_app() + return app.get_project_handler() - def __call__(self): + def create(self): + supported = self.project_handler.get_supported_project_generators() + supported_keys = list(supported) - # choices = OrderedDict([ - # ('has_db', {'prompt': "Does project need its own Rattail DB?", - # 'type': 'bool'}), - # ]) + project_type = self.request.matchdict.get('project_type') + if project_type: + form = self.make_project_form(project_type) + if form.validate(newstyle=True): + zipped = self.generate_project(project_type, form) + return self.file_response(zipped) - project_type = 'rattail' - if self.request.method == 'POST': - project_type = self.request.POST.get('project_type', 'rattail') - if project_type not in self.project_handler.get_supported_project_types(): - raise ValueError("Unknown project type: {}".format(project_type)) + else: # no project_type - if project_type == 'byjove': - schema = GenerateByjoveProject - elif project_type == 'fabric': - schema = GenerateFabricProject - elif project_type == 'rattail_integration': - schema = GenerateRattailIntegrationProject - elif project_type == 'tailbone_integration': - schema = GenerateTailboneIntegrationProject - else: - schema = GenerateProject - form = forms.Form(schema=schema(), request=self.request) - if form.validate(newstyle=True): - zipped = self.generate_project(project_type, form) - return self.file_response(zipped) - # self.request.session.flash("New project was generated: {}".format(form.validated['name'])) - # return self.redirect(self.request.current_route_url()) + # make form to accept user choice of report type + schema = colander.Schema() + values = [(typ, typ) for typ in supported_keys] + schema.add(colander.SchemaNode(name='project_type', + typ=colander.String(), + validator=colander.OneOf(supported_keys), + widget=dfwidget.SelectWidget(values=values))) + form = forms.Form(schema=schema, request=self.request) + form.submit_label = "Continue" - return { + # if form validates, then user has chosen a project type, so + # we redirect to the appropriate "generate project" page + if form.validate(newstyle=True): + raise self.redirect(self.request.route_url( + 'generate_specific_project', + project_type=form.validated['project_type'])) + + return self.render_to_response('create', { 'index_title': "Generate Project", - 'handler': self.handler, - # 'choices': choices, - } + 'project_type': project_type, + 'form': form, + }) def generate_project(self, project_type, form): - options = form.validated - slug = options['slug'] - path = self.handler.generate_project(project_type, slug, options) + context = dict(form.validated) + output = self.project_handler.generate_project(project_type, + context=context) + return self.project_handler.zip_output(output) - zipped = '{}.zip'.format(path) - with zipfile.ZipFile(zipped, 'w', zipfile.ZIP_DEFLATED) as z: - self.zipdir(z, path, slug) - return zipped + def make_project_form(self, project_type): - def zipdir(self, zipf, path, slug): - for root, dirs, files in os.walk(path): - relative_root = os.path.join(slug, root[len(path)+1:]) - for fname in files: - zipf.write(os.path.join(root, fname), - arcname=os.path.join(relative_root, fname)) + # make form + schema = self.project_handler.make_project_schema(project_type) + form = forms.Form(schema=schema, request=self.request) + form.auto_disable = False + form.auto_disable_save = False + form.submit_label = "Generate Project" + form.cancel_url = self.request.route_url('generated_projects.create') + + # apply normal config + self.configure_form_common(form, project_type) + + # let supplemental views further configure form + for supp in self.iter_view_supplements(): + configure = getattr(supp, 'configure_form_{}'.format(project_type), None) + if configure: + configure(form) + + # if master view has more configure logic, do that too + configure = getattr(self, 'configure_form_{}'.format(project_type), None) + if configure: + configure(form) + + return form + + def configure_form_common(self, form, project_type): + generator = self.project_handler.get_project_generator(project_type, + require=True) + + # python-based projects + if isinstance(generator, PythonProjectGenerator): + self.configure_form_python(form) + + # poser-based projects + if isinstance(generator, PoserProjectGenerator): + self.configure_form_poser(form) + + def configure_form_python(self, f): + + f.set_grouping([ + ("Naming", [ + 'name', + 'pkg_name', + 'pypi_name', + ]), + ]) + + # name + f.set_label('name', "Project Name") + f.set_helptext('name', "Human-friendly name generally used to refer to this project.") + f.set_default('name', "Poser Plus") + + # pkg_name + f.set_label('pkg_name', "Package Name in Python") + f.set_helptext('pkg_name', "`For example, ~/src/${field_model_pkg_name.replace(/_/g, '-')}/${field_model_pkg_name}/__init__.py`", + dynamic=True) + f.set_default('pkg_name', "poser_plus") + + # pypi_name + f.set_label('pypi_name', "Package Name for PyPI") + f.set_helptext('pypi_name', "It's a good idea to use org name as namespace prefix here") + f.set_default('pypi_name', "Acme-Poser-Plus") + + def configure_form_poser(self, f): + + # extends_config + f.set_label('extends_config', "Extend Config") + f.set_helptext('extends_config', "Needed to customize default config values etc.") + f.set_default('extends_config', True) + + # has_cli + f.set_label('has_cli', "Use Separate CLI") + f.set_helptext('has_cli', "`Needed for e.g. '${field_model_pkg_name} install' command.`", + dynamic=True) + f.set_default('has_cli', True) + + # organization + f.set_helptext('organization', 'For use with branding etc.') + f.set_default('organization', "Acme Foods") + + # has_db + f.set_label('has_db', "Use Rattail DB") + f.set_helptext('has_db', "Note that a DB is required for the Web App") + f.set_default('has_db', True) + + # extends_db + f.set_label('extends_db', "Extend DB Schema") + f.set_helptext('extends_db', "For adding custom tables/columns to the core schema") + f.set_default('extends_db', True) + + # has_batch_schema + f.set_label('has_batch_schema', "Add Batch Schema") + f.set_helptext('has_batch_schema', 'Usually not needed - it\'s for "dynamic" (e.g. import/export) batches') + + # has_web + f.set_label('has_web', "Use Tailbone Web App") + f.set_default('has_web', True) + + # has_web_api + f.set_label('has_web_api', "Use Tailbone Web API") + f.set_helptext('has_web_api', "Needed for e.g. Vue.js SPA mobile apps") + + # has_datasync + f.set_label('has_datasync', "Use DataSync Service") + + # uses_fabric + f.set_label('uses_fabric', "Use Fabric") + f.set_default('uses_fabric', True) + + def configure_form_rattail(self, f): + + f.set_grouping([ + ("Naming", [ + 'name', + 'pkg_name', + 'pypi_name', + 'organization', + ]), + ("Core", [ + 'extends_config', + 'has_cli', + ]), + ("Database", [ + 'has_db', + 'extends_db', + 'has_batch_schema', + ]), + ("Web", [ + 'has_web', + 'has_web_api', + ]), + ("Integrations", [ + # 'integrates_catapult', + # 'integrates_corepos', + # 'integrates_locsms', + 'has_datasync', + ]), + ("Deployment", [ + 'uses_fabric', + ]), + ]) + + # # integrates_catapult + # f.set_label('integrates_catapult', "Integrate w/ Catapult") + # f.set_helptext('integrates_catapult', "Add schema, import/export logic etc. for ECRS Catapult") + + # # integrates_corepos + # f.set_label('integrates_corepos', "Integrate w/ CORE-POS") + # f.set_helptext('integrates_corepos', "Add schema, import/export logic etc. for CORE-POS") + + # # integrates_locsms + # f.set_label('integrates_locsms', "Integrate w/ LOC SMS") + # f.set_helptext('integrates_locsms', "Add schema, import/export logic etc. for LOC SMS") + + def configure_form_rattail_integration(self, f): + + f.set_grouping([ + ("Naming", [ + 'integration_name', + 'integration_url', + 'name', + 'pkg_name', + 'pypi_name', + ]), + ("Options", [ + 'extends_config', + 'extends_db', + ]), + ]) + + # integration_name + f.set_helptext('integration_name', "Name of the system to be integrated") + f.set_default('integration_name', "Foo") + + # integration_url + f.set_label('integration_url', "Integration URL") + f.set_helptext('integration_url', "Reference URL for the system to be integrated") + f.set_default('integration_url', "https://www.example.com/") + + def configure_form_tailbone_integration(self, f): + + f.set_grouping([ + ("Naming", [ + 'integration_name', + 'integration_url', + 'name', + 'pkg_name', + 'pypi_name', + ]), + ("Options", [ + 'has_static_files', + ]), + ]) + + # integration_name + f.set_helptext('integration_name', "Name of the system to be integrated") + f.set_default('integration_name', "Foo") + + # integration_url + f.set_label('integration_url', "Integration URL") + f.set_helptext('integration_url', "Reference URL for the system to be integrated") + f.set_default('integration_url', "https://www.example.com/") + + # has_static_files + f.set_helptext('has_static_files', "Register a subfolder for static files (images etc.)") + + def configure_form_byjove(self, f): + + f.set_grouping([ + ("Naming", [ + 'name', + 'slug', + ]), + ]) + + # name + f.set_default('name', "Okay Then Mobile") + + # slug + f.set_default('slug', "okay-then-mobile") + + def configure_form_fabric(self, f): + + f.set_grouping([ + ("Naming", [ + 'name', + 'pkg_name', + 'pypi_name', + 'organization', + ]), + ("Theo", [ + 'integrates_with', + ]), + ]) + + # naming defaults + f.set_default('name', "Acme Fabric") + f.set_default('pkg_name', "acmefab") + f.set_default('pypi_name', "Acme-Fabric") + + # organization + f.set_helptext('organization', 'For use with branding etc.') + f.set_default('organization', "Acme Foods") + + # integrates_with + f.set_helptext('integrates_with', "Which POS system should Theo integrate with, if any") + f.set_enum('integrates_with', OrderedDict([ + ('', "(nothing)"), + ('catapult', "ECRS Catapult"), + ('corepos', "CORE-POS"), + ('locsms', "LOC SMS") + ])) + f.set_default('integrates_with', '') @classmethod def defaults(cls, config): - config.add_tailbone_permission('common', 'common.generate_project', - "Generate new project source code") - config.add_route('generate_project', '/generate-project') - config.add_view(cls, route_name='generate_project', - permission='common.generate_project', - renderer='/generate_project.mako') + cls._defaults(config) + cls._generated_project_defaults(config) + + @classmethod + def _generated_project_defaults(cls, config): + url_prefix = cls.get_url_prefix() + permission_prefix = cls.get_permission_prefix() + + # generate project (accept custom params, truly create) + config.add_route('generate_specific_project', + '{}/new/{{project_type}}'.format(url_prefix)) + config.add_view(cls, attr='create', + route_name='generate_specific_project', + permission='{}.create'.format(permission_prefix)) def defaults(config, **kwargs): base = globals() - GenerateProjectView = kwargs.get('GenerateProjectView', base['GenerateProjectView']) - GenerateProjectView.defaults(config) + GeneratedProjectView = kwargs.get('GeneratedProjectView', base['GeneratedProjectView']) + GeneratedProjectView.defaults(config) def includeme(config): diff --git a/tailbone/views/purchasing/receiving.py b/tailbone/views/purchasing/receiving.py index b180a9a7..511f8164 100644 --- a/tailbone/views/purchasing/receiving.py +++ b/tailbone/views/purchasing/receiving.py @@ -28,6 +28,7 @@ import os import re import decimal import logging +from collections import OrderedDict import humanize import sqlalchemy as sa @@ -35,7 +36,7 @@ import sqlalchemy as sa from rattail import pod from rattail.db import model, Session as RattailSession from rattail.time import localtime, make_utc -from rattail.util import pretty_quantity, prettify, OrderedDict, simple_error +from rattail.util import pretty_quantity, prettify, simple_error from rattail.threads import Thread import colander diff --git a/tailbone/views/reports.py b/tailbone/views/reports.py index d3345b75..5ded5c5f 100644 --- a/tailbone/views/reports.py +++ b/tailbone/views/reports.py @@ -29,13 +29,14 @@ import json import re import datetime import logging +from collections import OrderedDict import rattail from rattail.db import model, Session as RattailSession from rattail.files import resource_path from rattail.time import localtime from rattail.threads import Thread -from rattail.util import simple_error, OrderedDict +from rattail.util import simple_error import colander from deform import widget as dfwidget diff --git a/tailbone/views/settings.py b/tailbone/views/settings.py index 5677f579..472ea199 100644 --- a/tailbone/views/settings.py +++ b/tailbone/views/settings.py @@ -28,12 +28,13 @@ import os import re import subprocess import sys +from collections import OrderedDict import json from rattail.db import model from rattail.settings import Setting -from rattail.util import import_module_path, OrderedDict +from rattail.util import import_module_path import colander diff --git a/tailbone/views/tables.py b/tailbone/views/tables.py index 75a61086..d4b9ee8b 100644 --- a/tailbone/views/tables.py +++ b/tailbone/views/tables.py @@ -28,6 +28,7 @@ import os import sys import warnings +import sqlalchemy as sa from sqlalchemy_utils import get_mapper from rattail.util import simple_error @@ -96,8 +97,8 @@ class TableView(MasterView): where schemaname = 'public' order by n_live_tup desc; """ - result = self.Session.execute(sql) - return [dict(table_name=row['relname'], row_count=row['n_live_tup']) + result = self.Session.execute(sa.text(sql)) + return [dict(table_name=row.relname, row_count=row.n_live_tup) for row in result] def configure_grid(self, g): diff --git a/tailbone/views/upgrades.py b/tailbone/views/upgrades.py index f6df80d3..eddd677c 100644 --- a/tailbone/views/upgrades.py +++ b/tailbone/views/upgrades.py @@ -29,6 +29,7 @@ import os import re import logging import warnings +from collections import OrderedDict import sqlalchemy as sa @@ -36,7 +37,6 @@ from rattail.core import Object from rattail.db import model, Session as RattailSession from rattail.time import make_utc from rattail.threads import Thread -from rattail.util import OrderedDict from deform import widget as dfwidget from webhelpers2.html import tags, HTML