2022年5月4日 星期三

CKAN 前端開發(v2.9): form macros

http://docs.ckan.org/en/2.9/contributing/frontend/templating.html#form-macros

CKAN 用 form macro 產生基本的表單輸入欄位,複雜的自己寫。

這些 macros 可用 {% import %} 匯入網頁。如下:

{% import 'macros/form.html' as form %}

form.input()

包含產生對應的 label, error message。

name        - The name of the form parameter.
id          - The id to use on the input and label. Convention is to prefix with 'field-'.
label       - The human readable label.
value       - The value of the input.
placeholder - Some placeholder text.
type        - The type of input eg. email, url, date (default: text).
error       - A list of error strings for the field or just true to highlight the field.
classes     - An array of classes to apply to the control-group.
attrs       - Dictionary of extra tag attributes
is_required - Boolean of whether this input is required for the form to validate

如:

{% import 'macros/form.html' as form %}
{{ form.input('title', label=_('Title'), value=data.title, error=errors.title) }}

其餘參考: http://docs.ckan.org/en/2.9/contributing/frontend/templating.html#form-macros



CKAN 前端開發(v2.9): template

http://docs.ckan.org/en/2.9/contributing/frontend/templating.html

template 搜尋順序

以 user/index.html 為例,CKAN 會依以下順序去找:

  1. 所有載入 extension 的 template 目錄
  2. 所有載入 extension 的 template_legacy 目錄
  3. CKAN 的 template 目錄
  4. CKAN 的 template_legacy 目錄
以上的 legacy 指的是 Genshi template,它們全都被放在 templates_legacy 目錄。若 template 目錄找不到,才會去 legacy 目錄下找。在 CKAN 升級至新版向前相容。

檔案結構

師之前版本一樣,每個 controller 一個目錄、每個 action 一個 template 檔案。

controller 下的 snippets 目錄存放此 controller 專用的 snippet,通用的 snippet 則放在上層目錄。

templates/
  base.html             # A base template with just core HTML structure
  page.html             # A base template with default page layout
  header.html           # The site header.
  footer.html           # The site footer.
  snippets/             # A folder of generic sitewide snippets
  home/
    index.html          # Template for the index action of the home controller
    snippets/           # Snippets for the home controller
  user/
    ...
templates_legacy/
  # All ckan templates

templating system

Jinja2 使用很多 template 繼承的功能來產生網頁。action 的 template 大多也繼承自 page.html:

{% extends "page.html" %}

其中有許多 block 可讓子 template 覆寫。page.html 決定了基本頁面內容架構,通常只有 {% block primary_content %} 需要自訂。

{% extends "page.html" %}
{% block page_content.html %}
  <h1>My page title</h1>
  <p>This content will be added to the page</p>
{% endblock %}

慣例/Conventions

Includes

儘量少用 Include。 {% include %} 的內容需放在使用它的程式下的 _snippets_ 目錄。

通常會用 {% snippet %} ,除非 parents context 只能被此 snippet 使用。

Snippets

用 {% snippet %} 比 h.snippet() 好。

snippet 介於 include 和 macro 之間,透過參數決定那些資訊要傳入 snippet。(include 只能接收到 父 context).

儘量 include,因為可讓偵錯變簡單。

Macros

Macros 應節制使用在為通用的 code snippet 建立 custom generator。像是 macros/form.html  中用 macro 來建立表單欄位。

儘量少用,marcro不好擴充及自訂。

extension 中的 template

在 plugin 呼叫 update_config() 向 CKAN 註冊額外的 template 路徑/extra_template_paths。

Custom Control Structures

{% ckan_extends %}

用法和 {% extend %} 很像,但它會向上載入下一個 template。

如果要移除使用者資訊頁面的 breadcrumb。

  1. 先確定要覆寫的 template:
    ckan/templates/user/read.html
  2. 在你的 extension 的 template 目錄下,建立對應要覆寫的目錄及檔案:
    ckanext-mytheme/ckanext/mytheme/templates/user/read.html
  3. 在 read.html 中透過 {% ckan_extends %} 拉出 CKAN 核心網頁內容:
    {% ckan_extends %}
  4. 然後覆寫原本的 breadcrumb block:
    {% ckan_extends %}
    {# 以下會移除 breadcrumb #}
    {% block breadcrumb %}{% endblock %}

{% ckan_extends %} 會 recursively 執行,{% ckan_extend %} 只會套用在相同名稱的 template。

{% snippet [filepath], [arg1=arg1], [arg2=arg2]... %}

很像 Jinja2 的 {% include %},差別在 CKAN 的 snippet 不會繼承 parent template 的 context,也就是只有明確傳入的參數可用,好處是容易偵錯。用法如下:

{% snippet "package/snippets/package_form.html", data=data, errors=errors %}

{% url_for [arg1=arg1], [arg2=arg2]... %}

用法同 h.url_for():

<a href="{% url_for controller="home", action="index" %}">Home</a>

{% link_for text, [arg1=arg1], [arg2=arg2]... %}

用法同 h.link_for():

<li>{% link_for _("Home"), controller="home", action="index" %}</li>

{% url_for_static path %}

用法同 h.url_for_static():

<script src="{% url_for_static "/javascript/home.js" %}"></script>




CKAN 前端開發(v2.9): 概覽

http://docs.ckan.org/en/2.9/contributing/frontend/index.html 

前端的 stylesheet 採用 Less (系統需安裝 node.js ) 撰寫。

檔案結構

所有前端用到的檔案都放在 public 目錄下(ckan/lib/default/src/ckan/ckan/public/base)如下,檔名及目錄名稱都需小寫或用減號分隔:

css/             --- Less 編譯後的 production  版 CSS 檔
  main.css
less/            --- less 檔。vendor 的樣式檔放在 vendor 目錄,在 main.less 裡 include。
  main.less
  ckan.less
  ...
javascript/     --- 建議使用 main.js 做為起始檔
  main.js
  utils.js
  components/
  ... 
vendor/
--- 存放外部相依檔,版本數字不能出現在檔名,應放在檔頭註解。 
--- 函式庫需以函式庫名稱開頭。 
--- 若相依檔有多個(如bootstrap檔)則整個目錄應用 distributed 方式處理。  
  jquery.js
  jquery.plugin.js
  underscore.js
  bootstrap.css
  ...

Stylesheets

所有樣式檔都以 Less 方式處理,開發前需先編譯:

$ npm run watch

以上指令會偵測所有 less 檔的修改並自動重建。(ctrl-c結束)。以下指令啟用偵錯模式,可看到sourcemaps:

$ DEBUG=1 npm run watch

樣式檔很多,主要有以下兩群:

  • main.less
    網站所有 dependancies、local 樣式,只排除某些情況下才載入的樣式,像是IE才用的 CSS  和只在某個網頁才顯示的外部 apps (如 recline)。
  • ckan.less
    所有 ckan 本身使用的樣式。

    JavaScript

    三部分:

    • Core (such as i18n, pub/sub and API clients)
      • Modules
      • Publisher/Subscriber
      • Client
      • i18n/Jed

    • Modules (small HTML components or widgets)
    • jQuery Plugins (very small reusable components)

    Module

    網頁上所有可互動的元件都應該是一個 module。在 HTML 元素裡加入 data-module 屬性/attribute  進行初始化:

    <select name="format" data-module="autocomplete"></select>

    這種作法的好處是讓它是一個小的可測試元件。所有全域物件應該透過 sandbox 傳入。

    用 global factory 建立新 modules,jQuery 及 Localisation 則透過 this.sandbox.jQuery 及 this.sandbox.translate()。

    ckan.module('my-module', function (jQuery) {
      return {
        initialize: function () {
          // Called when a module is created.
          // jQuery and translate are available here.
        },
        teardown: function () {
          // Called before a module is removed from the page.
        }
      }
    });

    Publisher/subscriber

    在 ckan.pubsub 下有一個簡單的 pub/sub module,透過 this.sandbox.publish/subscribe/unsubscribe,可在 module 間發佈/publish 訊息。

    module 間應透過 publish/subscribe 的方式溝通,讓 UI 上相關區域/area 做對應的刷新。

    ckan.module('language-picker', function (jQuery) {
      return {
        initialize: function () {
          var sandbox = this.sandbox;
          this.el.on('change', function () {
            sandbox.publish('change:lang', this.selected);
          });
        }
      }
    });

    ckan.module('language-notifier', function (jQuery) {
      return {
        initialize: function () {
          this.sandbox.subscribe('change:lang', function (lang) {
            alert('language is now ' + lang);
          });
        }
      }
    });

    Client

    module 不該用 jQuery.ajax() 呼叫 CKAN API,應該透過 client object。

    ckan.module('my-module', function (jQuery) {
      return {
        initialize: function () {
          this.sandbox.client.getCompletions(this.options.completionsUrl);
        }
      }
    });

    多國語系/Internationalization

    參考 Internationalizing strings in JavaScript code.

    生命週期/Life cycle

    CKAN module 在 dom ready 後初始化,ckan.module.initialize() 找出網頁中有 data-module  屬性的的 HTML 元素,試著建立對應的物件。

    <select name="format" data-module="autocomplete" data-module-key="id"></select>

    module 會依 HTML 元素中設定的 data-module-* 屬性建立對應的 sandbox 物件,建立後便呼叫 module 的 initialize() 初始化。

    module 應提供 teardown() 回復原始狀態。


        CKAN Theming Tutorial 1(v2.9): 靜態檔案(圖片與CSS)

        https://docs.ckan.org/en/2.9/theming/static-files.html

        新增 CKAN 設定 extra_public_paths。plugin 可讓存放靜態檔案的目錄開放給 template 使用。以下以一個在首頁的 template 圖檔為例。

        延續 CKAN templates Tutorial1(v2.9): 空的example_theme ,切換到 /usr/lib/ckan/default/src/ 目錄下。

        public 目錄

        將圖片與CSS 放在如下目錄:

        ckanext-example_theme/
          ckanext/
             example_theme/
                public/
                   ckan-logo.jpg
                   example_theme.css


        example_theme.css 檔內容:

        .account-masthead {
            background-color: rgb(40, 40, 40);
        }

        420x220px ckan-logo.jpg

        plugin.py 註冊 public 目錄

        編輯 ckanext-example_theme/ckanext/example_theme/plugin.py,在 update_config() 中呼叫 add_public_directory() 向 CKAN 註冊 public 目錄:

            def update_config(self, config):
                略...

                # Add this plugin's public dir to CKAN's extra_public_paths, so
                # that CKAN will use this plugin's custom static files.
                toolkit.add_public_directory(config, 'public')

        覆寫 base.html

        在如下目錄新增 base.html:

        ckanext-example_theme/
          ckanext/
            example_theme/
              templates/
                base.html

        base.html 內容如下:

        {% ckan_extends %}

        {% block styles %}
          {{ super() }}
          <link rel="stylesheet" href="/example_theme.css" />
        {% endblock %}

        styles  block 中的內容會出現在HTML網頁的 <head>...</head> 中。

        安裝

        1. 進入Python Virtual Environment: 
          . /usr/lib/ckan/default/bin/activate
        2. 切換目錄:
          cd /usr/lib/ckan/default/src/ckanext-example_theme
        3. 安裝 Plugin: 
          python setup.py develop
        4. 重啟 CKAN 服務:
          (production版) sudo supervisorctl restart ckan-uwsgi:*
          (develop版) ckan -c /etc/ckan/default/ckan.ini run

        測試

        http://[CKAN Website IP]/ckan-logo.jpg、http://[CKAN Website IP]/example_theme.css 可看到對應內容。

        檢視 http://[CKAN Website IP] 的網頁原始碼,在HTML的 <head>...</head> 可發現:

        <link rel="stylesheet" href="/example_theme.css" />




        2022年5月3日 星期二

        CKAN Extensions Tutorial 3(v2.9): 在 plugin 中呼叫 toolkit

        https://docs.ckan.org/en/2.9/extensions/tutorial.html#using-the-plugins-toolkit

        CKAN plugins toolkit 是一個 Python module,提供 CKAN 相關的 functions, classes 及 exceptions 協助撰寫 CKAN extension。

        toolkit.get_action 可用來呼叫 CKAN 的 action function,這和透過 web interface 或 API 呼叫 的方法一樣。以下程式,ckan.plugins.toolkit.get_action 呼叫ckan.logic.action.get.member_list 取得 curators group 的成員名單,結果和 API 呼叫 /api/3/action/member_list 會是一樣的:

            members = toolkit.get_action('member_list')(
                data_dict={'id': 'curators', 'object_type': 'user'})


        延續上個 CKACKAN Extensions Tutorial(v2.9): 實作 IAuthFunctions 自訂授權規則,以下將實作只允許 curators 群組成員建立群組的功能。

        建立 curators 群組

        在 CKAN 中建立 curators 群組。

        修改 plugin.py:實作 IAuthFunctions

        修改 ckanext-iauthfunctions/ckanext/iauthfunctions/plugin.py (程式碼)

        import ckan.plugins as plugins
        import ckan.plugins.toolkit as toolkit

        def group_create(context, data_dict=None):

            # Get the user name of the logged-in user.
            user_name = context['user']

            # Get a list of the members of the 'curators' group.
            members = toolkit.get_action('member_list')(
                data_dict={'id': 'curators', 'object_type': 'user'})

            # 'members' is a list of (user_id, object_type, capacity) tuples, we're
            # only interested in the user_ids.
            member_ids = [member_tuple[0] for member_tuple in members]

            # We have the logged-in user's user name, get their user id.
            convert_user_name_or_id_to_id = toolkit.get_converter(
                'convert_user_name_or_id_to_id')
            user_id = convert_user_name_or_id_to_id(user_name, context)

            # Finally, we can test whether the user is a member of the curators group.
            if user_id in member_ids:
                return {'success': True}
            else:
                return {'success': False,
                        'msg': 'Only curators are allowed to create groups'}

        class ExmapleAuthFunctionsPlugin(plugins.SingletonPlugin):
            plugins.implements(plugins.IAuthFunctions)

            def get_auth_functions(self):
                return {'group_create': group_create}


        • 實作 IAuthFunctions
        • 覆寫 IAuthFunctions 介面的 get_auth_functions 方法
        • 呼叫 toolkit.get_action('member_list') 判斷目前使用者是否在 curators 群組

        安裝

        1. 進入Python Virtual Environment: 
          . /usr/lib/ckan/default/bin/activate
        2. 切換目錄:
          cd /usr/lib/ckan/default/src/ckanext-iauthfunctions
        3. 安裝 Plugin: 
          python setup.py develop
        4. 重啟 CKAN 服務:
          sudo supervisorctl restart ckan-uwsgi:*

        測試

        • 開 Postman 執行 api 呼叫
        curl POST 'http://[CKAN web site]/api/3/action/group_create
        --header 'Authorization: [測試會員的API Key]' 
        --header 'Content-Type: application/json' 
        --data-raw '{
            "name": "0506-Group",
            "owner_org": "0427-org",
            "description": "test ckanext-iauthfunctions"    
        }'

        視是否為 curators group 的帳號,success 回傳 true 或 false:

        {
            "error": {
                "__type": "Authorization Error",
                "message": "拒絕存取: Only curators are allowed to create groups[ExampleIAuthFunctionsPlugin]"
            },
            "help": "...",
            "success": false
        }
        • 以管理員登入 CKAN,建立 curators 群組
          • 把測試會員加入這個群組。
          • 再次以 Postman 執行以上 api 呼叫
          • 即可成功

        CKAN Extensions Tutorial(v2.9): 實作 IAuthFunctions 自訂授權規則

        https://docs.ckan.org/en/2.9/extensions/tutorial.html#implementing-the-iauthfunctions-plugin-interface

        CKAN 提供許多 plugin interfaces(v2.9) 以便掛勾/hook 到 CKAN 修改或擴充功能:

        • u'Interface',
        • u'IRoutes',
        • u'IMapper',
        • u'ISession',
        • u'IMiddleware',
        • u'IAuthFunctions',
        • u'IDomainObjectModification',
        • u'IFeed',
        • u'IGroupController',
        • u'IOrganizationController',
        • u'IPackageController',
        • u'IPluginObserver',
        • u'IConfigurable',
        • u'IConfigurer',
        • u'IActions',
        • u'IResourceUrlChange',
        • u'IDatasetForm',
        • u'IValidators',
        • u'IResourcePreview',
        • u'IResourceView',
        • u'IResourceController',
        • u'IGroupForm',
        • u'ITagController',
        • u'ITemplateHelpers',
        • u'IFacets',
        • u'IAuthenticator',
        • u'ITranslation',
        • u'IUploader',
        • u'IBlueprint',
        • u'IPermissionLabels',
        • u'IForkObserver',
        • u'IApiToken',
        • u'IClick',

        CKAN 如何驗證

        參考: plugin interfaces。凡可透過 CKAN web 介面或 API 呼叫的動作都是以 ckan/logic/action/{create,delete,get,update}.py 實作的 action function。

        例如資料集相關的 Web API 便對應到:

        • func:`ckan.logic.action.create.package_create`
        • func:`ckan.logic.action.get.package_show`
        • func:`ckan.logic.action.update.package_update`
        • func:`ckan.logic.action.delete.package_delete`

        每個 action function 都有對應的 authorization function (ckan/logic/auth/{create,delete,get,update}.py),CKAN 呼叫 authorization function 決定使用者是否有被授權執行。

        實作 ckan.plugins.interfaces.IAuthFunctions,並註冊與 CKAN 同名的 authorization function,如:'user_create', 'group_create',即可覆寫 CKAN 的授權規則。

        在 CKAN 呼叫 authorization function 之前,會先驗證呼叫端是否提供合法的 API key。若要允許匿名執行, authorization function 需標示為 auth_allow_anonymous_access。如:

                    @p.toolkit.auth_allow_anonymous_access
                    def my_search_action(context, data_dict):
                        略⋯⋯

        IAuthFunctions

        實作 ckan.plugins.interfaces.IAuthFunctions。所有 authorization function 都有兩個參數:

        • context
          • model: 可用來查詢資料庫
          • user: 內容使用者的 名稱/IP(匿名存取)
          • auth_user_obj: model.User物件/None(匿名存取)
        • data_dict
          • CKAN 會傳給所有 authorization and action functions data_dict,內容所有 post 進來的資料,可能是網頁中填寫的表單欄位內容,或 API 呼叫傳入的 JSON 內容如:
        {'description': u'A really cool group',
         'image_url': u'',
         'name': u'my_group',
         'title': u'My Group',
         'type': 'group',
         'users': [{'capacity': 'admin', 'name': u'seanh'}]}

        回傳 dictionary:

        • {'success': True} 授權
        • {'success': False} 拒絕

        範例如下:

        def user_create(context, data_dict=None):
          if (some condition):
            return {'success': True}
          else:
            return {'success': False, 'msg': 'Not allowed to register'}

        CKAN Extensions Tutorial(v2.9):一個空的 CKAN extension

        https://docs.ckan.org/en/2.9/extensions/tutorial.html

        建立:ckan generate extension  

        1. 進入Python Virtual Environment:
          . /usr/lib/ckan/default/bin/activate
        2. 切換目錄
          cd /usr/lib/ckan/default/src 
        3. 執行 ckan generate extension

        Extension's name [must begin 'ckanext-']: ckanext-你的extension名稱
        (如: ckanext-iauthfunctions )]

        ├── ckanext
        │   ├── iauthfunctions //Python package 程式碼目錄
        │   │   ├── assets
        │   │   │   └── webassets.yml
        │   │   ├── fanstatic
        │   │   ├── i18n
        │   │   ├── __init__.py
        │   │   ├── plugin.py //CKAN Extension的功能寫在這裡
        │   │   ├── public
        │   │   ├── templates
        │   │   └── tests
        │   │       ├── __init__.py
        │   │       └── test_plugin.py
        │   └── __init__.py
        ├── dev-requirements.txt
        ├── LICENSE
        ├── MANIFEST.in
        ├── README.md
        ├── requirements.txt
        ├── setup.cfg
        ├── setup.py  //用它安裝到Virtual Environment
        └── test.ini

        若出現錯誤 `cookiecutter` library is missing from import path.。則加裝 cookiecutter :

         pip install cookiecutter 

        修改 plugin.py:撰寫 plugin 類別

        修改 ckanext-iauthfunctions/ckanext/iauthfunctions/plugin.py。CKAN 的 extension 其實就是一個 plugin,所有 CKAN plugin 都需繼承自 plugins.SingletonPlugin:
         
        import ckan.plugins as plugins

        class ExampleIAuthFunctionsPlugin(plugins.SingletonPlugin):
            pass

        修改 setup.py:設定 entry_points

        修改 ckanext-iauthfunctions/setup.py 的 entry_points

        entry_points='''
                [ckan.plugins] 
                example_iauthfunctions=ckanext.iauthfunctions.plugin:ExampleIAuthFunctionsPlugin

        修改 ckan.ini:啟用 plugin

        修改 /etc/ckan/default/ckan.ini。CKAN 的 extension plugins 必須加到 CKAN 設定檔的 ckan.plugins 中,CKAN 才會呼叫。

        ckan.plugins = stats text_view 略... example_iauthfunctions

        安裝測試

        1. 進入Python Virtual Environment: 
          . /usr/lib/ckan/default/bin/activate
        2. 切換目錄:
          cd /usr/lib/ckan/default/src/ckanext-iauthfunctions
        3. 安裝 Plugin: 
          python setup.py develop
        4. 重啟 CKAN 服務:
          sudo supervisorctl restart ckan-uwsgi:*

        檢視錯誤

        cat /etc/ckan/default/uwsgi.ERR