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.path as op
from flask import Flask
from flask import Flask, Markup
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import composite
import uuid
import random
import string
from wtforms import validators
......@@ -13,16 +17,15 @@ from flask_admin.contrib.sqla import filters
from flask_admin.contrib.sqla.form import InlineModelConverter
from flask_admin.contrib.sqla.fields import InlineModelFormList
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
class EnumChoices(enum.Enum):
first = 1
second = 2
# Create application
app = Flask(__name__)
......@@ -39,20 +42,51 @@ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + app.config['DATABASE_FILE
app.config['SQLALCHEMY_ECHO'] = True
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
class User(db.Model):
AVAILABLE_TYPES = [
(u'admin', u'Admin'),
(u'regular-user', u'Regular user')
]
id = db.Column(db.Integer, primary_key=True)
id = db.Column(UUIDType(binary=False), default=uuid.uuid4, primary_key=True)
# use a regular string field, for which we can specify a list of available choices later on
type = db.Column(db.String(100))
# 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))
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)
pets = db.relationship('Pet', backref='owner')
enum_choice_field = db.Column(ChoiceType(EnumChoices, impl=db.Integer()), nullable=True)
website = db.Column(URLType)
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):
return "{}, {}".format(self.last_name, self.first_name)
......@@ -61,16 +95,6 @@ class User(db.Model):
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
post_tags_table = db.Table('post_tags', db.Model.metadata,
db.Column('post_id', db.Integer, db.ForeignKey('post.id')),
......@@ -84,9 +108,12 @@ class Post(db.Model):
text = db.Column(db.Text, nullable=False)
date = db.Column(db.Date)
user_id = db.Column(db.Integer(), db.ForeignKey(User.id))
user = db.relationship(User, backref='posts')
# some sqlalchemy_utils data types (see https://sqlalchemy-utils.readthedocs.io/)
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)
def __str__(self):
......@@ -95,25 +122,12 @@ class Post(db.Model):
class Tag(db.Model):
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):
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):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64))
......@@ -124,17 +138,6 @@ class Tree(db.Model):
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
@app.route('/')
def index():
......@@ -154,59 +157,109 @@ class FilterLastNameBrown(BaseSQLAFilter):
# Customized User model admin
inline_form_options = {
'form_label': "Info item",
'form_columns': ['id', 'key', 'value'],
'form_args': None,
'form_extra_fields': None,
}
def phone_number_formatter (view, context, model, name):
return Markup("<nobr>{}</nobr>".format(model.phone_number)) if model.phone_number else None
def is_numberic_validator(form, field):
if field.data and not field.data.isdigit():
raise validators.ValidationError(gettext('Only numbers are allowed.'))
class UserAdmin(sqla.ModelView):
can_view_details = True # show a modal dialog with records details
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 = [
'id',
'type',
'last_name',
'first_name',
'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',
'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
# 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
column_filters = [
'first_name',
FilterEqual(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):
return self._use_filtered_parent(
return self._filtered_posts(
super(UserAdmin, self).create_form()
)
def edit_form(self, obj):
return self._use_filtered_parent(
return self._filtered_posts(
super(UserAdmin, self).edit_form(obj)
)
def _use_filtered_parent(self, form):
form.pets.query_factory = self._get_parent_list
def _filtered_posts(self, form):
form.featured_post.query_factory = lambda: Post.query.filter(Post.user_id == form._obj.id).all()
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
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_sortable_list = [
'id',
......@@ -217,11 +270,13 @@ class PostAdmin(sqla.ModelView):
column_labels = dict(title='Post Title') # Rename 'title' column in list view
column_searchable_list = [
'title',
User.first_name,
User.last_name,
'user.first_name',
'user.last_name',
'tags.name',
]
column_filters = [
'background_color',
'created_at',
'user',
'title',
'date',
......@@ -233,10 +288,10 @@ class PostAdmin(sqla.ModelView):
export_types = ['csv', 'xls']
# Pass arguments to WTForms. In this case, change label for text field to
# be 'Big Text' and add required() validator.
form_args = dict(
text=dict(label='Big Text', validators=[validators.required()])
)
# be 'Big Text' and add DataRequired() validator.
form_args = {
'text': dict(label='Big Text', validators=[validators.DataRequired()])
}
form_ajax_refs = {
'user': {
......@@ -259,14 +314,6 @@ class TreeView(sqla.ModelView):
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
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(sqla.ModelView(Tag, 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(ScreenView(Screen, db.session, category="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='Google', url='http://www.google.com/', category='Links'))
admin.add_link(MenuLink(name='Mozilla', url='http://mozilla.org/', category='Links'))
admin.add_link(MenuLink(name='External link', url='http://www.example.com/', category='Links'))
def build_sample_db():
......@@ -307,13 +350,35 @@ def build_sample_db():
'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 = []
for i in range(len(first_names)):
user = User()
country = random.choice(countries)
user.type = random.choice(AVAILABLE_USER_TYPES)[0]
user.first_name = first_names[i]
user.last_name = last_names[i]
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)
db.session.add(user)
......@@ -370,6 +435,7 @@ def build_sample_db():
post.user = user
post.title = entry['title']
post.text = entry['content']
post.background_color = random.choice(["#cccccc", "red", "lightblue", "#0f0"])
tmp = int(1000*random.random()) # random number between 0 and 1000:
post.date = datetime.datetime.now() - datetime.timedelta(days=tmp)
post.tags = random.sample(tag_list, 2) # select a couple of tags at random
......@@ -389,15 +455,6 @@ def build_sample_db():
leaf.parent = branch
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()
return
......
......@@ -2,3 +2,6 @@ Flask
Flask-Admin
Flask-SQLAlchemy
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