Archive for November 2014

Email attachment with unicode filename in Django

使用 MIMEApplication 並設置 attachment 的 header
不然如果檔名有中文的話
用 Gmail 收到的附件檔名會是 noname

def withdraw_send_email(request, pk):
    withdraw = get_object_or_404(Withdraw, pk=pk)

    email = '[email protected]'
    subject = _(u'Packer 數位發行服務')
    body = render_to_string('dps/email/withdraw_notice.html', {})

    message = EmailMessage(
        subject=subject,
        body=body,
        to=[email, ],
    )
    message.content_subtype = 'html'

    from email.mime.application import MIMEApplication

    # 檔名包含中文的話,要用這種方式 Gmail 收到的附件檔名才不會是 noname
    filename = '%s 結算表.xlsx' % (withdraw.serial_number[:6].encode('utf-8'))
    file_data = withdraw.file.read()
    attach_file = MIMEApplication(file_data, _subtype='vnd.ms-excel')
    attach_file.add_header('Content-Disposition', 'attachment; filename="%s"' % (filename))

    message.attach(attach_file)

    message.send()

    return HttpResponse('OK')

ref:
https://docs.python.org/2/library/email.mime.html

Django ORM

defer / only / values / values_list

# retrieve everything but the `body` field
posts = Post.objects.all().defer('body')

# retrieve only the `title` field
posts = Post.objects.all().only('title')

# retrieve a list of {'id': id} dictionaries
posts = Post.objects.all().values('id')

# retrieve a list of (id,) tuples
posts = Post.objects.all().values_list('id')

# retrieve a list of ids
posts = Post.objects.all().values_list('id', flat=True)

使用 defer()only()
你得到的還是一個 model object

使用 values()values_list()
你得到的會是 list 或 dictionaries

如果你只會存取特定的幾個欄位而不會用到 methods
建議你用 values()values_list()
這樣可以省下把資料初始化成 Django model instance 的時間

distinct()

因為 MySQL 不支援 distinct('some_field')
會報錯 NotImplementedError: DISTINCT ON fields is not supported by this database backend
但是你其實可以結合 values()distinct() 來達成差不多的結果

AlbumDistribute.objects.filter(status=AlbumPublishingStatus.PUBLISHED).distinct('album').count()
# NotImplementedError: DISTINCT ON fields is not supported by this database backend

AlbumDistribute.objects.filter(status=AlbumPublishingStatus.PUBLISHED).values('album').count()
# 107

AlbumDistribute.objects.filter(status=AlbumPublishingStatus.PUBLISHED).values('album').distinct().count()
# 30

cast a queryset to a list

queryset = User.objects.filter(is_staff=True)
for user in queryset:
    setattr(user, 'extra_field', 'whatever')

object_list = list(queryset)

當你 assign 了值之後,最好把 queryset 轉成 list
這樣可以避免在其他地方不小心執行了 object_list.all() 之類的操作
因為 callable attributes cause DB lookups every time
每次 object_list.all() 又會產生一個新的 queryset

ref:
https://docs.djangoproject.com/en/dev/topics/db/optimization/

order_by()

.order_by() 不加參數表示不要任何排序

SQL 的 GROUP BY

如果你只是希望 query 的結果能夠隱式地 group 在一起
其實用 .order_by() 就可以了
例如 .order_by('album', '-created_at')
http://stackoverflow.com/questions/629551/how-to-query-as-group-by-in-django

如果你需要用到 aggregation
就要用 .values()

from django.db.models import Sum

"""
每一首歌在每一週的播放次數之和
group by song
[
    {'sum_count': 123553, 'song': 261009L},
    {'sum_count': 81889, 'song': 295336L},
    {'sum_count': 78596, 'song': 206349L},
    ...
]
"""
PlayRecordWeekly.objects \
.filter(song__created_at__range=(start_date, end_date)) \
.values('song') \
.annotate(sum_count=Sum('count')) \
.order_by('-sum_count')[:10]


"""
每一首歌的總播放次數
group by song
[
    {'count_song': 26054, 'song': 194512L},
    {'count_song': 18281, 'song': 201436L},
    {'count_song': 17802, 'song': 295336L},
    ...
]
"""
PlayRecord.objects \
.filter(created_at__year=2014, created_at__month=11) \
.values('song') \
.annotate(count_song=Count('song')) \
.order_by('-count_song')[:10]


"""
每一個用戶的播放次數之和
group by user
"""
PlayRecordArchive.objects \
.filter(last_modified__year=2014) \
.values('user_id') \
.annotate(sum_count=Sum('count')) \
.filter(sum_count__gte=10000) \
.order_by('-sum_count')


"""
每一種曲風的播放次數之和
group by song__genre
"""
PlayRecord.objects \
.filter(user=user) \
.exclude(song__genre=0) \
.values('song__genre') \
.annotate(count_song_genre=Count('song__genre')) \
.order_by('-count_song_genre')

ref:
https://docs.djangoproject.com/en/dev/ref/models/querysets/#values
http://fcamel-life.blogspot.tw/2010/04/django-group-by.html
http://blog.darkchoco.com/2011/12/20/group-by-in-django/
http://stackoverflow.com/questions/19101665/django-how-to-do-select-count-group-by-and-order-by

Q

所有的 .filter() 查詢都是 AND 操作
如果想要 OR 就得用 Q()

from django.db.models import Q
Q(question__startswith='What')

ref:
https://docs.djangoproject.com/en/dev/topics/db/queries/#complex-lookups-with-q-objects

F

你可以在 .filter() 裡用 F() 來表示同一個 model 的其他欄位
就是可以在資料庫層就直接運算
而不會把資料拿到 Python 層處理

不過跟 transaction.atomic() 一起用的時候有點問題
因為有可能資料還沒有真的被存進資料庫裡

from django.db.models import F

Entry.objects.filter(n_comments__gt=F('n_pingbacks'))

song.play_count = F('play_count') + 1
song.save(update_fields=['play_count', ])

ref:
https://docs.djangoproject.com/en/dev/topics/db/queries/#using-f-expressions-in-filters

aggregate, annotate

https://docs.djangoproject.com/en/dev/topics/db/aggregation/
aggregate 是「聚合」
a sum total of many heterogenous things taken together

annotate 是「註釋」
add explanatory notes to or supply with critical comments
https://docs.djangoproject.com/en/dev/ref/models/querysets/#annotate

annotate 作用在 queryset 裡的每一個 object
所以返回的還是一個一般的 queryset
只是每個 model instance 會有額外的 attribute 儲存 annotate 的結果

aggregate 作用在整個 queryset
通常會返回一個 dict
只包含你 aggregate 的結果
例如 model 裡某個欄位(例如 price)的總和

要訣就是 Avg, Sum, Count, Max, Min
能夠在 SQL 做的計算就在 SQL 做
不要拿到 Python 來做
https://docs.djangoproject.com/en/dev/ref/models/querysets/#aggregation-functions

ref:
https://docs.djangoproject.com/en/dev/topics/db/aggregation/
http://stackoverflow.com/questions/327807/django-equivalent-for-count-and-group-by/1317837
http://www.shellbye.com/blog/%E6%8A%80%E6%9C%AF%E4%B8%96%E7%95%8C/django-aggregate-annotate-%E8%AF%A6%E8%A7%A3/

提交你的 Java Library 到 Maven Central Repository

你需要:

  • 一個使用 Maven 管理的 Java project(廢話)
  • 一個 GPG key(deploy 的時候會用來 sign 要提交的 .jar)
  • 一個 Sonatype JIRA 的帳號
  • 開一張 JIRA 的 ticket 告訴 Sonatype 的人你要發佈 library,告知他們你的 groupId
  • 按照 Requirements 的指示完善你的 pom.xml
  • deploy 到 snapshot repository
  • deploy 到 staging repository
  • 在 OSSRH 的 Staging Repositories 把你剛剛 deploy 的 library 給 close 掉,這樣才算是 release
  • 回到那張 ticket,通知 Sonatype 讓他們把你的 library 同步到 Maven Central Repositir

最後一個步驟只有第一次 release 的時候才需要
之後 release 就會自動同步了

Requirements

http://maven.apache.org/guides/mini/guide-central-repository-upload.html
http://central.sonatype.org/pages/requirements.html
http://central.sonatype.org/pages/ossrh-guide.html
http://central.sonatype.org/pages/apache-maven.html
http://central.sonatype.org/pages/releasing-the-deployment.html

參考 Pangu.java 的 pom.xml
https://github.com/vinta/pangu.java/blob/master/pom.xml

Deployment

You need following plugins:

maven-source-plugin
maven-javadoc-plugin
maven-gpg-plugin
nexus-staging-maven-plugin
maven-release-plugin

deploy 之前
必須確定你的 local 的程式碼跟 scm 的程式碼是同步的
如果你要發布 1.0.0 版本的話
你的 pom.xml 裡要寫 1.0.0-SNAPSHOT
然後執行:

# deploy to snapshot repository
$ mvn clean deploy

你可以在 https://oss.sonatype.org/ 搜尋到
SNAPSHOT 版本測試都沒問題之後(當然你要先設定讓 Maven 能夠下載 SNAPSHOT 版本的 libraries)
就可以正式 release 了:

# cleanup for the release
$ mvn release:clean

# 要回答一些關於版本號的問題
# 它會自動幫你新增一個 tag 並且把 pom.xml 裡的 `<version>` 改成下個版本
$ mvn release:prepare

# deploy to staging repository
# 然後 Maven 會把上一步新增的 git tag 和 pom.xml 的變更直接 push 到 GitHub
$ mvn release:perform

Maven 會自動在 library 進到 staging repository 的時候把 -SNAPSHOT 字串拿掉

(第一次 release 才需要以下的動作)

然後你就可以在 https://oss.sonatype.org/#stagingRepositories
找到你剛剛 deploy 的 library
通常長得像是 wsvinta-1000(前面是 groupId)
要把它 close
然後再 release

除了第一次 release 要去 ticket 留言之外
之後 release 就會自動同步到 Maven Central Repository
不過通常會需要等一陣子才會在 Maven 上看到

ref:
http://dev.solita.fi/2014/10/22/publishing-to-maven-central-repository.html
http://lkrnac.net/blog/2014/03/deploy-to-maven-central/
http://kirang89.github.io/blog/2013/01/20/uploading-your-jar-to-maven-central/
http://superwang.me/2014/03/22/publish-your-library-to-maven-central-repository-part-1/
http://www.kongch.com/2013/05/deploy-to-central-repo/

--

如果你在 release 的過程中出了錯
要重新 release 的話
你得 revert 你的 git commit 到執行 mvn release:prepare 之前
然後再重新跑一次