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()`
This commit is contained in:
Lance Edgar 2023-05-05 00:18:16 -05:00
parent 026d98551c
commit 2ed63b1c1a
22 changed files with 424 additions and 700 deletions

View file

@ -24,10 +24,11 @@
Tailbone Web API - "Common" Views Tailbone Web API - "Common" Views
""" """
from collections import OrderedDict
import rattail import rattail
from rattail.db import model from rattail.db import model
from rattail.mail import send_email from rattail.mail import send_email
from rattail.util import OrderedDict
from cornice import Service from cornice import Service

View file

@ -26,6 +26,7 @@ Forms Core
import json import json
import logging import logging
from collections import OrderedDict
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import orm from sqlalchemy import orm
@ -346,6 +347,7 @@ class Form(object):
self.schema = schema self.schema = schema
if self.fields is None and self.schema: if self.fields is None and self.schema:
self.set_fields([f.name for f in self.schema]) self.set_fields([f.name for f in self.schema])
self.grouping = None
self.request = request self.request = request
self.readonly = readonly self.readonly = readonly
self.readonly_fields = set(readonly_fields or []) self.readonly_fields = set(readonly_fields or [])
@ -371,6 +373,7 @@ class Form(object):
self.validators = validators or {} self.validators = validators or {}
self.required = required or {} self.required = required or {}
self.helptext = helptext or {} self.helptext = helptext or {}
self.dynamic_helptext = {}
self.focus_spec = focus_spec self.focus_spec = focus_spec
self.action_url = action_url self.action_url = action_url
self.cancel_url = cancel_url self.cancel_url = cancel_url
@ -404,6 +407,9 @@ class Form(object):
return get_fieldnames(self.request.rattail_config, self.model_class, return get_fieldnames(self.request.rattail_config, self.model_class,
columns=True, proxies=True, relations=True) columns=True, proxies=True, relations=True)
def set_grouping(self, items):
self.grouping = OrderedDict(items)
def make_renderers(self): def make_renderers(self):
""" """
Return a default set of field renderers, based on :attr:`model_class`. Return a default set of field renderers, based on :attr:`model_class`.
@ -728,11 +734,15 @@ class Form(object):
""" """
self.defaults[key] = value 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. Set the help text for a given field.
""" """
self.helptext[key] = value 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): def has_helptext(self, key):
""" """
@ -935,7 +945,10 @@ class Form(object):
# TODO: older logic did this only if field was *not* # TODO: older logic did this only if field was *not*
# readonly, perhaps should add that back.. # readonly, perhaps should add that back..
if self.has_helptext(fieldname): 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 # show errors if present
error_messages = self.get_error_messages(field) if field else None error_messages = self.get_error_messages(field) if field else None

View file

@ -27,11 +27,11 @@ Grid Filters
import re import re
import datetime import datetime
import logging import logging
from collections import OrderedDict
import sqlalchemy as sa import sqlalchemy as sa
from rattail.gpc import GPC from rattail.gpc import GPC
from rattail.util import OrderedDict
from rattail.core import UNSPECIFIED from rattail.core import UNSPECIFIED
from rattail.time import localtime, make_utc from rattail.time import localtime, make_utc
from rattail.util import prettify from rattail.util import prettify

View file

@ -24,15 +24,13 @@
Template Context Helpers Template Context Helpers
""" """
from __future__ import unicode_literals, absolute_import
import os import os
import datetime import datetime
from decimal import Decimal from decimal import Decimal
from collections import OrderedDict
from rattail.time import localtime, make_utc from rattail.time import localtime, make_utc
from rattail.util import (pretty_quantity, pretty_hours, hours_as_decimal, from rattail.util import pretty_quantity, pretty_hours, hours_as_decimal
OrderedDict)
from rattail.db.util import maxlen from rattail.db.util import maxlen
from webhelpers2.html import * from webhelpers2.html import *

View file

@ -667,11 +667,18 @@ class MenuHandler(GenericHandler):
'route': 'appinfo', 'route': 'appinfo',
'perm': 'appinfo.list', 'perm': 'appinfo.list',
}, },
])
if kwargs.get('include_label_settings', False):
items.extend([
{ {
'title': "Label Settings", 'title': "Label Settings",
'route': 'labelprofiles', 'route': 'labelprofiles',
'perm': 'labelprofiles.list', 'perm': 'labelprofiles.list',
}, },
])
items.extend([
{ {
'title': "Raw Settings", 'title': "Raw Settings",
'route': 'settings', 'route': 'settings',
@ -807,7 +814,7 @@ def make_menu_entry(request, item):
try: try:
entry['url'] = request.route_url(entry['route']) entry['url'] = request.route_url(entry['route'])
except KeyError: # happens if no such 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['url'] = entry['route']
entry['key'] = entry['route'] entry['key'] = entry['route']
else: else:

View file

@ -11,6 +11,19 @@
<section> <section>
% if form_body is not Undefined and form_body: % if form_body is not Undefined and form_body:
${form_body|n} ${form_body|n}
% elif form.grouping:
% for group in form.grouping:
<nav class="panel">
<p class="panel-heading">${group}</p>
<div class="panel-block">
<div>
% for field in form.grouping[group]:
${form.render_buefy_field(field)}
% endfor
</div>
</div>
</nav>
% endfor
% else: % else:
% for field in form.fields: % for field in form.fields:
${form.render_buefy_field(field)} ${form.render_buefy_field(field)}

View file

@ -1,480 +0,0 @@
## -*- coding: utf-8; -*-
<%inherit file="/page.mako" />
<%def name="title()">Generate Project</%def>
<%def name="content_title()"></%def>
<%def name="page_content()">
<b-field horizontal label="Project Type">
<b-select v-model="projectType">
<option value="rattail">rattail</option>
<option value="rattail_integration">rattail-integration</option>
<option value="tailbone_integration">tailbone-integration</option>
## <option value="byjove">byjove</option>
<option value="fabric">fabric</option>
</b-select>
</b-field>
<div v-if="projectType == 'rattail'">
${h.form(request.current_route_url(), ref='rattailForm')}
${h.csrf_token(request)}
${h.hidden('project_type', value='rattail')}
<br />
<div class="card">
<header class="card-header">
<p class="card-header-title">Naming</p>
</header>
<div class="card-content">
<div class="content">
<b-field horizontal label="Name"
message="The &quot;canonical&quot; name generally used to refer to this project">
<b-input name="name" v-model="rattail.name"></b-input>
</b-field>
<b-field horizontal label="Slug"
message="Used for e.g. naming the project source code folder">
<b-input name="slug" v-model="rattail.slug"></b-input>
</b-field>
<b-field horizontal label="Organization"
message="For use with &quot;branding&quot; etc.">
<b-input name="organization" v-model="rattail.organization"></b-input>
</b-field>
<b-field horizontal label="Package Name for PyPI"
message="It&apos;s a good idea to use org name as namespace prefix here">
<b-input name="python_project_name" v-model="rattail.python_project_name"></b-input>
</b-field>
<b-field horizontal label="Package Name in Python"
:message="`For example, ~/src/${'$'}{rattail.slug}/${'$'}{rattail.python_package_name}/__init__.py`">
<b-input name="python_name" v-model="rattail.python_package_name"></b-input>
</b-field>
</div>
</div>
</div>
<br />
<div class="card">
<header class="card-header">
<p class="card-header-title">Database</p>
</header>
<div class="card-content">
<div class="content">
<b-field horizontal label="Has Rattail DB"
message="Note that a DB is required for the Web App">
<b-checkbox name="has_db"
v-model="rattail.has_rattail_db"
native-value="true">
</b-checkbox>
</b-field>
<b-field horizontal label="Extends Rattail DB Schema"
message="For adding custom tables/columns to the core schema">
<b-checkbox name="extends_db"
v-model="rattail.extends_rattail_db_schema"
native-value="true">
</b-checkbox>
</b-field>
<b-field horizontal label="Uses Rattail Batch Schema"
v-show="false"
message="Needed for &quot;dynamic&quot; (e.g. import/export) batches">
<b-checkbox name="has_batch_schema"
v-model="rattail.uses_rattail_batch_schema"
native-value="true">
</b-checkbox>
</b-field>
</div>
</div>
</div>
<br />
<div class="card">
<header class="card-header">
<p class="card-header-title">Web App</p>
</header>
<div class="card-content">
<div class="content">
<b-field horizontal label="Has Tailbone Web App">
<b-checkbox name="has_web"
v-model="rattail.has_tailbone_web_app"
native-value="true">
</b-checkbox>
</b-field>
<b-field horizontal label="Has Tailbone Web API"
v-show="false"
message="Needed for e.g. Vue.js SPA mobile apps">
<b-checkbox name="has_web_api"
v-model="rattail.has_tailbone_web_api"
native-value="true">
</b-checkbox>
</b-field>
</div>
</div>
</div>
<br />
<div class="card">
<header class="card-header">
<p class="card-header-title">Integrations</p>
</header>
<div class="card-content">
<div class="content">
<b-field horizontal label="Integrates w/ Catapult"
message="Add schema, import/export logic etc. for ECRS Catapult">
<b-checkbox name="integrates_catapult"
v-model="rattail.integrates_with_catapult"
native-value="true">
</b-checkbox>
</b-field>
<b-field horizontal label="Integrates w/ CORE-POS"
v-show="false">
<b-checkbox name="integrates_corepos"
v-model="rattail.integrates_with_corepos"
native-value="true">
</b-checkbox>
</b-field>
<b-field horizontal label="Integrates w/ LOC SMS"
message="Add schema, import/export logic etc. for LOC SMS">
<b-checkbox name="integrates_locsms"
v-model="rattail.integrates_with_locsms"
native-value="true">
</b-checkbox>
</b-field>
<b-field horizontal label="Has DataSync Service"
v-show="false">
<b-checkbox name="has_datasync"
v-model="rattail.has_datasync_service"
native-value="true">
</b-checkbox>
</b-field>
</div>
</div>
</div>
<br />
<div class="card">
<header class="card-header">
<p class="card-header-title">Deployment</p>
</header>
<div class="card-content">
<div class="content">
<b-field horizontal label="Uses Fabric">
<b-checkbox name="uses_fabric"
v-model="rattail.uses_fabric"
native-value="true">
</b-checkbox>
</b-field>
</div>
</div>
</div>
${h.end_form()}
</div>
<div v-if="projectType == 'rattail_integration'">
${h.form(request.current_route_url(), ref='rattail_integrationForm')}
${h.csrf_token(request)}
${h.hidden('project_type', value='rattail_integration')}
<br />
<div class="card">
<header class="card-header">
<p class="card-header-title">Naming</p>
</header>
<div class="card-content">
<div class="content">
<b-field horizontal label="Integration Name"
message="Name of the system to be integrated">
<b-input name="integration_name" v-model="rattail_integration.integration_name"></b-input>
</b-field>
<b-field horizontal label="Integration URL"
message="Reference URL for the system to be integrated">
<b-input name="integration_url" v-model="rattail_integration.integration_url"></b-input>
</b-field>
<b-field horizontal label="Package Name for PyPI"
message="Also will be used as slug, e.g. for folder name">
<b-input name="python_project_name" v-model="rattail_integration.python_project_name"></b-input>
</b-field>
${h.hidden('slug', **{'v-model': 'rattail_integration.python_project_name'})}
<b-field horizontal label="Package Name in Python"
:message="`For example, ~/src/${'$'}{rattail_integration.python_project_name}/${'$'}{rattail_integration.python_package_name}/__init__.py`">
<b-input name="python_name" v-model="rattail_integration.python_package_name"></b-input>
</b-field>
</div>
</div>
</div>
<br />
<div class="card">
<header class="card-header">
<p class="card-header-title">Options</p>
</header>
<div class="card-content">
<div class="content">
<b-field horizontal label="Extends Config"
message="Adds custom config extension">
<b-checkbox name="extends_config"
v-model="rattail_integration.extends_config"
native-value="true">
</b-checkbox>
</b-field>
<b-field horizontal label="Extends Rattail Schema"
message="Adds custom tables/columns to the Rattail DB schema">
<b-checkbox name="extends_db"
v-model="rattail_integration.extends_db"
native-value="true">
</b-checkbox>
</b-field>
</div>
</div>
</div>
${h.end_form()}
</div>
<div v-if="projectType == 'tailbone_integration'">
${h.form(request.current_route_url(), ref='tailbone_integrationForm')}
${h.csrf_token(request)}
${h.hidden('project_type', value='tailbone_integration')}
<br />
<div class="card">
<header class="card-header">
<p class="card-header-title">Naming</p>
</header>
<div class="card-content">
<div class="content">
<b-field horizontal label="Integration Name"
message="Name of the system to be integrated">
<b-input name="integration_name" v-model="tailbone_integration.integration_name"></b-input>
</b-field>
<b-field horizontal label="Integration URL"
message="Reference URL for the system to be integrated">
<b-input name="integration_url" v-model="tailbone_integration.integration_url"></b-input>
</b-field>
<b-field horizontal label="Package Name for PyPI"
message="Also will be used as slug, e.g. for folder name">
<b-input name="python_project_name" v-model="tailbone_integration.python_project_name"></b-input>
</b-field>
${h.hidden('slug', **{'v-model': 'tailbone_integration.python_project_name'})}
<b-field horizontal label="Package Name in Python"
:message="`For example, ~/src/${'$'}{tailbone_integration.python_project_name}/${'$'}{tailbone_integration.python_package_name}/__init__.py`">
<b-input name="python_name" v-model="tailbone_integration.python_package_name"></b-input>
</b-field>
</div>
</div>
</div>
<br />
<div class="card">
<header class="card-header">
<p class="card-header-title">Options</p>
</header>
<div class="card-content">
<div class="content">
<b-field horizontal label="Has Static Files"
message="Register a subfolder for static files (images etc.)">
<b-checkbox name="has_static_files"
v-model="tailbone_integration.has_static_files"
native-value="true">
</b-checkbox>
</b-field>
</div>
</div>
</div>
${h.end_form()}
</div>
<div v-if="projectType == 'byjove'">
${h.form(request.current_route_url(), ref='byjoveForm')}
${h.csrf_token(request)}
${h.hidden('project_type', value='byjove')}
<br />
<div class="card">
<header class="card-header">
<p class="card-header-title">Naming</p>
</header>
<div class="card-content">
<div class="content">
<b-field horizontal label="Name">
<b-input name="name" v-model="byjove.name"></b-input>
</b-field>
<b-field horizontal label="Slug">
<b-input name="slug" v-model="byjove.slug"></b-input>
</b-field>
</div>
</div>
</div>
${h.end_form()}
</div>
<div v-if="projectType == 'fabric'">
${h.form(request.current_route_url(), ref='fabricForm')}
${h.csrf_token(request)}
${h.hidden('project_type', value='fabric')}
<br />
<div class="card">
<header class="card-header">
<p class="card-header-title">Naming</p>
</header>
<div class="card-content">
<div class="content">
<b-field horizontal label="Name"
message="The &quot;canonical&quot; name generally used to refer to this project">
<b-input name="name" v-model="fabric.name"></b-input>
</b-field>
<b-field horizontal label="Slug"
message="Used for e.g. naming the project source code folder">
<b-input name="slug" v-model="fabric.slug"></b-input>
</b-field>
<b-field horizontal label="Organization"
message="For use with &quot;branding&quot; etc.">
<b-input name="organization" v-model="fabric.organization"></b-input>
</b-field>
<b-field horizontal label="Package Name for PyPI"
message="It&apos;s a good idea to use org name as namespace prefix here">
<b-input name="python_project_name" v-model="fabric.python_project_name"></b-input>
</b-field>
<b-field horizontal label="Package Name in Python"
:message="`For example, ~/src/${'$'}{fabric.slug}/${'$'}{fabric.python_package_name}/__init__.py`">
<b-input name="python_name" v-model="fabric.python_package_name"></b-input>
</b-field>
</div>
</div>
</div>
<br />
<div class="card">
<header class="card-header">
<p class="card-header-title">Theo</p>
</header>
<div class="card-content">
<div class="content">
<b-field horizontal label="Integrates With"
message="Which POS system should Theo integrate with, if any">
<b-select name="integrates_with" v-model="fabric.integrates_with">
<option value="">(nothing)</option>
<option value="catapult">ECRS Catapult</option>
<option value="corepos">CORE-POS</option>
## <option value="locsms">LOC SMS</option>
</b-select>
</b-field>
</div>
</div>
</div>
${h.end_form()}
</div>
<br />
<div class="buttons" style="padding-left: 8rem;">
<b-button type="is-primary"
@click="submitProjectForm()">
Generate Project
</b-button>
</div>
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
ThisPageData.projectType = 'rattail'
ThisPageData.rattail = {
name: "Okay-Then",
slug: "okay-then",
organization: "Acme Foods",
python_project_name: "Acme-Okay-Then",
python_package_name: "okay_then",
has_rattail_db: true,
extends_rattail_db_schema: true,
uses_rattail_batch_schema: false,
has_tailbone_web_app: true,
has_tailbone_web_api: false,
has_datasync_service: false,
integrates_with_catapult: false,
integrates_with_corepos: false,
integrates_with_locsms: false,
uses_fabric: true,
}
ThisPageData.rattail_integration = {
integration_name: "Foo",
integration_url: "https://www.example.com/",
python_project_name: "rattail-foo",
python_package_name: "rattail_foo",
extends_config: true,
extends_db: true,
}
ThisPageData.tailbone_integration = {
integration_name: "Foo",
integration_url: "https://www.example.com/",
python_project_name: "tailbone-foo",
python_package_name: "tailbone_foo",
}
ThisPageData.byjove = {
name: "Okay-Then-Mobile",
slug: "okay-then-mobile",
}
ThisPageData.fabric = {
name: "AcmeFab",
slug: "acmefab",
organization: "Acme Foods",
python_project_name: "Acme-Fabric",
python_package_name: "acmefab",
integrates_with: '',
}
ThisPage.methods.submitProjectForm = function() {
let form = this.$refs[this.projectType + 'Form']
form.submit()
}
</script>
</%def>
${parent.body()}

View file

@ -0,0 +1,24 @@
## -*- coding: utf-8; -*-
<%inherit file="/master/create.mako" />
<%def name="title()">${index_title}</%def>
<%def name="content_title()"></%def>
<%def name="page_content()">
% if project_type:
<b-field grouped>
<b-field horizontal expanded label="Project Type">
${project_type}
</b-field>
<once-button type="is-primary"
tag="a" href="${url('generated_projects.create')}"
text="Start Over">
</once-button>
</b-field>
% endif
${parent.page_content()}
</%def>
${parent.body()}

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2022 Lance Edgar # Copyright © 2010-2023 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -24,10 +24,9 @@
Views for handheld batches Views for handheld batches
""" """
from __future__ import unicode_literals, absolute_import from collections import OrderedDict
from rattail.db import model from rattail.db import model
from rattail.util import OrderedDict
import colander import colander
from webhelpers2.html import tags from webhelpers2.html import tags

View file

@ -27,12 +27,13 @@ Views for inventory batches
import re import re
import decimal import decimal
import logging import logging
from collections import OrderedDict
from rattail import pod from rattail import pod
from rattail.db import model from rattail.db import model
from rattail.db.util import make_full_description from rattail.db.util import make_full_description
from rattail.gpc import GPC from rattail.gpc import GPC
from rattail.util import pretty_quantity, OrderedDict from rattail.util import pretty_quantity
import colander import colander
from deform import widget as dfwidget from deform import widget as dfwidget

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2020 Lance Edgar # Copyright © 2010-2023 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -24,10 +24,9 @@
Views for generic product batches Views for generic product batches
""" """
from __future__ import unicode_literals, absolute_import from collections import OrderedDict
from rattail.db import model from rattail.db import model
from rattail.util import OrderedDict
import colander import colander
from webhelpers2.html import HTML from webhelpers2.html import HTML

View file

@ -25,9 +25,10 @@ Various common views
""" """
import os import os
from collections import OrderedDict
from rattail.batch import consume_batch_id 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 rattail.files import resource_path
from pyramid import httpexceptions from pyramid import httpexceptions

View file

@ -32,6 +32,7 @@ import getpass
import shutil import shutil
import tempfile import tempfile
import logging import logging
from collections import OrderedDict
import json import json
import sqlalchemy as sa 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 import model, Session as RattailSession
from rattail.db.continuum import model_transaction_query 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.time import localtime
from rattail.threads import Thread from rattail.threads import Thread
from rattail.csvutil import UnicodeDictWriter from rattail.csvutil import UnicodeDictWriter
@ -268,17 +269,7 @@ class MasterView(View):
return labels return labels
def get_class_hierarchy(self): def get_class_hierarchy(self):
hierarchy = [] return get_class_hierarchy(self.__class__)
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
def set_row_labels(self, obj): def set_row_labels(self, obj):
labels = self.collect_row_labels() labels = self.collect_row_labels()
@ -2215,6 +2206,7 @@ class MasterView(View):
""" """
Returns the master view's index URL. Returns the master view's index URL.
""" """
if self.listable:
route = self.get_route_prefix() route = self.get_route_prefix()
return self.request.route_url(route, **kwargs) return self.request.route_url(route, **kwargs)

View file

@ -26,6 +26,7 @@ Person Views
import datetime import datetime
import logging import logging
from collections import OrderedDict
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import orm from sqlalchemy import orm
@ -33,7 +34,7 @@ from sqlalchemy import orm
from rattail.db import model, api from rattail.db import model, api
from rattail.db.util import maxlen from rattail.db.util import maxlen
from rattail.time import localtime from rattail.time import localtime
from rattail.util import OrderedDict, simple_error from rattail.util import simple_error
import colander import colander
from pyramid.httpexceptions import HTTPFound, HTTPNotFound from pyramid.httpexceptions import HTTPFound, HTTPNotFound

View file

@ -25,9 +25,9 @@
""" """
import copy import copy
from collections import OrderedDict
from rattail.core import Object from rattail.core import Object
from rattail.util import OrderedDict
from webhelpers2.html import HTML from webhelpers2.html import HTML

View file

@ -26,7 +26,7 @@ Product Views
import re import re
import logging import logging
from collections import OrderedDict
import humanize import humanize
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import orm 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.gpc import GPC
from rattail.threads import Thread from rattail.threads import Thread
from rattail.exceptions import LabelPrintingError 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 from rattail.time import localtime, make_utc
import colander import colander

View file

@ -24,207 +24,358 @@
Project views Project views
""" """
import os from collections import OrderedDict
import zipfile
# from collections import OrderedDict
import colander import colander
from deform import widget as dfwidget
from rattail.projects import PythonProjectGenerator, PoserProjectGenerator
from tailbone import forms from tailbone import forms
from tailbone.views import View from tailbone.views import MasterView
class GenerateProject(colander.MappingSchema): class GeneratedProjectView(MasterView):
"""
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):
""" """
View for generating new project source code 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): def __init__(self, request):
super(GenerateProjectView, self).__init__(request) super(GeneratedProjectView, self).__init__(request)
self.project_handler = self.get_handler() self.project_handler = self.get_project_handler()
# TODO: deprecate / remove this
self.handler = self.project_handler
def get_handler(self): def get_project_handler(self):
from rattail.projects.handler import RattailProjectHandler app = self.get_rattail_app()
return RattailProjectHandler(self.rattail_config) 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([ project_type = self.request.matchdict.get('project_type')
# ('has_db', {'prompt': "Does project need its own Rattail DB?", if project_type:
# 'type': 'bool'}), form = self.make_project_form(project_type)
# ])
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))
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): if form.validate(newstyle=True):
zipped = self.generate_project(project_type, form) zipped = self.generate_project(project_type, form)
return self.file_response(zipped) 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())
return { else: # no project_type
# 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"
# 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", 'index_title': "Generate Project",
'handler': self.handler, 'project_type': project_type,
# 'choices': choices, 'form': form,
} })
def generate_project(self, project_type, form): def generate_project(self, project_type, form):
options = form.validated context = dict(form.validated)
slug = options['slug'] output = self.project_handler.generate_project(project_type,
path = self.handler.generate_project(project_type, slug, options) context=context)
return self.project_handler.zip_output(output)
zipped = '{}.zip'.format(path) def make_project_form(self, project_type):
with zipfile.ZipFile(zipped, 'w', zipfile.ZIP_DEFLATED) as z:
self.zipdir(z, path, slug)
return zipped
def zipdir(self, zipf, path, slug): # make form
for root, dirs, files in os.walk(path): schema = self.project_handler.make_project_schema(project_type)
relative_root = os.path.join(slug, root[len(path)+1:]) form = forms.Form(schema=schema, request=self.request)
for fname in files: form.auto_disable = False
zipf.write(os.path.join(root, fname), form.auto_disable_save = False
arcname=os.path.join(relative_root, fname)) 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 @classmethod
def defaults(cls, config): def defaults(cls, config):
config.add_tailbone_permission('common', 'common.generate_project', cls._defaults(config)
"Generate new project source code") cls._generated_project_defaults(config)
config.add_route('generate_project', '/generate-project')
config.add_view(cls, route_name='generate_project', @classmethod
permission='common.generate_project', def _generated_project_defaults(cls, config):
renderer='/generate_project.mako') 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): def defaults(config, **kwargs):
base = globals() base = globals()
GenerateProjectView = kwargs.get('GenerateProjectView', base['GenerateProjectView']) GeneratedProjectView = kwargs.get('GeneratedProjectView', base['GeneratedProjectView'])
GenerateProjectView.defaults(config) GeneratedProjectView.defaults(config)
def includeme(config): def includeme(config):

View file

@ -28,6 +28,7 @@ import os
import re import re
import decimal import decimal
import logging import logging
from collections import OrderedDict
import humanize import humanize
import sqlalchemy as sa import sqlalchemy as sa
@ -35,7 +36,7 @@ import sqlalchemy as sa
from rattail import pod from rattail import pod
from rattail.db import model, Session as RattailSession from rattail.db import model, Session as RattailSession
from rattail.time import localtime, make_utc 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 from rattail.threads import Thread
import colander import colander

View file

@ -29,13 +29,14 @@ import json
import re import re
import datetime import datetime
import logging import logging
from collections import OrderedDict
import rattail import rattail
from rattail.db import model, Session as RattailSession from rattail.db import model, Session as RattailSession
from rattail.files import resource_path from rattail.files import resource_path
from rattail.time import localtime from rattail.time import localtime
from rattail.threads import Thread from rattail.threads import Thread
from rattail.util import simple_error, OrderedDict from rattail.util import simple_error
import colander import colander
from deform import widget as dfwidget from deform import widget as dfwidget

View file

@ -28,12 +28,13 @@ import os
import re import re
import subprocess import subprocess
import sys import sys
from collections import OrderedDict
import json import json
from rattail.db import model from rattail.db import model
from rattail.settings import Setting from rattail.settings import Setting
from rattail.util import import_module_path, OrderedDict from rattail.util import import_module_path
import colander import colander

View file

@ -28,6 +28,7 @@ import os
import sys import sys
import warnings import warnings
import sqlalchemy as sa
from sqlalchemy_utils import get_mapper from sqlalchemy_utils import get_mapper
from rattail.util import simple_error from rattail.util import simple_error
@ -96,8 +97,8 @@ class TableView(MasterView):
where schemaname = 'public' where schemaname = 'public'
order by n_live_tup desc; order by n_live_tup desc;
""" """
result = self.Session.execute(sql) result = self.Session.execute(sa.text(sql))
return [dict(table_name=row['relname'], row_count=row['n_live_tup']) return [dict(table_name=row.relname, row_count=row.n_live_tup)
for row in result] for row in result]
def configure_grid(self, g): def configure_grid(self, g):

View file

@ -29,6 +29,7 @@ import os
import re import re
import logging import logging
import warnings import warnings
from collections import OrderedDict
import sqlalchemy as sa import sqlalchemy as sa
@ -36,7 +37,6 @@ from rattail.core import Object
from rattail.db import model, Session as RattailSession from rattail.db import model, Session as RattailSession
from rattail.time import make_utc from rattail.time import make_utc
from rattail.threads import Thread from rattail.threads import Thread
from rattail.util import OrderedDict
from deform import widget as dfwidget from deform import widget as dfwidget
from webhelpers2.html import tags, HTML from webhelpers2.html import tags, HTML