2019/10/03

Odoo ERP Calendar

這是一篇測試記錄,針對 Odoo 社群版剛安裝好的情境,導入 demo 資料,啟用 Calendar 模組,先觀察基本功能,再設定 Google Calendar 整合。

啟用 Calendar 會自動啟用 Discuss 模組,右上方 Conversations 有個 Push Notification 啟用請求,啟用後可以觀察系統通知的運作方式。

從 Settings / Dashboard / Users 看得到兩個帳號 Mitchell Admin 和 Marc Demo,但 Demo Meeting 的 Attendees 資料還包括 Wood Corner 之類的人員資料,很好奇是塞到哪個資料表裡,初步找到 odoo/addons/base/data/res_partner_demo.xml 裡有這筆資料,裡頭發現 res_partner 還分成 category, address, main 之類的細節。

傳統 Calendar 功能是為了 Meeting 新增和管理,欄位包括 Location, Duration, Description 等,編輯時有看到 Attendees 欄位,但找不到 Responsible 欄位,只知道 Responsible 可以用來當顯示 Meeting 的過濾條件,但還不清楚 Responsible 的設定方法。

Calendar 的瀏覽方式分成 Calendar 和 List 兩種畫面,當月曆功能跟 CRM 之類的其他模組整合時,其他模組也會出現 Calendar 畫面,但 Meeting 資料不會被同步顯示,從這角度來看,我覺得 Calendar 改稱 Meeting 更為精準才對。

從 Settings / General Settings / Integrations 可以指定 Google Calendar 整合設定,需要填寫 Client ID 和 Client Secret 兩個欄位,因為 Google Console 畫面已經跟線上文件呈現不吻合,初步猜測要在 Google APIs & Services / Credentials / Key restrictions 選擇 HTTP referrers (web sites) 再設定。測試網站要有對外 IP 才能整合設定,沒找到偷懶的辦法。

如果想要把 Meeting 整合 Task 來用,還要額外啟用 CRM 或 Project 模組,再找時間研究它們的整合情況。

2018/09/26

Plone5 New Features

對網頁技術人員而言,多少都要接觸 Javascript 語言衍生的生態圈,為了和這股潮流接軌,新版 Plone 透過 Mockup 讓 Javascript 客戶端的整合,變得更容易。這個導入及轉換工作,採用 PatternsLib 設計概念,會逐步改寫 Plone 的服務工具,例如 plone.app.widgets 和 plone.app.toolbar 這兩個模組,是利用 Mockup 機制做出的成果範例。

多國語系的支援是透過 plone.app.multilingual 模組,它在 Plone 4.3 時代就存在,新版環境就是預設安裝,啟用後就能套用。下列是批次匯入內容的範例:

#!/usr/bin/python
# -*- coding: utf-8 -*-

from Testing import makerequest
from AccessControl.SecurityManagement import newSecurityManager
from Products.CMFCore.utils import getToolByName

root = makerequest.makerequest(app)
site = root.mysite
admin = root.acl_users.getUserById('admin')
admin = admin.__of__(site.acl_users)

newSecurityManager(None, admin)

from zope.site.hooks import setHooks
from zope.component.hooks import setSite
setHooks()
setSite(site)
site.setupCurrentSkin(site.REQUEST)


from plone.dexterity.utils import createContentInContainer
from plone.app.multilingual.interfaces import ITranslationManager
from plone.app.multilingual import api as pamapi
from plone import api as ploneapi
from Products.CMFPlone.interfaces import ILanguage
from plone.dexterity.utils import iterSchemata, iterSchemataForType
#from plone.rfc822 import constructMessageFromSchemata
#from plone.rfc822 import initializeObjectFromSchemata

import csv
def unicode_csv_reader(utf8_data, dialect=csv.excel, **kwargs):
    csv_reader = csv.reader(utf8_data, dialect=dialect, **kwargs)
    for row in csv_reader:
        yield [unicode(cell, 'utf-8') for cell in row]

wftool = getToolByName(site, 'portal_workflow')

with open('/home/marr/plone511/zinstance/csv_script/item.csv', 'rb') as f:
    dialect = csv.Sniffer().sniff(f.read(), delimiters="\t")
    f.seek(0)
    reader = unicode_csv_reader(f, dialect)
    for row in reader:
        folder = site.en.events
        item = createContentInContainer(folder, 'Event', title=row[3])
        oldid = item.id
#       language = ILanguage(item).get_language()
#       print language
        manager = ITranslationManager(item)
        manager.add_translation('en')
        en = manager.get_translation('en')
#       manager.register_translation('en', en)
        en.title = row[3]
        en.description = row[4]
        manager.add_translation('zh')
        zh = manager.get_translation('zh')
        zh.title = row[1]
        zh.description = row[2]
        ploneapi.content.delete(obj=item)
        # site.en.events.manage_renameObject(oldid, str(row[0]))
        # site.zh.events.manage_renameObject(oldid, str(row[0]))
        #wftool.doActionFor(site.en.events[str(row[0])], 'publish')
        wftool.doActionFor(en, 'publish')
        #wftool.doActionFor(site.zh.events[str(row[0])], 'publish')
        wftool.doActionFor(zh, 'publish')
        en.reindexObject()
        #print row[0] + " en done."
        zh.reindexObject()
        #print row[0] + " zh done."
        #print pamapi.get_translation_manager(item).get_translations()

import transaction
transaction.commit()

2018/09/04

Archetypes Migration to Dexterity

Link Integrity Check

Be sure to set it disabled

Image Attributes in RichText Fields

<p><img class="image-inline" title="開基武廟外觀" src="kjwm-wg.jpg/@@images/c57ff855-3d5e-4eaf-8e5c-5e32db58867e.jpeg" alt="開基武廟外觀" /></p>
<p>開基武廟外觀   謝奇峰拍攝2011.8.6</p>
<p><img class="image-inline" title="開基武廟前殿" src="kjwm-qd.jpg/@@images/9c0d6de5-3295-49a5-970d-e26f51b117d6.jpeg" alt="開基武廟前殿" /></p>
<p>開基武廟前殿   謝奇峰拍攝2011.8.6</p>
<p><img class="image-inline" title="開基武廟前殿神龕" src="kjwm-qdsz.jpg/@@images/42c54979-0cad-4244-b692-80d736e25f77.jpeg" alt="開基武廟前殿神龕" /></p>
<p>開基武廟前殿神龕    謝奇峰拍攝2011.8.6</p>
<p> </p>
<p><img class="image-inline" title="開基武廟後殿" src="kjwm-hd.jpg/@@images/e74bc633-d278-47bd-8c13-223c16c1e5eb.jpeg" alt="開基武廟後殿" /></p>
<p>開基武廟後殿   謝奇峰拍攝2011.8.6</p>
<p><img class="image-inline" title="開基武廟後殿之三川殿" src="kjwm-hdscd.jpg/@@images/4919cf95-a664-48c1-a0d7-5cef3b91c4a3.jpeg" alt="開基武廟後殿之三川殿" /></p>
<p>開基武廟後殿之三川殿   謝奇峰拍攝2011.8.6</p>
<p><img class="image-inline" title="開基武廟後殿之拜殿" src="kjwm-hdbd.jpg/@@images/66b3e3e9-7be7-4513-beff-53b1a70bba18.jpeg" alt="開基武廟後殿之拜殿" /></p>
<p>開基武廟後殿之拜殿   謝奇峰拍攝2011.8.6</p>
<p><img class="image-inline" title="開基武廟後殿之正殿" src="kjwm-hdzd.jpg/@@images/76e89581-2bf4-4410-b08e-05882fada26d.jpeg" alt="開基武廟後殿之正殿" /></p>
<p>開基武廟後殿之正殿  謝奇峰拍攝2011.8.6</p>
import re

text_list = html_doc.split('\n')
result = []

for text in text_list:
    find = re.search(r'(^.* src\s*=\s*")(.*\.jpg/@@images/)(.*\.jpeg")( alt=.*$)', text)
    if find:
        result.append(find.group(1) + find.group(2) + 'image/preview"' + find.group(4))
    else:
        result.append(text)
print '\n'.join(result)

利用 bin/plonectl run update.py 來更新 RichText 內容

#!/usr/bin/python
# -*- coding: utf-8 -*-

from Testing import makerequest
from AccessControl.SecurityManagement import newSecurityManager
from Products.CMFCore.utils import getToolByName

root = makerequest.makerequest(app)
site = root.mysite
admin = root.acl_users.getUserById('admin')
admin = admin.__of__(site.acl_users)

newSecurityManager(None, admin)

from zope.site.hooks import setHooks
from zope.component.hooks import setSite
setHooks()
setSite(site)
site.setupCurrentSkin(site.REQUEST)


import re
from plone import api
from plone.app.textfield.value import RichTextValue
import transaction

for item in api.content.find(Type='MyType'):
    obj = item.getObject()
    if obj.myfield == None:
        text = []
    else:
        text = obj.myfield.raw.split('\n')
    result = []
    for line in text:
        find = re.search(r'(^.* src\s*=\s*")(.*\.jpg/@@images/)(.*\.jpeg")( .*$)', line)
        if find:
            result.append(find.group(1)+find.group(2)+'image/preview"'+find.group(4))
        else:
            result.append(line)
    result = '\n'.join(result)
    obj.myfield = RichTextValue(result, 'text/html', 'text/x-html-safe', 'utf-8')

    obj.reindexObject()

transaction.commit()

2016/08/12

Plone5 Theme Customization

Plone5 is released for quite a long time, but recently I finally get a chance to work with its default Barceloneta Theme. For Plone5 new comers, be sure to read first the article "Customize Plone 5 Default Theme On the Fly". One small thing about the Barceloneta theme, from the Front Page text, there's a big welcome string, named hero text, later turned into @@hero view in Plone 5.0.3. So you can reference the following code to highlight some text:

<div class="hero">
<h1>Welcome!</h1>
<p><a class="context" href="http://plone.com" target="_blank">Link</a></p>
</div>

Separated text, say in the end paragraph, with the hero class will display in the same box.

Barceloneta uses LESS as a pre-processor to generate the final CSS files. In addition, its LESS/CSS is not registered on the registry, so you need to compile it with gruntfile.

Then, if you want to develop your custom theme in the filesystem, take a look at plonetheme.persona. Persona Theme is good for blog-style site, not suitable for portal, anyway it is a good start to learn about customization based on Barceloneta. Here is the development buildout log:

$ bin/buildout -c develop.cfg
mr.developer: Queued 'plonetheme.persona' for checkout.
mr.developer: Filesystem package 'plonetheme.persona' doesn't need a checkout.
Develop: '/home/marr/plone505/zinstance/src/plonetheme.persona'
Uninstalling zopepy.
Uninstalling instance.
Installing _mr.developer.
Generated script '/home/marr/plone505/zinstance/bin/develop'.
Installing instance.
Getting distribution for 'mockup-highlightjs'.
Got mockup-highlightjs 1.0.0a2.
Getting distribution for 'plone.app.themingplugins'.
Got plone.app.themingplugins 1.0.
Getting distribution for 'z3c.jbot'.
Got z3c.jbot 0.7.2.
Generated script '/home/marr/plone505/zinstance/bin/instance'.
Generated interpreter '/home/marr/plone505/zinstance/parts/instance/bin/interpreter'.
Generated script '/home/marr/plone505/zinstance/bin/pilprint.py'.
Generated script '/home/marr/plone505/zinstance/bin/pilconvert.py'.
Generated script '/home/marr/plone505/zinstance/bin/enhancer.py'.
Generated script '/home/marr/plone505/zinstance/bin/thresholder.py'.
Generated script '/home/marr/plone505/zinstance/bin/viewer.py'.
Generated script '/home/marr/plone505/zinstance/bin/explode.py'.
Generated script '/home/marr/plone505/zinstance/bin/player.py'.
Generated script '/home/marr/plone505/zinstance/bin/pildriver.py'.
Generated script '/home/marr/plone505/zinstance/bin/pilfile.py'.
Generated script '/home/marr/plone505/zinstance/bin/painter.py'.
Generated script '/home/marr/plone505/zinstance/bin/createfontdatachunk.py'.
Generated script '/home/marr/plone505/zinstance/bin/gifmaker.py'.
Generated script '/home/marr/plone505/zinstance/bin/pilfont.py'.
Updating repozo.
Updating backup.
Installing zopepy.
Generated interpreter '/home/marr/plone505/zinstance/bin/zopepy'.
Updating unifiedinstaller.
Installing test.
Generated script '/home/marr/plone505/zinstance/bin/test'.
Installing diazotools.
Generated script '/home/marr/plone505/zinstance/bin/diazocompiler'.
Generated script '/home/marr/plone505/zinstance/bin/diazorun'.
Generated script '/home/marr/plone505/zinstance/bin/diazopreprocessor'.
Installing checkdocs.
Installing mrbob.
Getting distribution for 'MarkupSafe==0.23'.
Got MarkupSafe 0.23.
Generated script '/home/marr/plone505/zinstance/bin/mrbob'.
Installing releaser.
Generated script '/home/marr/plone505/zinstance/bin/fullrelease'.
Generated script '/home/marr/plone505/zinstance/bin/postrelease'.
Generated script '/home/marr/plone505/zinstance/bin/lasttagdiff'.
Generated script '/home/marr/plone505/zinstance/bin/addchangelogentry'.
Generated script '/home/marr/plone505/zinstance/bin/bumpversion'.
Generated script '/home/marr/plone505/zinstance/bin/prerelease'.
Generated script '/home/marr/plone505/zinstance/bin/release'.
Generated script '/home/marr/plone505/zinstance/bin/longtest'.
Generated script '/home/marr/plone505/zinstance/bin/lasttaglog'.
Generated script '/home/marr/plone505/zinstance/bin/pocompile'.
Versions had to be automatically picked.
The following part definition lists the versions picked:
[versions]

# Required by:
# plonetheme.persona==1.0a2.dev0
mockup-highlightjs = 1.0.0a2

# Required by:
# plonetheme.persona==1.0a2.dev0
plone.app.themingplugins = 1.0

# Required by:
# plone.app.themingplugins==1.0
z3c.jbot = 0.7.2

Look into profiles/default/metadata.xml you will see <dependency>profile-plonetheme.barceloneta:registerless</dependency>

2016/07/14

plone.app.event tzinfo

如果你使用新版 Plone 5.x 或 Plone 4.3.x 加上 plone.app.contenttypes 昇級,應該就會遇到 plone.app.event 帶來的 tzinfo 時區衝擊。假設 item = app.mysite.events['20160701'] 那我們可以用 item.creation_date 讀取建立時間,類似 DateTime('2016/07/01 00:30:23.674693 UTC') 這樣的格式,這表示它還是使用傳統的時間格式,試 item.start 和 item.end 的話,會發現類似 datetime.datetime(2016, 3, 23, 16, 0, tzinfo=) 的格式。

文件 Zope DateTime 有基本的說明,可惜並沒有幫到忙,我想要把上述時間轉成 GMT+8 的格式,這會影響到「三月23日」和「三月24日」的差別。找到 Converting Time Zones for datetime Ojbects 讓事情有了進展:

fmt = "%Y-%m-%d"
item.start.strftime(fmt)

from pytz import timezone
item.start.astimezone(timezone('Asia/Taipei')
datetime.datetime(2016, 3, 24, 0, 0, tzinfo=<DstTzInfo 'Asia/Taipei' CST+8:00:00 STD>)

2016/06/13

OpenGeo Suite

Boundless Exchange 是 Boundless GIS Platform 的一部份,它的功能看來是對應了 GeoNode,而 GeoNode 文件表示它是個 GIS CMS,能夠用來新增 Layer, Map, Document (例如 Excel, PPT, PDF 等),也有群組和簡易權限控制,加入我們的歷史圖資或 Shape File 內容,都可以做到,但我沒發現能直接編輯 HTML 或調整呈現的功能。

整體來說,Boundless Exchange 是編輯 GIS 素材最好的 Open Source 系統環境,算是提供地圖資訊的後台便利環境,但直接在上面「說故事」似乎還不適合,短時間內應該要搭配其他能夠編輯 HTML 的前台環境。如果搭配 LocalWiki 的話,方便共同編輯,但呈現形式要調整後,才容易具備彈性。

GeoNode fig1 GeoNode fig2 GeoServer fig1 GeoServer fig2

擴充開發的角度來看,Python 寫成的元件 (像 Django, pycsw) 偏向前端,以 Java 寫成 (例如 GeoServer) 是後端核心,在調整顯示的情境下,主要應是修改 JavaScript 和 Python 的前端元件部份。

2016/04/28

在地深耕, 迎向國際

2016 台灣 Python 年會 (PyCon Taiwan) 六月 3日 到 5日 在台北南港中研院 舉辦三天,總數超過 40場演講,主題涵蓋資料科學、人工智慧、金融科技、網站開發技術、雲端應用、青少年程式教育,會前搭配專案實作 (Sprint) 和專業課程 (Tutorial) 活動,會後在政治大學搭配新手村 (PyDay) 活動,更多資訊請參考 http://tw.pycon.org/2016/events/overview/ 介紹。

五位主題講者,包含網站技術專家、微軟 Python 工程師、迪士尼動畫工程師,還有黃敬群和唐鳳,兩位出身台灣、自學成才、享譽國際的自由軟體資深貢獻者。

本地出身的主題講者,佔了五分之二,這樣安排,是為了體現 PyCon Taiwan 的願景目標: 發展 Python 技術,提昇社群專業。簡單地說,我們要在台灣搞出一系列一流的產品服務,不限於 PyCon 或 Python,但我們選擇 Python 為核心,從年會出發累積。這個願景方向,我們努力了五年,交出讓自己驕傲的成績單,但還有很大的天地等我們去發展和成長。

如果能作夢,我期待成立台灣 Python 基金會,做什麼事呢? 推展青少年教育,讓 Python 成為各行各業的核心技術,社群專案和講者多到滿出來,台灣成為技術重鎮,工作效率提昇系統穩定成長,大家早點下班去做自己覺得有意義的事。如果要有這樣的一天,那麼搞好一個持續成長的 PyCon,也只是個起點罷。人生苦短,你想做的是什麼呢?

提到程式教學,幾場相關的講題摘要如下:

  • 主題演講 Jim Huang (jserv) 分享應用 Python 在大學作業系統和編譯器課程的經驗,以及 Python 編譯器與虛擬機器的內部設計。
  • 分享兒童程式教學經驗 Teach Kids to Learn Python: Eric Huang 針對小六到高一設計 Python 入門課程,說明學習 Python 應先具備的能力,入門的九個重點,提供上機練習體驗,最重要的是,如何引發學生學習的動機,培養寫程式的能力和自信心。
  • Learning the Basics: 來自香港的十一歲少年 Isaac Li (英文演講) 經由小遊戲的設計概念,分享自己學習 Python 的經驗談,用意是激勵更多青少年朋友接觸程式學習。
  • Coaching Teens to Learn Python: Edward Duh 檢視多種可能適合青少年學習的工具,包括 Raspberry Pi、Minecraft Pi、CheckiO、IPython Notebook 等,並分享教學設計的實務經驗,怎樣讓學習變得有趣。

早鳥優惠到五月1日 18:04,還沒買票的朋友,別錯過囉。

"The past cannot be changed. The future is yet in your power." -- Hugh White

Let's Implement the Future, Together!