Author Archives: ota

オブジェクト、ファクトリ、リポジトリ、サービスの実装例

ota|2019年12月12日
CakePHP

ランサーズ Advent Calendar 2019 12日目の記事です。

緑茶は生茶派のotaです。
ランサーズの開発の際用いているオブジェクト、ファクトリ、リポジトリ、サービスのCakePHPでの実装のサンプルコードを公開しました。
https://github.com/LancersDevTeam/lancers-php-sample/tree/master/ObjectFactoryRepositoryService

環境

言語 PHP7.3
フレームワーク CakePHP2.10

解説

ランサーズではオブジェクト、ファクトリ、リポジトリはデータベースのテーブルと1対1にしています(例 messagesテーブルに対してMessageRepository.php)。Serviceは複数のオブジェクトを跨ぐ処理、業務ロジックを記述しています。Lib配下にドメインごとにディレクトリを切っています。サンプルコードはメッセージ機能の例です。説明用のとてもシンプルでエッセンスだけをお伝えするコードです。

現在のランサーズではサンプルコードのようにオブジェクトにfromData()という記述を書いてそこからファクトリのfromData()を呼び出しているのですが、fromData()はオブジェクトを経由せずにファクトリから直接呼び出し、toData()はファクトリに記述する方が良いのではないかとは思います。

以上になります。開発の際の参考になりますと幸いです。
私の他の執筆記事はこちら

ランサーズのメッセージ機能のAPI設計

ota|2019年06月10日
DevOps

otaです。過去の執筆記事はこちら
ランサーズのメッセージ機能のAPIを2019年5月にリニューアルしました。どのような設計にしたかについて書きます。APIを設計する際の参考になれば幸いです。

レスポンスについて

HTTPステータスコードは成功時は200、失敗時は400を返すようにしています。失敗時には下記のレスポンスボディを返すようにしています。

{
"type": "bad_request",
"message": "不正なアクセスです"
}

type、messageの値は状況によって変えています。

エンドポイント一覧

実際のエンドポイントは下記以外のもの、若干異なるものもあります。汎用的に参考になりそうな部分に限っての抜粋です。ボードというのはSlackで言うところのチャンネル、Chatworkで言うところのルームです。

GET https://www.lancers.jp/message_api/v2/boards ボードの一覧を取得する

GET https://www.lancers.jp/message_api/v2/boards/unread 未読のメッセージがあるボードの一覧を取得する

POST https://www.lancers.jp/message_api/v2/boards ボードを作成する

GET https://www.lancers.jp/message_api/v2/boards/{board_id} ボードIDからボード情報取得する

PUT https://www.lancers.jp/message_api/v2/boards/{board_id} ボード情報を編集する

GET https://www.lancers.jp/message_api/v2/boards/{board_id}/users ボードに所属するユーザーの一覧を取得する

POST https://www.lancers.jp/message_api/v2/boards/{board_id}/users ボードにユーザーを追加する

DELETE https://www.lancers.jp/message_api/v2/boards/{board_id}/users/{name} ボードからユーザーを削除する

GET https://www.lancers.jp/message_api/v2/boards/{board_id}/messages ボードIDからメッセージの一覧を取得する

POST https://www.lancers.jp/message_api/v2/boards/{board_id}/messages メッセージを送信する

GET https://www.lancers.jp/message_api/v2/boards/{board_id}/files ボードに紐づくファイル一覧を取得する

PUT https://www.lancers.jp/message_api/v2/boards/{board_id}/messages/read 既読にする

GET https://www.lancers.jp/message_api/v2/boards/search 検索する

エンドポイント詳細

実際のエンドポイント、レスポンスボディは下記に含まれないもの、若干異なるものもあります。汎用的に参考になりそうな部分に限っての抜粋です。

GET https://www.lancers.jp/message_api/v2/boards

ボードの一覧を取得する

Request

Header Content-Type: application/json
Parameters

Name Type Required Default Description Example
limit int false 1 取得数 20
modified datetime false 指定した日時よりもボードの更新日時が古いものを取得する 2019-05-01 09:00:00

Response

Header Content-Type: application/json
Body

[
{
"id": int,
"title": string,
"created": datetime,
"modified": datetime,
"users": [
{
"name": string
},
],
"message": string,
"unread_count": int,
"unread_mention_count": int
}
]

Example body

[
{
"id": 4367792,
"title": "aliceさん,bobさんとのメッセージ",
"created": "2019-05-17 15:27:53",
"modified": "2019-05-17 15:28:12",
"users": [
{
"name": "alice"
},
{
"name": "bob"
}
],
"message": "ほげ",
"unread_count": 0,
"unread_mention_count": 0
},
{
"id": 4367789,
"title": "Webサイトの構築に関して",
"created": "2019-05-16 18:28:30",
"modified": "2019-05-16 19:36:03",
"users": [
{
"name": "alice",
},
{
"name": "carol",
}
],
"message": "ほげ",
"unread_count": 2,
"unread_mention_count": 1
}
]

GET https://www.lancers.jp/message_api/v2/boards/unread

未読のメッセージがあるボードの一覧を取得する

Request

Header Content-Type: application/json
Parameters

Name Type Required Default Description Example
limit int false 1 ボードの取得数 20
modified datetime false null 指定日時よりも更新日時が古いボードを取得する 2019-04-22 01:18:37

Response

Header Content-Type: application/json
Body

[
{
"id": int,
"board_id": int,
"description": string,
"status": string,
"created": datetime,
"modified": datetime,
"files": array,
"send_user": {
"name": string
},
"read_users": array
}
]

Example body

[
{
"id": 21995612,
"board_id": 4367793,
"description": "ほげ",
"status": "ok",
"created": "2019-05-20 14:13:37",
"modified": "2019-05-20 14:13:37",
"files": [],
"send_user": {
"name": "alice",
},
"read_users": [
"alice"
]
},
{
"id": 21995613,
"board_id": 4367793,
"description": "ほげ",
"status": "ok",
"created": "2019-05-20 14:14:03",
"modified": "2019-05-20 14:14:03",
"files": [
{
"id": 18960266,
"name": "screenshot.png",
"type": "image/png",
"size": "87521",
"comment": null,
"created": "2019-05-20 14:14:04",
"modified": "2019-05-20 14:14:04"
}
],
"send_user": {
"name": "dave",
},
"read_users": [
"alice", "carol"
]
}
]

POST https://www.lancers.jp/message_api/v2/boards

ボードを作成する

Request

Header Content-Type: multipart/form-data
Parameters

Name Type Required Default Description Example
users array true ボードに追加するユーザーを指定する [“alice”,”bob”]
title string false nameさん,nameさんとのメッセージ ボードのタイトル aliceさん,bobさんとのメッセージ
message[‘description’] string false メッセージ文 はじめまして
message[‘files’] array false ファイル

Response

Header Content-Type: application/json
Body

{
"id": int,
"title": string,
"description": null,
"created": "2019-05-16 17:51:40",
"modified": "2019-05-16 17:51:40",
"owner": {
"name": string,
},
"with": array
}

Example body

{
"id": 4367787,
"title": aliceさん,bobさんとのメッセージ",
"description": null,
"created": "2019-05-16 17:51:40",
"modified": "2019-05-16 17:51:40",
"owner": {
"name": "alice",
},
"with": []
}

GET https://www.lancers.jp/message_api/v2/boards/{board_id}

ボードIDからボード情報取得する

Request

Header Content-Type: application/json
Parameters: なし

Response

Header Content-Type: application/json
Body

{
"id": int,
"title": string,
"description": null,
"created": datetime,
"modified": datetime,
"owner": {
"name": string,
},
"with": array
}

Example body

{
"id": 210,
"title": "イラスト入り年賀状の件",
"description": null,
"created": "2008-12-22 19:56:11",
"modified": "2008-12-23 10:12:39",
"owner": {
"name": "alice",
},
"with": {
"proposal": {
"id": 24,
"work": {
"id": 3,
"title": "イラスト入り年賀状の作成をお願いします。",
"type": "competition",
"status": "completed"
}
}
}
}

PUT https://www.lancers.jp/message_api/v2/boards/{board_id}

ボード情報を編集する

Request

Header Content-Type: application/json
Parameters

Name Type Required Default Description Example
title string false ボードのタイトル aliceさん,bobさんとのメッセージ
description string false ボードの概要

Response

Header Content-Type: application/json
Body

{
"title": string,
"description": string,
}

Example body

{
"title": "プロジェクトについて"
}

GET https://www.lancers.jp/message_api/v2/boards/{board_id}/users

ボードに所属するユーザーの一覧を取得する

Request

Header Content-Type: application/json
Parameters

Name Type Required Default Description Example
user int string 指定したユーザネームより大きいユーザネームを取得する alice
limit int false ユーザーの取得数を指定する。指定しなかったときは全てのユーザーを取得する。 20

Response

Header Content-Type: application/json
Body

[
{
"name": string
},
]

Example body

[
{
"name": "alice"
},
{
"name": "bob"
}
]

POST https://www.lancers.jp/message_api/v2/boards/{board_id}/users

ボードにユーザーを追加する

Request

Header Content-Type: application/json
Parameters

Name Type Required Default Description Example
users array true ボードに追加するユーザー [“alice”, “bob”]

Response

Header Content-Type: application/json
Body

[
{
"name": string
},
]

Example body

[
{
"name": "alice"
},
{
"name": "bob"
}
]

DELETE https://www.lancers.jp/message_api/v2/boards/{board_id}/users/{name}

ボードからユーザーを削除する

Request

Header Content-Type: application/json
Parameters: なし

Response

Header Content-Type: application/json
Body

{
"name": string
},

Example body

{
"name": "alice"
}

GET https://www.lancers.jp/message_api/v2/boards/{board_id}/messages

ボードIDからメッセージの一覧を取得する

Request

Header Content-Type: application/json
Parameters

Name Type Required Default Description Example
message_id int false false メッセージIDを指定する 120
direction string(next or prev) false next nextのとき指定のメッセージID以上のIDのデータを取得する。prevのとき指定のメッセージID以下のIDのデータを取得する。 next
limit int false 1 メッセージの取得数 20

Response

Header Content-Type: application/json
Body

[
{
"id": int,
"board_id": int,
"description": string,
"status": string,
"created": datetime,
"modified": datetime,
"files": array,
"send_user": {
"name": string
},
"read_users": array
}
]

Example body

[
{
"id": 21995612,
"board_id": 4367793,
"description": "ほげ",
"status": "ok",
"created": "2019-05-20 14:13:37",
"modified": "2019-05-20 14:13:37",
"files": [],
"send_user": {
"name": "alice"
},
"read_users": [
"alice"
]
},
{
"id": 21995613,
"board_id": 4367793,
"description": "ほげ",
"status": "ok",
"created": "2019-05-20 14:14:03",
"modified": "2019-05-20 14:14:03",
"files": [
{
"id": 18960266,
"name": "LWScreenShot 2019-02-04 at 10.32.30.png",
"type": "image/png",
"size": "87521",
"comment": null,
"created": "2019-05-20 14:14:04",
"modified": "2019-05-20 14:14:04"
}
],
"send_user": {
"name": "carol",
},
"read_users": [
"alice", "bob"
]
}
]

POST https://www.lancers.jp/message_api/v2/boards/{board_id}/messages

メッセージを送信する

Request

Header Content-Type: multipart/form-data
Parameters

Name Type Required Default Description Example
description string false null メッセージ文 はじめまして
files array false ファイル

Response

Header Content-Type: application/json
Body

{
"id": int,
"board_id": int,
"description": string,
"status": string,
"created": datetime,
"modified": datetime,
"files": array,
"send_user": {
"name": string,
},
"read_users": array
}

Example body

{
"id": 21995615,
"board_id": 4367793,
"description": "ほげ",
"status": "ok",
"created": "2019-05-20 14:30:57",
"modified": "2019-05-20 14:30:57",
"files": [
{
"id": 18960267,
"name": "screenshot.png",
"type": "image/png",
"size": "87521",
"comment": null,
"created": "2019-05-20 14:30:57",
"modified": "2019-05-20 14:30:57"
}
],
"send_user": {
"name": "alice",
},
"read_users": []
}

GET https://www.lancers.jp/message_api/v2/boards/{board_id}/files

ボードに紐づくファイル一覧を取得する

Request

Header Content-Type: application/json
Parameters

Name Type Required Default Description Example
file_id int false false 指定のファイルID以上のIDのファイルを取得する 14
limit int false 1 ファイルの取得数 20

Response

Header Content-Type: application/json
Body

[
{
"id": int,
"name": string,
"type": string,
"size": int,
"comment": string or null,
"created": datetime,
"modified": datetime,
"user": {
"name": string
}
},
]

Example body

[
{
"id": 18960267,
"name": "screenshot 2019-02-04 at 10.32.30.png",
"type": "image/png",
"size": "87521",
"comment": null,
"created": "2019-05-20 14:30:57",
"modified": "2019-05-20 14:30:57",
"user": {
"name": "alice"
}
},
{
"id": 18960266,
"name": "screenshot.png",
"type": "image/png",
"size": "87521",
"comment": null,
"created": "2019-05-20 14:14:04",
"modified": "2019-05-20 14:14:04",
"user": {
"name": "bob"
}
}
]

PUT https://www.lancers.jp/message_api/v2/boards/{board_id}/messages/read

既読にする

Request

Header Content-Type: application/json
Parameters

Name Type Required Default Description Example
message_id int false null 指定のメッセージIDまで既読にする 2

Response

Header Content-Type: application/json
Body

{
"board_id": int,
"unread_count": int
}

Example body

{
"board_id": "4367793",
"unread_count": 2
}

GET https://www.lancers.jp/message_api/v2/boards/search

検索する

Request

Header Content-Type: application/json
Parameters

Name Type Required Default Description Example
q string true 検索ワード alice
offset int false 0 検索結果の取得のスタート地点 20
limit int false 20 検索結果の取得数 10

Response

Header Content-Type: application/json
Body

[
{
"id": int,
"title": string,
"created": datetime,
"modified": datetime,
"users": [
{
"name": string
}
],
"message": string,
"unread_count": int,
"unread_mention_count": int
}
]

Example body

[
{
"id": 4332096,
"title": "プロジェクトについて",
"created": "2019-04-22 01:18:37",
"modified": "2019-04-22 01:18:37",
"users": [
{
"name": "alice"
}
{
"name": "bob"
}
],
"message": "ほげ",
"unread_count": 1,
"unread_mention_count": 1
},
{
"id": 4332097,
"title": "コンペについて",
"created": "2019-04-22 01:18:37",
"modified": "2019-04-22 01:18:37",
"users": [
{
"name": "carol"
},
{
"name": "alice"
}
],
"message": "ほげ",
"unread_count": 0,
"unread_mention_count": 0
}
]

以上になります。今回実装するにあたってchatwork APIドキュメントslack apiを参考にさせていただきました。そちらもぜひ参考にしてみてください。

ブロックチェーンを活用したidentitiyの実装とそこから考える展望

ota|2018年12月19日
Blockchain

otaです。「ランサーズ Advent Calendar 2018 」18日目の記事です。

ランサーズにはブロックチェーンのプロジェクトがあり、identitiyまわりのリサーチおよび実装による検証を行ったので、それらでやったことの説明とそこから考える展望について書きます。

実装したもの

ERC725ERC735を活用して、Ethereumのアドレスにランサーズのアカウントを紐づけ、ランサーズのアカウントを持っているアドレスであれば指定のアクションを行えるというものを実装しました。社内でのみの運用で一般には公開していません。
コードはこちらです
技術的な詳細の記事はこちらです

展望

実装したものから考えたこのようなものがつくれるのではないか、というものをいくつかあげてみます。

信用情報の蓄積とスコアリング

アドレスに各種Webサービスのアカウント情報を紐付ける

各種サービスのアカウント情報をもとにスクレイピングをして各サービスの実績情報を取得

各種サービスのアカウント情報をランサーズのそのアカウントのプロフィールページに実績情報として表示する(現状でもプロフィールページにリンク等を埋め込むことは可能だが、そのリンクがそのユーザーのものであるかを証明することができているわけではないです。今回実装した技術を活用することによってたしかにそのアカウントを保持しているユーザーであるということが証明できます。)。

各種サービスの実績情報を用いて独自のスコアリングを行い、それらをもとに例えばランサーズ内での検索表示順位のロジックに加味するなどが考えられます。レンディングサービスなどであればそのスコアリングをもとに与信枠を決めるなども可能だと思います。

アドレスに各種サービスのアカウント情報を紐付けること、それらがたしかに本人のものであることを証明することは別にブロックチェーンでなくても可能なことです。しかしながら、非中央集権的で特定の企業や団体に依存しないところにそういったデータを蓄積することによって多数のプレイヤーがよりオープンな形でオープンなコードというルールをもとにした自由競争の中より自由な発想でそのデータを元に新しいサービス、新しい信用情報の活用方法を見言い出すことができるのではないかと思っています。

参考情報
Keybase
scouty

各サービスごとの本人確認を不要にする

アドレスに本人確認がされているという情報を紐付ける

本人確認済みである情報を持つEthereumのアドレスを使ってWebサービスに登録することでWebサービスに登録後、本人確認のステップをスキップできるようにする

本人確認を必要とするWebサービスは多く、ユーザーがそのたびに本人確認を行うのは手間だと思っています。それを一度だけで済ますことができればユーザーとして良いですし、Webサービス提供者としても本人確認を行うコストが削減できて良いと思います。

参考情報
TRUSTDOCK

非中央集権なログインID

アドレスを使ってログインする(Googleログインをブロックチェーンで実装するイメージ)

参考情報
DIF
OpenID
https://github.com/block-base/ethereum-login

展望と現実との差分

技術的には展望のところであげさせていただいたことはいま現在で可能であるものもあると思います。しかしながら、それが一般に普及するか、世の中にとって便利なものであるか、というところには隔たりがあると思っています。とくにUIUX部分での課題が大きく、今回実装したコードであればMETAMASK等を使った操作が必要になるのですが、広く一般の方に使ってもらうためには敷居が高いように思います。技術的にできるということはもちろんのことより扱いやすいクライアントを普及させていく必要もあるように思います。

まとめ

ブロックチェーンを活用したidentityまわりはなかなかおもしろいトピックだと思います!

Implementing ERC725 for account managemenet in inter-company Ethereum tipping service

ota|2018年11月15日
Blockchain

This post is a contribution from BlockBase that is working on a blockchain project together.

Hi, I’m Taiju from BlockBase, we’ve implemented ERC725 for account management for
ERC725 for account management in inter-company Ethereum tipping service.
https://github.com/LancersDevTeam/ethertip

ERC725

ERC725 is a proposed standard for blockchain-based identity. ERC 725 describes proxy smart contracts that can be controlled by multiple keys and other smart contracts. ERC735 is an associated standard to add and remove claims to an ERC 725 identity smart contract.

 

We implemented ERC725 so that “individual can manages own identity by themselves”.
This is one of the gateway for “public key is my identity” world, and bring us new user experience.

In this article I focus on technical perspective.

Process Detail

Identity and claim creation process is like this.

1. Lancers member deploys a new identity contract.
2. Lancers member input id and password and obtains a cryptographic signature when id and password is verified lancers auth API.
3. Lancers member adds this claim to the identity contract.

Smart contract verification in Ethertip

KeyHolder.sol
Identity contract store claimId and Claims (this includes lancers id and above mentioned signature).

ClaimVerifier.sol
Get Claims from identity contract and verify signature is properly signed by claim signer.

Ethertip withdraw function uses above verifies user identity, and user who has claim for lancers account can withdraw Ethereum.

This time, we’ve implemented ERC725 in inter-company use only.
We’ll keep researching and development to do better for blockchain Identity!

Amazon Linux再起動時にUnicornを自動的に起動させる方法

ota|2017年12月20日
DevOps

ランサーズ Advent Calendar 2017の20日目を担当します、ota@purratto)です。
今回はAmazon Linux再起動時にUnicornを自動的に起動させる方法を解説します。

実装方法

Unicornを自動的に起動させたいサーバ内に /etc/init.d/unicorn を作成し、下記のように編集します。

#!/bin/sh

#chkconfig:2345 84 16
#description:unicorn shell

. /etc/rc.d/init.d/functions

NAME="Unicorn"
ENV="hoge_env"
ROOT_DIR="/var/www/hoge_dir/current/"
PID_FILE="/var/www/hoge_dir/shared/tmp/pids/unicorn.pid"
PS_PID=$(ps -aef | grep master | grep unicorn | awk '{print $2}')
CONF="${ROOT_DIR}config/unicorn/${ENV}.rb"

start()
{
  PS_PID=$(ps -aef | grep master | grep unicorn | awk '{print $2}')
  if [[ ! -z ${PS_PID} ]] ; then
    echo "${NAME} already started"
    exit 0
  fi
  echo -n "start ${NAME} : "
  daemon $(su -l hoge_user -c "(cd ${ROOT_DIR} && RAILS_ENV=${ENV} /usr/local/rbenv/shims/bundle exec unicorn -c ${CONF} -E ${ENV} -D )")
  echo
}

stop()
{
  if [[ -z ${PS_PID} ]]; then
    echo "${NAME} not started"
    exit 1
  fi

  echo -n "stop ${NAME} : "
  daemon $(kill ${PS_PID})
  echo
  rm -f ${PID_FILE}

  while :
  do
    PS_PID=$(ps -aef | grep master | grep unicorn | awk '{print $2}')
    if [[ -z "${PS_PID}" ]]; then
      break
    fi
    usleep 100000
  done
}

status()
{
  if [[ -z ${PS_PID} ]]; then
    echo "${NAME} is stop"
  else
    echo "${NAME} is running"
  fi
}

restart()
{
  if [[ -z ${PS_PID} ]]; then
    echo "${NAME} not started"
  else
    echo -n "stop ${NAME} : "
    daemon $(kill ${PS_PID})
    echo
    rm -f ${PID_FILE}

    while :
    do
      PS_PID=$(ps -aef | grep master | grep unicorn | awk '{print $2}')
      if [[ -z ${PS_PID} ]]; then
        break
      fi
      usleep 100000
    done
  fi

  start
}

case "$1" in
  start)
    start
    ;;
  stop)
    stop
    ;;
  restart)
    restart
    ;;
  status)
    status
    ;;
  *)
    echo "Syntax Error: release [start|stop|restart|status]"
    ;;
esac

下記のコマンドを実行します。

$ chkconfig –add unicorn
$ chkconfig unicorn on
$ chmod 755 /etc/init.d/unicorn

以上です。参考になれば幸いです。

WordPressで日本語のパーマリンクで投稿しようとしたときに警告を出し投稿できないようにする方法

ota|2017年06月23日
WordPress

キーボードはREALFORCE派のota@purratto)です。REALFORCE91UG-Sが一番お気に入りです。

WordPressで日本語のパーマリンクで投稿しようとしたときに警告を出し投稿できないようにする実装を行ったので、その実装方法を解説します。
デモはこちら

実装方法

テーマのfunctions.phpに下記のコードを追記します。

function prevent_japanese_permalink() {
    // この例ではfunctions.phpにJavaScriptをべた書きしていますが、必要に応じて別ファイルに切り出してください。
    echo "<script>
            jQuery(document).ready(
                function($) {
                    $('#publish').on(
                        'click',
                        function(event) {
                            var permalink = $('#editable-post-name-full').text();
                            if (!permalink.match(/^[a-zA-Z0-9_-]+$/)) {
                                alert('日本語のパーマリンクでは投稿できません。アルファベットに変更してください。');
                                event.preventDefault();
                            }
                        }
                    );
                }
            );
        </script>";
}

// 新規投稿時
add_action('admin_footer-post-new.php', 'prevent_japanese_permalink', 11, 0);

// 更新時
add_action('admin_footer-post.php', 'prevent_japanese_permalink', 11, 0);

指定した投稿以降の投稿のみ日本語のパーマリンクでの投稿を防ぐ場合には上記の24、25行目を削除し、下記のコードを追記します。

// 指定した投稿ID以降の投稿は日本語のパーマリンクでの投稿を防ぐ
function prevent_japanese_permalink_after_specified_post_id() {
    // 投稿IDが100以降の投稿のみ日本語のパーマリンクの投稿を防いでいます。100の数字を指定したい投稿IDに変えて使ってください。
    if (get_the_ID() <= 100) {
        return false;
    }

    prevent_japanese_permalink();
}
// 更新時
add_action('admin_footer-post.php', 'prevent_japanese_permalink_after_specified_post_id', 11, 0);

すでに日本語のパーマリンクでの投稿があり、それらのURLを変えるつもりはなく、記事の編集が発生する場合は上記の実装にしておくのが良いと思います。このブログも過去に日本語のパーマリンクでの投稿があるため、指定した投稿以降の投稿のみ日本語のパーマリンクでの投稿を防ぐ実装にしています。

参考になれば幸いです。

WordPressで投稿者ごとの記事一覧ページを実装する方法

ota|2017年05月12日
WordPress

読書はカフェよりも大学図書館派のota@purratto)です。実は一般にも開放しているところが多く、広々としていて開放的で、閲覧席がたくさんあるので席が埋まっていて座れないことがほぼなくて良いです(大学にもよるんでしょうが笑)。

WordPressで投稿者ごとの記事一覧ページを作成したので、その実装方法を解説します。
デモはこちら

下記のファイルを作成すると {ドメイン名}/author/{ユーザー名} にアクセスすると投稿者ごとの記事一覧が表示されます。
wp-content/themes/{テーマ名}/author.php

 <?php $userId = get_query_var('author'); ?>
 <?php $user = get_userdata($userId); ?>
 <h1><?php echo $user->display_name; ?><span> の投稿一覧</span></h1>
 
 <?php if (!empty($user->description)) { ?>
     <p><?php echo $user->description; ?></p>
 <?php } ?>
 
 <?php $posts = get_posts("author=$userId&orderby=date&post_type=post&numberposts=1000"); ?>
 <?php if (!empty($posts)) { ?>
     <ul> 
         <?php foreach( $posts as $post ) : setup_postdata($post); ?>
             <li><a href="<?php the_permalink() ?>"><?php the_title(); ?></a> <?php echo get_the_date("Y/n/j");?></li>
         <?php endforeach; ?>
         <?php wp_reset_postdata(); ?>
     </ul>
 <?php } ?>

参考になれば幸いです。

SEOフレンドリーな無限スクロールの実装方法

ota|2017年04月21日
JavaScript

飲み物は常温派のota@purratto)です。自動販売機で常温のものも販売してくれればいいのになあ、と常々思っています。

ランサーズストアでSEOフレンドリーな無限スクロールをjQueryで実装しました。
サンプル向けに一部修正したコードを公開します。参考になれば幸いです。
検索エンジンとの相性を考慮した無限スクロールのベストプラクティス | Googleウェブマスター向け公式ブログに準拠した実装になっています。

デモ(PCでみてください。スマホでは無限スクロールになっていません 2017/4/21現在。サンプルコード自体はPCのみならずスマホでも無限スクロールの実装として使えるコードになっています。)
コード

/**
 * SEOフレンドリーな無限スクロール
 */
(function () {
  'use strict';
  // 事前に読み込む次ページのページ数
  const LOAD_PAGES = 1.5;
  // スクロール位置がページの下限からどのぐらい上にあったときそのページを閲覧中とするのかの設定値
  const BOTTOM_POSITION_RATE = 0.25;
  // ブラウザのウィンドウのどの高さを閲覧中の中心の高さとするのかの設定値
  const FORCUS_POSITION_RATE = 0.5; 

  var seoInfiniteScroll = {
    /**
     * スクロール位置に応じて、次のページを読み込む
     */
    init: function () {
      setPosition();
      loadPage();
    }
  };

  /**
   * 2ページ目以降にアクセスしたときは前のページも読み込まれるため、アクセスしたページにスクロール位置を調整する
   */
  function setPosition() {
    var pageArray = $(location).attr('search').match(/[?&]page=\d+/);
    if (pageArray) {
      var page = pageArray[0].replace(/[?&]page=/, '');
      if (page >= 2) {
        $(window).load(function(){
          setTimeout(function() {
            if ($(".item:last")[0]) {
              $(window).scrollTop($(".item:last").offset().top);
            }
          }, 300);
        });
      }
    }
  }

  /**
   * 前後のページを読み込み表示する
   *
   * 前のページの無限スクロールの仕組み
   * loadPrevPage()で前のページを読み込み、prevDataCacheに読み込んだデータを保持
   * showPrevPage()でprevDataCacheに保持しているデータを表示
   *
   * 読み込みと表示で処理を分け、タイミングをずらしている理由は以下。
   * 上にコンテンツが追加されるとユーザーがみている位置が下にずれてしまうためのを防ぐためにjsで位置を修正しており、
   * そのときに読み込みまで行うと処理に時間がかかってしまう、jsでの位置の調整がカクカクするため。
   * 
   * 次のページの無限スクロールの仕組み
   * loadNextPage()で次のページを読み込み表示
   */
  function loadPage() {
    var isLoading = false;
    var prevDataCache = false;
    var nextPageExist = true;

    $(window).scroll(function() {
      var infiniteScrollTopPosition = $(".item:first").offset().top;
      var itemHeight = $(".item:first").outerHeight();
      var scrollTopPosition = $(window).scrollTop();
      loadPrevPage();
      showPrevPage();
      loadNextPage();
      updateBrowserHistory();

      /**
       * 前のページを読み込んでprev_data_cacheに入れる
       */
      function loadPrevPage() {
        if (!prevDataCache && !isLoading) {
          // ユーザーに読込みの待ち時間を発生させないためにLOAD_PAGESページ分前で読み込みイベントを発火する
          // 読み込みする位置までスクロールされていたらtrue、そうでなかったらfalseを返す
          var isScrollPosionToLoad = infiniteScrollTopPosition + itemHeight * LOAD_PAGES >= scrollTopPosition;
          // 前のページのURLが存在していたらtrue、そうでなかったらfalseを返す
          var isExistPrevUrl = Boolean($(".item:first").attr('data-prev-url'));
          // 読み込み位置までスクロールされており、data-prev-urlに読み込むべきURLがあるときに読み込み処理を行う
          if (isScrollPosionToLoad && isExistPrevUrl) {
            isLoading = true;
            var loadUrl = $(".item:first").attr('data-prev-url') + '&type=part';
            $.ajax({
              type:'GET',
              url:loadUrl,
              dataType:'json',
              'success': function(data) {
                prevDataCache = data.data;
                setTimeout(function() {
                  isLoading = false;
                }, 200);
              },
              'error': function(data) {}
            });
          }
        }
      }

      /**
       * loadPrevPage()によってprev_data_cacheに入れた前のページのデータを表示する
       */
      function showPrevPage() {
        if (prevDataCache && !isLoading) {
          // ユーザーに読込みの待ち時間を発生させないために1ページ分前で表示イベントを発火する
          var isScrollPosionToShow = infiniteScrollTopPosition + itemHeight >= scrollTopPosition;
          if (isScrollPosionToShow) {
            isLoading = true;
            $(".item-container").prepend(prevDataCache);
            $(window).scrollTop($(window).scrollTop() + itemHeight);
            prevDataCache = false;
            setTimeout(function() {
              isLoading = false;
            }, 200);
          }
        }
      }

      /**
       * 次のページを読み込み表示する
       */
      function loadNextPage() {
        // 読み込み中でなく、読み込む次のページが存在しているか
        if (!isLoading && nextPageExist) {
          // ユーザーに読込みの待ち時間を発生させないために1ページ分前で読込みイベントを発火する
          var scrollPositionBottom = scrollTopPosition + $(window).height();
          var isScrollPosionToLoad = $(".item:last").offset().top - itemHeight <= scrollPositionBottom;
          if (isScrollPosionToLoad) {
            isLoading = true;
            $(".loading").show();
            var loadUrl = $(".item:last").attr('data-next-url') + '&type=part';
            $.ajax({
              type:'GET',
              url:loadUrl,
              dataType:'json',
              'success': function(data) {
                if (data.data) {
                  $(".item-container").append(data.data);
                  setTimeout(function() {
                    isLoading = false;
                    $(".loading").hide();
                  }, 200);
                } else {
                  nextPageExist = false;
                  isLoading = false;
                  $(".loading").hide();
                  $(".finished").show();
                }
              },
              'error': function(data) {}
            });
          }
        }
      }
      /**
       * 検索窓に表示されるURLを現在みているページのものにする
       */
      function updateBrowserHistory() {
        $(".item").each(function(index) {
          if (mostlyVisible(this) && $(this).attr("data-url") !== $(location).attr('pathname') + $(location).attr('search')) {
            history.pushState(null, null, $(this).attr("data-url"));
          }
        });
      }

      /**
       * 現在みているページであるかを返す
       * @return boolean
       */
      function mostlyVisible(element) {
        var scrollPosition = $(window).scrollTop();
        var windowHeight = $(window).height();
        var elementTop = $(element).offset().top;
        var elementHeight = $(element).height();
        var elementBottom = elementTop + elementHeight;
        // スクロール位置がページの下限から指定した高さ分だけ上にあるかチェック && 閲覧の中心の高さがページの上限よりも下にあることをチェック
        return ((elementBottom - elementHeight * BOTTOM_POSITION_RATE > scrollPosition) && (elementTop < (scrollPosition + windowHeight * FORCUS_POSITION_RATE)));
      }
    });
  }

  window.seoInfiniteScroll = function () {
    return Object.create(seoInfiniteScroll);
  };
})();

サンプルコードの使い方

・クエリストリングにtype=partを含むURLでアクセスがあったときは読み込ませたい前後のページのみの部分をjsonで返すようにします。
・各ページごとにitemクラスを指定します。
・各ページを囲んだものにitem-containerをidに指定します。
・読み込み中に表示するものにloadingクラスを指定します。
・これ以上ページが存在しないときに表示するものにfinishedクラスを指定します。

WordPressでCSSがすぐに反映されないのを解消する方法

ota|2017年04月17日
WordPress

ota@purratto)です。

WordPressでCSSを編集したときに即座に反映されなかったことはありませんか?
これはブラウザキャッシュが原因です。

style.css読み込み箇所を下記のように変更するだけでstyle.cssを即座に確実に反映できるようになります。

<link rel=”stylesheet” href=”<?php echo get_stylesheet_uri(); >”>

の後ろに

?v=<?php echo filemtime( get_stylesheet_directory() . ‘/style.css’) ?>

を追記して、下記のようにします

<link rel=”stylesheet” href=”<?php echo get_stylesheet_uri(); ?>?v=<?php echo filemtime( get_stylesheet_directory() . ‘/style.css’) ?>”>

※シングルクオーテーションとダブルクオーテーションが環境によっては別のものに変換されてしまっていることがあるので、その場合は直して使ってください。

ちょっとしたコードですが、便利なのでぜひ使ってみてください。

デスクチェアとしてのバランスボールのすすめ

ota|2016年12月09日
開発よもやま

エンジニアのota@purratto)です。

すっかり市民権を得たと思っている(私だけ?)デスクチェアとしてのバランスボールですが、弊社では現在バランスボールの上でコードを書いているのは私一人というマイノリティなので、椅子としてのバランスボールの良さについて書いてみます。

バランスボールのメリット

腰が痛くならない

私がバランスボールをデスクチェアとして利用している最大の理由です。私は椅子に長時間座っているとどうも腰が痛くなってしまうのですが、バランスボールだとそれがまったくないです。バランスボールだとバランスを取るために強制的に姿勢が良くなるためのようです。ただこれは個人差があるかもしれません。

持ち運びが便利

軽いので持ち運びがかんたん。空気を抜いてコンパクトにすることもできます。オフィス移転が頻繁なベンチャーにとっては強い味方です。

安い

通常のデスクチェアに比べて圧倒的に安いです。1000円もかからずに十分に良いバランスボールを買うことができます。アーロンチェアを1台買うお金で、スタートアップであれば全員分の椅子を用意できてしまうでしょう。

バランスボールのデメリット

転がる

ボールなのでころころと転がってしまう恐れがあります。私はデスクの下にスリッパを置いていて、席にいるときはそのスリッパを履いています。席を離れるときはそのスリッパーをバランスボールのストッパー代わりにして転がるのを防いでいます。この件に関しては何らかのちょっとした工夫をして対応する必要があります。

メンテナンスが必要

使っているうちに空気が抜けてしまい、空気を入れる必要があります。私の場合は数ヶ月に1度程度です。

おすすめの座り方

ノーマル

balance_ball_nomal
両足を地面につけて座ります。もっとも一般的なバランスボールの座り方だと思います。私はこの座り方で過ごしている時間がもっとも長いです。

正座

balance_ball_seiza
バランスボールの上で正座します。背筋がぴんと伸びる感じがして好きな座り方の一つです。バランスボール愛好家の中には正座派も多いのではないでしょうか?
難点としてはあまり長時間だと足がしびれる(私だけ?)ことと、他の座り方と目線が高くなりがちなので、モニターと目線の高さがずれてしまうことです。

体育座り

balance_ball_taikuzuwari
バランスボールの上で体育座りします。体育座りってなんだか落ち着きません?

バランスボールに足をひっかけて座る

balance_ball_asihikkakeru
これは言葉で説明じづらいので、図をみてください笑

足を直角に伸ばす

balance_ball_asinobasu
バランスボールの上で足を直角にピンと伸ばします。足と腹筋に負荷がかかります。眠いときは目覚ましがてらこの座り方をして、腹筋をプルプルさせてコード書いてます笑