Commit 424108be authored by PJ Janse van Rensburg's avatar PJ Janse van Rensburg

Add sqla_utils types to 'sqla' example app.

parent d36672b2
import os import os
import os.path as op import os.path as op
from flask import Flask from flask import Flask, Markup
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import composite
import uuid
import random
import string
from wtforms import validators from wtforms import validators
...@@ -13,16 +17,15 @@ from flask_admin.contrib.sqla import filters ...@@ -13,16 +17,15 @@ from flask_admin.contrib.sqla import filters
from flask_admin.contrib.sqla.form import InlineModelConverter from flask_admin.contrib.sqla.form import InlineModelConverter
from flask_admin.contrib.sqla.fields import InlineModelFormList from flask_admin.contrib.sqla.fields import InlineModelFormList
from flask_admin.contrib.sqla.filters import BaseSQLAFilter, FilterEqual from flask_admin.contrib.sqla.filters import BaseSQLAFilter, FilterEqual
from flask_admin.babel import gettext
from sqlalchemy_utils.types import ChoiceType, EmailType from sqlalchemy_utils import ChoiceType, EmailType, UUIDType, URLType, CurrencyType, Currency
from colour import Color
from sqlalchemy_utils import ColorType, ArrowType, IPAddressType, TimezoneType
import arrow
import enum import enum
class EnumChoices(enum.Enum):
first = 1
second = 2
# Create application # Create application
app = Flask(__name__) app = Flask(__name__)
...@@ -39,20 +42,51 @@ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + app.config['DATABASE_FILE ...@@ -39,20 +42,51 @@ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + app.config['DATABASE_FILE
app.config['SQLALCHEMY_ECHO'] = True app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app) db = SQLAlchemy(app)
class EnumChoices(enum.Enum):
first = 1
second = 2
AVAILABLE_USER_TYPES = [
(u'admin', u'Admin'),
(u'content-writer', u'Content writer'),
(u'editor', u'Editor'),
(u'regular-user', u'Regular user'),
]
# Create models # Create models
class User(db.Model): class User(db.Model):
AVAILABLE_TYPES = [ id = db.Column(UUIDType(binary=False), default=uuid.uuid4, primary_key=True)
(u'admin', u'Admin'),
(u'regular-user', u'Regular user') # use a regular string field, for which we can specify a list of available choices later on
] type = db.Column(db.String(100))
id = db.Column(db.Integer, primary_key=True)
# fixed choices can be handled in a number of different ways:
enum_choice_field = db.Column(db.Enum(EnumChoices), nullable=True)
sqla_utils_choice_field = db.Column(ChoiceType(AVAILABLE_USER_TYPES), nullable=True)
sqla_utils_enum_choice_field = db.Column(ChoiceType(EnumChoices, impl=db.Integer()), nullable=True)
first_name = db.Column(db.String(100)) first_name = db.Column(db.String(100))
last_name = db.Column(db.String(100)) last_name = db.Column(db.String(100))
type = db.Column(ChoiceType(AVAILABLE_TYPES), nullable=True)
# some sqlalchemy_utils data types (see https://sqlalchemy-utils.readthedocs.io/)
email = db.Column(EmailType, unique=True, nullable=False) email = db.Column(EmailType, unique=True, nullable=False)
pets = db.relationship('Pet', backref='owner') website = db.Column(URLType)
enum_choice_field = db.Column(ChoiceType(EnumChoices, impl=db.Integer()), nullable=True) ip_address = db.Column(IPAddressType)
currency = db.Column(CurrencyType, nullable=True, default=None)
timezone = db.Column(TimezoneType(backend='pytz'))
dialling_code = db.Column(db.Integer())
local_phone_number = db.Column(db.String(10))
featured_post_id = db.Column(db.Integer, db.ForeignKey('post.id'))
featured_post = db.relationship('Post', foreign_keys=[featured_post_id])
@hybrid_property
def phone_number(self):
if self.dialling_code and self.local_phone_number:
number = str(self.local_phone_number)
return "+{} ({}){} {} {}".format(self.dialling_code, number[0], number[1:3], number[3:6], number[6::])
return
def __str__(self): def __str__(self):
return "{}, {}".format(self.last_name, self.first_name) return "{}, {}".format(self.last_name, self.first_name)
...@@ -61,16 +95,6 @@ class User(db.Model): ...@@ -61,16 +95,6 @@ class User(db.Model):
return "{}: {}".format(self.id, self.__str__()) return "{}: {}".format(self.id, self.__str__())
class Pet(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), nullable=False)
person_id = db.Column(db.Integer, db.ForeignKey('user.id'))
available = db.Column(db.Boolean)
def __str__(self):
return self.name
# Create M2M table # Create M2M table
post_tags_table = db.Table('post_tags', db.Model.metadata, post_tags_table = db.Table('post_tags', db.Model.metadata,
db.Column('post_id', db.Integer, db.ForeignKey('post.id')), db.Column('post_id', db.Integer, db.ForeignKey('post.id')),
...@@ -84,9 +108,12 @@ class Post(db.Model): ...@@ -84,9 +108,12 @@ class Post(db.Model):
text = db.Column(db.Text, nullable=False) text = db.Column(db.Text, nullable=False)
date = db.Column(db.Date) date = db.Column(db.Date)
user_id = db.Column(db.Integer(), db.ForeignKey(User.id)) # some sqlalchemy_utils data types (see https://sqlalchemy-utils.readthedocs.io/)
user = db.relationship(User, backref='posts') background_color = db.Column(ColorType)
created_at = db.Column(ArrowType, default=arrow.utcnow())
user_id = db.Column(UUIDType(binary=False), db.ForeignKey(User.id))
user = db.relationship(User, foreign_keys=[user_id], backref='posts')
tags = db.relationship('Tag', secondary=post_tags_table) tags = db.relationship('Tag', secondary=post_tags_table)
def __str__(self): def __str__(self):
...@@ -95,25 +122,12 @@ class Post(db.Model): ...@@ -95,25 +122,12 @@ class Post(db.Model):
class Tag(db.Model): class Tag(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode(64)) name = db.Column(db.Unicode(64), unique=True)
def __str__(self): def __str__(self):
return "{}".format(self.name) return "{}".format(self.name)
class UserInfo(db.Model):
id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.String(64), nullable=False)
value = db.Column(db.String(64))
user_id = db.Column(db.Integer(), db.ForeignKey(User.id))
user = db.relationship(User, backref='info')
def __str__(self):
return "{} - {}".format(self.key, self.value)
class Tree(db.Model): class Tree(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64)) name = db.Column(db.String(64))
...@@ -124,17 +138,6 @@ class Tree(db.Model): ...@@ -124,17 +138,6 @@ class Tree(db.Model):
return "{}".format(self.name) return "{}".format(self.name)
class Screen(db.Model):
__tablename__ = 'screen'
id = db.Column(db.Integer, primary_key=True)
width = db.Column(db.Integer, nullable=False)
height = db.Column(db.Integer, nullable=False)
@hybrid_property
def number_of_pixels(self):
return self.width * self.height
# Flask views # Flask views
@app.route('/') @app.route('/')
def index(): def index():
...@@ -154,59 +157,109 @@ class FilterLastNameBrown(BaseSQLAFilter): ...@@ -154,59 +157,109 @@ class FilterLastNameBrown(BaseSQLAFilter):
# Customized User model admin # Customized User model admin
inline_form_options = { def phone_number_formatter (view, context, model, name):
'form_label': "Info item", return Markup("<nobr>{}</nobr>".format(model.phone_number)) if model.phone_number else None
'form_columns': ['id', 'key', 'value'],
'form_args': None, def is_numberic_validator(form, field):
'form_extra_fields': None, if field.data and not field.data.isdigit():
} raise validators.ValidationError(gettext('Only numbers are allowed.'))
class UserAdmin(sqla.ModelView): class UserAdmin(sqla.ModelView):
can_view_details = True # show a modal dialog with records details
action_disallowed_list = ['delete', ] action_disallowed_list = ['delete', ]
column_display_pk = True
form_choices = {
'type': AVAILABLE_USER_TYPES,
}
form_args = {
'dialling_code': {'label': 'Dialling code'},
'local_phone_number': {
'label': 'Phone number',
'validators': [is_numberic_validator]
},
}
form_widget_args = {
'id':{
'readonly':True
}
}
column_list = [ column_list = [
'id', 'type',
'last_name', 'last_name',
'first_name', 'first_name',
'email', 'email',
'ip_address',
'currency',
'timezone',
'phone_number',
]
column_searchable_list = [
'first_name',
'last_name',
'email',
]
column_editable_list = ['type', 'currency', 'timezone']
column_details_list = [
'id',
'featured_post',
'website',
'enum_choice_field',
'sqla_utils_choice_field',
'sqla_utils_enum_choice_field',
] + column_list
form_columns = [
'id',
'type', 'type',
'pets', 'featured_post',
'enum_choice_field',
'sqla_utils_choice_field',
'sqla_utils_enum_choice_field',
'last_name',
'first_name',
'email',
'website',
'dialling_code',
'local_phone_number',
] ]
column_auto_select_related = True
column_default_sort = [('last_name', False), ('first_name', False)] # sort on multiple columns column_default_sort = [('last_name', False), ('first_name', False)] # sort on multiple columns
# custom filter: each filter in the list is a filter operation (equals, not equals, etc) # custom filter: each filter in the list is a filter operation (equals, not equals, etc)
# filters with the same name will appear as operations under the same filter # filters with the same name will appear as operations under the same filter
column_filters = [ column_filters = [
'first_name',
FilterEqual(column=User.last_name, name='Last Name'), FilterEqual(column=User.last_name, name='Last Name'),
FilterLastNameBrown(column=User.last_name, name='Last Name', FilterLastNameBrown(column=User.last_name, name='Last Name',
options=(('1', 'Yes'), ('0', 'No'))) options=(('1', 'Yes'), ('0', 'No'))),
'email',
'ip_address',
'currency',
'timezone',
] ]
inline_models = [(UserInfo, inline_form_options), ] column_formatters = {'phone_number': phone_number_formatter}
# setup create & edit forms so that only 'available' pets can be selected # setup create & edit forms so that only posts created by this user can be selected as 'featured'
def create_form(self): def create_form(self):
return self._use_filtered_parent( return self._filtered_posts(
super(UserAdmin, self).create_form() super(UserAdmin, self).create_form()
) )
def edit_form(self, obj): def edit_form(self, obj):
return self._use_filtered_parent( return self._filtered_posts(
super(UserAdmin, self).edit_form(obj) super(UserAdmin, self).edit_form(obj)
) )
def _use_filtered_parent(self, form): def _filtered_posts(self, form):
form.pets.query_factory = self._get_parent_list form.featured_post.query_factory = lambda: Post.query.filter(Post.user_id == form._obj.id).all()
return form return form
def _get_parent_list(self):
# only show available pets in the form
return Pet.query.filter_by(available=True).all()
# Customized Post model admin # Customized Post model admin
class PostAdmin(sqla.ModelView): class PostAdmin(sqla.ModelView):
column_list = ['id', 'user', 'title', 'date', 'tags'] column_display_pk = True
column_list = ['id', 'user', 'title', 'date', 'tags', 'background_color', 'created_at',]
column_editable_list = ['background_color', ]
column_default_sort = ('date', True) column_default_sort = ('date', True)
column_sortable_list = [ column_sortable_list = [
'id', 'id',
...@@ -217,11 +270,13 @@ class PostAdmin(sqla.ModelView): ...@@ -217,11 +270,13 @@ class PostAdmin(sqla.ModelView):
column_labels = dict(title='Post Title') # Rename 'title' column in list view column_labels = dict(title='Post Title') # Rename 'title' column in list view
column_searchable_list = [ column_searchable_list = [
'title', 'title',
User.first_name, 'user.first_name',
User.last_name, 'user.last_name',
'tags.name', 'tags.name',
] ]
column_filters = [ column_filters = [
'background_color',
'created_at',
'user', 'user',
'title', 'title',
'date', 'date',
...@@ -233,10 +288,10 @@ class PostAdmin(sqla.ModelView): ...@@ -233,10 +288,10 @@ class PostAdmin(sqla.ModelView):
export_types = ['csv', 'xls'] export_types = ['csv', 'xls']
# Pass arguments to WTForms. In this case, change label for text field to # Pass arguments to WTForms. In this case, change label for text field to
# be 'Big Text' and add required() validator. # be 'Big Text' and add DataRequired() validator.
form_args = dict( form_args = {
text=dict(label='Big Text', validators=[validators.required()]) 'text': dict(label='Big Text', validators=[validators.DataRequired()])
) }
form_ajax_refs = { form_ajax_refs = {
'user': { 'user': {
...@@ -259,14 +314,6 @@ class TreeView(sqla.ModelView): ...@@ -259,14 +314,6 @@ class TreeView(sqla.ModelView):
form_excluded_columns = ['children', ] form_excluded_columns = ['children', ]
class ScreenView(sqla.ModelView):
column_list = ['id', 'width', 'height', 'number_of_pixels'] # not that 'number_of_pixels' is a hybrid property, not a field
column_sortable_list = ['id', 'width', 'height', 'number_of_pixels']
# Flask-admin can automatically detect the relevant filters for hybrid properties.
column_filters = ('number_of_pixels', )
# Create admin # Create admin
admin = admin.Admin(app, name='Example: SQLAlchemy', template_mode='bootstrap3') admin = admin.Admin(app, name='Example: SQLAlchemy', template_mode='bootstrap3')
...@@ -274,14 +321,10 @@ admin = admin.Admin(app, name='Example: SQLAlchemy', template_mode='bootstrap3') ...@@ -274,14 +321,10 @@ admin = admin.Admin(app, name='Example: SQLAlchemy', template_mode='bootstrap3')
admin.add_view(UserAdmin(User, db.session)) admin.add_view(UserAdmin(User, db.session))
admin.add_view(sqla.ModelView(Tag, db.session)) admin.add_view(sqla.ModelView(Tag, db.session))
admin.add_view(PostAdmin(db.session)) admin.add_view(PostAdmin(db.session))
admin.add_view(sqla.ModelView(Pet, db.session, category="Other"))
admin.add_view(sqla.ModelView(UserInfo, db.session, category="Other"))
admin.add_view(TreeView(Tree, db.session, category="Other")) admin.add_view(TreeView(Tree, db.session, category="Other"))
admin.add_view(ScreenView(Screen, db.session, category="Other"))
admin.add_sub_category(name="Links", parent_name="Other") admin.add_sub_category(name="Links", parent_name="Other")
admin.add_link(MenuLink(name='Back Home', url='/', category='Links')) admin.add_link(MenuLink(name='Back Home', url='/', category='Links'))
admin.add_link(MenuLink(name='Google', url='http://www.google.com/', category='Links')) admin.add_link(MenuLink(name='External link', url='http://www.example.com/', category='Links'))
admin.add_link(MenuLink(name='Mozilla', url='http://mozilla.org/', category='Links'))
def build_sample_db(): def build_sample_db():
...@@ -307,13 +350,35 @@ def build_sample_db(): ...@@ -307,13 +350,35 @@ def build_sample_db():
'Ali', 'Mason', 'Mitchell', 'Rose', 'Davis', 'Davies', 'Rodriguez', 'Cox', 'Alexander' 'Ali', 'Mason', 'Mitchell', 'Rose', 'Davis', 'Davies', 'Rodriguez', 'Cox', 'Alexander'
] ]
countries = [
("ZA", "South Africa", 27, "ZAR", "Africa/Johannesburg"),
("BF", "Burkina Faso", 226, "XOF", "Africa/Ouagadougou"),
("US", "United States of America", 1, "USD", "America/New_York"),
("BR", "Brazil", 55, "BRL", "America/Sao_Paulo"),
("TZ", "Tanzania", 255, "TZS", "Africa/Dar_es_Salaam"),
("DE", "Germany", 49, "EUR", "Europe/Berlin"),
("CN", "China", 86, "CNY", "Asia/Shanghai"),
]
user_list = [] user_list = []
for i in range(len(first_names)): for i in range(len(first_names)):
user = User() user = User()
country = random.choice(countries)
user.type = random.choice(AVAILABLE_USER_TYPES)[0]
user.first_name = first_names[i] user.first_name = first_names[i]
user.last_name = last_names[i] user.last_name = last_names[i]
user.email = first_names[i].lower() + "@example.com" user.email = first_names[i].lower() + "@example.com"
user.info.append(UserInfo(key="foo", value="bar"))
user.website = "https://www.example.com"
user.ip_address = "127.0.0.1"
user.coutry = country[1]
user.currency = country[3]
user.timezone = country[4]
user.dialling_code = country[2]
user.local_phone_number = '0' + ''.join(random.choices('123456789', k=9))
user_list.append(user) user_list.append(user)
db.session.add(user) db.session.add(user)
...@@ -370,6 +435,7 @@ def build_sample_db(): ...@@ -370,6 +435,7 @@ def build_sample_db():
post.user = user post.user = user
post.title = entry['title'] post.title = entry['title']
post.text = entry['content'] post.text = entry['content']
post.background_color = random.choice(["#cccccc", "red", "lightblue", "#0f0"])
tmp = int(1000*random.random()) # random number between 0 and 1000: tmp = int(1000*random.random()) # random number between 0 and 1000:
post.date = datetime.datetime.now() - datetime.timedelta(days=tmp) post.date = datetime.datetime.now() - datetime.timedelta(days=tmp)
post.tags = random.sample(tag_list, 2) # select a couple of tags at random post.tags = random.sample(tag_list, 2) # select a couple of tags at random
...@@ -389,15 +455,6 @@ def build_sample_db(): ...@@ -389,15 +455,6 @@ def build_sample_db():
leaf.parent = branch leaf.parent = branch
db.session.add(leaf) db.session.add(leaf)
db.session.add(Pet(name='Dog', available=True))
db.session.add(Pet(name='Fish', available=True))
db.session.add(Pet(name='Cat', available=True))
db.session.add(Pet(name='Parrot', available=True))
db.session.add(Pet(name='Ocelot', available=False))
db.session.add(Screen(width=500, height=2000))
db.session.add(Screen(width=550, height=1900))
db.session.commit() db.session.commit()
return return
......
...@@ -2,3 +2,6 @@ Flask ...@@ -2,3 +2,6 @@ Flask
Flask-Admin Flask-Admin
Flask-SQLAlchemy Flask-SQLAlchemy
tablib tablib
sqlalchemy_utils
arrow
colour
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment