给activeadmin的filter添加自定义搜索条件

背景

默认情况下,如果我们定义一个activeadmin的filter。

1
filter :school_name, as: :string, label: "学校名称"

我们就可以按照school_name进行搜索。相应的SQL的mapping就会变成

1
2
3
...
where school_name like "%school_name%"
...

然而,很多时候我们想输入多个空格隔开的关键词,比如学校名清华大学 北京大学这种来匹配任何学校名包含这些学校的记录。

那么这个filter如何自定义呢?

自定义filter的搜索条件

首先在model中新建一个自定义的scope。

1
2
3
4
5
6
7
8
9
10
scope :school_name_includes, ->(search) {
current_scope = self
query = ""
search.split.uniq.each do |word|
query += "school_name LIKE '%#{word}%' or "
end
query=query[0..-4]
current_scope = current_scope.where(query)
current_scope
}

search是我们获取到的输入的搜索条件。比如上面的清华 北大
我们首先将单词分开,之后逐个加入LIKE搜索条件,并用or隔开。
去掉末尾多余的or之后,加入当前的scope条件中去。
这样,当我们输入清华 北大的时候,对应的SQL就会变成

1
where school_naame like "%清华%" or school_name like "%北大%"

最后,为了使得activeadmin能找到这个filter,我们需要定义。

1
2
3
def self.ransackable_scopes(auth_object = nil)
[ :school_name_includes]
end

以上,我们就可以在DSL中使用

1
filter :school_name_includes, as: :string, label: "学校名称"

来匹配符合空格隔开的多个学校中的任意一个的记录了。

后话

一开始总想着往current_scope上加or条件,后来发现,其实query里面,我们自己嵌入就行了。
总而言之,了解自己能够更改的范围以及在这个范围内思考问题很是重要。问题卡顿的时候,可以再三阅读文档,了解接口的特征,反复思考需求。

给既存的WatchOS Extension添加Complication

前言

写下这个标题的时候我也是比较懵逼的,Complication到底中文叫什么的?日文叫什么呢?我也不知道。
众所周知,apple Watch的表盘上有着显示各种信息的小模块,这些模块的统称就是Complication。
从0创建Compliation并不难,在创建WatchOS Extension的时候,直接勾选同时创建Complication,然后勾选需要支持的Complication的模块类型就行了。
那么,问题是,当我们写好了整个Extenstion,才想到需要添加一个Complication作为表盘上的快捷方式,那该如何是好呢?

添加方法

首先在WatchOS的Extension下面创建一个新的Class,实现NSObject, CLKComplicationDataSource两个协议。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import UIKit
import ClockKit

class ComplicationController: NSObject, CLKComplicationDataSource {

func getSupportedTimeTravelDirections(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimeTravelDirections) -> Void) {
handler([.forward, .backward])
}
func getLocalizableSampleTemplate(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) {
//todo
}
func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) {
//todo
}
}

接下来,我们要在WatchOS Extension中添加使用Complication的申明和配置。

1
2
3
4
5
6
<key>CLKComplicationPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).ComplicationController</string>
<key>CLKComplicationSupportedFamilies</key>
<array>
<string>CLKComplicationFamilyGraphicCircular</string>
</array>

这里的CLKComplicationFamilyGraphicCircular是可以选择的Complication模块的一种。WatchOS提供了十几种可供选择的模块来实现。
具体查看文档即可。

最后就是要实现CLKComplicationDataSource协议的方法。

1
2
3
4
5
6
7
8
func getLocalizableSampleTemplate(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) {
let template = CLKComplicationTemplateGraphicCircularImage()
let image = UIImage(named: "modular_image")

template.imageProvider = CLKFullColorImageProvider(fullColorImage: image!)

handler(template)
}

这个方法实现的是,当我们自定义表盘,选择到这个Extension的时候,显示的内容。这里是一张名为modular_image的图片。

1
2
3
4
5
6
7
8
9
10
func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) {
let template = CLKComplicationTemplateGraphicCircularImage()
let image = UIImage(named: "modular_image")

template.imageProvider = CLKFullColorImageProvider(fullColorImage: image!)
let entry = CLKComplicationTimelineEntry.init(date: Date(), complicationTemplate: template)

handler(entry)

}

同样,这个方法实现的是我们抬起表,手表亮起来的时候,在表盘上该Extension所需要显示的内容,这里我们不具体实现根据时刻变化显示不同内容,仅仅作为一个快捷方式,我们可是和上面一样,显示一张图片。

后记

上面的总结是在编写Gopro的手表遥控器的时候遇到的一些问题。
作为一个非IOS的程序员,通过查阅资料翻阅文档来解决自己的疑惑,再加之Xcode强大的补完和提示功能,其实使用Swift编写IOS程序也并不是什么难事。

Apache httpd ProxyPass 和 ProxyRemote

当我们的client无法直接access backend_server的时候,我们通常在webserver端有如下配置。

1
2
client->backend_server NG
client->webserver->backend_server OK
1
2
ProxyPass /example https://backend_server/example
ProxyPassReverse /example https://backend_server/example

这样,当client访问https://webserver/example的时候,我们获取到的其实就是backend_server/example的内容。

需要注意的是,从client的角度是无从得知这个内部的Forward的。因此用户访问https://webserver/example的时候,总是返回通常的200 response。

这点和用Redirect的Module是不一样的。

但是,如果当我们的webserver也无法直接访问backend_server的时候改怎么办呢?

答案是使用ProxyRemote

1
2
3
ProxyRemote https://backend_server/ https://proxy_server:port
ProxyPass /example https://backend_server/example
ProxyPassReverse /example https://backend_server/example

我们在设置ProxyPass之前事先指定哪些Forward的Request需要通过Proxy获取便可。

使用API调用TPLINK智能插座

最近智能电器的概念很火,但是很多都是傻瓜式的,通过手机应用实现开关,留给自己DIY的空间并不多。

为了实现更多便利的DIY,我们有必要实现通过发送API请求来实现智能电器的控制。

为了实现通过调用API来控制插座通电这个功能,我找到了TPPINK HS105 这个东西。

默认的操作方式是通过手机应用,将智能插座连上自己加的WIFI,注册TPLINK CLOUND账号之后,你的智能插座就可以通过TPLINK CLOUD远程控制了。

具体原理是,你的手机应用发送请求给TPLINK CLOUD,TPLINK CLOUD找到你注册在上面的智能插座,并发送通断命令,这个命令通过你家WIFI传递到你的智能插座上,智能插座作出相应通断。

而我们想要实现的功能是,在和智能插座相连的同一内网中,通过直接向智能插座的HOST发送API来实现智能插座的控制。

与上面的傻瓜方式不同,我的PC(或者树莓派)向智能插座发起控制请求,这个请求通过WIFI的内网直接到达智能插座,智能插座作出相应通断。

为了解决上述DIY需求,我们需要两个东西,一个是这个请求的具体格式,另一个是智能插座在我们内网上的IP地址。

发现智能插座的内网IP

发现内网IP方法有很多种,可以上WIFI的控制页面去查所有链接的设备。可惜我家的WIFI似乎并不提供这一信息,于是,暴力一点,我们向左右内网子网地址广播PING包来侦测链接的设备。

我的子网掩码是24位,所以广播地址是192.168.0.255

1
2
3
4
5
6
7
8
9
10
11
➜  tplink ping 192.168.0.255
PING 192.168.0.255 (192.168.0.255): 56 data bytes
64 bytes from 192.168.0.3: icmp_seq=0 ttl=64 time=0.082 ms
64 bytes from 192.168.0.1: icmp_seq=0 ttl=255 time=3.807 ms
64 bytes from 192.168.0.5: icmp_seq=0 ttl=64 time=6.477 ms
64 bytes from 192.168.0.2: icmp_seq=0 ttl=64 time=37.603 ms
64 bytes from 192.168.0.4: icmp_seq=0 ttl=64 time=38.634 ms
64 bytes from 192.168.0.3: icmp_seq=1 ttl=64 time=0.130 ms
64 bytes from 192.168.0.1: icmp_seq=1 ttl=255 time=4.424 ms
64 bytes from 192.168.0.5: icmp_seq=1 ttl=64 time=4.472 ms
64 bytes from 192.168.0.4: icmp_seq=1 ttl=64 time=58.922 ms

1是路由器,2是我的手机,3是我的PC,4是IPAD,那基本上5就是我们的智能家电了。

接下来考虑如何发起请求。

十分庆幸有人已经把所有API包装起来了。我们直接调用封装后的方法就行。

1
2
3
4
5
6
7
const { Client  } = require('tplink-smarthome-api');

const client = new Client();
const plug = client.getDevice({host: your_host_address}).then((device)=>{
device.getSysInfo().then(console.log);
device.setPowerState(true);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
➜  tplink node index.js
{ sw_ver: '1.5.7 Build 180202 Rel.075643',
hw_ver: '1.0',
type: 'IOT.SMARTPLUGSWITCH',
model: 'HS105(JP)',
mac: 'xxxxx',
dev_name: 'Smart Wi-Fi Plug Mini',
alias: '只是个插座',
relay_state: 1,
on_time: 0,
active_mode: 'none',
feature: 'TIM',
updating: 0,
icon_hash: '',
rssi: -47,
led_off: 0,
longitude_i: xxxxx,
latitude_i: xxxxx,
hwId: 'xxxxx',
fwId: '00000000000000000000000000000000',
deviceId: 'xxxx',
oemId: 'xxxx',
tid: '',
err_code: 0 }

后话

陆续等空气质量传感器和温湿度传感器到货之后,可以通过室内的空气质量来控制空气净化器的开关。