iT邦幫忙

2022 iThome 鐵人賽

DAY 14
0

Day 14 貼上照片牆,Django 多租戶圖片上傳

能有一面照片牆是非常美好的事情,但若要和不認識的人共用就會覺得很尷尬!因此讓每個租戶擁有自己獨立的照片牆是我們這次的要實現的目標。

本章節我們將要來實作圖片上傳功能,並且在多租戶架構下將每個租戶上傳的圖片進行區隔,讓我們開始吧!

設定上傳環境

首先我們要在主設定擋 settings.py 進行設定,

調整 context_processors

# main/settings.py

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                # ...
                "django.template.context_processors.media",
                # ...
            ],
        },
    },
]

設定上傳目錄路徑

# main/settings.py

# ... 

MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

MEDIA_URL = '/media/'

在 urls.py 新增 media URL


# main/urls.py

from django.conf.urls.static import static

urlpatterns = [
    # ...
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

# 新增 media
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

新增跨應用的共用應用程式 core

隨著進行開發,可能會編寫一些能重複使用的程式,例如在 A 與 B 兩個應用程式同時使用到,如果沒有明確定義好共用程式擺放的位置會很難在需要時找到它,或是將這些程式散落在專案根目錄也會顯得非常雜亂,因此我們將建立一個專門放置共用程式的應用程式 —— core

使用 startapp 指令新增 core 應用程式

docker exec --workdir /opt/app/web example_tenant_web \
python3.10 manage.py startapp core

新增 helpers.py 放置輔助函數

docker exec --workdir /opt/app/web/core example_tenant_web \
touch helpers.py

多租戶下的圖片上傳路徑

為了讓每個租戶有獨自的照片牆,我們要在上傳圖片時做一些小變化。
在 media 目錄下我們會再有兩個目錄層級,media/租戶目錄名稱_media/模型目錄名稱
例如 example01 的租戶在 ProductCategory 模型上傳了一張 image.png 圖片,則我們的圖片路徑就會是:

/media/example01/productcategory_media/image.png

而最後避免重複檔名會被覆蓋,要再替檔名加上當前時間

/media/example01/productcategory_media/image11664204841.png

我們編寫一個 upload_handle 函數,透過 connection.schema_name 取得當前的 schema 來判斷租戶,根據傳入的 intance 判斷 model 的名稱。
當該層級沒有對應的目錄名稱時就會進行建立,若以建立則會對檔名進行時間加工,最後組合出 url 並返回。

完整程式碼如下:

# core/helpers.py

import os, time

from django.conf import settings
from django.db import connection

def upload_handle(instance, filename):
    schema_dir = settings.MEDIA_ROOT + '/' + connection.schema_name
    if not os.path.exists(schema_dir):
        os.mkdir(schema_dir)
    dir = schema_dir + '/' + instance.__class__.__name__.lower() + '_media'
    if not os.path.exists(dir):
        os.mkdir(dir)
    name = filename.find('.') and '%s' % os.path.basename(filename).split('.', 1)[0] or ''
    ext = filename.find('.') and '.%s' % os.path.basename(filename).split('.', 1)[1] or ''
    return '/'.join([connection.schema_name, instance.__class__.__name__.lower() + '_media', name + str(int(time.time())) + ext])

新增圖片模型與欄位

商品的圖片可能會有不只一張,所以我們需要為商品圖片建立一個模型進行關聯

# products/models.py

class ProductImage(models.Model):
    name = models.CharField('商品圖片說明', max_length=50)
    product = models.ForeignKey('products.Product', on_delete=models.CASCADE, related_name='product_image_set')
    image = models.ImageField("圖片", null=True, blank=True, upload_to=core_helpers.upload_handle)
    order = models.PositiveIntegerField(null=True, blank=True)

    class Meta:
        verbose_name = '商品圖片'
        verbose_name_plural = '商品圖片'
        ordering = ['order']

    def __str__(self):
        return f'{self.name}'

商品類型的圖片只需要一張,我們在 ProductCategory 模型新增一個 ImageField 欄位,upload_to 指定我們寫好的 upload_handle 輔助函數

# products/models.py

class ProductCategory(models.Model):
    # ...
    image = models.ImageField("圖片", null=True, blank=True, upload_to=core_helpers.upload_handle)

    class Meta:
        # ...

對模型進行資料庫遷移

docker exec --workdir /opt/app/web example_tenant_web \
python3.10 manage.py makemigrations

...

ERRORS:
products.ProductCategory.image: (fields.E210) Cannot use ImageField because Pillow is not installed.
        HINT: Get Pillow at https://pypi.org/project/Pillow/ or run command "python -m pip install Pillow".

竟然出現了 ERROR!這是為何呢?因為 ImageField 需要安裝 pillow 這個套件,而我們的 requirements.txt 並沒有設定,因此我們需要新增 pillow 套件與重新打包 Docker

# requirements.txt

django-tenants==3.4.3
django==3.2.5
django-tenants-q==1.0.0
django-elasticsearch-dsl==7.1.1
django_q==1.3.9
django_redis==5.2.0
psycopg2==2.9.3
uwsgi==2.0.20
pillow == 9.2.0

建立 .dockerignore 檔案,避免重新 build 的時候將 data 一起打包,(會出現權限問題)。

cd ~/example_tenant
touch .dockerignore

寫入以下內容

data

使用 docker-compose 重新 build(要在放 docker-compose.yml 的目錄執行)

docker-compose up -d --build

再次生成資料庫遷移檔案

docker exec --workdir /opt/app/web example_tenant_web \
python3.10 manage.py makemigrations

...

Migrations for 'products':
  products/migrations/0002_auto_20220926_1409.py
    - Add field image to productcategory
    - Alter field category on product
    - Create model ProductImage

生成了!進行遷移

docker exec --workdir /opt/app/web example_tenant_web \
python3.10 manage.py migrate
...

=== Starting migration
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, customers, products, sessions, sites
Running migrations:
  Applying products.0002_auto_20220926_1409...
 OK
=== Starting migration
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, customers, products, sessions, sites
Running migrations:
  Applying products.0002_auto_20220926_1409...
 OK
=== Starting migration
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, customers, products, sessions, sites
Running migrations:
  Applying products.0002_auto_20220926_1409...
 OK

管理介面新增圖片欄位

這裡我們要使用 inlines 內嵌管理介面。
建立一個商品圖片的內嵌管理介面 ProductImageInline,再將商品圖片的介面加入到商品管理介面 ProductAdmin 中。
而商品分類管理介面 ProductCategoryAdmin 也要加入剛才新增 image 欄位。

# products/admin.py
from django.contrib import admin
from products.models import Product, ProductCategory, ProductImage # 新增 商品圖片模型

class ProductImageInline(admin.TabularInline): # 內嵌管理介面
    model = ProductImage
    fields = ('name', 'image', 'order')
    # ...

class ProductAdmin(admin.ModelAdmin):
    # ...
    inlines = [ProductImageInline, ] # 新增 inlines

class ProductCategoryAdmin(admin.ModelAdmin):
    # ...
    fields = ('name', 'description', 'created', 'modified', 'image') # 新增 image 欄位
    # ...

開始上傳

讓我們來看看新的商品管理介面吧!
https://ithelp.ithome.com.tw/upload/images/20220926/20151656jZNP6e2Lea.png

下方出現了內嵌的商品圖片欄位,後來有選擇檔案的按鈕,馬上來上傳3張圖片
https://ithelp.ithome.com.tw/upload/images/20220926/20151656zJiHESbWsw.png

上傳成功!
https://ithelp.ithome.com.tw/upload/images/20220926/20151656h5BqNikn2s.png

接著是商品分類介面
https://ithelp.ithome.com.tw/upload/images/20220926/201516562t5ulisGHA.png

上傳成功!
https://ithelp.ithome.com.tw/upload/images/20220926/20151656arTdPpk6i6.png

目錄結構

可以清楚的看到 media 目錄底下的租戶目錄與模型目錄兩個層級

.
├── core
│   ├── admin.py
│   ├── apps.py
│   ├── helpers.py
│   ├── __init__.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── customers
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── migrations
│   │   ├── 0001_initial.py
│   │   └── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── main
│   ├── asgi.py
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py
├── media
│   ├── example01
│   │   ├── productcategory_media
│   │   │   └── 圖片11664206168.jpg
│   │   └── productimage_media
│   │       ├── 圖片11664206061.jpg
│   │       ├── 圖片21664206061.jpg
│   │       └── 圖片31664206061.jpg
│   └── example02
│       └── productcategory_media
│           └── image2-11664206335.jpg
└── products
    ├── admin.py
    ├── apps.py
    ├── __init__.py
    ├── migrations
    │   ├── 0001_initial.py
    │   ├── 0002_auto_20220926_1409.py
    │   └── __init__.py
    ├── models.py
    ├── templates
    │   ├── base.html
    │   └── products
    ├── tests.py
    ├── urls.py
    └── views.py

今天我們上傳完圖片,下一章節我們要來美化我們的首頁 『裝潢大廳,套用 Template 版面』。


上一篇
Day 13 打造大廳,動手開發你的首頁
下一篇
Day 15 裝潢大廳,套用 Template 版面
系列文
全能住宅改造王,Django 多租戶架構的應用 —— 實作一個電商網站30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
terrytsang0618
iT邦新手 5 級 ‧ 2022-12-18 19:59:22

在 urls.py 新增 media URL​的步驟中,
需要from django.conf import settings

在新增圖片模型與欄位的步驟中,
因為需要handle core_helper
不先import會出現 core_helper not defined

在上傳檔案中的步驟中,
如果不先在 /opt/app/web create media folder,
會報Error

雖然是minor error,不過可以作為少少reminder for其他學生

我要留言

立即登入留言