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

View file

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

View file

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

View file

@ -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 *

View file

@ -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:

View file

@ -11,10 +11,23 @@
<section>
% if form_body is not Undefined and form_body:
${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:
% for field in form.fields:
${form.render_buefy_field(field)}
% endfor
% for field in form.fields:
${form.render_buefy_field(field)}
% endfor
% endif
</section>

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

View file

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

View file

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

View file

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

View file

@ -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)

View file

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

View file

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

View file

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

View file

@ -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):

View file

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

View file

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

View file

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

View file

@ -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):

View file

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