2014年8月28日 星期四

[Python] Google App Engines 發送 POST 與 JSON-RPC

初始資料:

url = 'http://server/cgi'
data = {
"arg1" : "val1",
"arg2" : "val2"
}


發送 POST:

import urllib
from google.appengine.api import urlfetch
post_data = urllib.urlencode(data)
response = urlfetch.fetch(url=url,payload=post_data,method=urlfetch.POST,headers={'Content-Type': 'application/x-www-form-urlencoded'})


發送 JSONRPC:

import urllib2
headers = {
"Content-Type": "application/json"
}
post_data = json.dumps(data)
req = urllib2.Request(url, data, headers)
response = urllib2.urlopen(req).read()

2014年8月27日 星期三

[PHP] Mantis Plugin 開發筆記 @ Mantis 1.2.17

由於公司管理者熟悉 Mantis 系統,最近挑了 Mantis 來做類似 Customer Relationship Management (CRM) 服務,接著很容易碰到 User Group 的需求,這是在系統本身不存在的功能。

在網路上找了許久,有發現 User Group 相關的 Plugin,但是拿出來搭配最新版 Mantis 1.2.17 時,出現很多 Link 失效、或是某些表單送出時,導向回來失敗等。由於 trace/fix 完就等於把整個 plugin 做完了,就順便把 Mantis Plugin 開發筆記起來 Orz

首先,在 Mantis 架構下,有提供 Plugin 開發機制,提供在系統任何一個端點加入 Event Notification 架構,透過這個 notification 導入 Plugin 程式碼,以 User Group 來說,就是管理者在編輯使用者帳號時,希望也有張表可以查看目前使用者歸屬的 User Group Management,以及簡易的編輯、刪除等。

其中 Mantis Event 架構可以查詢:mantis/core/event_api.php,而 Mantis Plugin 架構可參考:mantis/core/plugin_api.php。開發 Mantis Plugin 文件,請參考這份:Building a Plugin,看完大概也了解了。

整體流程:
  1. 寫個 class ExamplePlugin extends MantisPlugin 
  2. 定義 plugin 需要的 db schema
  3. 綁定 event 串接自己定義的 function
其他心得:
  • 若 plugin db schema 想要變更時,除了刪除既定存在的外,還需要去刪除 mantis_config_table 裡頭的記錄,例如刪掉 config_id 等價於自己 plugin 名稱才行。如此一來,plugin 重新安裝時,才會重新建立對應的 table

2014年8月25日 星期一

[PHP] Mantis Email Notification Settings @ Ubuntu 12.04

在 Ubuntu 12.04 透過 apt-get 安裝後,配置好系統管理後,又手動把它生成 Mantis 1.2.17 了。預設 Mantis 是有啟動 Email Notification,包含新增帳號後,還要由對方 email 去設定密碼等。對於 Email Notification 的設定,如事件通知的寄信者資訊等,都是直接更改 /path/mantis/config_inc.php。

可以參考 http://www.mantisbt.org/manual/admin.config.email.htmlhttp://www.mantisbt.org/manual/admin.customize.email.html,簡易筆記:

//$g_enable_email_notification = OFF;
$g_mail_receive_own = OFF;
$g_default_notify_flags = array(
'reporter' => ON,
'handler' => ON,
'monitor' => ON,
'bugnotes' => ON,
'threshold_min' => NOBODY,
'threshold_max' => NOBODY
);

//
// Select the method to mail by: PHPMAILER_METHOD_MAIL for use of mail() function, PHPMAILER_METHOD_SENDMAIL for sendmail (or postfix), PHPMAILER_METHOD_SMTP for SMTP. Default is PHPMAILER_METHOD_MAIL.
//
$g_phpMailer_method = PHPMAILER_METHOD_MAIL;
$g_smtp_host = 'localhost';
$g_smtp_username = '';
$g_smtp_password = '';
$g_administrator_email = 'admin@example.com';
$g_from_email = 'noreply@example.com';
$g_from_name = 'Mantis Bug Tracker';

2014年8月24日 星期日

iOS 開發筆記 - UICollectionView resize after device rotation

最近在把玩 UICollectionView 並著手處理 resize 的時機點,經 StackOverflow - Change UICollectionViewCell size on different device orientations 的討論,純 coding without storyboard 時,需處理兩件事:

  • - (CGSize)collectionView:layout:sizeForItemAtIndexPath:
  • - (void)didRotateFromInterfaceOrientation:

就像在 UITableViewController 時,告訴 ViewController 這個 UITableViewCell 到底有多高,只是在 UICollectionViewCell 時,還能定義有多寬;在 didRotateFromInterfaceOrientation 事件中,則是告訴 UIViewController 更新畫面。

只是單純上述兩者並無法改變已經存在的 UICollectionViewCell ,對於已經存在的,必須強制處理,簡言之,需要再多幾件事:

  • 在 - (void)didRotateFromInterfaceOrientation: 時,請記得重新定義 UICollectionView frame 資訊,並且使用 [yourCollectionView reloadData] 的方式重繪資料
  • 在 - (UICollectionViewCell *)collectionView:cellForItemAtIndexPath: 中,若每一個 UICollectionViewCell 並非有規則的,則需要動態偵測並重新設定 frame 資訊
如此一來,就能達到 auto resize 的效果了 Orz  若沒這麼龜毛,大概用一下 Storyboard 就解掉了 XD

2014年8月23日 星期六

iOS 開發筆記 - UITextAlignmentLeft/UITextAlignmentCenter/UITextAlignmentRight is deprecated first deprecated in ios 6.0

從 UITextAlignment* 改用 NSTextAlignment* 即可。

iOS 開發筆記 - Use UICollectionView without Storyboard and XIB


最近才發現這個 Class ,真是乃覺三十里 XD 有可能已經習慣自己刻 UITableViewCell 了 Orz 所以一直沒去學習新技能,這次看到一些特效後,原本以為單純手動安排 View 變化,仔細一看才發現 UICollectionView 啦,就順手筆記一下,此外,坊間多為使用 Storyboard、XIB 的做法。
  1. Create an ViewControler extends UIViewController
  2. Add UICollectionView *collectionView property
  3. Init collectionView with UICollectionViewFlowLayout
  4. Use UICollectionViewDelegate and UICollectionViewDataSource
  5. Implement collectionView:numberOfItemsInSection: and collectionView:cellForItemAtIndexPath:
如此一來,就可以動了 XD

//
// TestCollectionViewController.h:
//
#import <UIKit/UIKit.h>

@interface TestCollectionViewController : UIViewController
@property (nonatomic, strong) UICollectionView *collectionView;
@end


//
// TestCollectionViewController.m:
//
#import "TestCollectionViewController.h"

@interface TestCollectionViewController () <UICollectionViewDelegate, UICollectionViewDataSource>

@end

@implementation TestCollectionViewController

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return 10;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewCell * collectionViewCell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
    
        CGFloat comps[3];
        for (int i = 0; i < 3; i++)
            comps[i] = (CGFloat)arc4random_uniform(256)/255.f;
        collectionViewCell.backgroundColor = [UIColor colorWithRed:comps[0] green:comps[1] blue:comps[2] alpha:1.0];
    return collectionViewCell;
}

- (UICollectionView *)collectionView
{
    if (!_collectionView) {
        UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init];
        flowLayout.itemSize = CGSizeMake(100, 100);
        flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
     
        _collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 300, 300) collectionViewLayout:flowLayout];
        _collectionView.delegate = self;
        _collectionView.dataSource = self;
        _collectionView.backgroundColor = [UIColor whiteColor];
        [_collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"Cell"];
    }
    return _collectionView;
}

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
 
    [self.view addSubview:self.collectionView];
 
    // Do any additional setup after loading the view.
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

2014年8月22日 星期五

[PHP] 擴充 Mantis SOAP API 、處理 DB Query 心得

前置準備:
  • soap api source code 就擺在 mantis/api/soap 中
  • 記得清掉 SoapServer 跟 SoapClient 的 cache 資料,可以用 ini_set("soap.wsdl_cache_enabled", 0); 或是在 new SoapServer 或 SoapClient 時,指定 'cache_wsdl' => WSDL_CACHE_NONE 資訊
預計新增一個 api 名為 mc_user_group ,需要做的動作:
  • 規劃 mc_user_group 的 input 跟 output 資訊,此例 input 為 username, password,而 output 是 ObjectRefArray,由於都是既定 type ,所以不需額外定義
  • 更新 WSDL 定義,可以從已存在的 mc_enum_access_levels api 來仿照
    • <message name="mc_enum_groupRequest">
      <part name="username" type="xsd:string" />
      <part name="password" type="xsd:string" /></message>
      <message name="mc_enum_groupResponse">
      <part name="return" type="tns:ObjectRefArray" /></message>
    • <operation name="mc_enum_group">
      <documentation>Get the enumeration for group</documentation>
      <input message="tns:mc_enum_groupRequest"/>
      <output message="tns:mc_enum_groupResponse"/>
      </operation>
    • <operation name="mc_enum_group">
      <soap:operation soapAction="http://www.mantisbt.org/bugs/api/soap/mantisconnect.php/mc_enum_group" style="rpc"/>
      <input><soap:body use="encoded" namespace="http://futureware.biz/mantisconnect" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/></input>
      <output><soap:body use="encoded" namespace="http://futureware.biz/mantisconnect" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/></output>
      </operation>
  • 實作 function mc_enum_group( $p_username, $p_password ),若此 function 定義在其他檔案內,記得要更新 mantis/api/soap/mc_core.php 即可
如此一來,就完成擴充 Mantis SOAP api 的任務了,記得要把 Server 跟 Client 的 SOAP Cache 都必須清掉才會正常,不然 client site 會看到類似的訊息(扣除還沒實作的情況):

PHP Fatal error:  Uncaught SoapFault exception: [SOAP-ENV:Server] Procedure 'your_func' not present

以下是片段程式碼:

// Client
$TARGET_API='https://host/mantis/api/soap/mantisconnect.php?wsdl';
ini_set("soap.wsdl_cache_enabled","0");
$c = new SoapClient($TARGET_API);

// debug
// print_r($c->__getFunctions ());

print_r( $c->mc_enum_group($user, $pass) );


// Server
        require_once( 'mc_core.php' );
        ini_set("soap.wsdl_cache_enabled","0");
        $server = new SoapServer("mantisconnect.wsdl",
                        array('features' => SOAP_USE_XSI_ARRAY_TYPE + SOAP_SINGLE_ELEMENT_ARRAYS)
        );
        $server->addFunction(SOAP_FUNCTIONS_ALL);
        $server->handle();


其他心得:

  • 自定 Mantis SOAP API 時,需要留意 input, output 的地方,假設 output 的是 ObjectRefArray 形態,那輸出只能有 id, name ,其他欄位都會被濾掉
  • Mantis SOAP API 使用上要留意權限問題,例如發 issue 用的帳號,若是 repoter 的等級,不能去做 assign monitors 的行為,會有權限問題,有問題時,可以試著用 mc_issue_add 跟 mc_issue_update 交叉測試。

最後,再補一個 DB 操作方式,output 仍以 ObjectRefArray 為例:

function mc_enum_group( $p_username, $p_password ) {
        $t_user_id = mci_check_login( $p_username, $p_password );
        if ( $t_user_id === false ) {
                return mci_soap_fault_login_failed();
        }
         
        $t_result = array();
         
        $query = "SELECT * FROM mantis_user_table";
     
        $result = db_query( $query );
        //file_put_contents('/tmp/debug', print_r($result, true) );

// 此例把 username 充當回傳的 name 資訊、user id 當作回傳 id 資訊
        for( $i=0, $cnt=db_num_rows($result) ; $i<$cnt; ++$i ) {
                $row = db_fetch_array( $result );
                $obj = new stdClass();
                $obj->id = $row['id'];
                $obj->name = $row['username'];
                array_push( $t_result, $obj );
        }    
        return $t_result;
}

2014年8月21日 星期四

[Linux] 安裝 nicstat @ Ubuntu 12.04

在 Ubuntu 14.04 只需用 atp-get install nicstat 即可,但在 12.04 就得自己編了:

$ sudo apt-get install gcc gcc-multilib
$ cd /tmp
$ wget -qO- http://nchc.dl.sourceforge.net/project/nicstat/nicstat-1.92.tar.gz | tar -xvzf -
$ cp /tmp/nicstat-1.92/Makefile.Linux /tmp/nicstat-1.92/Makefile
$ cd /tmp/nicstat-1.92 && make && sudo make install

[Linux] 使用常見的指令進行系統監控 @ Ubuntu 14.04

取得 Server 代號 - 使用 hostname 指令:

$ hostname

取得 Server OS 資訊 - 使用 lsb_release 指令:

$ lsb_release -a

取得 CPU 使用率 - 使用 vmstat、tail 和 awk 指令:

$ echo $((100-$(vmstat|tail -1|awk '{print $15}')))

取得 System Loading 資訊 - 使用 uptime 和 awk 指令:

$ uptime | egrep -o 'load average[s]*: [0-9,\. ]+' | awk -F',' '{print $1$2$3}' | awk -F' ' '{print $3}'
$ uptime | egrep -o 'load average[s]*: [0-9,\. ]+' | awk -F',' '{print $1$2$3}' | awk -F' ' '{print $4}'
$ uptime | egrep -o 'load average[s]*: [0-9,\. ]+' | awk -F',' '{print $1$2$3}' | awk -F' ' '{print $5}'


取得 Apache Web Server Processes 資訊 - 使用 pgrep 跟 wc 指令:

$ pgrep apache2 | wc -l

取得 Memory 使用率 - 使用 free、grep 跟 awk 指令:

$ free -m | grep Mem | awk '{print $3/$2 * 100}'

取得 Network 即時流量 - 使用 nicstat、grep 跟 awk,假定網路卡是 eth 開頭:

In:
$ nicstat | grep eth |  awk '{print $3}'

Out:
$ nicstat | grep eth |  awk '{print $4}'


最後,檢查上述指令是否都存在:

#!/bin/sh
CMD_USAGE=$(echo 'hostname curl pgrep wc awk tail uptime vmstat free nicstat' | tr ";" "\n")
for cmd in $CMD_USAGE
do
        path=`which $cmd`
        if [ -z $path ] || [ ! -x $path ] ; then
                echo "$cmd not found"
                exit
        fi
done


其他資源:

[Linux] 找尋 PHP 檔案內,用到 mysql_* 函數的檔案清單 @ Ubuntu 14.04

三個月前用過又忘了 Orz  還是筆記一下:

$ find /path/target -name "*.php" -exec sh -c 'cnt=`grep -c "mysql_" {}` && test $cnt -gt 0 && echo {}' \;

2014年8月19日 星期二

iOS 開發筆記 - 使用 NSURLConnection 取得 HTTP Content-type / MIME type Only

原先 NSURLConnection 本身就可以在 connection: didReceiveResponse: 時,從 NSURLResponse 中取得 MIMEType 了,但如果就只是需要 MIMEType 的話,不仿對 NSURLRequest 中,多設定 [req setHTTPMethod:@"HEAD"]; 資訊,如此一來,就只抓 Header 資訊而已。

- (void)checkMIMEType:(NSURL *)url
{
NSMutableURLRequest *req  = [NSMutableURLRequest requestWithURL:url];
[req setHTTPMethod:@"HEAD"];
[[NSURLConnection connectionWithRequest:req delegate:self] start];
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
NSLog(@"didReceiveResponse: %@, URL: %@", [response MIMEType], connection.currentRequest.URL);
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
NSLog(@"didReceiveData: %lu", [data length]);
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(@"didReceiveLoading");
}


以上述的例子中,當 [req setHTTPMethod:@"HEAD"]; 時,不會跑進 connection: didReceiveData: 裡。

[Linux] 安裝與使用 influxDB @ Ubuntu 14.04

最近想搞系統監控圖表,雖然已經有一些常見做法,不過,還是先試一下新東西吧,用用最近很夯的 Grafana 圖表神器,但還是得搭配一套儲存系統,就先試試 influxDB 了。

首先,先裝 influxDB,用它記錄時間屬性的資料流:

$ wget http://s3.amazonaws.com/influxdb/influxdb_latest_amd64.deb
$ sudo dpkg -i influxdb_latest_amd64.deb
$ sudo service influxdb start
Starting the process influxdb [ OK ]
influxdb process was started [ OK ]


安裝後,就可以從 http://localhost:8083 登入,預設帳密 root/root。登入後,記得更改一下帳密,以下仍以 root/root 為例。



先建立一個 Database : server



若要新增資料,可以透過 POST API 進行,且 API 預設採用 8086 port:

例如建立一個 table 名為 http ,並有 id, val 兩個欄位,依序傳入 3 筆資料 ["server1", 23] , ["server1", 80], ["server1", 36], ["server2",70]

$ curl -X POST -d '[{"name":"http","columns":["id","val"],"points":[["server1",23]]}]' 'http://localhost:8086/db/server/series?u=root&p=root'

...


瀏覽時,使用 SELECT * FROM http WHERE id = 'server1' 即可:



未來就可以開放 API 提供 Grafana 撈資料出來。

[Linux] 初次使用 Docker 筆記 @ Ubuntu 14.04

最近找尋系統監控的方式時,無意間看到 Docker 的消息,雖然以前用過 LXC 一陣子,久了沒用又都忘光光,這次 Docker 還滿紅的,就順手學一下吧!若安裝前,想體驗一下,可以試看看官網推的教學模式:https://www.docker.com/tryit/

在使用前,想說找一下 Docker 的商業模式,找了很久都沒看到 XD 直到使用 Docker hub 時才發現,商業模式就像 github/bitbucket 一樣,想要建立 private repo 的則需要付費,此例就是可以打包自己的開發環境送到 Docker hub 上使用,免費方案有提供 1 個 private 單位哦。


回到筆記,根據官網介紹 https://docs.docker.com/installation/ubuntulinux/,依序幾個動作就完成安裝了:

$ sudo apt-get update
$ sudo apt-get install docker.io
$ sudo ln -sf /usr/bin/docker.io /usr/local/bin/docker
$ sudo sed -i '$acomplete -F _docker docker' /etc/bash_completion.d/docker.io


開始使用:

$ sudo docker run ubuntu:12.04 echo "hello world"
Unable to find image 'ubuntu:12.04' locally
Pulling repository ubuntu
822a01ae9a15: Download complete
511136ea3c5a: Download complete
93c381d2c255: Download complete
a5208e800234: Download complete
9fccf650672f: Download complete
1186c90e2e28: Download complete
f6a1afb93adb: Download complete
hello world


此命令是說要跑一個 ubuntu 12.04 環境,在其環境執行 echo "hello world" 的意思,整個過程就是包含初始化(下載image)後,最後在執行 echo "hello world",有興趣可以再執行一次,這次就不會再去下載 image 了。

然而,上述的動作只是一次性的,舉例來說,初始環境執行 apt-get install curl 是會出錯的:

$ sudo docker run ubuntu:12.04 apt-get install curl
Reading package lists...
Building dependency tree...
Reading state information...
E: Unable to locate package curl


但只要 apt-get update 後就會正常,但不能分開執行:

$ sudo docker run ubuntu:12.04 apt-get update
Get:1 http://archive.ubuntu.com precise Release.gpg [198 B]
Get:2 http://archive.ubuntu.com precise-updates Release.gpg [198 B]
Get:3 http://archive.ubuntu.com precise-security Release.gpg [198 B]
...
Reading package lists...
$ sudo docker run ubuntu:12.04 apt-get install curl
Reading package lists...
Building dependency tree...
Reading state information...
E: Unable to locate package curl


因此,正式的使用其實是開個 terminal 進去連續動作:

$ sudo docker run -t -i ubuntu:12.04
root@431bb00c701d:/#


其中 431bb00c701d 則是此 container ID,

root@431bb00c701d:/# apt-get install wget
Reading package lists... Done
Building dependency tree    
Reading state information... Done
E: Unable to locate package wget
root@431bb00c701d:/# apt-get update
...
root@431bb00c701d:/# apt-get install wget
Reading package lists... Done
Building dependency tree      
Reading state information... Done
The following extra packages will be installed:
  libidn11
The following NEW packages will be installed:
  libidn11 wget
0 upgraded, 2 newly installed, 0 to remove and 0 not upgraded.
Need to get 391 kB of archives.
After this operation, 966 kB of additional disk space will be used.
Do you want to continue [Y/n]?


接著按 exit 離開後,其實就跟上述單行指令操作一樣,什麼都沒留下,因此在離開之前,想要保存環境則是要進行 commit。

此時,必須額外再開一個 terminal 用 docker ps 查看目前已執行的 image 環境:

$ sudo docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
5db62a9bf492        ubuntu:14.04        /bin/bash           11 seconds ago      Up 10 seconds                           goofy_curie      
431bb00c701d        ubuntu:12.04        /bin/bash           35 seconds ago      Up 34 seconds                           agitated_thompson


此例代表有2個 container 在運行,此例目標是 ubuntu:12.04 的 431bb00c701d ,想要儲存環境變化,就來個 commit 吧

$ sudo docker commit -m 'ubuntu 12.04 with wget' 431bb00c701d
$ sudo docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
<none>              <none>              aac0f5ce2936        9 minutes ago       137.7 MB
ubuntu              14.04               c4ff7513909d        7 days ago          213 MB
ubuntu              12.04               431bb00c701d        7 days ago          108 MB


想要有更佳的描述,就在 docker commit  時,多加一點資訊 REPOSITORY[:TAG] 吧:

$ sudo docker commit -m 'ubuntu 12.04 with wget' e7e35769932d MyUbuntu:12.04
$ sudo docker images
REPOSITORY          TAG                 IMAGE ID            CREATED              VIRTUAL SIZE
MyUbuntu            12.04               d3466dce0e51        About a minute ago   108 MB
<none>              <none>              aac0f5ce2936        10 minutes ago       137.7 MB
ubuntu              14.04               c4ff7513909d        7 days ago           213 MB
ubuntu              12.04               431bb00c701d        7 days ago           108 MB


下次要用時:

$ sudo docker run -i -t MyUbuntu:12.04
root@600d3fee924d:/# wget
wget: missing URL
Usage: wget [OPTION]... [URL]...

Try `wget --help' for more options.


@ 2014-10-24 加映場:[Linux] Docker 使用筆記 - 常用指令 @ Ubuntu 14.04

2014年8月18日 星期一

iOS 開發教學 - 邀請使用者評論、評分 iOS app (Review/Rate your iOS app)

原理就是第一次使用時,埋一個時間進去,等下次使用者使用 iOS app 時,判斷時間是否夠長,達到時間間距時,使用 UIAleterView 詢問使用者是否願意 Rate your app。

假設 iOS app 預設啟用時,停留在某個 ViewController:

@interface ViewController () <UIAlertViewDelegate>
// ...
@end


@implementation ViewController

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    switch (alertView.tag) {
        case YOUR_APP_ID:
        {
            //NSLog(@"buttonIndex: %d", buttonIndex);
            NSDate * now = [[NSDate alloc] init];
            switch (buttonIndex) {
                case 1: // YES
                    [[UIApplication sharedApplication] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"itms-apps://itunes.apple.com/app/id%d", alertView.tag]]];
                    [[NSUserDefaults standardUserDefaults] setObject:now forKey:@"rateDone"];
                    break;
                case 2: // Remind me later
                    [[NSUserDefaults standardUserDefaults] setObject:now forKey:@"rateDate"];
                    break;
                default:
                    [[NSUserDefaults standardUserDefaults] setObject:now forKey:@"rateDone"];
                    break;
            }
            [[NSUserDefaults standardUserDefaults] synchronize];
        }
            break;
    }
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    @try {
        //[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"rateDone"];
        //[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"rateDate"];
        //[[NSUserDefaults standardUserDefaults] synchronize];
     
        if (![[NSUserDefaults standardUserDefaults] objectForKey:@"rateDone"]) {
            NSDate * now = [[NSDate alloc] init];
            if (![[NSUserDefaults standardUserDefaults] objectForKey:@"rateDate"]) {
                [[NSUserDefaults standardUserDefaults] setObject:now forKey:@"rateDate"];
                [[NSUserDefaults standardUserDefaults] synchronize];
            } else {
                NSDate *prevDate = [[NSUserDefaults standardUserDefaults] objectForKey:@"rateDate"];
                if ([now timeIntervalSinceDate:prevDate] > 60 * 60 * 10) {
                    UIAlertView *alterView = [[UIAlertView alloc] initWithTitle:@"Rate this app" message:@"If you enjoy using this app, would you mind taking a moment to rate it?" delegate:self cancelButtonTitle:@"NO, Thanks" otherButtonTitles:@"YES", @"Remind me later", nil];
                    alterView.tag = YOUR_APP_ID;
                    [alterView show];
                }
            }
        }
    }
    @catch (NSException *exception) {
    }
    @finally {
    }
}

@end

2014年8月17日 星期日

iOS 開發教學 - UITableViewController 與 UIRefreshControl 用法 (Pull to Refresh)

呃,太久沒摸 UITableViewController 了,有一陣子都用 Storyboard 去刻,最近想說返樸歸真,改用純 coding 的方式,馬上想到如何處理 Pull to refresh 這個議題。

因為用 CocoaPods 一陣子,馬上就用關鍵字搜尋一下,發現有幾款不錯,但總是覺得差那麼一點,例如超過一年沒更新,當年熱門的甚至四年沒更新了 Orz 想了一會兒,才驚醒是不是變成內建了 XD 才想到之前去年底複習 CS193P 時,就看到教學過,那時是使用 Storyboard 的用法,隨意勾一勾就有的東西。

總之,回到主題,純寫 code 的方式,以 UITableViewController 而已,就是配置 self.refreshControl 就對了!

- (void)refreshData
{
    self.refreshControl.attributedTitle = [[NSAttributedString alloc] initWithString:@"Loading..."];
    dispatch_async(dispatch_queue_create("loading...", NULL), ^{
        [NSThread sleepForTimeInterval:5];
      
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.refreshControl endRefreshing];
            self.refreshControl.attributedTitle = [[NSAttributedString alloc] initWithString:@"Pull to refresh"];
        });
    });
}

- (void)viewWillAppear:(BOOL)animated
{
    if (!self.refreshControl)
        self.refreshControl = [[UIRefreshControl alloc] init];
    if (!self.refreshControl.isRefreshing) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.tableView setContentOffset:CGPointMake(0, -self.refreshControl.frame.size.height - self.navigationController.navigationBar.frame.size.height) animated:YES];
            [self.refreshControl beginRefreshing];
            [self refreshData];
        });
    }
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    if (!self.refreshControl)
        self.refreshControl = [[UIRefreshControl alloc] init];
    if (self.refreshControl) {
        self.refreshControl.attributedTitle = [[NSAttributedString alloc] initWithString:@"Pull to refresh"];
        [self.refreshControl addTarget:self action:@selector(refreshData) forControlEvents:UIControlEventValueChanged];
    }
}


雖然很簡單,還是筆記一下 Orz

2014年8月14日 星期四

[PHP] 使用 Mantis SOAP API - 新增 issue custom_fields

架好 Mantis 後,預設是有開啟 SOAP API 的,可以瀏覽 http://server_ip/mantis/api/soap/mantisconnect.php?wsdl 試試,應該會看到 SOAP API 的使用列表跟使用方式。

對於 custom_fields 的用法,可用在新增一些額外的資料屬性來記錄,必須先新增 custom_fields:

Manage -> Manage Custom Fields -> 填寫想的欄位名字 -> New Custom Field -> 在 Write Access 請填寫 Reporter 以及 Display 的區域也勾一勾 -> update custom field,別忘了下方也要選擇使用的 project 並點選 Link Custom Field

接著,在使用 Mantis SOAP API 時,必須先用 mc_project_get_custom_fields 把 custom_fields 的資料撈出來,因為在使用 mc_issue_add 時,對於 custom_fields 必須填寫 id 跟 name:

// 使用 Hash 來記錄
$custom_fields = array();
foreach( $c->mc_project_get_custom_fields($user, $pass, $project_id) as $raw )
        if (isset($raw->field->name) )
                $custom_fields[$raw->field->name] = $raw->field;


最後再新增 issue data 時,記得 custom_fields 是一個 Array of Object,所以新增的額外欄位資料,請用:

$field_item = new stdClass();
$field_item->field = $custom_fields["Custom_field_name"];
$field_item->value = $custom_fields_value["Custom_field_name"];


例如:

$issue_data = array(
        'project' => array(
                        'id' => $project_id
                ),
        'category' => $issus_category,
        'summary' => $issue_summary,
        'description' => $issue_description,
'custom_fields' => array()
);

$field_item = new stdClass();
$field_item->field = $custom_fields["Custom_field_name"];
$field_item->value = $custom_fields_value["Custom_field_name"];

array_push( $issue_data['custom_fields'] , $field_item );

$issus_id = $c->mc_issue_add($user, $pass, $issue_data);

2014年8月12日 星期二

[PHP] 使用 Mantis SOAP API - Automatically Create an Issue



架好 Mantis 後,預設是有開啟 SOAP API 的,可以瀏覽 /mantis/api/soap/mantisconnect.php?wsdl 試試,應該會看到 SOAP API 的使用列表跟使用方式。

使用上需要指定 user, password 跟 project id,目前沒找到一口氣列出所有 project 的方法?倒是可以用 project name 找 project id,而發一則 issue 必須要有的資訊:project id, category, summary, description,連續動作:

<?php
$c = new SoapClient('http://YourMantisServerIP/mantis/api/soap/mantisconnect.php?wsdl');

$user = 'report_account';
$pass = 'report_password';
$project_name = 'ProjectName';

$issue_summary = 'Report title';
$issue_description = 'Report body';
$issus_category = 'iOS';

$project_id = $c->mc_project_get_id_from_name($user, $pass, $project_name);
if ($project_id == 0) {
        echo "[ERROR] Project ID Not Found: [$project_name]\n";
        exit;
}

$categories = $c->mc_project_get_categories($user, $pass, $project_id);
print_r($categories);

//$custom_fields = $c->mc_project_get_custom_fields($user, $pass, $project_id);
//print_r($custom_fields);

$issue_data = array(
        'project' => array(
                        'id' => $project_id
                ),
        'category' => $issus_category,
        'summary' => $issue_summary,
        'description' => $issue_description
);

$issus_id = $c->mc_issue_add($user, $pass, $issue_data);

echo "Issue ID: $issus_id\n";


成功的話,就會看到最後印出的 Issue ID。

對於 SOAP 的簡介可以參考:如何透過PHP、SOAP 及 WSDL撰寫Web Service

2014年8月11日 星期一

AWS 筆記 - 透過 Script 半自動化處理 MySQL Replication Error

用了 AWS RDS 一陣子了,之前一直沒搞懂為何 call mysql.rds_skip_repl_error; 一次只能清一條 Error ,直到另一台 MySQL Slave Server 出錯,變成也要手動清除錯誤訊息時,才搞懂原理就是這樣,一次只能清一條。

所以,對於 MySQL Replication Error 不知不覺就生了兩種 script 來應付,一種是 RDS 當 MySQL Replication Slave 用的,另一個則是一般 MySQL Replication Slave Server 用的:

處理 AWS RDS MySQL Replication Slave Servers:

#!/bin/sh
a=0
while [ $a -lt 1000 ]
do
        check=`echo "SHOW SLAVE STATUS \G" | mysql -h YOUR_AWS_RDS_SERVER -u root -ppassword | grep "Last_SQL_Error:" | grep -c "Could not execute"`
        if [ $check -eq 0 ] ; then
                break
        fi
        echo "CALL mysql.rds_skip_repl_error;" | mysql -h YOUR_AWS_RDS_SERVER -u root -ppassword
        a=$(( $a + 1 ))
done


處理 MySQL Replication Slave Servers:

#!/bin/sh

a=0
while [ $a -lt 2000 ]
do
        check=`echo "SHOW SLAVE STATUS \G" | mysql -h YOUR_MYSQL_SLAVE_SERVER -u root -ppassword | grep "Last_SQL_Error:" | grep -c "Error "`
        if [ $check -eq 0 ] ; then
                break
        fi
        check=`echo "STOP SLAVE; SET GLOBAL SQL_SLAVE_SKIP_COUNTER = 1; START SLAVE;" | mysql -h localhost -u root -ppassword`
        a=$(( $a + 1 ))
        sleep 3
done


以上是用在 MySQL Server 彼此算異質系統,只能先透過這招頂替了。

2014年8月10日 星期日

iOS 開發筆記 - 快速使用 Google Analytics SDK for iOS : Screens Usage



工作上看著老闆很重視統計資料,比我這個本業搞 Web Service 還認真,因此,想嘗試用在 Mobile app 會有如何成果!過去在 Blog 也曾用過,大概就可以得知哪篇文章比較多人看、哪個國家比較多人等等

這次也用 CocoaPods 安裝 Google Analytics SDK for iOS (pod 'GoogleAnalytics-iOS-SDK'),過程:

Step 1: 先在 Google Analytics 網站上註冊一個 App ,以此得到 tracking id

Google Analytics 首頁 -> 管理員 -> 資源(Click) -> 新建資源 -> 行動應用程式 -> 取得追蹤編號 -> 例如 @"UA-#######-#" 等

Step 2: 使用 CocoaPods 管理 Google Analytics SDK for iOS

$ vim Podfile
pod 'GoogleAnalytics-iOS-SDK'
$ pod install
...
Using GoogleAnalytics-iOS-SDK (3.0.9)
...


Step 3: 在 AppDelegate.m 初始化 Google Analytics 資訊

#import "GAI.h"

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[GAI sharedInstance].trackUncaughtExceptions = YES;
[GAI sharedInstance].dispatchInterval = 30;
//[[[GAI sharedInstance] logger] setLogLevel:kGAILogLevelVerbose];
[[[GAI sharedInstance] logger] setLogLevel:kGAILogLevelNone];
[[GAI sharedInstance] trackerWithTrackingId:@"UA-########-#"];

id<GAITracker> tracker = [[GAI sharedInstance] defaultTracker];
tracker.allowIDFACollection = YES;

// ...

return YES;
}


Step 4: 在任何想回報的地方埋下 Codes

#import "GAIDictionaryBuilder.h"
#import "GAIFields.h"

- (void)reportStatus:(NSString *)pattern {
id<GAITracker> tracker = [[GAI sharedInstance] defaultTracker];
[tracker set:kGAIScreenName value: pattern];
[tracker send:[[GAIDictionaryBuilder createScreenView] build]];
}


如此一來,則可以透過 Google analytics 網站上觀察到多少使用者用了 App,並且透過上述 reportStatus 搭配的 pattern 字眼,可以用在使用者用了個功能就回報一次等。此外,在 Google Analytics SDK for iOS 上,其實有整合一個 GAITrackedViewController 供人繼承使用,仿造 Web 上頭的經驗,直接幫你記錄哪個 ViewController 用了多久 :) 有興趣的可以翻翻官方文件。
以下就是送出 [self reportStatus:@"test"]; 的統計資訊,需要留意的離開某個功能時,也該回報另一個狀態才能完整的終止。因此,透過 Screen 的用法,可以快速不破壞原先的程式架構。



註:由於 CocoaPods 裡頭維護的 GoogleAnalytics-iOS-SDK 沒有 link libAdIdAccess.a 這支,這將導致無法正確使用 IDFA 訊息,所以我自己額外再包了一個:changyy / GoogleAnalyticsSdkiOSUsingIDFA 來用用。

iOS 開發筆記 - 透過 CocoaPods - FMDB / FMDatabase 管理 SQLite Databases

好久沒用 C/C++ 處理 SQLite 的操作,原本有意直接在 Objective-C 一樣寫 C 來處理,但想起來最近一直把玩 CocoaPods ,就搜尋一下,發現 FMDB 還滿多的推薦的,且 github.com/ccgus/fmdb 上頭也有很猛的人數 XD 就下海來使用 FMDB 啦!

主要看中 FMDB 的特色:提供 Data Sanitization 機制!就是寫 PHP 時,會透過 mysql_real_escape_string 來處理 raw data ,避免資料格式破壞 SQL 語法。

把玩筆記:

#import "FMDatabase.h"
// $ vim Podfile
// pod 'FMDB'

- (void)insert {
// Documents/test.db
NSString *dbPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0] stringByAppendingPathComponent:@"test.db"];
BOOL needInitTable = ![[NSFileManager defaultManager] fileExistsAtPath:dbPath];
FMDatabase *db = [FMDatabase databaseWithPath:dbPath];
if ([db open]) {
// init table
if (needInitTable && ![db executeStatements:@"CREATE TABLE IF NOT EXISTS t (id VARCHAR(8), number INT)"]) {
NSLog(@"table init error");
return;
}

// insert data
if (![db executeUpdate:@"INSERT OR IGNORE INTO t (id, number) VALUES ( :id, :number )" withParameterDictionary:@{
@"id" : @"id_data",
@"number": @(12345)
}] ) {
NSLog(@"insert error");
}
[db close];
}
}

- (NSArray *)query {
// Documents/test.db
NSString *dbPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0] stringByAppendingPathComponent:@"test.db"];
BOOL needInitTable = ![[NSFileManager defaultManager] fileExistsAtPath:dbPath];
FMDatabase *db = [FMDatabase databaseWithPath:dbPath];
if (!needInitTable) {
if ([db open]) {
NSMutableArray *output = [[NSMutableArray alloc] init];
FMResultSet *rs = [db executeQuery:"SELECT id, number FROM test;"];
while ([rs next]) {
// records to NSArray
NSMutableDictionary *item = [[NSMutableDictionary alloc] init];

item[@"id"] = [rs stringForColumn:@"id"];
item[@"number"] = @([rs intForColumn:@"number");

[output addObject:item];
}
[db close];
return output;
}
}
return @[];
}

2014年8月8日 星期五

iOS 開發筆記 - Timezone / NSTimeZone 使用方式

處理 iOS App Push Notification 時,面對著 Global User 時,希望可以依照時區來進行,所以就需要取得使用者現況時區資訊。

NSLog(
@"\nlocalTimeZone = [%@]\nDisplay = [%@]\nGMT = [%d] hours",
[NSTimeZone localTimeZone],
[[NSTimeZone localTimeZone] name],
(int)[[NSTimeZone localTimeZone] secondsFromGMT] / 60 /60
);


localTimeZone = [Local Time Zone (Asia/Taipei (GMT+8) offset 28800)]
Display = [Asia/Taipei]
GMT = [8] hours

[PHP] 從 Country Code 判斷 Timezone 以及計算 GMT 時差

原本正在考慮要不要自己刻,研究一下還是有一些接近內建方式查詢:

<?php
foreach( array(
'US', 'CN', 'GB', 'DE', 'NL', 'FR', 'ES', 'IT', 'TR', 'RU', 'TW', 'HK', 'BR', 'KR', 'AE', 'TH', 'AU'
) as $code )
{
$timezone = geoip_time_zone_by_country_and_region($code)."\n";
$timezone = trim($timezone);
if(empty($timezone))
continue;
$dtz = new DateTimeZone($timezone);
$hourOffset = (int)( $dtz->getOffset(new DateTime('now', $dtz)) / 60 / 60 );
echo "Timezone: $timezone, $hourOffset hours\n";
}


結果:

Timezone: Europe/London, 1 hours
Timezone: Europe/Berlin, 2 hours
Timezone: Europe/Amsterdam, 2 hours
Timezone: Europe/Paris, 2 hours
Timezone: Europe/Rome, 2 hours
Timezone: Asia/Istanbul, 3 hours
Timezone: Asia/Taipei, 8 hours
Timezone: Asia/Hong_Kong, 8 hours
Timezone: Asia/Seoul, 9 hours
Timezone: Asia/Dubai, 4 hours
Timezone: Asia/Bangkok, 7 hours

其中有些國家的腹地廣,所以時間變化大,就無法查到,正解應該是要再搭配 region 資訊才行,請參考 php - geoip_time_zone_by_country_and_region

2014年8月4日 星期一

iOS 開發筆記 - Warning: Attempt to present YourViewController on ViewController whose view is not in the window hierarchy!

有點久沒有純手工寫 UI 互動類的 Orz 測試時不知為何在 viewDidLoad 彈跳新的 YourViewController 時,會出現這個 Warning 並且沒有任何結果。

查詢了一下,發現要在 viewDidAppear 呼叫就能避免現象,有人說是在 viewDidLoad 時沒有 Window Hierarchy 資訊。擺在 viewDidAppear 時,若彈跳的 YourViewController 關閉時,切入 ViewController 時,又進入 viewDidAppear 又會彈跳 YourViewController 出來。

總之,測試時應該可以先這樣用吧,若要正式使用大概要多加一些條件判斷:

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    [self presentViewController:[[YourViewController alloc] init] animated:YES completion:NULL];
}
參考資料
@ Updated 2014-08-17: 另一招則是在 viewDidLoad 中,採用 dispatch_async -> dispatch_get_main_queue 使用也行。

- (void)viewDidLoad
{
    [super viewDidLoad];

    dispatch_async(dispatch_get_main_queue(), ^{
        YourViewController *v = [[YourViewController alloc] init];
        [self presentViewController:v animated:YES completion:^{}];
    });
}