- 取得連結
- X
- 以電子郵件傳送
- 其他應用程式
程式語言:Python
簡介:簡單的專案管理網站,包含後台管理
- Package:
- flask
- flask_script
- flask_sqlalchemy
- flask_admin
- flask_login
- pandas
簡介:簡單的專案管理網站,包含後台管理
初步配置 flask & flask_script
Project-Control-Web │ basic_config.py (new) │ manage.py (new) │ ├─instance (new) │ debug_config.py (new) │ └─pcf (new) │ main.py (new) │ views.py (new) │ __init__.py (new) │ ├─static (new) │ style.css (new) │ └─templates (new) index.html (new) layout.html (new)
首先,在 manage.py 定義 cli 會用到的指令
基本框架 layout.html
''' manage.py ''' from flask_script import Manager from pcf import app manager = Manager(app) @manager.command def initDB(): """初始化資料庫""" pass if __name__ == '__main__': manager.run()在 main.py 中,初始化 app,並利用 register_blueprint 控管 view 路徑
''' main.py ''' from flask import Flask # instance_relative_config 設定有 instance 資料夾的存在,預設路徑 ../instance app = Flask(__name__, instance_relative_config=True) # 基本設定,來自於上層的 config.py app.config.from_object("basic_config") # 來自於 instance 中的 config.py,直接覆蓋之前的設定 # 路徑可用 app.instance_path 得知 # debug 設定,不用時可註解 app.config.from_pyfile('debug_config.py') # 產品設定,來自於環境變數所提供的路徑 app.config.from_envvar('FLASKR_SETTINGS', silent=True) # 因為 .views 中有用到 app,所以只能把 .views 往後擺 from .views import pcf app.register_blueprint(pcf) @app.before_request def before_request(): """在 request 前做的事""" pass各個設定檔
''' basic_config.py ''' # debug 模式 DEBUG = False
''' debug_config.py ''' # debug 模式 DEBUG = True讓 manage.py 可以看得到 app,所以需建立一個 __init__.py
''' __init__.py ''' from .main import app基本 view 的功能
''' views.py ''' from flask import render_template, Blueprint from .main import app pcf = Blueprint('pcf', __name__) @pcf.route('/', methods=["GET"]) def index(): return render_template('index.html', title="總覽")網頁程式碼
基本框架 layout.html
<!doctype html> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content="Project Controler" /> <meta name="author" content="子風" /> <title>{{title}}</title> <!-- Bootstrap --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous"> <!-- custom --> <link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}"> </head> <body> <div class="container"> <nav class="navbar fixed-top navbar-expand-lg navbar-dark bg-dark"> <a class="navbar-brand" href="{{ url_for('pcf.index') }}">首頁</a> </nav> </div> <div class="container"> {% for message in get_flashed_messages() %} <div class="alert alert-primary" role="alert">{{ message }}</div> {% endfor %} </div> {% block body %}{% endblock %} {% block script %}{% endblock %} </body>首頁 index.html
{% extends "layout.html" %} {% block body %} <div> <div class="flexbox-table table-dark"> <div class='thead'> <div class='tr'> <div class='th' style="flex: 2;">JIRA</div> <div class='th' style="flex: 2;">Model Name</div> <div class='th' style="flex: 2;">Customer<br/>Model Name</div> <div class='th' style="flex: 2;">Product ID</div> <div class='th' style="flex: 1;">Project Code</div> <div class='th' style="flex: 1;">Stage</div> <div class='th' style="flex: 1;">EE Owner</div> <div class='th' style="flex: 1;">PM</div> <div class='th' style="flex: 1;">APM</div> <div class='th' style="flex: 1;">Customer</div> <div class='th' style="flex: 1;">Model Name Code</div> <div class='th' style="flex: 4;">Remark</div> </div> </div> </div> </div> {% endblock %}style.css 利用 flex 構建出 table
body { /* 避免被導覽列蓋住 */ padding-top: 65px; background-color: #AAAAAA; } td, th { min-width: 90px; text-align: center; } /* 定義 div table */ .flexbox-table {} .flexbox-table .thead {} .flexbox-table .tbody {} .flexbox-table .tr { display: flex; flex-flow: row nowrap; justify-content: space-around; width: 100%; } .flexbox-table .tbody .tr:hover { background-color: #343a40; } .flexbox-table .th { font-weight: bold; } .flexbox-table .th, .flexbox-table .td { flex: 2 0 0; text-align: center; width: 100%; border: 1px solid #32383e; } .flexbox-form { display: flex; flex-flow: row wrap; justify-content: flex-start; }
建立資料庫 flask_sqlalchemy
Project-Control-Web │ basic_config.py (modify) │ manage.py (modify) │ ├─instance │ debug_config.py │ product_config.py (new) │ └─pcf │ main.py │ models.py (new) │ views.py (modify) │ __init__.py (modify) │ ├─static │ style.css │ └─templates index.html layout.html
在 manage.py 加入初始化資料庫的程式碼
''' manage.py ''' from flask_script import Manager, prompt_bool from pcf import app, db manager = Manager(app) @manager.command def initDB(): """初始化資料庫""" if prompt_bool("將失去所有資料,確定嗎?"): db.drop_all() db.create_all() if __name__ == '__main__': manager.run()在 main.py 中加入 db 的宣告,在此使用的 ORM 是 SQLAlchemy
''' main.py ''' from flask import Flask from flask_sqlalchemy import SQLAlchemy # instance_relative_config 設定有 instance 資料夾的存在,預設路徑 ../instance app = Flask(__name__, instance_relative_config=True) # 基本設定,來自於上層的 config.py app.config.from_object("basic_config") # 來自於 instance 中的 config.py,直接覆蓋之前的設定 # 路徑可用 app.instance_path 得知 # debug 設定,不用時可註解 app.config.from_pyfile('debug_config.py') # product 設定 app.config.from_pyfile('product_config.py') # 產品設定,來自於環境變數所提供的路徑 app.config.from_envvar('FLASKR_SETTINGS', silent=True) # 資料庫宣告 # 資料庫路徑 app.config['SQLALCHEMY_DATABASE_URI'] db = SQLAlchemy(app) # 因為 .views 中有用到 app,所以只能把 .views 往後擺 from .views import pcf app.register_blueprint(pcf) @app.before_request def before_request(): """在 request 前做的事""" pass在各個設定檔加入 SQLAlchemy 的設定
''' basic_config.py ''' # debug 模式 DEBUG = False # 顯示 sql 查詢語句 SQLALCHEMY_ECHO = False # SQLAlchemy 將會追蹤對象的修改並且發送信號。這需要額外的內存,如果不必要的可以禁用它。 SQLALCHEMY_TRACK_MODIFICATIONS = False
''' product_config.py ''' import os appFolder = os.path.dirname(__file__) # 資料庫的路徑 SQLALCHEMY_DATABASE_URI = 'sqlite:///' \ + os.path.join(appFolder, r"../", 'data.db')讓 manage.py 看得到 db,故在 __init__.py 加入 db
''' __init__.py ''' from .main import app, db建立 models.py 定義資料的架構,綠色線表示 many to many,藍色線表示 one to many
''' models.py ''' import datetime from flask_sqlalchemy import SQLAlchemy from .main import db productIDs_components = db.Table('productIDs_components', db.Model.metadata, db.Column('productIDs_id', db.Integer, db.ForeignKey('productIDs.id')), db.Column('components_id', db.Integer, db.ForeignKey('components.id'))) productIDs_documents = db.Table('productIDs_documents', db.Model.metadata, db.Column('productIDs_id', db.Integer, db.ForeignKey('productIDs.id')), db.Column('documents_id', db.Integer, db.ForeignKey('documents.id'))) productIDs_histories = db.Table('productIDs_histories', db.Model.metadata, db.Column('productIDs_id', db.Integer, db.ForeignKey('productIDs.id')), db.Column('histories_id', db.Integer, db.ForeignKey('histories.id'))) documents_histories = db.Table('documents_histories', db.Model.metadata, db.Column('documents_id', db.Integer, db.ForeignKey('documents.id')), db.Column('histories_id', db.Integer, db.ForeignKey('histories.id'))) class Models(db.Model): # 若不寫則看 class name __tablename__ = 'models' # 設定 primary_key id = db.Column( db.Integer, primary_key=True, autoincrement=True, nullable=False) JIRA = db.Column(db.String(20)) name = db.Column(db.String(20), nullable=False) projectCode = db.Column(db.String(20)) stage = db.Column(db.String(5), nullable=False) PM = db.Column(db.String(4), nullable=False) APM = db.Column(db.String(4), nullable=False) modelNameCode = db.Column(db.String(20)) remark = db.Column(db.Text) owner_id = db.Column( db.Integer, db.ForeignKey('owners.id'), nullable=False) owner = db.relationship( "Owners", back_populates="models", foreign_keys=[owner_id]) productIDs = db.relationship( "ProductIDs", back_populates="model", foreign_keys='ProductIDs.model_id') def __repr__(self): return '並在 views.py import models 以供在 view 中使用'.format(self.name) class Owners(db.Model): # 若不寫則看 class name __tablename__ = 'owners' # 設定 primary_key id = db.Column( db.Integer, primary_key=True, autoincrement=True, nullable=False) name = db.Column(db.String(10), nullable=False) models = db.relationship( "Models", back_populates="owner", foreign_keys='Models.owner_id') def __repr__(self): return ' '.format(self.name) class ProductIDs(db.Model): # 若不寫則看 class name __tablename__ = 'productIDs' # 設定 primary_key id = db.Column( db.Integer, primary_key=True, autoincrement=True, nullable=False) name = db.Column(db.Text, nullable=False, unique=True) customer = db.Column(db.String(20), nullable=False) customerModelName = db.Column(db.String(20)) model_id = db.Column( db.Integer, db.ForeignKey('models.id'), nullable=False) model = db.relationship( "Models", back_populates="productIDs", foreign_keys=[model_id]) components = db.relationship( "Components", secondary=productIDs_components, back_populates="productIDs") documents = db.relationship( "Documents", secondary=productIDs_documents, back_populates="productIDs") histories = db.relationship( "Histories", secondary=productIDs_histories, back_populates="productIDs") def __repr__(self): return ' '.format(self.name) class Components(db.Model): # 若不寫則看 class name __tablename__ = 'components' # 設定 primary_key id = db.Column( db.Integer, primary_key=True, autoincrement=True, nullable=False) PN_number = db.Column(db.String(30), unique=True) name = db.Column(db.String(30), nullable=False) type = db.Column(db.String(30), nullable=False) vendor = db.Column(db.String(30)) remark = db.Column(db.Text) productIDs = db.relationship( "ProductIDs", secondary=productIDs_components, back_populates="components") def __repr__(self): return ' '.format(self.PN_number, self.name, self.vendor) class Documents(db.Model): # 若不寫則看 class name __tablename__ = 'documents' # 設定 primary_key id = db.Column( db.Integer, primary_key=True, autoincrement=True, nullable=False) DN_number = db.Column(db.String(9), nullable=False, unique=True) type = db.Column(db.String(30), nullable=False) stage = db.Column(db.String(10)) remark = db.Column(db.Text) productIDs = db.relationship( "ProductIDs", secondary=productIDs_documents, back_populates="documents") histories = db.relationship( "Histories", secondary=documents_histories, back_populates="documents") def __repr__(self): return ' '.format(self.DN_number, self.type, self.remark) class Histories(db.Model): # 若不寫則看 class name __tablename__ = 'histories' # 設定 primary_key id = db.Column( db.Integer, primary_key=True, autoincrement=True, nullable=False) date = db.Column(db.Date, default=datetime.datetime.now(), nullable=False) type = db.Column(db.String(30), nullable=False) PCBAs = db.Column(db.Text, nullable=False) PCBA_version = db.Column(db.Integer, nullable=False) circuitVersion = db.Column(db.Integer, nullable=False) PCBs = db.Column(db.Text) PCB_version = db.Column(db.Integer) remark = db.Column(db.Text) productIDs = db.relationship( "ProductIDs", secondary=productIDs_histories, back_populates="histories") documents = db.relationship( "Documents", secondary=documents_histories, back_populates="histories") def __repr__(self): return ' '.format(self.date, self.PCBAs, self.remark)
''' views.py ''' from flask import render_template, Blueprint from .main import app from . import models pcf = Blueprint('pcf', __name__) @pcf.route('/', methods=["GET"]) def index(): return render_template('index.html', title="總覽")初始化資料庫是必要的,除非已存在 data.db
python manage.py initDB
建立後台管理 flask_admin
Project-Control-Web │ basic_config.py │ data.db │ manage.py │ ├─instance │ debug_config.py │ product_config.py (modify) │ └─pcf │ adminView.py (new) │ main.py │ models.py │ views.py (modify) │ __init__.py │ ├─static │ style.css │ └─templates │ index.html │ layout.html (modify) │ └─admin index.html (new)
設定檔加入 SECRET_KEY ,讓內部資料能互相傳遞
''' product_config.py ''' import os appFolder = os.path.dirname(__file__) # 資料庫的路徑 SQLALCHEMY_DATABASE_URI = 'sqlite:///' \ + os.path.join(appFolder, r"../", 'data.db') # 用 os.urandom(24) 產生一組即可 SECRET_KEY = b'j\x92\xedV\xab~\xd1U#\xef\x9cp\xb0\x90\x1c]\x99\xd6\xd1\xf94\x8f\xb1\xe7'新增 adminView.py,建立後台管理
''' adminView.py ''' from flask_admin import Admin from .main import app # Create admin admin = Admin(app, 'PCF', template_mode='bootstrap3')在 views.py 加入希望顯示的 models
''' views.py ''' from flask import render_template, Blueprint from flask_admin.contrib.sqla import ModelView from flask_admin.form import SecureForm from .main import app, db from . import models from .adminView import admin pcf = Blueprint('pcf', __name__) # 定義一基本的設定,可適用於其他的 modelview class BaseModelView(ModelView): # 可用 Ajax 建立 models create_modal = False # 可用 Ajax 修改 models edit_modal = False # 可 export 資料 can_export = True # 可看詳細資料,但會看到密碼 # can_view_details = True # 加入 CSRF 驗證 form_base_class = SecureForm # 顯示所有 one to many 和 many to many 的資料 column_display_all_relations = True # 一頁顯示的個數 page_size = 50 class ModelsView(BaseModelView): # 想顯示的資料 column_list = ('JIRA', 'name', 'projectCode', 'stage', 'PM', 'APM', 'modelNameCode', 'remark', 'owner', 'productIDs') # 建立時可順便建立 one to many or many to many 的 relationship 資料 inline_models = (models.ProductIDs, ) def __init__(self, session, **kwargs): super().__init__(models.Models, session, **kwargs) admin.add_view(ModelsView(db.session)) class ProductIDsView(BaseModelView): # 想顯示的資料 column_list = ('name', 'model', 'customer', 'customerModelName', 'components', 'documents', 'histories') # 欲直接搜尋的資料 column_searchable_list = ('name', 'customerModelName') # 欲篩選的資料 column_filters = ('model', 'customer', 'components') # 建立時可順便建立 one to many or many to many 的 relationship 資料 inline_models = (models.Components, models.Histories) def __init__(self, session, **kwargs): super().__init__(models.ProductIDs, session, **kwargs) admin.add_view(ProductIDsView(db.session)) class ComponentsView(BaseModelView): # 想顯示的資料 column_list = ('PN_number', 'name', 'type', 'vendor', 'remark', 'productIDs') # 欲直接搜尋的資料 column_searchable_list = (models.Components.PN_number, models.Components.name) # 欲篩選的資料 column_filters = ('type', 'vendor') # 預設排列 column_default_sort = 'type' def __init__(self, session, **kwargs): super().__init__(models.Components, session, **kwargs) admin.add_view(ComponentsView(db.session)) class HistoriesView(BaseModelView): # 想顯示的資料 column_list = ('date', 'type', 'PCBAs', 'PCBA_version', 'circuitVersion', 'PCBs', 'PCB_version', 'remark', 'productIDs', 'documents') # 欲直接搜尋的資料 column_searchable_list = ('PCBAs', ) # 欲篩選的資料 column_filters = ('type', ) # 建立時可順便建立 one to many or many to many 的 relationship 資料 inline_models = (models.Documents, ) def __init__(self, session, **kwargs): super().__init__(models.Histories, session, **kwargs) admin.add_view(HistoriesView(db.session)) class DocumentsView(BaseModelView): # 想顯示的資料 column_list = ('DN_number', 'type', 'stage', 'remark', 'productIDs', 'histories') # 欲直接搜尋的資料 column_searchable_list = ('DN_number', ) # 欲篩選的資料 column_filters = ('type', 'stage') # 預設排列 column_default_sort = 'type' def __init__(self, session, **kwargs): super().__init__(models.Documents, session, **kwargs) admin.add_view(DocumentsView(db.session)) class OwnersView(BaseModelView): # 想顯示的資料 column_list = ('name', 'models') def __init__(self, session, **kwargs): super().__init__(models.Owners, session, **kwargs) admin.add_view(OwnersView(db.session)) @pcf.route('/', methods=["GET"]) def index(): return render_template('index.html', title="總覽")在 layout.html 加入後台管理的連結
<!doctype html> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content="Project Controler" /> <meta name="author" content="子風" /> <title>{{title}}</title> <!-- Bootstrap --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous"> <!-- custom --> <link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}"> </head> <body> <div class="container"> <nav class="navbar fixed-top navbar-expand-lg navbar-dark bg-dark"> <a class="navbar-brand" href="{{ url_for('pcf.index') }}">首頁</a> <div class="collapse navbar-collapse" id="navbarNavDropdown"> <ul class="navbar-nav"> <a class="nav-link" href="{{ url_for('admin.index') }}">Admin</a> </ul> </div> </nav> </div> <div class="container"> {% for message in get_flashed_messages() %} <div class="alert alert-primary" role="alert">{{ message }}</div> {% endfor %} </div> {% block body %}{% endblock %} {% block script %}{% endblock %} </body>在 templates 中 新建 admin 資料夾,並加入 index.html 後台管理的首頁
{% extends 'admin/master.html' %} {% block body %} {{ super() }} <div class="row-fluid"> <div> <p class="lead"> 後台管理 </p> <p> Project Control File <br/> </p> </div> <a class="btn btn-primary" href="{{ url_for('pcf.index') }}"> <i class="icon-arrow-left icon-white"></i> 回到首頁</a> </div> {% endblock body %}
使用者登入 flask_login
Project-Control-Web │ basic_config.py │ data.db │ manage.py (modify) │ ├─instance │ debug_config.py │ product_config.py │ └─pcf │ adminView.py (modify) │ main.py │ models.py (modify) │ views.py (modify) │ __init__.py │ ├─static │ style.css │ └─templates │ index.html │ layout.html │ └─admin dropdown.html (new) index.html (modify)
models.py 加入 Users
''' models.py ''' import datetime from flask_sqlalchemy import SQLAlchemy from .main import db productIDs_components = db.Table('productIDs_components', db.Model.metadata, db.Column('productIDs_id', db.Integer, db.ForeignKey('productIDs.id')), db.Column('components_id', db.Integer, db.ForeignKey('components.id'))) productIDs_documents = db.Table('productIDs_documents', db.Model.metadata, db.Column('productIDs_id', db.Integer, db.ForeignKey('productIDs.id')), db.Column('documents_id', db.Integer, db.ForeignKey('documents.id'))) productIDs_histories = db.Table('productIDs_histories', db.Model.metadata, db.Column('productIDs_id', db.Integer, db.ForeignKey('productIDs.id')), db.Column('histories_id', db.Integer, db.ForeignKey('histories.id'))) documents_histories = db.Table('documents_histories', db.Model.metadata, db.Column('documents_id', db.Integer, db.ForeignKey('documents.id')), db.Column('histories_id', db.Integer, db.ForeignKey('histories.id'))) class Models(db.Model): # 若不寫則看 class name __tablename__ = 'models' # 設定 primary_key id = db.Column( db.Integer, primary_key=True, autoincrement=True, nullable=False) JIRA = db.Column(db.String(20)) name = db.Column(db.String(20), nullable=False) projectCode = db.Column(db.String(20)) stage = db.Column(db.String(5), nullable=False) PM = db.Column(db.String(4), nullable=False) APM = db.Column(db.String(4), nullable=False) modelNameCode = db.Column(db.String(20)) remark = db.Column(db.Text) owner_id = db.Column( db.Integer, db.ForeignKey('owners.id'), nullable=False) owner = db.relationship( "Owners", back_populates="models", foreign_keys=[owner_id]) productIDs = db.relationship( "ProductIDs", back_populates="model", foreign_keys='ProductIDs.model_id') def __repr__(self): return 'adminView.py 加入登入、驗證、註冊機制'.format(self.name) class Owners(db.Model): # 若不寫則看 class name __tablename__ = 'owners' # 設定 primary_key id = db.Column( db.Integer, primary_key=True, autoincrement=True, nullable=False) name = db.Column(db.String(10), nullable=False) models = db.relationship( "Models", back_populates="owner", foreign_keys='Models.owner_id') def __repr__(self): return ' '.format(self.name) class ProductIDs(db.Model): # 若不寫則看 class name __tablename__ = 'productIDs' # 設定 primary_key id = db.Column( db.Integer, primary_key=True, autoincrement=True, nullable=False) name = db.Column(db.Text, nullable=False, unique=True) customer = db.Column(db.String(20), nullable=False) customerModelName = db.Column(db.String(20)) model_id = db.Column( db.Integer, db.ForeignKey('models.id'), nullable=False) model = db.relationship( "Models", back_populates="productIDs", foreign_keys=[model_id]) components = db.relationship( "Components", secondary=productIDs_components, back_populates="productIDs") documents = db.relationship( "Documents", secondary=productIDs_documents, back_populates="productIDs") histories = db.relationship( "Histories", secondary=productIDs_histories, back_populates="productIDs") def __repr__(self): return ' '.format(self.name) class Components(db.Model): # 若不寫則看 class name __tablename__ = 'components' # 設定 primary_key id = db.Column( db.Integer, primary_key=True, autoincrement=True, nullable=False) PN_number = db.Column(db.String(30), unique=True) name = db.Column(db.String(30), nullable=False) type = db.Column(db.String(30), nullable=False) vendor = db.Column(db.String(30)) remark = db.Column(db.Text) productIDs = db.relationship( "ProductIDs", secondary=productIDs_components, back_populates="components") def __repr__(self): return ' '.format(self.PN_number, self.name, self.vendor) class Documents(db.Model): # 若不寫則看 class name __tablename__ = 'documents' # 設定 primary_key id = db.Column( db.Integer, primary_key=True, autoincrement=True, nullable=False) DN_number = db.Column(db.String(9), nullable=False, unique=True) type = db.Column(db.String(30), nullable=False) stage = db.Column(db.String(10)) remark = db.Column(db.Text) productIDs = db.relationship( "ProductIDs", secondary=productIDs_documents, back_populates="documents") histories = db.relationship( "Histories", secondary=documents_histories, back_populates="documents") def __repr__(self): return ' '.format(self.DN_number, self.type, self.remark) class Histories(db.Model): # 若不寫則看 class name __tablename__ = 'histories' # 設定 primary_key id = db.Column( db.Integer, primary_key=True, autoincrement=True, nullable=False) date = db.Column(db.Date, default=datetime.datetime.now(), nullable=False) type = db.Column(db.String(30), nullable=False) PCBAs = db.Column(db.Text, nullable=False) PCBA_version = db.Column(db.Integer, nullable=False) circuitVersion = db.Column(db.Integer, nullable=False) PCBs = db.Column(db.Text) PCB_version = db.Column(db.Integer) remark = db.Column(db.Text) productIDs = db.relationship( "ProductIDs", secondary=productIDs_histories, back_populates="histories") documents = db.relationship( "Documents", secondary=documents_histories, back_populates="histories") def __repr__(self): return ' '.format(self.date, self.PCBAs, self.remark) # Create user model. class Users(db.Model): # 若不寫則看 class name __tablename__ = 'users' # 設定 primary_key id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(10)) userName = db.Column(db.String(80), unique=True) email = db.Column(db.String(120)) password = db.Column(db.String(64)) # Flask-Login integration def is_authenticated(self): return True def is_active(self): return True def is_anonymous(self): return False def get_id(self): return self.id def __repr__(self): return ' '.format(self.name, self.userName)
''' adminView.py ''' from flask import url_for, redirect, render_template, request from flask_admin import Admin, AdminIndexView, helpers, expose from wtforms import form, fields, validators from flask_login import LoginManager, current_user, login_user, logout_user from werkzeug.security import generate_password_hash, check_password_hash from .main import app, db from . import models # Define login and registration forms (for flask-login) class LoginForm(form.Form): userName = fields.TextField(validators=[validators.required()]) password = fields.PasswordField(validators=[validators.required()]) def validate_login(self, field): user = self.get_user() if user is None: raise validators.ValidationError('Invalid user') # we're comparing the plaintext pw with the the hash from the db if not check_password_hash(user.password, self.password.data): # to compare plain text passwords use # if user.password != self.password.data: raise validators.ValidationError('Invalid password') def get_user(self): return db.session.query( models.Users).filter_by(userName=self.userName.data).first() class RegistrationForm(form.Form): name = fields.TextField() userName = fields.TextField(validators=[validators.required()]) email = fields.TextField() password = fields.PasswordField('New Password', [ validators.DataRequired(), validators.EqualTo('confirm', message='Passwords must match') ]) confirm = fields.PasswordField('Repeat Password') def validate_login(self, field): if db.session.query(models.Users).filter_by( userName=self.userName.data).count() > 0: raise validators.ValidationError('Duplicate username') # Initialize flask-login def init_login(): login_manager = LoginManager() login_manager.init_app(app) # Create user loader function @login_manager.user_loader def load_user(user_id): return db.session.query(models.Users).get(user_id) # Create customized index view class that handles login & registration class MyAdminIndexView(AdminIndexView): @expose('/') def index(self): if not current_user.is_authenticated: return redirect(url_for('.login_view')) return super(MyAdminIndexView, self).index() @expose('/login/', methods=('GET', 'POST')) def login_view(self): # handle user login form = LoginForm(request.form) if helpers.validate_form_on_submit(form): user = form.get_user() if user: login_user(user) if current_user.is_authenticated: return redirect(url_for('.index')) link = 'Don\'t have an account? Click here to register. ' self._template_args['form'] = form self._template_args['link'] = link return super(MyAdminIndexView, self).index() @expose('/register/', methods=('GET', 'POST')) def register_view(self): form = RegistrationForm(request.form) if helpers.validate_form_on_submit(form): user = models.Users() form.populate_obj(user) # we hash the users password to avoid saving it as plaintext in the db, # remove to use plain text: user.password = generate_password_hash(form.password.data) db.session.add(user) db.session.commit() login_user(user) return redirect(url_for('.index')) link = 'Already have an account? Click here to log in. ' self._template_args['form'] = form self._template_args['link'] = link return super(MyAdminIndexView, self).index() @expose('/logout/') def logout_view(self): logout_user() return redirect(url_for('.index')) # Initialize flask-login init_login() # Create admin admin = Admin( app, 'PCF', index_view=MyAdminIndexView(), base_template='admin/dropdown.html', template_mode='bootstrap3')在 views.py 加入驗證機制,並增加 Users 顯示
''' views.py ''' from flask import render_template, Blueprint from flask_admin.contrib.sqla import ModelView from flask_admin.form import SecureForm import flask_login from wtforms import PasswordField from werkzeug.security import generate_password_hash from .main import app, db from . import models from .adminView import admin pcf = Blueprint('pcf', __name__) # 定義一基本的設定,可適用於其他的 modelview class BaseModelView(ModelView): # 可用 Ajax 建立 models create_modal = False # 可用 Ajax 修改 models edit_modal = False # 可 export 資料 can_export = True # 可看詳細資料,但會看到密碼 # can_view_details = True # 加入 CSRF 驗證 form_base_class = SecureForm # 顯示所有 one to many 和 many to many 的資料 column_display_all_relations = True # 一頁顯示的個數 page_size = 50 def is_accessible(self): return flask_login.current_user.is_authenticated class ModelsView(BaseModelView): # 想顯示的資料 column_list = ('JIRA', 'name', 'projectCode', 'stage', 'PM', 'APM', 'modelNameCode', 'remark', 'owner', 'productIDs') # 建立時可順便建立 one to many or many to many 的 relationship 資料 inline_models = (models.ProductIDs, ) def __init__(self, session, **kwargs): super().__init__(models.Models, session, **kwargs) admin.add_view(ModelsView(db.session)) class ProductIDsView(BaseModelView): # 想顯示的資料 column_list = ('name', 'model', 'customer', 'customerModelName', 'components', 'documents', 'histories') # 欲直接搜尋的資料 column_searchable_list = ('name', 'customerModelName') # 欲篩選的資料 column_filters = ('model', 'customer', 'components') # 建立時可順便建立 one to many or many to many 的 relationship 資料 inline_models = (models.Components, models.Histories) def __init__(self, session, **kwargs): super().__init__(models.ProductIDs, session, **kwargs) admin.add_view(ProductIDsView(db.session)) class ComponentsView(BaseModelView): # 想顯示的資料 column_list = ('PN_number', 'name', 'type', 'vendor', 'remark', 'productIDs') # 欲直接搜尋的資料 column_searchable_list = (models.Components.PN_number, models.Components.name) # 欲篩選的資料 column_filters = ('type', 'vendor') # 預設排列 column_default_sort = 'type' def __init__(self, session, **kwargs): super().__init__(models.Components, session, **kwargs) admin.add_view(ComponentsView(db.session)) class HistoriesView(BaseModelView): # 想顯示的資料 column_list = ('date', 'type', 'PCBAs', 'PCBA_version', 'circuitVersion', 'PCBs', 'PCB_version', 'remark', 'productIDs', 'documents') # 欲直接搜尋的資料 column_searchable_list = ('PCBAs', ) # 欲篩選的資料 column_filters = ('type', ) # 建立時可順便建立 one to many or many to many 的 relationship 資料 inline_models = (models.Documents, ) def __init__(self, session, **kwargs): super().__init__(models.Histories, session, **kwargs) admin.add_view(HistoriesView(db.session)) class DocumentsView(BaseModelView): # 想顯示的資料 column_list = ('DN_number', 'type', 'stage', 'remark', 'productIDs', 'histories') # 欲直接搜尋的資料 column_searchable_list = ('DN_number', ) # 欲篩選的資料 column_filters = ('type', 'stage') # 預設排列 column_default_sort = 'type' def __init__(self, session, **kwargs): super().__init__(models.Documents, session, **kwargs) admin.add_view(DocumentsView(db.session)) class OwnersView(BaseModelView): # 想顯示的資料 column_list = ('name', 'models') def __init__(self, session, **kwargs): super().__init__(models.Owners, session, **kwargs) admin.add_view(OwnersView(db.session)) class UsersView(BaseModelView): # 想顯示的資料 column_list = ('name', 'userName', 'email') # 另外新建表格欄位,以免密碼可見 form_extra_fields = {'password': PasswordField('Password')} def __init__(self, session, **kwargs): super().__init__(models.Users, session, **kwargs) # 變更時,重新加密密碼 def on_model_change(self, form, User, is_created): if form.password.data is not None: User.password = generate_password_hash(form.password.data) admin.add_view(UsersView(db.session)) @pcf.route('/', methods=["GET"]) def index(): return render_template('index.html', title="總覽")amdin 的 index.html 加入登入、驗證、註冊等連結
{% extends 'admin/master.html' %} {% block body %} {{ super() }} <div class="row-fluid"> <div> {% if current_user.is_authenticated %} <p class="lead"> 後台管理 </p> <p> Project Control File<br/> {{ current_user }} </p> {% else %} <form method="POST" action=""> {{ form.hidden_tag() if form.hidden_tag }} {% for f in form if f.type != 'CSRFTokenField' %} <div> {{ f.label }} {{ f }} {% if f.errors %} <ul> {% for e in f.errors %} <li>{{ e }}</li> {% endfor %} </ul> {% endif %} </div> {% endfor %} <button class="btn" type="submit">Submit</button> </form> {{ link | safe }} {% endif %} </div> <a class="btn btn-primary" href="/"><i class="icon-arrow-left icon-white"></i> 回到首頁</a> </div> {% endblock body %}新增 dropdown.html,可供使用者登出,與顯示目前登入的使用者
{% extends 'admin/base.html' %} {% block access_control %} {% if current_user.is_authenticated %} <div class="btn-group pull-right"> <a class="btn dropdown-toggle" data-toggle="dropdown" href="#"> <i class="icon-user"></i> {{ current_user.name }} <span class="caret"></span> </a> <ul class="dropdown-menu"> <li><a href="{{ url_for('admin.logout_view') }}">Log out</a></li> </ul> </div> {% endif %} {% endblock %}manage.py 新增 cli 指令 updateDB
''' manage.py ''' from flask_script import Manager, prompt_bool from pcf import app, db manager = Manager(app) @manager.command def initDB(): """初始化資料庫""" if prompt_bool("將失去所有資料,確定嗎?"): db.drop_all() db.create_all() @manager.command def updateDB(): """建立不存在的 table""" db.create_all() if __name__ == '__main__': manager.run()更新資料庫是必要的,因要新增 Users,此舉不會破壞原本的資料
python manage.py updateDB
首頁顯示資料 pandas
Project-Control-Web │ basic_config.py │ data.db │ manage.py │ ├─instance │ debug_config.py │ product_config.py │ └─pcf │ adminView.py │ main.py │ models.py │ tools.py (new) │ views.py (modify) │ __init__.py │ ├─static │ style.css │ └─templates │ index.html (modify) │ layout.html │ └─admin dropdown.html index.html
因 pandas 處理資料較為方便,故新增 tools.py,內有 sql to pandas 的 fucntion
''' tools.py ''' import pandas as pd # sql to pandas def sqltoDF(query): return pd.read_sql(query.statement, query.session.bind)讓 views.py 的 index 可顯示資料
''' views.py ''' from flask import render_template, Blueprint from flask_admin.contrib.sqla import ModelView from flask_admin.form import SecureForm import flask_login from wtforms import PasswordField from werkzeug.security import generate_password_hash from .main import app, db from . import models from .adminView import admin from .tools import * pcf = Blueprint('pcf', __name__) # 定義一基本的設定,可適用於其他的 modelview class BaseModelView(ModelView): # 可用 Ajax 建立 models create_modal = False # 可用 Ajax 修改 models edit_modal = False # 可 export 資料 can_export = True # 可看詳細資料,但會看到密碼 # can_view_details = True # 加入 CSRF 驗證 form_base_class = SecureForm # 顯示所有 one to many 和 many to many 的資料 column_display_all_relations = True # 一頁顯示的個數 page_size = 50 def is_accessible(self): return flask_login.current_user.is_authenticated class ModelsView(BaseModelView): # 想顯示的資料 column_list = ('JIRA', 'name', 'projectCode', 'stage', 'PM', 'APM', 'modelNameCode', 'remark', 'owner', 'productIDs') # 建立時可順便建立 one to many or many to many 的 relationship 資料 inline_models = (models.ProductIDs, ) def __init__(self, session, **kwargs): super().__init__(models.Models, session, **kwargs) admin.add_view(ModelsView(db.session)) class ProductIDsView(BaseModelView): # 想顯示的資料 column_list = ('name', 'model', 'customer', 'customerModelName', 'components', 'documents', 'histories') # 欲直接搜尋的資料 column_searchable_list = ('name', 'customerModelName') # 欲篩選的資料 column_filters = ('model', 'customer', 'components') # 建立時可順便建立 one to many or many to many 的 relationship 資料 inline_models = (models.Components, models.Histories) def __init__(self, session, **kwargs): super().__init__(models.ProductIDs, session, **kwargs) admin.add_view(ProductIDsView(db.session)) class ComponentsView(BaseModelView): # 想顯示的資料 column_list = ('PN_number', 'name', 'type', 'vendor', 'remark', 'productIDs') # 欲直接搜尋的資料 column_searchable_list = (models.Components.PN_number, models.Components.name) # 欲篩選的資料 column_filters = ('type', 'vendor') # 預設排列 column_default_sort = 'type' def __init__(self, session, **kwargs): super().__init__(models.Components, session, **kwargs) admin.add_view(ComponentsView(db.session)) class HistoriesView(BaseModelView): # 想顯示的資料 column_list = ('date', 'type', 'PCBAs', 'PCBA_version', 'circuitVersion', 'PCBs', 'PCB_version', 'remark', 'productIDs', 'documents') # 欲直接搜尋的資料 column_searchable_list = ('PCBAs', ) # 欲篩選的資料 column_filters = ('type', ) # 建立時可順便建立 one to many or many to many 的 relationship 資料 inline_models = (models.Documents, ) def __init__(self, session, **kwargs): super().__init__(models.Histories, session, **kwargs) admin.add_view(HistoriesView(db.session)) class DocumentsView(BaseModelView): # 想顯示的資料 column_list = ('DN_number', 'type', 'stage', 'remark', 'productIDs', 'histories') # 欲直接搜尋的資料 column_searchable_list = ('DN_number', ) # 欲篩選的資料 column_filters = ('type', 'stage') # 預設排列 column_default_sort = 'type' def __init__(self, session, **kwargs): super().__init__(models.Documents, session, **kwargs) admin.add_view(DocumentsView(db.session)) class OwnersView(BaseModelView): # 想顯示的資料 column_list = ('name', 'models') def __init__(self, session, **kwargs): super().__init__(models.Owners, session, **kwargs) admin.add_view(OwnersView(db.session)) class UsersView(BaseModelView): # 想顯示的資料 column_list = ('name', 'userName', 'email') # 另外新建表格欄位,以免密碼可見 form_extra_fields = {'password': PasswordField('Password')} def __init__(self, session, **kwargs): super().__init__(models.Users, session, **kwargs) # 變更時,重新加密密碼 def on_model_change(self, form, User, is_created): if form.password.data is not None: User.password = generate_password_hash(form.password.data) admin.add_view(UsersView(db.session)) @pcf.route('/', methods=["GET"]) def index(): user = flask_login.current_user query = db.session.query(models.Models, models.ProductIDs, models.Owners) \ .filter(models.Owners.name==user.name) \ .filter(models.Models.id==models.Owners.id) \ .outerjoin(models.ProductIDs, models.Models.id==models.ProductIDs.model_id) \ .with_labels() df = sqltoDF(query) df = df[['models_name', 'models_JIRA', 'models_stage', 'models_PM', 'models_APM', 'models_projectCode', 'models_modelNameCode', 'models_remark', 'owners_name', 'productIDs_name', 'productIDs_customer', 'productIDs_customerModelName']] return render_template('index.html', df=df, title="總覽")index.html 加入對應的程式碼
{% extends "layout.html" %} {% block body %} <div> <div class="flexbox-table table-dark"> <div class='thead'> <div class='tr'> <div class='th' style="flex: 2;">JIRA</div> <div class='th' style="flex: 2;">Model Name</div> <div class='th' style="flex: 2;">Customer<br/>Model Name</div> <div class='th' style="flex: 2;">Product ID</div> <div class='th' style="flex: 1;">Project Code</div> <div class='th' style="flex: 1;">Stage</div> <div class='th' style="flex: 1;">EE Owner</div> <div class='th' style="flex: 1;">PM</div> <div class='th' style="flex: 1;">APM</div> <div class='th' style="flex: 1;">Customer</div> <div class='th' style="flex: 1;">Model Name Code</div> <div class='th' style="flex: 4;">Remark</div> </div> </div> {% for index, value in df.iterrows() %} <div class='tbody'> <div class='tr'> <div class='td' style="flex: 2;">{{ value['models_JIRA'] }}</div> <div class='td' style="flex: 2;">{{ value['models_name'] }}</div> <div class='td' style="flex: 2;">{{ value['productIDs_customerModelName'] }}</div> <div class='td' style="flex: 2;">{{ value['productIDs_name'] }}</div> <div class='td' style="flex: 1;">{{ value['models_projectCode'] }}</div> <div class='td' style="flex: 1;">{{ value['models_stage'] }}</div> <div class='td' style="flex: 1;">{{ value['owners_name'] }}</div> <div class='td' style="flex: 1;">{{ value['models_PM'] }}</div> <div class='td' style="flex: 1;">{{ value['models_APM'] }}</div> <div class='td' style="flex: 1;">{{ value['productIDs_customer'] }}</div> <div class='td' style="flex: 1;">{{ value['models_modelNameCode'] }}</div> <div class='td' style="flex: 4;">{{ value['models_remark'] }}</div> </div> </div> {% endfor %} </div> </div> {% endblock %}
留言
張貼留言