From 9d2a24ea4d797dad37b26163fc288d028d6000bb Mon Sep 17 00:00:00 2001 From: Daniel Gaspar Date: Sun, 10 Dec 2017 18:21:10 +0000 Subject: [PATCH] Docs and example for composite keys --- docs/relations.rst | 13 +++- examples/composite_keys/README.rst | 16 +++++ examples/composite_keys/app/__init__.py | 28 ++++++++ examples/composite_keys/app/models.py | 41 +++++++++++ .../translations/pt/LC_MESSAGES/messages.mo | Bin 0 -> 516 bytes .../translations/pt/LC_MESSAGES/messages.po | 27 ++++++++ examples/composite_keys/app/views.py | 51 ++++++++++++++ examples/composite_keys/babel/babel.cfg | 3 + examples/composite_keys/babel/messages.pot | 27 ++++++++ examples/composite_keys/config.py | 65 ++++++++++++++++++ examples/composite_keys/run.py | 3 + examples/composite_keys/testdata.py | 57 +++++++++++++++ 12 files changed, 329 insertions(+), 2 deletions(-) create mode 100644 examples/composite_keys/README.rst create mode 100644 examples/composite_keys/app/__init__.py create mode 100644 examples/composite_keys/app/models.py create mode 100644 examples/composite_keys/app/translations/pt/LC_MESSAGES/messages.mo create mode 100644 examples/composite_keys/app/translations/pt/LC_MESSAGES/messages.po create mode 100644 examples/composite_keys/app/views.py create mode 100644 examples/composite_keys/babel/babel.cfg create mode 100644 examples/composite_keys/babel/messages.pot create mode 100644 examples/composite_keys/config.py create mode 100644 examples/composite_keys/run.py create mode 100644 examples/composite_keys/testdata.py diff --git a/docs/relations.rst b/docs/relations.rst index b562eb8c77..a2c7665680 100644 --- a/docs/relations.rst +++ b/docs/relations.rst @@ -1,5 +1,5 @@ -Model Relations -=============== +Model Relations/Composite keys +============================== On this chapter we are going to show how to setup model relationships and their view integration on the framework @@ -218,3 +218,12 @@ on the employee detail view:: Take a look and run the example on `Employees example `_ It includes extra functionality like readonly fields, pre and post update logic, etc... + +Composite Keys +-------------- + +Composite keys is supported for SQLAlchemy only, you can reference them using SQLAlchemy 'relationship', +and use them on combo boxes and/or related views, take a look at the +`example `_ + +.. note:: This feature is only supported since 1.9.6 diff --git a/examples/composite_keys/README.rst b/examples/composite_keys/README.rst new file mode 100644 index 0000000000..35a68d49be --- /dev/null +++ b/examples/composite_keys/README.rst @@ -0,0 +1,16 @@ +Composite keys Example +---------------------- + +Simple application showing the use of SQLAlchemy composite keys. + +Create an Admin user:: + + $ fabmanager create-admin + +Insert test data:: + + $ python testdata.py + +Run it:: + + $ fabmanager run diff --git a/examples/composite_keys/app/__init__.py b/examples/composite_keys/app/__init__.py new file mode 100644 index 0000000000..e4b61a2d28 --- /dev/null +++ b/examples/composite_keys/app/__init__.py @@ -0,0 +1,28 @@ +import logging + +from flask import Flask + +from flask_appbuilder import SQLA, AppBuilder + +#from sqlalchemy.engine import Engine +#from sqlalchemy import event + +logging.basicConfig(format='%(asctime)s:%(levelname)s:%(name)s:%(message)s') +logging.getLogger().setLevel(logging.DEBUG) + +app = Flask(__name__) +app.config.from_object('config') +db = SQLA(app) +appbuilder = AppBuilder(app, db.session) + +""" +Only include this for SQLLite constraints + +@event.listens_for(Engine, "connect") +def set_sqlite_pragma(dbapi_connection, connection_record): + cursor = dbapi_connection.cursor() + cursor.execute("PRAGMA foreign_keys=ON") + cursor.close() +""" + +from app import models, views diff --git a/examples/composite_keys/app/models.py b/examples/composite_keys/app/models.py new file mode 100644 index 0000000000..9ce39bcff2 --- /dev/null +++ b/examples/composite_keys/app/models.py @@ -0,0 +1,41 @@ +import datetime +from sqlalchemy import Column, Integer, String, ForeignKey, Date, Enum, Float, ForeignKeyConstraint +from sqlalchemy.orm import relationship +from flask_appbuilder import Model + +mindate = datetime.date(datetime.MINYEAR, 1, 1) + + +class Item(Model): + serial_number = Column(String, primary_key=True) + model = Column(String(150), nullable=False) + + def __repr__(self): + return "%s (%s)" % (self.model, self.serial_number) + + +class Datacenter(Model): + id = Column(Integer, primary_key=True) + name = Column(String(150), unique = True, nullable=False) + address = Column(String(564)) + + def __repr__(self): + return self.name + + +class Rack(Model): + num = Column(Integer, primary_key=True) + datacenter_id = Column(Integer, ForeignKey('datacenter.id'), primary_key=True, nullable=False) + datacenter = relationship("Datacenter") + + def __repr__(self): + return "%d-%s" % (self.num, self.datacenter) + + +class Inventory(Model): + item_serial_number = Column(Integer, ForeignKey('item.serial_number'), primary_key=True, nullable=False) + item = relationship("Item") + rack_num = Column(Integer, ForeignKey('rack.num'), primary_key=True, nullable=False) + rack_datacenter_id = Column(Integer, ForeignKey('rack.datacenter_id'), primary_key=True, nullable=False) + rack = relationship('Rack', + primaryjoin="and_(Inventory.rack_num==Rack.num, Inventory.rack_datacenter_id==Rack.datacenter_id)") diff --git a/examples/composite_keys/app/translations/pt/LC_MESSAGES/messages.mo b/examples/composite_keys/app/translations/pt/LC_MESSAGES/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7713f481f4c5f8abc88ce279b0830ba783c53591 GIT binary patch literal 516 zcmZuu%TB{E5DbD-5JKY2VYz{jOHz6$?n9I|R7Fl(rJ)y2ZsLZZU`Mut;KE<qtoi_)ujXXrQ>gE?1)_cehxr@9*zHT{AhFYJ0V7-s}iR!T2 zSQsj4d1B3`8*5dS#+hsNAQDR^DOn0_awRFuquEU`jA0o>i^**2Mxs>OlF(#%O0KHR zkXTU&!oh@H4o0IWSS;N6EG9!OxWyaDh+9FS>G>_<`NTg5&!f$YQxAbg5{ak0p7gKJ zZ*P2z8%tu%r75^on!@!hV=x_r0d)V2?z@c3tm2sN%qX;1xpl`v3XP#)d@mHP_(~MuH_qLEK88aoV63ZB8RrXo CSC6;= literal 0 HcmV?d00001 diff --git a/examples/composite_keys/app/translations/pt/LC_MESSAGES/messages.po b/examples/composite_keys/app/translations/pt/LC_MESSAGES/messages.po new file mode 100644 index 0000000000..3299f7099e --- /dev/null +++ b/examples/composite_keys/app/translations/pt/LC_MESSAGES/messages.po @@ -0,0 +1,27 @@ +# Portuguese translations for PROJECT. +# Copyright (C) 2014 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2014. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2014-01-13 00:29+0000\n" +"PO-Revision-Date: 2014-01-13 00:18+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: pt \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" + +#: app/views.py:42 +msgid "List Groups" +msgstr "Lista de Grupos" + +#: app/views.py:43 +msgid "List Contacts" +msgstr "" + diff --git a/examples/composite_keys/app/views.py b/examples/composite_keys/app/views.py new file mode 100644 index 0000000000..6464e3d13c --- /dev/null +++ b/examples/composite_keys/app/views.py @@ -0,0 +1,51 @@ +import calendar +from flask_appbuilder import ModelView +from flask_appbuilder.models.sqla.interface import SQLAInterface +from flask_appbuilder.charts.views import GroupByChartView +from flask_appbuilder.models.group import aggregate_count +from flask_appbuilder.widgets import FormHorizontalWidget, FormInlineWidget, FormVerticalWidget +from flask_babel import lazy_gettext as _ + + +from app import db, appbuilder +from .models import Datacenter, Rack, Item, Inventory + + +class RackModelView(ModelView): + datamodel = SQLAInterface(Rack) + list_columns = ["num", "datacenter.name"] + add_columns = ["num", "datacenter"] + edit_columns = ["num", "datacenter"] + show_columns = ["num", "datacenter"] + + +class DatacenterModelView(ModelView): + datamodel = SQLAInterface(Datacenter) + list_columns = ["name", "address"] + add_columns = ["name", "address"] + edit_columns = ["name", "address"] + show_columns = ["name", "address"] + related_views = [RackModelView] + + +class ItemModelView(ModelView): + datamodel = SQLAInterface(Item) + list_columns = ["serial_number", "model"] + add_columns = ["serial_number", "model"] + edit_columns = ["serial_number", "model"] + show_columns = ["serial_number", "model"] + + +class InventoryModelView(ModelView): + datamodel = SQLAInterface(Inventory) + list_columns = ["item", "rack"] + add_columns = ["item", "rack"] + edit_columns = ["item", "rack"] + show_columns = ["item", "rack"] + + +db.create_all() +appbuilder.add_view(DatacenterModelView, "List Datacenters", icon="fa-folder-open-o", category="Datacenters", category_icon='fa-envelope') +appbuilder.add_view(RackModelView, "List Racks", icon="fa-envelope", category="Datacenters") +appbuilder.add_view(ItemModelView, "List Items", icon="fa-folder-open-o", category="Datacenters", category_icon='fa-envelope') +appbuilder.add_view(InventoryModelView, "List Inventory", icon="fa-envelope", category="Datacenters") diff --git a/examples/composite_keys/babel/babel.cfg b/examples/composite_keys/babel/babel.cfg new file mode 100644 index 0000000000..70e23ac634 --- /dev/null +++ b/examples/composite_keys/babel/babel.cfg @@ -0,0 +1,3 @@ +[python: **.py] +[jinja2: **/templates/**.html] +encoding = utf-8 diff --git a/examples/composite_keys/babel/messages.pot b/examples/composite_keys/babel/messages.pot new file mode 100644 index 0000000000..2e5a6fb97a --- /dev/null +++ b/examples/composite_keys/babel/messages.pot @@ -0,0 +1,27 @@ +# Translations template for PROJECT. +# Copyright (C) 2014 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2014. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2014-01-13 00:29+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" + +#: app/views.py:42 +msgid "List Groups" +msgstr "" + +#: app/views.py:43 +msgid "List Contacts" +msgstr "" + diff --git a/examples/composite_keys/config.py b/examples/composite_keys/config.py new file mode 100644 index 0000000000..8cb110528e --- /dev/null +++ b/examples/composite_keys/config.py @@ -0,0 +1,65 @@ +import os + +basedir = os.path.abspath(os.path.dirname(__file__)) + +CSRF_ENABLED = True +SECRET_KEY = '\2\1thisismyscretkey\1\2\e\y\y\h' + +OPENID_PROVIDERS = [ + {'name': 'Google', 'url': 'https://www.google.com/accounts/o8/id'}, + {'name': 'Yahoo', 'url': 'https://me.yahoo.com'}, + {'name': 'AOL', 'url': 'http://openid.aol.com/'}, + {'name': 'Flickr', 'url': 'http://www.flickr.com/'}, + {'name': 'MyOpenID', 'url': 'https://www.myopenid.com'}] + +SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'app.db') +#SQLALCHEMY_DATABASE_URI = 'mysql://username:password@mysqlserver.local/quickhowto' +#SQLALCHEMY_DATABASE_URI = 'postgresql://scott:tiger@localhost:5432/myapp' +#SQLALCHEMY_ECHO = True +SQLALCHEMY_POOL_RECYCLE = 3 + +BABEL_DEFAULT_LOCALE = 'en' +BABEL_DEFAULT_FOLDER = 'translations' +LANGUAGES = { + 'en': {'flag': 'gb', 'name': 'English'}, + 'pt': {'flag': 'pt', 'name': 'Portuguese'}, + 'pt_BR': {'flag':'br', 'name': 'Pt Brazil'}, + 'es': {'flag': 'es', 'name': 'Spanish'}, + 'de': {'flag': 'de', 'name': 'German'}, + 'zh': {'flag': 'cn', 'name': 'Chinese'}, + 'ru': {'flag': 'ru', 'name': 'Russian'}, + 'pl': {'flag': 'pl', 'name': 'Polish'}, + 'ja_JP': {'flag': 'jp', 'name': 'Japanese'} +} + + +#------------------------------ +# GLOBALS FOR GENERAL APP's +#------------------------------ +UPLOAD_FOLDER = basedir + '/app/static/uploads/' +IMG_UPLOAD_FOLDER = basedir + '/app/static/uploads/' +IMG_UPLOAD_URL = '/static/uploads/' +AUTH_TYPE = 1 +#AUTH_LDAP_SERVER = "ldap://dc.domain.net" +AUTH_ROLE_ADMIN = 'Admin' +AUTH_ROLE_PUBLIC = 'Public' +APP_NAME = "F.A.B. Example" +APP_THEME = "" # default +#APP_THEME = "cerulean.css" # COOL +#APP_THEME = "amelia.css" +#APP_THEME = "cosmo.css" +#APP_THEME = "cyborg.css" # COOL +#APP_THEME = "flatly.css" +#APP_THEME = "journal.css" +#APP_THEME = "readable.css" +#APP_THEME = "simplex.css" +#APP_THEME = "slate.css" # COOL +#APP_THEME = "spacelab.css" # NICE +#APP_THEME = "united.css" +#APP_THEME = "darkly.css" +#APP_THEME = "lumen.css" +#APP_THEME = "paper.css" +#APP_THEME = "sandstone.css" +#APP_THEME = "solar.css" +#APP_THEME = "superhero.css" + diff --git a/examples/composite_keys/run.py b/examples/composite_keys/run.py new file mode 100644 index 0000000000..85b1074626 --- /dev/null +++ b/examples/composite_keys/run.py @@ -0,0 +1,3 @@ +from app import app + +app.run(host='0.0.0.0', port=8080, debug=True) diff --git a/examples/composite_keys/testdata.py b/examples/composite_keys/testdata.py new file mode 100644 index 0000000000..74cda581f9 --- /dev/null +++ b/examples/composite_keys/testdata.py @@ -0,0 +1,57 @@ +import logging +from app import db +from app.models import Inventory, Datacenter, Rack, Item +import random +import string +from datetime import datetime + +log = logging.getLogger(__name__) + +DC_RACK_MAX = 20 +ITEM_MAX = 1000 + +cities = ['Lisbon', 'Porto', 'Madrid', 'Barcelona', 'Frankfurt', 'London'] + +models = ['Server MX', 'Server MY', 'Server DL380', 'Server x440', 'Server x460'] + +datacenters = list() + + +def get_random_name(names_list, size=1): + return names_list[random.randrange(0, len(names_list))] + +def serial_generator(size = 6, chars = string.ascii_uppercase + string.digits): + return ''.join(random.choice(chars) for _ in range(size)) + + +for city in cities: + datacenter = Datacenter() + datacenter.name = "DC %s" % city + datacenter.address = city + datacenters.append(datacenter) + db.session.add(datacenter) + log.info(datacenter) + try: + db.session.commit() + for num in range(1, DC_RACK_MAX): + rack = Rack() + rack.num = num + rack.datacenter = datacenter + db.session.add(rack) + + except Exception as e: + log.error("Creating Datacenter: %s", e) + db.session.rollback() + +for i in range(1, ITEM_MAX): + item = Item() + item.serial_number = serial_generator() + item.model = get_random_name(models) + db.session.add(item) + log.info(item) + try: + db.session.commit() + except Exception as e: + log.error("Creating Item: %s", e) + db.session.rollback() +