Create a custom migration in Django

Create a custom migration in Django

# create an empty migration file
$ ./manage.py makemigrations --empty --name convert_to_utf8mb4 your_app

in your_app/migrations/0002_convert_to_utf8mb4.py

from __future__ import unicode_literals

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('your_app', '0001_initial'),
    ]

    operations = [
        migrations.RunSQL(
            'ALTER TABLE app_repostarring CHANGE repo_description repo_description VARCHAR(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'
        ),
    ]

ref:
https://docs.djangoproject.com/en/dev/ref/migration-operations/#runsql

Lazy evaluation in Django middlewares

Lazy evaluation in Django middlewares

Attach a lazy evaluated function as a property of request in a middleware.

from django.contrib.gis.geoip import GeoIP
from django.utils.functional import SimpleLazyObject


def get_country_code(request):
    g = GeoIP()
    location = g.country(request.META['REMOTE_ADDR'])
    country_code = location.get('country_code', 'TW')

    return country_code


class CountryAndSiteMiddleware(object):

    def process_request(self, request):
        request.COUNTRY_CODE = SimpleLazyObject(lambda: get_country_code(request))

Then you could use request.COUNTRY_CODE whenever you want.

i18n Translation in Django

Internationalization and localization for your Django project.

Configuration

in settings.py

USE_I18N = True

# 默認的語言
# 中文的話是 'zh-tw' 或 'zh-cn',不過 Django 1.7 之後要改用 'zh-hant' 或 'zh-hans'
LANGUAGE_CODE = 'zh-tw'

# 支援的語言,不指定的話就會列出全部的語言
LANGUAGES = (
    ('en', 'English'),
    ('zh-cn', 'Simplified Chinese'),
    ('zh-tw', 'Traditional Chinese'),
)

LOCALE_PATHS = (
    os.path.join(BASE_DIR, 'locale/'),
)

# 要記得加上 LocaleMiddleware,才會針對 request headers 使用對應的語言
MIDDLEWARE_CLASSES = (
    ...
    'django.middleware.locale.LocaleMiddleware',
    ...
)

ref:
https://docs.djangoproject.com/en/dev/ref/settings/#languages
https://docs.djangoproject.com/en/dev/topics/i18n/translation/#how-django-discovers-language-preference

in urls.py

urlpatterns = patterns('',
    ...
    url(r'^i18n/', include('django.conf.urls.i18n')),
    ...
)

ref:
https://docs.djangoproject.com/en/dev/topics/i18n/translation/#the-set-language-redirect-view

Usage

in models.py

from django.db import models
from django.utils.translation import ugettext_lazy as _

class HighHeels(models.Model):
    name = models.CharField(_('Name'), max_length=255)
    brand = models.CharField(_('Brand'), max_length=255, null=True, blank=True)
    created = models.DateTimeField(_('Created'), auto_now_add=True)

    class Meta:
        verbose_name = _('High Heels')
        verbose_name_plural = _('High Heels')
        get_latest_by = 'created'
        ordering = ('-created', )

    def __unicode__(self):
        return self.name

在 models.py 中通常都會使用 ugettext_lazy() 而不是 ugettext()。因為 gettext_lazy() 其中的值是在被訪問的時候才翻譯,而不是在呼叫 gettext_lazy() 的時候就翻譯。

另外,ugettext()ugettext_lazy() 出來的字串都是 unicode。

pgettext() 的作用跟 ugettext() 一樣,只是多了一個參數可以傳 context 進去。

ref:
https://docs.djangoproject.com/en/dev/topics/i18n/translation/#lazy-translation
https://docs.djangoproject.com/en/dev/ref/utils/#django.utils.translation.pgettext

in views.py

from django.shortcuts import render_to_response
from django.template import RequestContext
from django.utils.translation import ugettext as _

def home(request):
    contexts = {
        'VAR_1': _('string 1'),
        'VAR_2': _('string 2'),
        'VAR_3': 'string 3',
        'VAR_4': 'string 4',
    }

    return render_to_response('home.html', contexts, RequestContext(request))

要在 ugettext 中使用 string format
必須要用以下的形式:

msg = _(u'您有 %(not_ready_count)s 項行程的尚未填寫「司機備忘錄」。') % {'not_ready_count': not_ready_count}

ref:
https://docs.djangoproject.com/en/dev/topics/i18n/translation/#internationalization-in-python-code

in base.html

{% load i18n %}

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Heelstagram</title>
  </head>
  <body>
    {% block content %}{% endblock %}

    <form action="/i18n/setlang/" method="POST">
      {% csrf_token %}
      <input name="next" type="hidden" value="{{ redirect_to }}" />
      <select name="language">
      {% get_language_info_list for LANGUAGES as LANGUAGES %}
      {% for language in LANGUAGES %}
        <option value="{{ language.code }}">{{ language.name_local }} ({{ language.code }})</option>
      {% endfor %}
      </select>
      <input type="submit" value="{% trans 'Change Language' %}" />
    </form>
  </body>
</html>

in home.html

{% extends 'base.html' %}

{# 即使 base.html 已經 load i18n 了,在每個 template 還是得在 load 一次 #}
{% load i18n %}

{% block content %}
<p>current language: "{{ LANGUAGE_CODE }}"</p>

{# 在 .po 中會表示為 msgid "string 1" #}
<p>1: {{ VAR_1 }} 會被翻譯</p>

{# 這兩行是等價的,在 .po 中會表示為 msgid "string 3" #}
<p>2: {% trans VAR_3 %} 會被翻譯</p>
<p>3: {% trans "string 3" %} 會被翻譯</p>

{# 在 .po 中會表示為: #}
{# msgctxt "說明的文字" #}
{# msgid "User" #}
<p>4: {% trans 'User' context '說明的文字' %} 會被翻譯</p>

<p>5: {{ VAR_3 }} 不會被翻譯</p>

{# 在 .po 中會表示為 msgid "%(VAR_2)s 這整句會被翻譯,包含 VAR_2 的值" #}
{# VAR_2 也會被翻譯是因為它等於 _('string 2') #}
{# blocktrans 中的變數不能是 artist.name 這種形式,必須用 artist_name 或是 with #}
<p>6: {% blocktrans %}{{ VAR_2 }} 這整句會被翻譯,包含 VAR_2 的值{% endblocktrans %}</p>

{# VAR_4 不會被翻譯是因為它等於 'string 4',只是一個單純的字串 #}
<p>7: {% blocktrans %}{{ VAR_4 }} 這整句會被翻譯,但是不包含 VAR_4 的值{% endblocktrans %}</p>
{% endblock %}

blocktrans 的作用是讓你在翻譯字串中插入 template contexts 變數。

ref:
https://docs.djangoproject.com/en/dev/topics/i18n/translation/#trans-template-tag
https://docs.djangoproject.com/en/dev/topics/i18n/translation/#internationalization-in-template-code

Create Language Files

要先安裝 gettext,否則會出現 /bin/sh: xgettext: command not found 的錯誤

# 必須手動建立 locale 目錄(放在 project 或 app 的根目錄)
$ mkdir -p locale

# 第一次執行,必須指定 locale name,不能直接用 `-a` 參數,這樣才會在 locale 底下產生相對應的目錄
# 注意!是 zh_TW 而不是 zh-tw
# 在專案根目錄執行 makemessages 就是對整個專案的所有 apps 產生翻譯檔案
$ ./manage.py makemessages -l en
$ ./manage.py makemessages -l ja
$ ./manage.py makemessages -l zh_CN
$ ./manage.py makemessages -l zh_TW
$ ./manage.py makemessages --all

# .js 的翻譯要額外指定 `-d djangojs` 才會產生 djangojs.po
$ ./manage.py makemessages -l zh_TW -d djangojs --ignore=node_modules

# 如果你的 django.po 裡有些字串被標記為 `#, fuzzy` 的話,要記得刪掉,否則該字串不會被翻譯
$ ./manage.py compilemessages

# 只對特定 app 產生翻譯檔案
$ cd mamba_client/contrib/django
$ django-admin.py makemessages -l zh_CN
$ django-admin.py compilemessages

ref:
https://docs.djangoproject.com/en/dev/topics/i18n/translation/#localization-how-to-create-language-files
https://docs.djangoproject.com/en/dev/ref/django-admin/#makemessages

每次 compilemessages 完要記得重啟 server

How Django discovers translations

django-admin.py makemessages -a 這個指令會去收集整個 project 裡的所有 apps 的 locale 字串。
優先權最高的是 LOCALE_PATHS 定義的那個目錄,找不到的話才會去找個別 app 之下的 locale 目錄。

ref:
https://docs.djangoproject.com/en/dev/topics/i18n/translation/#how-django-discovers-translations

強制使用某種語言

除了可以用上面那個 /i18n/setlang/ 的 form 表單之外,也可以在 views 裡面這樣寫:

from django.shortcuts import render_to_response
from django.template import RequestContext
from django.utils.translation import activate
from django.utils.translation import ugettext as _

def common_processor(request):
    contexts = {
        'T1': _('string 1'),
    }

    return contexts

def home(request):
    activate('zh-tw')  # 強制使用正體中文,覆蓋掉 user 和 browser 的設定

    contexts = {
        'T2': _('string 2'),
        'T3': 'string 3',
    }

    return render_to_response('home.html', contexts, RequestContext(request, processors=[common_processor]))

ref:
https://docs.djangoproject.com/en/dev/ref/utils/#module-django.utils.translation

JavaScript

in urls.py

urlpatterns = patterns(
    '',
    ...
    url(r'^jsi18n/$', 'django.views.i18n.javascript_catalog'),
    ...
)

in your_shit.html

<script src="{% url 'django.views.i18n.javascript_catalog' %}"></script>
<script>
    var text = gettext('要被翻譯的字串');
</script>

gettext() 而不是 ugettext()

# 要加上 -d djangojs 才會去 parse .js 裡的字串
$ ./manage.py makemessages --all -d djangojs

South: Database migration for Django 1.6

South is a tool to provide consistent, easy-to-use and database-agnostic migrations for Django applications. You should use the built-in migration tool in Django 1.7+ instead of South.

ref:
https://south.readthedocs.org/en/latest/

https://docs.djangoproject.com/en/dev/topics/migrations/

Configuration

south 加入 INSTALLED_APPS 之後:

# 你可以在 settings.py 加上這個設定,這樣跑測試的時候就會直接 syncdb
SOUTH_TESTS_MIGRATE = False

Usage

$ python manage.py syncdb --all

第一次使用,尚未修改 models.py 時

$ python manage.py schemamigration your_app --initial

會在該 app 底下產生一個 migrations 目錄
裡頭會有一個 0001_initial.py

修改 models.py 後

$ python manage.py schemamigration your_app --auto

會產生 0002_auto__xxx.py

開始 migrate:

# 你可以先看一下要執行的 SQL
$ python manage.py migrate your_app --db-dry-run --verbosity=2

$ python manage.py migrate your_app

# 一口氣 migrate 所有需要 migrate 的 models
$ python manage.py migrate

Issues

DatabaseError: (1050, "Table 'YOUR_TABLE' already exists") 之一

表示資料庫中沒有 south 的 migrate 資料
但是該 app 的 table 已經被建立了
可能是因為之前已經 syncdb 過
之後才把 south 加入到 INSTALLED_APPS

則:

$ python manage.py migrate your_app --fake
$ python manage.py migrate

DatabaseError: (1050, "Table 'YOUR_TABLE' already exists") 之二

假設你有三個 migration 資料,分別是 0001、0002 和 0003
0003 是新產生的 migration,還沒被 migrate 過
就先 fake 到前一次的 migration
再真的 migrate 到最新的那一次 migration

則:

$ python manage.py migrate your_app 0002 --fake
$ python manage.py migrate your_app

GhostMigrations: These migrations are in the database but not on disk

$ python manage.py migrate --delete-ghost-migrations

NoMigrations: Application '' has no migrations

$ python manage.py reset south

Show migration SQL

# 先回到上一個 migration
$ ./manage.py migrate music --fake 0010

$ ./manage.py migrate music --db-dry-run --verbosity=2

$ ./manage.py migrate music 0011
# or
$ ./manage.py migrate music --fake 0011

django-elastic-transcoder

Integrating with Amazon Elastic Transcoder in Django via django-elastic-transcoder.

ref:
https://github.com/StreetVoice/django-elastic-transcoder
https://aws.amazon.com/elastictranscoder/

Glossary

pipeline
差不多就是 project 的概念
要先指定檔案輸入和輸出的 S3 bucket

preset
預先定義好的轉檔格式
例如轉成 128k 的 MP3 或 720p 的影片之類的

job
一個要轉檔的檔案就是一個 job
一個 job 可以有多個 outputs
也就是說你可以輸入一個 .wav
然後同時轉檔成 192k 的 .mp3 和 128k 但是只有 90 秒的 .mp3 兩個檔案

ref:
http://docs.aws.amazon.com/elastictranscoder/latest/developerguide/create-job.html

Configuration (Amazon Elastic Transcoder)

比較重要的是在建立 Pipeline 的時候
要記得手動建立 Notifications
選擇 Create a New SNS Topic
分別建立以下的 SNS Topic:

  • whatever-transcode-on-progress
  • whatever-transcode-on-warning
  • whatever-transcode-on-complete
  • whatever-transcode-on-error

然後在 Amazon SNS 的 Topics 介面就會出現你剛剛新增的那些 Topics
https://ap-northeast-1.console.aws.amazon.com/sns/v2/home?region=ap-northeast-1#/topics

接著再分別幫每個 Topics 建立一個 HTTP Subscription

Topic ARN: arn:aws:sns:ap-northeast-1:123456789:whatever-transcode-on-progress
Protocol: HTTP
Endpoint: http://your_domain.com/dj_elastictranscoder/endpoint/

Amazon SNS 就會送一個 request 到你設定的 Endpoint URL
你再手動訪問一下 request body 裡頭的 SubscribeURL
這樣就完成驗證了

PS. 你可以用 http://request.lesschat.com/ 來測試

Amazon Elastic Transcoder 就會在不同的轉檔階段打一個 request 到你設定的 Endpoint URL

Usage

in settings.py

AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY')
AWS_REGION = 'ap-northeast-1'

in views.py

from dj_elastictranscoder.transcoder import Transcoder

# 這個路徑是相對於你在 pipeline 裡指定的 S3 bucket 的路徑,而不是完整的 URL
key = 'media/{}'.format(track.audio_file.name)

input_name = {
    'Key': key,
}

name, ext = os.path.splitext(key)
full_preview_filename = '{}_preview.mp3'.format(name)
short_preview_filename = '{}_short_preview.mp3'.format(name)

outputs = [
    {
        'Key': full_preview_filename,
        'PresetId': '1351620000001-300040',  # System preset: Audio MP3 - 128k
    },
    {
        'Key': short_preview_filename,
        'PresetId': '1351620000001-300040',  # System preset: Audio MP3 - 128k
        'Composition': [
            {
                'TimeSpan': {
                    'StartTime': '00:00:00.000',
                    'Duration': '00:01:30.000',
                },
            },
        ],
    },
]

transcoder = Transcoder(settings.AWS_TRANSCODER_PIPELINE_ID)
transcoder.encode(input_name, outputs)
transcoder.create_job_for_object(track)

outpus 的格式可以參考這個:
http://docs.aws.amazon.com/elastictranscoder/latest/developerguide/create-job.html

in models.py

from django.dispatch import receiver
from dj_elastictranscoder.signals import transcode_oncomplete

@receiver(transcode_oncomplete)
def encode_complete(sender, job, message, **kwargs):
    full_preview_file_info = message['outputs'][0]
    short_preview_file_info = message['outputs'][1]

    do_your_shit()

你的 endpoint 收到的內容大概會長這樣:

{
  "Type" : "Notification",
  "MessageId" : "xxx",
  "TopicArn" : "xxx",
  "Subject" : "Amazon Elastic Transcoder has finished transcoding job xxx.",
  "Message" : "內容是 JSON 字串",
  "Timestamp" : "2015-09-22T07:12:56.448Z",
  "SignatureVersion" : "1",
  "Signature" : "xxx",
  "SigningCertURL" : "https://sns.ap-northeast-1.amazonaws.com/SimpleNotificationService-xxx.pem",
  "UnsubscribeURL" : "https://sns.ap-northeast-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=xxx"
}

其中 Message 差不多會長這樣:

{'input': {'key': 'xxx.wav'},
 'jobId': 'xxx',
 'outputs': [{'duration': 1,
               'id': '1',
               'key': 'xxx_track_short_preview.mp3',
               'presetId': '1351620000001-300040',
               'status': 'Complete',
               'statusDetail': 'The starting time plus the duration of a clip exceeded the length of the input file. Amazon Elastic Transcoder created an output file that is shorter than the specified duration.'}],
 'pipelineId': 'xxx',
 'state': 'COMPLETED',
 'userMetadata': {'output_name': 'track_short_preview'},
 'version': '2012-09-25'}