Flask & Vue. Друга серія прикладів


01.05.2021

Вітаю. Починаю наступну, другу серію прикладів Flask + Vue. У цій серії будуть наступні статті:

Попередження. Все, що я буду куралесити на бекенді (мається на увазі до прикладу з SQLAlchemy), то так вже ніхто не робить. Цю "дичину" я буду творити тільки щоб показати як працює фронтенд, показати як воно працює у нутрях бекенду, і чому не треба морочити собі...голову, а використовувати вже стандартні готові рішення. Поки що будемо страждати збоченнями на бекенді і виписувати фронтенд. А потім ми перепишемо бекенд на сучасні рівні рейки, а вже в наступній серії переробимо фронтенд щоб він збирався по модньому. І тоді, я так думаю, Ви відчуєте різницю і зрозумієте чому зараз роблять саме так як це буде у наступній серії прикладів. А поки що починаємо другу серію прикладів з неофіційною назвою "збочення з бекендом".

Нагадаю, що попередня серія прикладів на тему Vue + трішечки Flask була присвячена побудові структур додатків. Ми зробили простий додаток: backend - на Flask, frontend - на Vue. У якості бази даних використовували SQLite. Для Flask побудували структуру для простого і швидкого додавання нових об'єктів даних. Для Vue створили комплекс компонент для роботи з даними. А саме: таблицю (standard-table), поля (form-field), форму (standard-form), і стандартну сторінку (standard-page) для перегляду і редагування даних. Компоненту standard-page можна вказати які саме дані використовувати з тих, що описані у const appDataset, і компонент буде будувати таблицю з даними, формую для редагування, додавання, видалення даних. Які поля будуть у таблиці, а які у формі теж описується у const appDataset.

Тобто, ми зробили компонент standard-page для того, щоб кожного разу не виписувати код для редагування наступної таблички у базі, а використовувати готові компоненти. "standard" у назві компоненти натякає на те, що це стандартна компонента і її зовнішній вигляд і функціонал буде дуже схожий для усіх даних, до яких ми будемо її застосовувати. Тобто, зовнішній вигляд усіх таблиць і форм суворо стандартизований. Нехай трохи йому не вистачає різноманітності, але він незмінний. У такому випадку, завдяки стандартній компоненті standard-page просто вказуємо які дані ми хочемо редагувати і все. Ми вже не програмуємо, а збираємо додаток з готових, стандартних блоків. Якщо Вам потрібно відійти від стандартів - це не проблема. Можна написати свою компоненту і використовувати, скажімо, компоненти standard-form, standard-table. Тобто, використовувати трохи дрібніші блоки. Якщо і форма не підходить - робимо свою форму і використовуємо ще більш дрібніші компоненти, наприклад компонент form-field. І так далі. Але це потребує значно більше часу. Тому стандартизація значно прискорює розробку.

У минулій серії прикладів ми зупинилися на простих формах з "простими" даними.

Задача на сьогодні:

  • Створити у базі даних нову табличку "COUNTRIES" (довідник країн), додати у таблицю "CLIENT" поле "country_id"
  • Додати у web-додаток можливість редагувати довідник країн.
  • Додати у форму редагування Клієнта можливість вказувати країну зі списку. Список має містити перелік країн.

Цей приклад - є продовженням попереднього прикладу Vue + Flask. Ви можете завантажити попередній приклад Flask + Vue і виконати всі дії, описані у статті власноруч. Або завантажити готовий приклад Flask + Vue.

Модифікація Бази даних

Запускаємо скрипт modifyDB.py - цей файл містить мінімально необхідні SQL для модифікації бази даних і наповнення нової таблички тестовими даними.

modifyDB.py:


import sqlite3

conn = sqlite3.connect('myApp.db')
cur = conn.cursor()

print ("Opened database successfully")

conn.execute('''CREATE TABLE COUNTRIES
	(id		INTEGER PRIMARY KEY AUTOINCREMENT    NOT NULL,
	name		TEXT,
	code		TEXT,
	flag_img	TEXT
	);''')

print ("Table created successfully")

#############################
# Insert Data into table COUNTRIES
#############################

countries = [
        {'id':1, 'name': 'Ukraine', 'code':'380', 'flag_img': 'ua.png'},
	{'id':2, 'name': 'USA', 'code':'380', 'flag_img': 'us.png'},
	{'id':3, 'name': 'France', 'code':'33', 'flag_img': 'fr.png'}
]

for item in countries:
	SQL = 'INSERT INTO COUNTRIES (id, name, code, flag_img) VALUES({0}, "{1}", "{2}", "{3}")'.format(item['id'], item['name'], item['code'], item['flag_img'])
	cur.execute(SQL)

print ("Table 'COUNTRIES' is done")


conn.execute('''ALTER TABLE CLIENT ADD COLUMN country_id INTEGER;''')
print ("Table CLIENT modified successfully")

conn.execute('''CREATE INDEX IF NOT EXISTS client_country_id ON CLIENT (country_id)''')
print ("Index created successfully")

conn.commit()

print ("Commit is done")

conn.close()

print ("Database is closed")
print ("Good luck!")

Модифікація Backend

Треба додати файл countries.py в директорію model

Файл countries.py містить наступне:


from model import default

class model(default.model):
	def __init__(self, connection):
		self.connection = connection
		self.objName = 'COUNTRIES'

У файлі app_db.py додати "countries" у рядок:


from model import empty, menu, client, seller, product, countries

у цьому ж файлі додати рядок:


self.model['countries'] = product.model(self.connection)

Все! База даних і Backend готовий!

Frontend

Створюємо редактор довідника країн.

В const appDataset описуємо "countries" - посилання на backend, описуємо поля, які потрібно показати у таблиці, і які у формі:


  'countries': {
    'instance': 'countries',
    'url': 'http://localhost:5000/countries/',
    'fields': {
      'table': [
        {name:'name', 'title': 'Name', type:'string', sort: true},
        {name:'flag_img', 'title': 'Flag', type:'string', sort: false},
      ],
      'form': [
        {name:'name', 'title': 'Name', type:'string', required:true},
        {name:'code', 'title': 'Code', type:'string', required:true},
        {name:'flag_img', 'title': 'Flag', type:'string' required:false},
    ]
    }
  }

в роутах додамо рядок:


  { path: '/countries', component: { template: '' } },

в menu.json додамо:


            {
                "item": "Countries",
                "path": "#/countries"
            },

Нагадаю: menu віддає backend. Ми його запхали у json винятково для демонстрації гнучкості і універсальності підходу.

Все! Редактор на Frontend готовий! От і все! Ми нічого нового не програмували! Ми використали раніше написаний компонент standard-page і лише вказали нові дані і у якій таблиці і формі їх показувати.

Поїхали далі

Редактор довідника країн готовий. Тепер додамо список (select) у форму редагування клієнта.

Основний підхід такий - основні дані (клієнта) завантажуються, як і раніше. Додаткові дані, (список країн) завантажуються окремо. При чому всі дані завантажує основний компонент. Це важливо! (щось не ясно - дивись попередні статті). Не треба функцію завантаження даних перекладати на компонент списку. Він тупо список - його робота просто показати, і коли щось змінилось повернути event. Що робити з даними - це клопіт основного компонента.

1. Міксин "crud_ext"

Новий функціонал завантаження додаткових даних зробимо у окремому міксині "crud_ext". Цей міксин ми потім будемо додавати при створенні компонентів.

Міксин "crud_ext":


var crud_ext = {
  computed: {
    form_fields: function () {
      let fields = appDataset[this.instance]['fields']['form']
      for (field of fields) {

        // Prepare @select fields
        if (field['type'] == 'select') {
          field.items = []
          if (field['dataset']) {
            if (field['dataset']['src']) {
              if (this[field['dataset']['src']]) {
                let src = this[field['dataset']['src']]
                for (item of src) {
                  field.items.push({value: item[field['dataset']['value']], caption: item[field['dataset']['caption']]})
                }
              }
            }
          }
        }

      }
      return fields
    }
  },
  methods: {
    read_ext_data: function(instance) {
      let url = appDataset[instance]['url']
      let options = {method: 'GET'}
      this.fetch_execute(url, options,
        (response)=>{
          this.$set(this, instance, response)
        },
        (response)=>{
          this.show_error(response.errors)
        }
      )
    }
  }
}

В ньому є метод read_ext_data, який і завантажує додаткові дані, та computed поле form_fields яке обробляє і готує дані для форми на основі даних вказаних в описі instance (див. const appDataset) і завантажених додаткових даних. Зараз ми навчили form_fields готувати дані тільки для полів типу "select", але у подальшому його можна розширювати для інших типів. І робити це доведеться у одному місці - тут у міксині crud_ext.

2. Доробили компонент standard-form

Трохи доробили компонент standard-form (довелося додати свою data для забеспечення інтерактивності (щоб списки могли динамічно оновлюватися, коли в цьому виникне потреба)).

3. Доробили компонент client-edit

Компонент client-edit відповідає за редагування клієнта (дивись попередні приклади і статті)

В опис компонента client-edit додаємо створений міксин "crud_ext":


  mixins: [crud, crud_ext],

додаємо дані "countries:


data : function () {
    return {
      countries: this.countries
    }
  },

у "mounted" додаємо виклик метода завантаження додаткових даних:


  ...
  this.read_ext_data('countries')
  ...

4. Опис поля Country

Опис поля select для 'Country' тепер виглядає так:


  {name:'country_id', 'title': 'Country', type:'select', dataset: {src: 'countries', value: 'id', caption: 'name'}},

  • src - назва instance, звідки завантажувати дані
  • value - поле, яке використовується, як значення
  • caption - поле, яке використовується, як надпис у списку

Ми щойно розглянути, як доробити свою власну компоненту "client-edit". Ми її робили для того, щоб під час редагування був окремий роут (окремий URL). Але в нас є ще і універсальний компонент "standard-page", який показує таблицю з даними, форму для додавання / редагування даних і т.п. Треба і її навчити працювати з випадаючими списками.

Доробка standard-page

1. Добавили mixin crud_ext:


mixins: [crud, crud_front, crud_ext],

2. Модифікували "data", щоб зберігалася інтерактивність і в завантажених додатково даних


  data: function () {
    let obj = {
        current_row: this.current_row,
        form_data: this.form_data
    }
    if (this.datasets) {
      for (ext_data of this.datasets) {
        obj[ext_data] = this[ext_data]
      }
    }
    return obj
  },

3. У "mounted" для завантаження додаткових даних додали наступний код

Перелік (це масив) необхідних додаткових даних передається параметром "datasets":


    if (this.datasets) {
      this.datasets_array = []
      for (ext_data of this.datasets) {
        console.log(ext_data)
        this.read_ext_data(ext_data)
        this.datasets_array.push(ext_data)
      }
    }

4. В роутах опис стандартної сторінки виглядає так


{ path: '/client_old', component: { template: '' } },

Ми додали параметр datasets і вказали в ньому які додаткові дані треба завантажити.

Висновок

Раніше побудований CRUD - підхід на бекенді і на фронтенді дозволив швидко зробити новий об'єкт даних і створити форму для його редагування практично не програмуючи нічого нового. Вся пророблена сьогодні робота стосується тільки нового функціоналу. Звісно, це стосується досить простих, стандартних даних. Зі складними все цікавіше і доводиться дещо робити руками. Але саме для того, щоб позбутися рутини у своїй роботі і займатися виключно новими і цікавими речами і використовують сучасні практики.

P.S. У наступній статті: пагінація - взаємодія з сервером; що не так з компонентою standard-table?; як зробити правильно.

Як запустити приклад?

  1. Завантажити архів з прикладом, розпакувати
  2. Запустити приклад командою:
    python ./my_app.py
    або
    python3 ./my_app.py
  3. У браузері відкрити посилання: http://localhost:5000/

Дивись також:

Web-dev склерозник
Коментарі:
Додати коментар
Code
* - обов'язкові поля

Архіви