Redmine 3.4.0、3.4.1 がリリースされました。(3.4.0が7月2日、バグフィックスで3.4.1が7月9日)
View Customizeだけは動作確認済みですが、他のプラグインも後ほど確認します。
いつもどおりVagrantのboxもあげておきました。
Redmine 3.4.0、3.4.1 がリリースされました。(3.4.0が7月2日、バグフィックスで3.4.1が7月9日)
View Customizeだけは動作確認済みですが、他のプラグインも後ほど確認します。
いつもどおりVagrantのboxもあげておきました。
カスタムフィールドの値に応じて行を装飾する方法について問い合わせをいただいたので、「カスタムフィールド1の値が'A'の場合には、行のフォントを太字に変える」をView customizeで行ってみます。
チケットの優先度など、デフォルトで存在するフィールドについては、もとからclassが振られているのでCSSで装飾しやすいですが、カスタムフィールドだとclassが振られていないので、CSSだけではできません。 そのためJavaScriptにて値を判定して、スタイルを設定することによって対応します。
チケット一覧を対象にします。
/issues$
Type:JavaScript
として下記を設定します。cf_1
の部分は、該当のカスタムフィールドに応じて変更してください。
$(function() { $('table.issues td.cf_1') .filter(function() { return $(this).text() == 'A'; }) .parent() .css('color', 'red'); });
一覧に該当のカスタムフィールドが表示されていないと判定ができないので、ご注意ください。
複数の属性を指定するような場合だと、JavaScriptだと面倒かと思います。そういった場合には、JavaScript側ではclassを追加するだけで、そのclassに応じたCSSを別途定義しておくような形がよいかと思います。
追記@2017-07-02
下記Issueで既に対応していただいています。次のバージョン(3.4.5)に入る予定です。
MyBatisのSQLへのパラメータ埋め込みで、size
という名前を使うと、JavaTypeがintとなってしまうようです。
現象を確認したのは、mybatis-spring-boot-starter
の1.3.0(MyBatis自体のバージョンは3.4.4)です。
下記のようなMapperを作ります。size
という名前で、long型の引数をマッピングします。
package com.example.demo; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.springframework.stereotype.Repository; @Repository @Mapper public interface TestRepository { @Insert("INSERT INTO test(number, size) VALUES(#{number}, #{size})") public void insert(@Param("number") long number, @Param("size") long size); }
上記のメソッドを呼び出すと、下記のようなエラーが発生します。
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.type.TypeException: Could not set parameters for mapping: ParameterMapping{property='size', mode=IN, javaType=int, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}. Cause: org.apache.ibatis.type.TypeException: Error setting non null for parameter #2 with JdbcType null . Try setting a different JdbcType for this parameter or a different configuration property. Cause: java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.Integer
なぜかsize
がintとしてマッピングされようとしてClassCastExceptionが発生しています。LongTypeHandler
が使われるべきなのに、IntTypeHandler
が使用されてしまっているようです。
試しに下記のように別の名前に変えるとうまく動作します。
@Insert("INSERT INTO test(number, size) VALUES(#{number}, #{size2})") public void insert2(@Param("number") long number, @Param("size2") long size);
size
という名前は、何か特別なものとして使われていて、それと競合することによっておきてしまっているのかもしれません。
Redmine 3.3時点では、ニュースの参照に関する権限が無く、一部のユーザにニュースを見せないということが出来ません。
@onozaty Feature #7068 が採用されればRedmine 3.4.0でロールごとにニュースの参照が制限できるようになります。 https://t.co/fXkrc4UB5E
— MAEDA, Go (@g_maeda) 2017年5月11日
それをつぶやいたところ、前田さんにRedmine本家のFeatureにあがっていて、3.4.0で入るかもといった情報を頂いたので、先行してパッチを当てて試してみました。
現時点のパッチは下記になっています。
軽微な変更だったのと、最新じゃないバージョンに当てたかったので、手作業で差分当てました。下記の3ファイル(合計3行)さえ直せば、すぐに試せます。(日本語の表示確認してなくて良ければ、ja.yml
もいらないです)
diff --git a/config/locales/en.yml b/config/locales/en.yml index a8eca2dfe..f2a8cbc0b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -497,6 +497,7 @@ en: permission_view_time_entries: View spent time permission_edit_time_entries: Edit time logs permission_edit_own_time_entries: Edit own time logs + permission_view_news: View news permission_manage_news: Manage news permission_comment_news: Comment news permission_view_documents: View documents
diff --git a/config/locales/ja.yml b/config/locales/ja.yml index a018e7240..8d1ae6ce1 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -427,6 +427,7 @@ ja: permission_edit_time_entries: 作業時間の編集 permission_edit_own_time_entries: 自身が記入した作業時間の編集 permission_manage_project_activities: 作業分類 (時間管理) の管理 + permission_view_news: ニュースの閲覧 permission_manage_news: ニュースの管理 permission_comment_news: ニュースへのコメント permission_view_documents: 文書の閲覧
diff --git a/lib/redmine.rb b/lib/redmine.rb index e3ff60fd1..204429cc6 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -131,7 +131,7 @@ Redmine::AccessControl.map do |map| end map.project_module :news do |map| - map.permission :view_news, {:news => [:index, :show]}, :public => true, :read => true + map.permission :view_news, {:news => [:index, :show]}, :read => true map.permission :manage_news, {:news => [:new, :create, :edit, :update, :destroy], :comments => [:destroy], :attachments => :upload}, :require => :member map.permission :comment_news, {:comments => :create} end
ロールの編集画面に「ニュースの閲覧」という権限が追加されて、制御可能になりました。実際のニュースの表示もばっちりです。
こないだSTOMP over WebSocketを試してみましたが、今度はSTOMPを使わずに、TextWebSocketHandler
を使ったWebSocketを試してみます。題材も同じくチャットです。
ルームの情報をどのように渡そうか悩みました。コードを書き始めるまでは、WebSocketで接続する先にヘッダとかで渡せばいいかなと思っていましたが、調べてみたら渡せなかったので、やもえずURLのクエリパラメータとして渡すようにしました。
テキストでのやり取りを行うので、TextWebSocketHandler
を利用します。TextWebSocketHandler
の各メソッドをoverrideして必要な処理を実装するだけです。
ルームの情報は、URLのクエリとしてクライアントから送っているので、接続が確立したタイミング(afterConnectionEstablished
)にて、ルーム毎にWebSocketSession
を保持するようにします。
メッセージを受け取ったら、自分のルームと同じWebSocketSession
に対して、メッセージを送るだけです。
package com.example; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; import org.springframework.stereotype.Component; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; @Component public class ChatHandler extends TextWebSocketHandler { private ConcurrentHashMap<String, Set<WebSocketSession>> roomSessionPool = new ConcurrentHashMap<>(); @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { String roomName = session.getUri().getQuery(); roomSessionPool.compute(roomName, (key, sessions) -> { if (sessions == null) { sessions = new CopyOnWriteArraySet<>(); } sessions.add(session); return sessions; }); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { String roomName = session.getUri().getQuery(); for (WebSocketSession roomSession : roomSessionPool.get(roomName)) { roomSession.sendMessage(message); } } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { String roomName = session.getUri().getQuery(); roomSessionPool.compute(roomName, (key, sessions) -> { sessions.remove(session); if (sessions.isEmpty()) { // 1件もない場合はMapからクリア sessions = null; } return sessions; }); } }
WebSocketの設定として、URLとHandlerを紐付けます。
package com.example; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; import lombok.AllArgsConstructor; @Configuration @EnableWebSocket @AllArgsConstructor public class WebSocketConfig implements WebSocketConfigurer { private final ChatHandler chatHandler; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(chatHandler, "/endpoint"); } }
クライアント側では、下記のようなコードになりました。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8" /> <title>チャット</title> <link rel="stylesheet" href="/webjars/bootstrap/3.3.7/css/bootstrap.min.css" /> <link rel="stylesheet" href="/webjars/bootstrap/3.3.7/css/bootstrap-theme.min.css" /> </head> <body> <div class="container"> <h2>チャット</h2> <div class="form-horizontal"> <div class="form-group"> <label for="roomName" class="col-sm-2 control-label">ルーム</label> <div class="col-sm-2"> <input id="roomName" type="text" class="form-control" value="example" /> </div> <div class="col-sm-3"> <button id="connectButton" type="button" class="btn btn-default">接続</button> <button id="disconnectButton" class="btn btn-default">切断</button> </div> </div> <div class="form-group"> <label for="message" class="col-sm-2 control-label">メッセージ</label> <div class="col-sm-4"> <input id="message" type="text" class="form-control" /> </div> <div class="col-sm-2"> <button id="sendButton" type="button" class="btn btn-default">送信</button> </div> </div> <div class="row"> <div class="col-sm-4 col-sm-offset-2"> <ul id="messageList" class="list-unstyled"> </ul> </div> </div> </div> </div> <script src="/webjars/jquery/1.12.4/jquery.min.js"></script> <script src="/webjars/bootstrap/3.3.7/js/bootstrap.min.js"></script> <script> $(function() { var endpoint = 'ws://' + location.host + '/endpoint'; var webSocket = null; $('#connectButton').click(function() { $("#messageList").empty(); webSocket = new WebSocket(endpoint + '?' + encodeURIComponent($('#roomName').val())); webSocket.onopen = function() { $('#roomName').prop('disabled', true); $('#connectButton').prop('disabled', true); $('#disconnectButton').prop('disabled', false); }; webSocket.onclose = function() { }; webSocket.onmessage = function(message) { $('#messageList').prepend($('<li>').text(message.data)); }; webSocket.onerror = function() { alert('エラーが発生しました。'); }; }); $('#disconnectButton').click(function() { webSocket.close(); webSocket = null; $('#roomName').prop('disabled', false); $('#connectButton').prop('disabled', false); $('#disconnectButton').prop('disabled', true); }); $('#sendButton').click(function() { if (!webSocket) { alert('未接続です。'); return; } webSocket.send($('#message').val()); }); }); </script> </body> </html>
STOMPの時のほうが、いろいろシンプルに書けるので、ブラウザがクライアントならば、STOMPを使わない理由はないかなと思っています。
Spring Boot
でSTOMP Over WebSocket
を使って、試しにチャットっぽいものを作ってみました。
同じルームに接続しているクライアントに、同じメッセージを配信するだけならば、Controller
の定義はいりません。
WebSocket
のConfig
として、エンドポイントとSimpleBroker
でハンドリングする宛先を定義するだけです。
package com.example; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; @EnableWebSocketMessageBroker @Configuration public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/endpoint"); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/topic"); } }
画面側では、配信と購読で同じ宛先を指定しておきます。自分で送ったものも受け取ることになります。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8" /> <title>チャット</title> <link rel="stylesheet" href="/webjars/bootstrap/3.3.7/css/bootstrap.min.css" /> <link rel="stylesheet" href="/webjars/bootstrap/3.3.7/css/bootstrap-theme.min.css" /> </head> <body> <div class="container"> <h2>チャット</h2> <div class="form-horizontal"> <div class="form-group"> <label for="roomName" class="col-sm-2 control-label">ルーム</label> <div class="col-sm-2"> <input id="roomName" type="text" class="form-control" value="example" /> </div> <div class="col-sm-3"> <button id="connectButton" type="button" class="btn btn-default">接続</button> <button id="disconnectButton" class="btn btn-default">切断</button> </div> </div> <div class="form-group"> <label for="message" class="col-sm-2 control-label">メッセージ</label> <div class="col-sm-4"> <input id="message" type="text" class="form-control" /> </div> <div class="col-sm-2"> <button id="sendButton" type="button" class="btn btn-default">送信</button> </div> </div> <div class="row"> <div class="col-sm-4 col-sm-offset-2"> <ul id="messageList" class="list-unstyled"> </ul> </div> </div> </div> </div> <script src="/webjars/jquery/1.12.4/jquery.min.js"></script> <script src="/webjars/stomp-websocket/2.3.3-1/stomp.js"></script> <script src="/webjars/bootstrap/3.3.7/js/bootstrap.min.js"></script> <script> $(function() { var endpoint = 'ws://' + location.host + '/endpoint'; var subscribePrefix = '/topic/'; var stompClient = null; $('#connectButton').click(function() { $("#messageList").empty(); stompClient = Stomp.over(new WebSocket(endpoint)); stompClient.connect({}, function() { stompClient.subscribe( subscribePrefix + $('#roomName').val(), function(message) { $('#messageList').prepend($('<li>').text(message.body)); }); $('#roomName').prop('disabled', true); $('#connectButton').prop('disabled', true); $('#disconnectButton').prop('disabled', false); }); }); $('#disconnectButton').click(function() { stompClient.disconnect(); stompClient = null; $('#roomName').prop('disabled', false); $('#connectButton').prop('disabled', false); $('#disconnectButton').prop('disabled', true); }); $('#sendButton').click(function() { if (!stompClient) { alert('未接続です。'); return; } stompClient.send(subscribePrefix + $('#roomName').val(), {}, $('#message').val()); }); }); </script> </body> </html>
クライアント間でメッセージをやり取りするといったことが、少しのコードで簡単に実現できますね。
POPSPRING 2017 幕張公演に、嫁さんと2人で参加してきました。フェスは初参加です。
GOLDチケットだったので、とても余裕もって見れました。最初は前の方にいたのですが、逆に人が密集していてみずらかったので、途中からちょっと後ろ目で見ていました。それでもこんな感じで十分近くに見えました。
11時から21時過ぎまでの長丁場だったので、疲れたらちょっと下がって座って見れるのも良かったです。
POPSPRINGでは、アーティストの写真撮影NGとのことだったので、今回は写真がほとんど撮れませんでした。 海外アーティストだと珍しいなぁと思いましたが、いろんなアーティストが出る都合上、NGにせざるを得ないのかもしれませんね。(周りで結構撮っている人がいたけど、特に注意が入ることもなく、、、)
会場前から並んで参加しました。GOLDだと入場も優先だったので、会場ぎりぎりでもスムーズに入れました。
DJ SHOTA(@djshotamusic)のOpening DJで、かなりテンションあがりました。早く来たかいがありました。
続くOpening Actは FlowBack、RIRI、FAKY と日本人アーティストが続きましたが、知らなかったアーティストと出会えるのはいいですね。とくにRIRI(@riri_tone)が良かったです。
まだ17歳なのにびっくりなのと、歌のときの大人の雰囲気と、MCのときの可愛さのギャップに、すぐにファンというか、応援したくなりました。可愛くて歌はパワフルなところが、アリアナ・グランデに似てるねと、嫁さんと盛り上がりました。
#POPSPRING2017 #オフィシャルライブ写真📸
— POPSPRING (@POPSPRINGFEST) 2017年3月29日
本当に女子高生⁈ 東京と神戸のオーディエンスを驚かせた@riri_tone のボーカルパワー!5Hのメンバーも太鼓判押してましたよ!💁🏻https://t.co/j4ftD9mAtH#サイコーの思い出 #春フェス🌸 pic.twitter.com/KbOpTbyWKk
機器のトラブルでかなり遅れて、Jordan Fisherとなりました。POPSPRINGに出演するということで、初めて知ったアーティストでしたが、All About UsをYouTubeで見て、すぐに好きになりました。
Jordan Fisher - All About Us (Official Video)
ちなみにダンスみて、なぜかUsherを思い出しました。似てるかな、、
パフォーマンスを見て、これからさらに売れるんだろうなぁと確信しました。また見たい!
ちょうどお昼休憩とっていて、ほとんど見逃してしまいました、、ごめんなさい。
昨年に続いてのAustin Mahone。ダンサーさんが色っぽい。
Dirty Work でブルゾンちえみが出てこないかなぁと思いましたが、出てきませんでした(笑)。
#POPSPRING2017 #オフィシャルライブ写真📸
— POPSPRING (@POPSPRINGFEST) 2017年3月29日
セクシーになってもサイコーの笑顔😃でライブしてくれた #オーくん @AustinMahone
💁♂️https://t.co/JgdORGKJ4e#サイコーの思い出 #春フェス🌸 #ライブ pic.twitter.com/BCw1XJshU6
最後に発表となったアーティストで、まぁ、Austin最後に持ってこられたら、全てOKになりますよね!(日本人アーティストが多いことで、ちょっと話題になっていましたが、最後のAustinの発表で全て吹き飛んだ気が)
Sabrina Carpenter もPOPSPRINGに出演するということで初めて知ったアーティストでしたが、ほんと今となってはすごい好きなアーティストになりました。とくに1stアルバムの曲に好きなのが多いです。
Can’t Blame a Girl for Trying や Eyes Wide Open とか。
Sabrina Carpenter - Eyes Wide Open (Official Video)
ステージのSabrinaはすごい輝いてました。
#POPSPRING2017 #オフィシャルライブ写真📸
— POPSPRING (@POPSPRINGFEST) 2017年3月29日
とにかくキュート過ぎて会場中のみんなが❤️になった @SabrinaAnnLynn
💁🏼https://t.co/jMi6r2tx66#サイコーの思い出 #春フェス🌸 #ライブ #Sabちゃん pic.twitter.com/IHPntOAHWQ
We Don’t Need To Talk Anymore を初めて聞きましたが、これはいい曲だと思いました。
We Don’t Need To Talk Anymore(MUSIC VIDEO Full ver.+15s SPOT) / w-inds.
一番盛り上がったと思います。楽しくて、一番疲れました(笑)。ほんと弾けすぎです。
#POPSPRING2017 #オフィシャルライブ写真📸
— POPSPRING (@POPSPRINGFEST) 2017年3月29日
アゲアゲのライブでサイコーなパーティー🎉にしてくれたのは @DNCE
💁🏼https://t.co/6pbkaABMQb#サイコーの思い出 #春フェス🌸 #ライブ pic.twitter.com/JcG8val4zb
いや、ほんとすごいというか、感動しました。
曲は好きで良く聞いてましたが、ギター一本であんな聞かせるパフォーマンスするなんて。すいません、全然Shownのことわかって無かったです。
#POPSPRING2017 #オフィシャルライブ写真📸
— POPSPRING (@POPSPRINGFEST) 2017年3月29日
ハッピ姿でステージに登場した @ShawnMendes 😍
実はDa-iCEの皆さんからの🎁
似合ってますね!
フォトアルバムは💁🏼♂️https://t.co/nYPmHqUdop#サイコーの思い出 #春フェス🌸 pic.twitter.com/H5c2qdOl5m
ずっとハッピ姿のままいくとも思ってませんでした。ちょっとパフォーマンスとアンマッチな気が。
Fifth Harmonyが一番のお目当てだったので、待ちに待った… といった感じでした。
いろいろな形でのパフォーマンスを見せてくれて、疲れがふっとびました。4人みんな特徴があって、いいグループですよね!(Allyのキュートな声が、一番好きです)
#POPSPRING2017
— POPSPRING (@POPSPRINGFEST) 2017年3月29日
ライブ🎤写真📸が続々アップされてます💓
パワフルでダイナミックなステージで盛り上げたヘッドライナー @FifthHarmony の写真はこちら💁https://t.co/3wZU7osckc#サイコーの思い出 #春フェス🌸 #ライブ pic.twitter.com/AuFwxzQxcJ
自分が知らなかったアーティストに出会えたこともあって、ほんと値段以上の価値を感じたフェスでした。 また来年も行きたいです!!(サマソニにも興味がわいてきています…)