Add feature to generate new features...

at least that's the idea.  probably requires a web UI to be useful
This commit is contained in:
Lance Edgar 2021-01-17 12:07:56 -06:00
parent fd043c4c2f
commit 37eb1ec564
12 changed files with 520 additions and 6 deletions

View file

@ -1,4 +1,4 @@
# -*- mode: conf -*-
# -*- mode: conf; -*-
include *.cfg
include *.txt
@ -15,4 +15,4 @@ include rattail/db/alembic/README
recursive-include rattail/db/alembic *.mako
recursive-include rattail/db/alembic *.py
recursive-include rattail/templates/mail *.mako
recursive-include rattail/templates *.mako

View file

@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2020 Lance Edgar
# Copyright © 2010-2021 Lance Edgar
#
# This file is part of Rattail.
#
@ -64,6 +64,12 @@ class AppHandler(object):
self.employment_handler = get_employment_handler(self.config, **kwargs)
return self.employment_handler
def get_feature_handler(self, **kwargs):
if not hasattr(self, 'feature_handler'):
from rattail.features import FeatureHandler
self.feature_handler = FeatureHandler(self.config, **kwargs)
return self.feature_handler
def get_report_handler(self, **kwargs):
if not hasattr(self, 'report_handler'):
from rattail.reporting import get_report_handler

View file

@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2020 Lance Edgar
# Copyright © 2010-2021 Lance Edgar
#
# This file is part of Rattail.
#
@ -374,6 +374,14 @@ class RattailConfig(object):
self.app = make_app(self)
return self.app
def app_package(self, default=None):
"""
Returns the name of Python package for the top-level app.
"""
if not default:
return self.require('rattail', 'app_package')
return self.get('rattail', 'app_package', default=default)
def app_title(self, default=None):
"""
Returns official display title for the current app.

View file

@ -0,0 +1,30 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2021 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Generating Features
"""
from __future__ import unicode_literals, absolute_import
from .base import Feature
from .handlers import FeatureHandler

61
rattail/features/base.py Normal file
View file

@ -0,0 +1,61 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2021 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Features
"""
from __future__ import unicode_literals, absolute_import
from mako.lookup import TemplateLookup
from rattail.files import resource_path
class Feature(object):
"""
Base class for features.
"""
def __init__(self, config):
self.config = config
# TODO: make templates dir configurable
templates = [resource_path('rattail:templates/feature')]
self.templates = TemplateLookup(directories=templates)
def get_defaults(self):
return {}
def get_template(self, path):
"""
Locate and return the given Mako feature template.
"""
return self.templates.get_template(path)
def generate_mako(self, template, **context):
"""
Generate and return output from the given template.
"""
template = self.get_template(template)
text = template.render(**context)
return text

View file

@ -0,0 +1,91 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2021 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Handler for Generating Features
"""
from __future__ import unicode_literals, absolute_import
import six
import colander
from rattail.app import GenericHandler
from rattail.util import load_entry_points
class FeatureHandler(GenericHandler):
"""
Base class for feature handlers.
"""
entry_point_section = 'rattail.features'
def all_features(self):
"""
Returns a dict of available features, which are registered via
setuptools entry points.
"""
return load_entry_points(self.entry_point_section)
def all_feature_types(self):
"""
Returns a list of available feature type keys.
"""
return list(self.all_features())
def iter_features(self):
"""
Iterate over all features.
"""
for factory in six.itervalues(self.all_features()):
yield factory(self.config)
def get_feature(self, key):
"""
Returns the specific feature identified by type key.
"""
features = self.all_features()
if key in features:
return features[key](self.config)
def make_schema(self, **kwargs):
class Schema(colander.MappingSchema):
app_prefix = colander.SchemaNode(colander.String())
app_cap_prefix = colander.SchemaNode(colander.String())
return Schema(**kwargs)
def get_defaults(self):
return {
'app_prefix': self.config.app_title().lower(),
'app_cap_prefix': self.config.app_title(),
}
def do_generate(self, feature, **kwargs):
"""
Generate code and instructions for new feature.
"""
return feature.generate(**kwargs)

View file

@ -0,0 +1,66 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2021 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
New Report Feature
"""
from __future__ import unicode_literals, absolute_import
import re
from rattail.features import Feature
import colander
class NewReportFeature(Feature):
"""
New Report Feature
"""
feature_key = 'new-report'
feature_title = "New Report"
def make_schema(self, **kwargs):
class Schema(colander.MappingSchema):
name = colander.SchemaNode(colander.String())
description = colander.SchemaNode(colander.String())
return Schema(**kwargs)
def get_defaults(self):
return {
'name': "Latest Widgets",
'description': "Shows all the latest widgets.",
}
def generate(self, **kwargs):
context = dict(kwargs)
context['app_package'] = self.config.app_package()
context['app_slug'] = context['app_package'] # TODO
context['code_name'] = re.sub(r'[\s-]', '_', context['name']).lower()
context['cap_name'] = context['name'].replace(' ', '')
return self.generate_mako('/new-report/instructions.mako', **context)

View file

@ -0,0 +1,79 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2021 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
New Table Feature
"""
from __future__ import unicode_literals, absolute_import
from rattail.features import Feature
import colander
class NewTableFeature(Feature):
"""
New Table Feature
"""
feature_key = 'new-table'
feature_title = "New Table"
def make_schema(self, **kwargs):
class Schema(colander.MappingSchema):
table_name = colander.SchemaNode(colander.String())
model_name = colander.SchemaNode(colander.String())
model_title = colander.SchemaNode(colander.String())
model_title_plural = colander.SchemaNode(colander.String())
description = colander.SchemaNode(colander.String())
versioned = colander.SchemaNode(colander.Boolean())
return Schema(**kwargs)
def get_defaults(self):
return {
'table_name': 'latest_widget',
'model_name': 'LatestWidget',
'model_title': "Latest Widget",
'model_title_plural': "Latest Widgets",
'description': "Represents a Latest Widget.",
'versioned': True,
}
def generate(self, **kwargs):
context = dict(kwargs)
context['app_package'] = self.config.app_package()
context['app_slug'] = context['app_package'] # TODO
context['envroot'] = '/srv/envs/{}'.format(context['app_slug']) # TODO
context['prefixed_table_name'] = '{}_{}'.format(
context['app_prefix'], context['table_name'])
context['prefixed_model_name'] = '{}{}'.format(
context['app_cap_prefix'], context['model_name'])
return self.generate_mako('/new-table/instructions.mako', **context)

View file

@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2020 Lance Edgar
# Copyright © 2010-2021 Lance Edgar
#
# This file is part of Rattail.
#
@ -24,6 +24,8 @@
Handler for Generating Projects
"""
from __future__ import unicode_literals, absolute_import
import os
import shutil
import subprocess

View file

@ -0,0 +1,62 @@
## -*- mode: markdown; -*-
# New Report: ${name}
Create a new Python module under `${app_package}.reports`, for instance at
`~/src/${app_slug}/${app_package}/reports/${code_name}.py`, with contents:
```python
"""
"${name}" report
"""
# from sqlalchemy import orm
from rattail.reporting import ExcelReport
from ${app_package}.db import model
class ${cap_name}(ExcelReport):
"""
${description}
"""
type_key = '${app_prefix}_${code_name}'
name = "${name}"
# TODO: you must declare all desired output columns for Excel
output_fields = [
'customer_number',
'customer_name',
]
def make_data(self, session, params, progress=None, **kwargs):
# TODO: obviously you must do whatever queries you need, to gather
# all data required for your report
# looking for all customers
customers = session.query(model.Customer)\
.order_by(model.Customer.number) #\
# .options(orm.joinedload(model.Customer._people)\
# .joinedload(model.CustomerPerson.person))
# returned object will be a list of dicts
rows = []
def include(customer, i):
# person = customer.only_person()
rows.append({
'customer_number': customer.number,
'customer_name': customer.name,
})
self.progress_loop(include, customers, progress,
message="Fetching data for report")
return rows
```
For more help please see the
[Rattail Manual](https://rattailproject.org/docs/rattail-manual/),
in particular these sections:
- [Reports](https://rattailproject.org/docs/rattail-manual/data/reports/index.html)

View file

@ -0,0 +1,104 @@
## -*- mode: markdown; -*-
# New Table: ${prefixed_model_name}
Create a new Python module under `${app_package}.db.model`, for instance at
`~/src/${app_slug}/${app_package}/db/model/${table_name}.py`, with contents:
```python
"""
Table schema for ${model_title_plural}
"""
import sqlalchemy as sa
# from sqlalchemy import orm
from rattail.db.model import Base, uuid_column
class ${prefixed_model_name}(Base):
"""
${description}
"""
__tablename__ = '${prefixed_table_name}'
model_title = "${model_title}"
model_title_plural = "${model_title_plural}"
% if versioned:
# this enables data versioning for the table
__versioned__ = {}
% endif
# universally-unique identifier
uuid = uuid_column()
# TODO: you must explicitly define remaining columns
# id = sa.Column(sa.Integer(), nullable=True, doc="""
# Presumably unique ID number for the record.
# """)
# TODO: we include this only as an example. your table may not need a name
# column, in which case please remove. but if so be sure to also update
# the __str__() method below.
name = sa.Column(sa.String(length=255), nullable=True, doc="""
Name of the ${model_title}.
""")
# flag = sa.Column(sa.Boolean(), nullable=True, doc="""
# Flag indicating something.
# """)
# timestamp = sa.Column(sa.DateTime(), nullable=True, doc="""
# Time and date when something happens.
# """)
def __str__(self):
return self.name or ""
```
You should review all code in the module, and edit as needed, before
continuing.
Make sure to bring it into your root model namespace also, for instance in
`~/src/${app_slug}/${app_package}/db/model/__init__.py` add this:
```python
from .${table_name} import ${prefixed_model_name}
```
Then generate a new Alembic version script for the schema migration:
```sh
cd ${envroot}
bin/alembic -c app/rattail.conf revision --autogenerate --head ${app_package}@head -m "add ${prefixed_table_name}"
```
That should create a new script somewhere in e.g.
`~/src/${app_slug}/${app_package}/db/alembic/versions/`.
Edit the migration script as needed, then apply it to your database:
```sh
cd ${envroot}
bin/alembic -c app/rattail.conf upgrade heads
```
At this point your DB should have the new table. You can see details with:
```sh
sudo -u postgres psql -c '\d ${prefixed_table_name}' ${app_package}
```
Once you are happy with the result, don't forget to commit!
```sh
cd ~/src/${app_slug}
git add .
git commit -m "add model for ${prefixed_model_name}"
```
For more help please see the
[Rattail Manual](https://rattailproject.org/docs/rattail-manual/),
in particular these sections:
- [Rattail Database](https://rattailproject.org/docs/rattail-manual/data/db/index.html)

View file

@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2020 Lance Edgar
# Copyright © 2010-2021 Lance Edgar
#
# This file is part of Rattail.
#
@ -63,6 +63,7 @@ requires = [
# package # low high
'bcrypt', # 3.1.4
'colander', # 1.8.3
'humanize', # 0.5.1
'lockfile', # 0.9.1
'openpyxl', # 2.5.0
@ -241,6 +242,10 @@ new-batch = rattail.commands.dev:NewBatch
rattail.db = rattail.db:ConfigExtension
rattail.trainwreck = rattail.trainwreck.config:TrainwreckConfig
[rattail.features]
new-report = rattail.features.newreport:NewReportFeature
new-table = rattail.features.newtable:NewTableFeature
[rattail.sil.column_providers]
rattail = rattail.sil.columns:provide_columns